diff --git a/src/commands.def b/src/commands.def index 49e9d2f11..aede27a02 100644 --- a/src/commands.def +++ b/src/commands.def @@ -1177,6 +1177,7 @@ commandHistory CLIENT_KILL_History[] = { {"3.2.0","Added `master` type in for `TYPE` option."}, {"5.0.0","Replaced `slave` `TYPE` with `replica`. `slave` still supported for backward compatibility."}, {"6.2.0","`LADDR` option."}, +{"8.0.0","`MAXAGE` option."}, }; #endif @@ -1213,12 +1214,13 @@ struct COMMAND_ARG CLIENT_KILL_filter_new_format_Subargs[] = { {MAKE_ARG("addr",ARG_TYPE_STRING,-1,"ADDR",NULL,NULL,CMD_ARG_OPTIONAL,0,NULL),.display_text="ip:port"}, {MAKE_ARG("laddr",ARG_TYPE_STRING,-1,"LADDR",NULL,"6.2.0",CMD_ARG_OPTIONAL,0,NULL),.display_text="ip:port"}, {MAKE_ARG("skipme",ARG_TYPE_ONEOF,-1,"SKIPME",NULL,NULL,CMD_ARG_OPTIONAL,2,NULL),.subargs=CLIENT_KILL_filter_new_format_skipme_Subargs}, +{MAKE_ARG("maxage",ARG_TYPE_INTEGER,-1,"MAXAGE",NULL,"8.0.0",CMD_ARG_OPTIONAL,0,NULL)}, }; /* CLIENT KILL filter argument table */ struct COMMAND_ARG CLIENT_KILL_filter_Subargs[] = { {MAKE_ARG("old-format",ARG_TYPE_STRING,-1,NULL,NULL,NULL,CMD_ARG_NONE,0,"2.8.12"),.display_text="ip:port"}, -{MAKE_ARG("new-format",ARG_TYPE_ONEOF,-1,NULL,NULL,NULL,CMD_ARG_MULTIPLE,6,NULL),.subargs=CLIENT_KILL_filter_new_format_Subargs}, +{MAKE_ARG("new-format",ARG_TYPE_ONEOF,-1,NULL,NULL,NULL,CMD_ARG_MULTIPLE,7,NULL),.subargs=CLIENT_KILL_filter_new_format_Subargs}, }; /* CLIENT KILL argument table */ @@ -1543,7 +1545,7 @@ struct COMMAND_STRUCT CLIENT_Subcommands[] = { {MAKE_CMD("help","Returns helpful text about the different subcommands.","O(1)","5.0.0",CMD_DOC_NONE,NULL,NULL,"connection",COMMAND_GROUP_CONNECTION,CLIENT_HELP_History,0,CLIENT_HELP_Tips,0,clientCommand,2,CMD_LOADING|CMD_STALE|CMD_SENTINEL,ACL_CATEGORY_CONNECTION,CLIENT_HELP_Keyspecs,0,NULL,0)}, {MAKE_CMD("id","Returns the unique client ID of the connection.","O(1)","5.0.0",CMD_DOC_NONE,NULL,NULL,"connection",COMMAND_GROUP_CONNECTION,CLIENT_ID_History,0,CLIENT_ID_Tips,0,clientCommand,2,CMD_NOSCRIPT|CMD_LOADING|CMD_STALE|CMD_SENTINEL,ACL_CATEGORY_CONNECTION,CLIENT_ID_Keyspecs,0,NULL,0)}, {MAKE_CMD("info","Returns information about the connection.","O(1)","6.2.0",CMD_DOC_NONE,NULL,NULL,"connection",COMMAND_GROUP_CONNECTION,CLIENT_INFO_History,0,CLIENT_INFO_Tips,1,clientCommand,2,CMD_NOSCRIPT|CMD_LOADING|CMD_STALE|CMD_SENTINEL,ACL_CATEGORY_CONNECTION,CLIENT_INFO_Keyspecs,0,NULL,0)}, -{MAKE_CMD("kill","Terminates open connections.","O(N) where N is the number of client connections","2.4.0",CMD_DOC_NONE,NULL,NULL,"connection",COMMAND_GROUP_CONNECTION,CLIENT_KILL_History,5,CLIENT_KILL_Tips,0,clientCommand,-3,CMD_ADMIN|CMD_NOSCRIPT|CMD_LOADING|CMD_STALE|CMD_SENTINEL,ACL_CATEGORY_CONNECTION,CLIENT_KILL_Keyspecs,0,NULL,1),.args=CLIENT_KILL_Args}, +{MAKE_CMD("kill","Terminates open connections.","O(N) where N is the number of client connections","2.4.0",CMD_DOC_NONE,NULL,NULL,"connection",COMMAND_GROUP_CONNECTION,CLIENT_KILL_History,6,CLIENT_KILL_Tips,0,clientCommand,-3,CMD_ADMIN|CMD_NOSCRIPT|CMD_LOADING|CMD_STALE|CMD_SENTINEL,ACL_CATEGORY_CONNECTION,CLIENT_KILL_Keyspecs,0,NULL,1),.args=CLIENT_KILL_Args}, {MAKE_CMD("list","Lists open connections.","O(N) where N is the number of client connections","2.4.0",CMD_DOC_NONE,NULL,NULL,"connection",COMMAND_GROUP_CONNECTION,CLIENT_LIST_History,6,CLIENT_LIST_Tips,1,clientCommand,-2,CMD_ADMIN|CMD_NOSCRIPT|CMD_LOADING|CMD_STALE|CMD_SENTINEL,ACL_CATEGORY_CONNECTION,CLIENT_LIST_Keyspecs,0,NULL,2),.args=CLIENT_LIST_Args}, {MAKE_CMD("no-evict","Sets the client eviction mode of the connection.","O(1)","7.0.0",CMD_DOC_NONE,NULL,NULL,"connection",COMMAND_GROUP_CONNECTION,CLIENT_NO_EVICT_History,0,CLIENT_NO_EVICT_Tips,0,clientCommand,3,CMD_ADMIN|CMD_NOSCRIPT|CMD_LOADING|CMD_STALE|CMD_SENTINEL,ACL_CATEGORY_CONNECTION,CLIENT_NO_EVICT_Keyspecs,0,NULL,1),.args=CLIENT_NO_EVICT_Args}, {MAKE_CMD("no-touch","Controls whether commands sent by the client affect the LRU/LFU of accessed keys.","O(1)","7.2.0",CMD_DOC_NONE,NULL,NULL,"connection",COMMAND_GROUP_CONNECTION,CLIENT_NO_TOUCH_History,0,CLIENT_NO_TOUCH_Tips,0,clientCommand,3,CMD_NOSCRIPT|CMD_LOADING|CMD_STALE,ACL_CATEGORY_CONNECTION,CLIENT_NO_TOUCH_Keyspecs,0,NULL,1),.args=CLIENT_NO_TOUCH_Args}, diff --git a/src/commands/client-kill.json b/src/commands/client-kill.json index bd0262d4e..7efd80508 100644 --- a/src/commands/client-kill.json +++ b/src/commands/client-kill.json @@ -27,6 +27,10 @@ [ "6.2.0", "`LADDR` option." + ], + [ + "8.0.0", + "`MAXAGE` option." ] ], "command_flags": [ @@ -136,6 +140,13 @@ "token": "NO" } ] + }, + { + "token": "MAXAGE", + "name": "maxage", + "type": "integer", + "optional": true, + "since": "8.0.0" } ] } diff --git a/src/networking.c b/src/networking.c index 9c969c137..69c183d6e 100644 --- a/src/networking.c +++ b/src/networking.c @@ -2850,7 +2850,7 @@ sds catClientInfoString(sds s, client *client) { " laddr=%s", getClientSockname(client), " %s", connGetInfo(client->conn, conninfo, sizeof(conninfo)), " name=%s", client->name ? (char*)client->name->ptr : "", - " age=%I", (long long)(server.unixtime - client->ctime), + " age=%I", (long long)(commandTimeSnapshot() / 1000 - client->ctime), " idle=%I", (long long)(server.unixtime - client->lastinteraction), " flags=%s", flags, " db=%i", client->db->id, @@ -3042,6 +3042,10 @@ void clientCommand(client *c) { " Kill connections authenticated by .", " * SKIPME (YES|NO)", " Skip killing current connection (default: yes).", +" * ID ", +" Kill connections by client id.", +" * MAXAGE ", +" Kill connections older than the specified age.", "LIST [options ...]", " Return information about client connections. Options:", " * TYPE (NORMAL|MASTER|REPLICA|PUBSUB)", @@ -3153,6 +3157,7 @@ NULL user *user = NULL; int type = -1; uint64_t id = 0; + long long max_age = 0; int skipme = 1; int killed = 0, close_this_client = 0; @@ -3174,6 +3179,18 @@ NULL "client-id should be greater than 0") != C_OK) return; id = tmp; + } else if (!strcasecmp(c->argv[i]->ptr,"maxage") && moreargs) { + long long tmp; + + if (getLongLongFromObjectOrReply(c, c->argv[i+1], &tmp, + "maxage is not an integer or out of range") != C_OK) + return; + if (tmp <= 0) { + addReplyError(c, "maxage should be greater than 0"); + return; + } + + max_age = tmp; } else if (!strcasecmp(c->argv[i]->ptr,"type") && moreargs) { type = getClientTypeByName(c->argv[i+1]->ptr); if (type == -1) { @@ -3223,6 +3240,7 @@ NULL if (id != 0 && client->id != id) continue; if (user && client->user != user) continue; if (c == client && skipme) continue; + if (max_age != 0 && (long long)(commandTimeSnapshot() / 1000 - client->ctime) < max_age) continue; /* Kill it. */ if (c == client) { diff --git a/tests/unit/introspection.tcl b/tests/unit/introspection.tcl index 3bb721275..0a5dac351 100644 --- a/tests/unit/introspection.tcl +++ b/tests/unit/introspection.tcl @@ -32,8 +32,35 @@ start_server {tags {"introspection"}} { assert_error "ERR No such user*" {r client kill user wrong_user} assert_error "ERR syntax error*" {r client kill skipme yes_or_no} + + assert_error "ERR *not an integer or out of range*" {r client kill maxage str} + assert_error "ERR *not an integer or out of range*" {r client kill maxage 9999999999999999999} + assert_error "ERR *greater than 0*" {r client kill maxage -1} } + test {CLIENT KILL maxAGE will kill old clients} { + set rd1 [redis_deferring_client] + r debug sleep 2 + set rd2 [redis_deferring_client] + + r acl setuser dummy on nopass +ping + $rd1 auth dummy "" + $rd1 read + $rd2 auth dummy "" + $rd2 read + + # Should kill rd1 but not rd2 + set res [r client kill user dummy maxage 1] + assert {$res == 1} + + # rd2 should still be connected + $rd2 ping + assert_equal "PONG" [$rd2 read] + + $rd1 close + $rd2 close + } {0} {"needs:debug"} + test {CLIENT KILL SKIPME YES/NO will kill all clients} { # Kill all clients except `me` set rd1 [redis_deferring_client]