Repository: redis/redis

Problem statement:
[NEW] Perform a GET without changing the idletime
First of all, let me say I love Redis! And I am really sorry if there is an obvious solution I missed.

**The problem/use-case that the feature addresses**

I want to run some maintenance on my Redis instance, that has a LRU eviction policy and a max memory limit.
The idea is to loop over the keys, get the values length with *strlen*, and evict the keys whose values have the biggest lengths.
The issue: commands like get and strlen reset the _idletime_, which is not great regarding the eviction policy.

**Description of the feature**

I would like a way to have a way to run get/strlen commands without updating the idletime. Also, perhaps something similar for "freq" when on LFU policy.

**Alternatives you've considered**

I don't think it is possible to manually set the idletime, but if I am wrong, then I could store the idletime before doing the get, then restore it after.

**Additional information**

I will use Python with redis-py to implement this.



Hints:
I suppose the exact solution you want is adding some argument onto `STRLEN`, but a more generic solution might be to add a new argument on `GET` to have the notouch configuration. Would that solve your use case? 
Exactly! Though for my precise use case, `STRLEN` would suffice, I can see my maintenance scripts evolve to a point where I will need `GET` as well. And I guess it would serve more use cases for other users.
Speaking of generics, one (me) could argue that the best way to go about this is to have a connection flag that says "do not touch LFU/LRU stats for read operations" as strings are just one data type (albeit the most common).

As an applicative alternative, the client can maintain a zset of keys and their sizes to make the admin task easier.
I also think this could be a client flag rather than a command flag.
something like `CLIENT STEALTHMODE 1` :smile:

Patch:
diff --git a/src/commands.c b/src/commands.c
index 7698f2055cf..e24ab9dfcce 100644
--- a/src/commands.c
+++ b/src/commands.c
@@ -890,6 +890,27 @@ struct redisCommandArg CLIENT_NO_EVICT_Args[] = {
 {0}
 };
 
+/********** CLIENT NO_TOUCH ********************/
+
+/* CLIENT NO_TOUCH history */
+#define CLIENT_NO_TOUCH_History NULL
+
+/* CLIENT NO_TOUCH tips */
+#define CLIENT_NO_TOUCH_tips NULL
+
+/* CLIENT NO_TOUCH enabled argument table */
+struct redisCommandArg CLIENT_NO_TOUCH_enabled_Subargs[] = {
+{"on",ARG_TYPE_PURE_TOKEN,-1,"ON",NULL,NULL,CMD_ARG_NONE},
+{"off",ARG_TYPE_PURE_TOKEN,-1,"OFF",NULL,NULL,CMD_ARG_NONE},
+{0}
+};
+
+/* CLIENT NO_TOUCH argument table */
+struct redisCommandArg CLIENT_NO_TOUCH_Args[] = {
+{"enabled",ARG_TYPE_ONEOF,-1,NULL,NULL,NULL,CMD_ARG_NONE,.subargs=CLIENT_NO_TOUCH_enabled_Subargs},
+{0}
+};
+
 /********** CLIENT PAUSE ********************/
 
 /* CLIENT PAUSE history */
@@ -1027,6 +1048,7 @@ struct redisCommand CLIENT_Subcommands[] = {
 {"kill","Kill the connection of a client","O(N) where N is the number of client connections","2.4.0",CMD_DOC_NONE,NULL,NULL,COMMAND_GROUP_CONNECTION,CLIENT_KILL_History,CLIENT_KILL_tips,clientCommand,-3,CMD_ADMIN|CMD_NOSCRIPT|CMD_LOADING|CMD_STALE|CMD_SENTINEL,ACL_CATEGORY_CONNECTION,.args=CLIENT_KILL_Args},
 {"list","Get the list of client connections","O(N) where N is the number of client connections","2.4.0",CMD_DOC_NONE,NULL,NULL,COMMAND_GROUP_CONNECTION,CLIENT_LIST_History,CLIENT_LIST_tips,clientCommand,-2,CMD_ADMIN|CMD_NOSCRIPT|CMD_LOADING|CMD_STALE|CMD_SENTINEL,ACL_CATEGORY_CONNECTION,.args=CLIENT_LIST_Args},
 {"no-evict","Set client eviction mode for the current connection","O(1)","7.0.0",CMD_DOC_NONE,NULL,NULL,COMMAND_GROUP_CONNECTION,CLIENT_NO_EVICT_History,CLIENT_NO_EVICT_tips,clientCommand,3,CMD_ADMIN|CMD_NOSCRIPT|CMD_LOADING|CMD_STALE|CMD_SENTINEL,ACL_CATEGORY_CONNECTION,.args=CLIENT_NO_EVICT_Args},
+{"no-touch","Controls whether commands sent by the client will alter the LRU/LFU of the keys they access.","O(1)",NULL,CMD_DOC_NONE,NULL,NULL,COMMAND_GROUP_CONNECTION,CLIENT_NO_TOUCH_History,CLIENT_NO_TOUCH_tips,clientCommand,3,CMD_NOSCRIPT|CMD_LOADING|CMD_STALE,ACL_CATEGORY_CONNECTION,.args=CLIENT_NO_TOUCH_Args},
 {"pause","Stop processing commands from clients for some time","O(1)","3.0.0",CMD_DOC_NONE,NULL,NULL,COMMAND_GROUP_CONNECTION,CLIENT_PAUSE_History,CLIENT_PAUSE_tips,clientCommand,-3,CMD_ADMIN|CMD_NOSCRIPT|CMD_LOADING|CMD_STALE|CMD_SENTINEL,ACL_CATEGORY_CONNECTION,.args=CLIENT_PAUSE_Args},
 {"reply","Instruct the server whether to reply to commands","O(1)","3.2.0",CMD_DOC_NONE,NULL,NULL,COMMAND_GROUP_CONNECTION,CLIENT_REPLY_History,CLIENT_REPLY_tips,clientCommand,3,CMD_NOSCRIPT|CMD_LOADING|CMD_STALE|CMD_SENTINEL,ACL_CATEGORY_CONNECTION,.args=CLIENT_REPLY_Args},
 {"setname","Set the current connection name","O(1)","2.6.9",CMD_DOC_NONE,NULL,NULL,COMMAND_GROUP_CONNECTION,CLIENT_SETNAME_History,CLIENT_SETNAME_tips,clientCommand,3,CMD_NOSCRIPT|CMD_LOADING|CMD_STALE|CMD_SENTINEL,ACL_CATEGORY_CONNECTION,.args=CLIENT_SETNAME_Args},
diff --git a/src/commands/client-no-touch.json b/src/commands/client-no-touch.json
new file mode 100644
index 00000000000..71292e952dd
--- /dev/null
+++ b/src/commands/client-no-touch.json
@@ -0,0 +1,36 @@
+{
+    "NO-TOUCH": {
+        "summary": "Controls whether commands sent by the client will alter the LRU/LFU of the keys they access.",
+        "complexity": "O(1)",
+        "group": "connection",
+        "arity": 3,
+        "container": "CLIENT",
+        "function": "clientCommand",
+        "command_flags": [
+            "NOSCRIPT",
+            "LOADING",
+            "STALE"
+        ],
+        "acl_categories": [
+            "CONNECTION"
+        ],
+        "arguments": [
+            {
+                "name": "enabled",
+                "type": "oneof",
+                "arguments": [
+                    {
+                        "name": "on",
+                        "type": "pure-token",
+                        "token": "ON"
+                    },
+                    {
+                        "name": "off",
+                        "type": "pure-token",
+                        "token": "OFF"
+                    }
+                ]
+            }
+        ]
+    }
+}
\ No newline at end of file
diff --git a/src/db.c b/src/db.c
index e1d28a3e465..ddfb04ebb0c 100644
--- a/src/db.c
+++ b/src/db.c
@@ -113,6 +113,9 @@ robj *lookupKey(redisDb *db, robj *key, int flags) {
         /* Update the access time for the ageing algorithm.
          * Don't do it if we have a saving child, as this will trigger
          * a copy on write madness. */
+        if (server.current_client && server.current_client->flags & CLIENT_NO_TOUCH &&
+            server.current_client->cmd->proc != touchCommand)
+            flags |= LOOKUP_NOTOUCH;
         if (!hasActiveChildProcess() && !(flags & LOOKUP_NOTOUCH)){
             if (server.maxmemory_policy & MAXMEMORY_FLAG_LFU) {
                 updateLFU(val);
diff --git a/src/networking.c b/src/networking.c
index bb4b203b0b7..ad425b7f77a 100644
--- a/src/networking.c
+++ b/src/networking.c
@@ -1493,8 +1493,8 @@ void clearClientConnectionState(client *c) {
     }
 
     /* Selectively clear state flags not covered above */
-    c->flags &= ~(CLIENT_ASKING|CLIENT_READONLY|CLIENT_PUBSUB|
-                  CLIENT_REPLY_OFF|CLIENT_REPLY_SKIP_NEXT);
+    c->flags &= ~(CLIENT_ASKING|CLIENT_READONLY|CLIENT_PUBSUB|CLIENT_REPLY_OFF|
+                  CLIENT_REPLY_SKIP_NEXT|CLIENT_NO_TOUCH|CLIENT_NO_EVICT);
 }
 
 void freeClient(client *c) {
@@ -2692,7 +2692,7 @@ char *getClientSockname(client *c) {
 /* Concatenate a string representing the state of a client in a human
  * readable format, into the sds string 's'. */
 sds catClientInfoString(sds s, client *client) {
-    char flags[16], events[3], conninfo[CONN_INFO_LEN], *p;
+    char flags[17], events[3], conninfo[CONN_INFO_LEN], *p;
 
     p = flags;
     if (client->flags & CLIENT_SLAVE) {
@@ -2715,6 +2715,7 @@ sds catClientInfoString(sds s, client *client) {
     if (client->flags & CLIENT_UNIX_SOCKET) *p++ = 'U';
     if (client->flags & CLIENT_READONLY) *p++ = 'r';
     if (client->flags & CLIENT_NO_EVICT) *p++ = 'e';
+    if (client->flags & CLIENT_NO_TOUCH) *p++ = 'T';
     if (p == flags) *p++ = 'N';
     *p++ = '\0';
 
@@ -2905,6 +2906,8 @@ void clientCommand(client *c) {
 "    Report tracking status for the current connection.",
 "NO-EVICT (ON|OFF)",
 "    Protect current client connection from eviction.",
+"NO-TOUCH (ON|OFF)",
+"    Will not touch LRU/LFU stats when this mode is on.",
 NULL
         };
         addReplyHelp(c, help);
@@ -3374,6 +3377,17 @@ NULL
         } else {
             addReplyArrayLen(c,0);
         }
+    } else if (!strcasecmp(c->argv[1]->ptr, "no-touch")) {
+        /* CLIENT NO-TOUCH ON|OFF */
+        if (!strcasecmp(c->argv[2]->ptr,"on")) {
+            c->flags |= CLIENT_NO_TOUCH;
+            addReply(c,shared.ok);
+        } else if (!strcasecmp(c->argv[2]->ptr,"off")) {
+            c->flags &= ~CLIENT_NO_TOUCH;
+            addReply(c,shared.ok);
+        } else {
+            addReplyErrorObject(c,shared.syntaxerr);
+        }
     } else {
         addReplySubcommandSyntaxError(c);
     }
diff --git a/src/server.h b/src/server.h
index 11629c71806..7d8e65b36f1 100644
--- a/src/server.h
+++ b/src/server.h
@@ -390,6 +390,7 @@ extern int configOOMScoreAdjValuesDefaults[CONFIG_OOM_COUNT];
                                       memory eviction. */
 #define CLIENT_ALLOW_OOM (1ULL<<44) /* Client used by RM_Call is allowed to fully execute
                                        scripts even when in OOM */
+#define CLIENT_NO_TOUCH (1ULL<<45) /* This client will not touch LFU/LRU stats. */
 
 /* Client block type (btype field in client structure)
  * if CLIENT_BLOCKED flag is set. */


Test patch:
diff --git a/tests/unit/introspection-2.tcl b/tests/unit/introspection-2.tcl
index 9576c39ab6f..2992f6aa378 100644
--- a/tests/unit/introspection-2.tcl
+++ b/tests/unit/introspection-2.tcl
@@ -2,6 +2,17 @@ proc cmdstat {cmd} {
     return [cmdrstat $cmd r]
 }
 
+proc getlru {key} {
+    set objinfo [r debug object $key]
+    foreach info $objinfo {
+        set kvinfo [split $info ":"]
+        if {[string compare [lindex $kvinfo 0] "lru"] == 0} {
+            return [lindex $kvinfo 1]
+        }
+    }
+    fail "Can't get LRU info with DEBUG OBJECT"
+}
+
 start_server {tags {"introspection"}} {
     test {The microsecond part of the TIME command will not overflow} {
         set now [r time]
@@ -26,6 +37,20 @@ start_server {tags {"introspection"}} {
         assert {[r object idletime foo] < 2}
     }
 
+    test {Operations in no-touch mode do not alter the last access time of a key} {
+        r set foo bar
+        r client no-touch on
+        set oldlru [getlru foo]
+        after 1000
+        r get foo
+        set newlru [getlru foo]
+        assert_equal $newlru $oldlru
+        r client no-touch off
+        r get foo
+        set newlru [getlru foo]
+        assert_morethan $newlru $oldlru
+    } {} {needs:debug}
+
     test {TOUCH returns the number of existing keys specified} {
         r flushdb
         r set key1{t} 1