From e518d9332be5f732d265ab115737636f1c261603 Mon Sep 17 00:00:00 2001 From: Sarthak Aggarwal Date: Fri, 20 Dec 2024 08:29:44 -0800 Subject: [PATCH] supporting more filters for Client List and Client Kill Signed-off-by: Sarthak Aggarwal --- src/commands.def | 16 +- src/commands/client-kill.json | 42 +++++ src/commands/client-list.json | 42 +++++ src/networking.c | 212 ++++++++++++++++++++++++- tests/unit/introspection.tcl | 284 +++++++++++++++++++++++++++++++++- 5 files changed, 585 insertions(+), 11 deletions(-) diff --git a/src/commands.def b/src/commands.def index a4c3146397..9089b994c3 100644 --- a/src/commands.def +++ b/src/commands.def @@ -1327,12 +1327,18 @@ struct COMMAND_ARG CLIENT_KILL_filter_new_format_Subargs[] = { {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)}, +{MAKE_ARG("name",ARG_TYPE_STRING,-1,"NAME",NULL,"8.1.0",CMD_ARG_OPTIONAL,0,NULL)}, +{MAKE_ARG("minidle",ARG_TYPE_INTEGER,-1,"MINIDLE",NULL,"8.1.0",CMD_ARG_OPTIONAL,0,NULL)}, +{MAKE_ARG("flags",ARG_TYPE_STRING,-1,"FLAGS",NULL,"8.1.0",CMD_ARG_OPTIONAL,0,NULL)}, +{MAKE_ARG("pattern",ARG_TYPE_STRING,-1,"PATTERN",NULL,"8.1.0",CMD_ARG_OPTIONAL,0,NULL)}, +{MAKE_ARG("channel",ARG_TYPE_STRING,-1,"CHANNEL",NULL,"8.1.0",CMD_ARG_OPTIONAL,0,NULL)}, +{MAKE_ARG("shardchannel",ARG_TYPE_STRING,-1,"SHARDCHANNEL",NULL,"8.1.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,7,NULL),.subargs=CLIENT_KILL_filter_new_format_Subargs}, +{MAKE_ARG("new-format",ARG_TYPE_ONEOF,-1,NULL,NULL,NULL,CMD_ARG_MULTIPLE,13,NULL),.subargs=CLIENT_KILL_filter_new_format_Subargs}, }; /* CLIENT KILL argument table */ @@ -1390,6 +1396,12 @@ struct COMMAND_ARG CLIENT_LIST_Args[] = { {MAKE_ARG("laddr",ARG_TYPE_STRING,-1,"LADDR",NULL,"8.1.0",CMD_ARG_OPTIONAL,0,NULL),.display_text="ip:port"}, {MAKE_ARG("skipme",ARG_TYPE_ONEOF,-1,"SKIPME",NULL,"8.1.0",CMD_ARG_OPTIONAL,2,NULL),.subargs=CLIENT_LIST_skipme_Subargs}, {MAKE_ARG("maxage",ARG_TYPE_INTEGER,-1,"MAXAGE",NULL,"8.1.0",CMD_ARG_OPTIONAL,0,NULL)}, +{MAKE_ARG("name",ARG_TYPE_STRING,-1,"NAME",NULL,"8.1.0",CMD_ARG_OPTIONAL,0,NULL)}, +{MAKE_ARG("minidle",ARG_TYPE_INTEGER,-1,"MINIDLE",NULL,"8.1.0",CMD_ARG_OPTIONAL,0,NULL)}, +{MAKE_ARG("flags",ARG_TYPE_STRING,-1,"FLAGS",NULL,"8.1.0",CMD_ARG_OPTIONAL,0,NULL)}, +{MAKE_ARG("pattern",ARG_TYPE_STRING,-1,"PATTERN",NULL,"8.1.0",CMD_ARG_OPTIONAL,0,NULL)}, +{MAKE_ARG("channel",ARG_TYPE_STRING,-1,"CHANNEL",NULL,"8.1.0",CMD_ARG_OPTIONAL,0,NULL)}, +{MAKE_ARG("shardchannel",ARG_TYPE_STRING,-1,"SHARDCHANNEL",NULL,"8.1.0",CMD_ARG_OPTIONAL,0,NULL)}, }; /********** CLIENT NO_EVICT ********************/ @@ -1672,7 +1684,7 @@ struct COMMAND_STRUCT CLIENT_Subcommands[] = { {MAKE_CMD("import-source","Mark this client as an import source when server is in import mode.","O(1)","8.1.0",CMD_DOC_NONE,NULL,NULL,"connection",COMMAND_GROUP_CONNECTION,CLIENT_IMPORT_SOURCE_History,0,CLIENT_IMPORT_SOURCE_Tips,0,clientCommand,3,CMD_NOSCRIPT|CMD_LOADING|CMD_STALE,ACL_CATEGORY_CONNECTION,CLIENT_IMPORT_SOURCE_Keyspecs,0,NULL,1),.args=CLIENT_IMPORT_SOURCE_Args}, {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,7,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,7,CLIENT_LIST_Tips,1,clientCommand,-2,CMD_ADMIN|CMD_NOSCRIPT|CMD_LOADING|CMD_STALE|CMD_SENTINEL,ACL_CATEGORY_CONNECTION,CLIENT_LIST_Keyspecs,0,NULL,7),.args=CLIENT_LIST_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,7,CLIENT_LIST_Tips,1,clientCommand,-2,CMD_ADMIN|CMD_NOSCRIPT|CMD_LOADING|CMD_STALE|CMD_SENTINEL,ACL_CATEGORY_CONNECTION,CLIENT_LIST_Keyspecs,0,NULL,13),.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}, {MAKE_CMD("pause","Suspends commands processing.","O(1)","3.0.0",CMD_DOC_NONE,NULL,NULL,"connection",COMMAND_GROUP_CONNECTION,CLIENT_PAUSE_History,1,CLIENT_PAUSE_Tips,0,clientCommand,-3,CMD_ADMIN|CMD_NOSCRIPT|CMD_LOADING|CMD_STALE|CMD_SENTINEL,ACL_CATEGORY_CONNECTION,CLIENT_PAUSE_Keyspecs,0,NULL,2),.args=CLIENT_PAUSE_Args}, diff --git a/src/commands/client-kill.json b/src/commands/client-kill.json index 97fa932cd8..7e45439bf7 100644 --- a/src/commands/client-kill.json +++ b/src/commands/client-kill.json @@ -157,6 +157,48 @@ "type": "integer", "optional": true, "since": "8.0.0" + }, + { + "token": "NAME", + "name": "name", + "type": "string", + "optional": true, + "since": "8.1.0" + }, + { + "token": "MINIDLE", + "name": "minidle", + "type": "integer", + "optional": true, + "since": "8.1.0" + }, + { + "token": "FLAGS", + "name": "flags", + "type": "string", + "optional": true, + "since": "8.1.0" + }, + { + "token": "PATTERN", + "name": "pattern", + "type": "string", + "optional": true, + "since": "8.1.0" + }, + { + "token": "CHANNEL", + "name": "channel", + "type": "string", + "optional": true, + "since": "8.1.0" + }, + { + "token": "SHARDCHANNEL", + "name": "shardchannel", + "type": "string", + "optional": true, + "since": "8.1.0" } ] } diff --git a/src/commands/client-list.json b/src/commands/client-list.json index 0962bd592d..8b8d007cb1 100644 --- a/src/commands/client-list.json +++ b/src/commands/client-list.json @@ -140,6 +140,48 @@ "type": "integer", "optional": true, "since": "8.1.0" + }, + { + "token": "NAME", + "name": "name", + "type": "string", + "optional": true, + "since": "8.1.0" + }, + { + "token": "MINIDLE", + "name": "minidle", + "type": "integer", + "optional": true, + "since": "8.1.0" + }, + { + "token": "FLAGS", + "name": "flags", + "type": "string", + "optional": true, + "since": "8.1.0" + }, + { + "token": "PATTERN", + "name": "pattern", + "type": "string", + "optional": true, + "since": "8.1.0" + }, + { + "token": "CHANNEL", + "name": "channel", + "type": "string", + "optional": true, + "since": "8.1.0" + }, + { + "token": "SHARDCHANNEL", + "name": "shardchannel", + "type": "string", + "optional": true, + "since": "8.1.0" } ] } diff --git a/src/networking.c b/src/networking.c index 60f214e63a..9cfda21edd 100644 --- a/src/networking.c +++ b/src/networking.c @@ -63,6 +63,20 @@ typedef struct { int type; /* Boolean flag to determine if the current client (`me`) should be filtered. 1 means "skip me", 0 means otherwise. */ int skipme; + /* Client name to filter. If NULL, no name filtering is applied. */ + char *name; + /* Minimum idle time (in seconds) of a client connection for filtering. + * Connections with idle time more than this value will match. + * A value of 0 means no idle time filtering. */ + long long min_idle; + /* Client flags for filtering. If NULL, no filtering is applied. */ + char *flags; + /* Client pattern for filtering. If NULL, no filtering is applied. */ + robj *pattern; + /* Client channel for filtering. If NULL, no filtering is applied. */ + robj *channel; + /* Client shard channel for filtering. If NULL, no filtering is applied. */ + robj *shard_channel; } clientFilter; static void clientCommandHelp(client *c); @@ -91,6 +105,11 @@ char *getClientSockname(client *c); static int parseClientFiltersOrReply(client *c, int index, clientFilter *filter); static int clientMatchesFilter(client *client, clientFilter client_filter); static sds getAllFilteredClientsInfoString(clientFilter *client_filter, int hide_user_data); +static int clientMatchesFlagFilter(client *c, const char *flag_filter); +static int clientSubscribedToChannel(client *client, robj *channel); +static int clientSubscribedToShardChannel(client *client, robj *channel); +static int clientSubscribedToPattern(client *client, robj *pattern); +static void freeClientFilter(clientFilter *filter); int ProcessingEventsWhileBlocked = 0; /* See processEventsWhileBlocked(). */ __thread sds thread_shared_qb = NULL; @@ -3689,6 +3708,34 @@ static int parseClientFiltersOrReply(client *c, int index, clientFilter *filter) return C_ERR; } index += 2; + } else if (!strcasecmp(c->argv[index]->ptr, "minidle") && moreargs) { + long long tmp; + + if (getLongLongFromObjectOrReply(c, c->argv[index + 1], &tmp, + "minidle is not an integer or out of range") != C_OK) + return C_ERR; + if (tmp <= 0) { + addReplyError(c, "minidle should be greater than 0"); + return C_ERR; + } + + filter->min_idle = tmp; + index += 2; + } else if (!strcasecmp(c->argv[index]->ptr, "flags") && moreargs) { + filter->flags = c->argv[index + 1]->ptr; + index += 2; + } else if (!strcasecmp(c->argv[index]->ptr, "name") && moreargs) { + filter->name = c->argv[index + 1]->ptr; + index += 2; + } else if (!strcasecmp(c->argv[index]->ptr, "pattern") && moreargs) { + filter->pattern = createObject(OBJ_STRING, sdsnew(c->argv[index + 1]->ptr)); + index += 2; + } else if (!strcasecmp(c->argv[index]->ptr, "channel") && moreargs) { + filter->channel = createObject(OBJ_STRING, sdsnew(c->argv[index + 1]->ptr)); + index += 2; + } else if (!strcasecmp(c->argv[index]->ptr, "shardchannel") && moreargs) { + filter->shard_channel = createObject(OBJ_STRING, sdsnew(c->argv[index + 1]->ptr)); + index += 2; } else { addReplyErrorObject(c, shared.syntaxerr); return C_ERR; @@ -3706,11 +3753,126 @@ static int clientMatchesFilter(client *client, clientFilter client_filter) { if (client_filter.user && client->user != client_filter.user) return 0; if (client_filter.skipme && client == server.current_client) return 0; if (client_filter.max_age != 0 && (long long)(commandTimeSnapshot() / 1000 - client->ctime) < client_filter.max_age) return 0; + if (client_filter.min_idle != 0 && (long long)(commandTimeSnapshot() / 1000 - client->last_interaction) < client_filter.min_idle) return 0; + if (client_filter.flags && clientMatchesFlagFilter(client, client_filter.flags) == 0) return 0; + if (client_filter.name) { + if (!client->name || !client->name->ptr || strcmp(client->name->ptr, client_filter.name) != 0) { + return 0; + } + } + if (client_filter.pattern && !clientSubscribedToPattern(client, client_filter.pattern)) return 0; + if (client_filter.channel && !clientSubscribedToChannel(client, client_filter.channel)) return 0; + if (client_filter.shard_channel && !clientSubscribedToShardChannel(client, client_filter.shard_channel)) return 0; /* If all conditions are satisfied, the client matches the filter. */ return 1; } +/* Function to check if the client has the required flags as per the filter string */ +static int clientMatchesFlagFilter(client *c, const char *flag_filter) { + /* Iterate through the provided flag filter string */ + for (int i = 0; flag_filter[i] != '\0'; i++) { + const char flag = flag_filter[i]; + + /* Check each flag */ + switch (flag) { + case 'O': + if (!(c->flag.replica && c->flag.monitor)) return 0; + break; + case 'S': /* Replica flag */ + if (!c->flag.replica) return 0; + break; + case 'M': /* Primary flag */ + if (!c->flag.primary) return 0; + break; + case 'P': /* PubSub flag */ + if (!c->flag.pubsub) return 0; + break; + case 'x': /* Multi flag */ + if (!c->flag.multi) return 0; + break; + case 'b': /* Blocked flag */ + if (!c->flag.blocked) return 0; + break; + case 't': /* Tracking flag */ + if (!c->flag.tracking) return 0; + break; + case 'R': /* Invalid Client flag */ + if (!c->flag.tracking_broken_redir) return 0; + break; + case 'B': /* Tracking Bcast flag */ + if (!c->flag.tracking_bcast) return 0; + break; + case 'd': /* Dirty CAS flag */ + if (!c->flag.dirty_cas) return 0; + break; + case 'c': /* Close after reply flag */ + if (!c->flag.close_after_reply) return 0; + break; + case 'u': /* Unblocked flag */ + if (!c->flag.unblocked) return 0; + break; + case 'A': /* Close ASAP flag */ + if (!c->flag.close_asap) return 0; + break; + case 'U': /* Unix socket flag */ + if (!c->flag.unix_socket) return 0; + break; + case 'r': /* Readonly flag */ + if (!c->flag.readonly) return 0; + break; + case 'e': /* No evict flag */ + if (!c->flag.no_evict) return 0; + break; + case 'T': /* No touch flag */ + if (!c->flag.no_touch) return 0; + break; + case 'I': /* Import source flag */ + if (!c->flag.import_source) return 0; + break; + case 'N': /* Check for no flags */ + if (!c->flag.replica && !c->flag.primary && !c->flag.pubsub && + !c->flag.multi && !c->flag.blocked && !c->flag.tracking && + !c->flag.tracking_broken_redir && !c->flag.tracking_bcast && + !c->flag.dirty_cas && !c->flag.close_after_reply && + !c->flag.unblocked && !c->flag.close_asap && + !c->flag.unix_socket && !c->flag.readonly && + !c->flag.no_evict && !c->flag.no_touch && + !c->flag.import_source) { + return 1; /* Matches 'N' */ + } + break; + default: + /* Invalid flag, return false */ + return 0; + } + } + /* If the loop completes, the client matches the flag filter */ + return 1; +} + +static int clientSubscribedToChannel(client *client, robj *channel) { + if (client == NULL || client->pubsub_channels == NULL) { + return 0; + } + return dictFind(client->pubsub_channels, channel) != NULL; +} + +static int clientSubscribedToShardChannel(client *client, robj *channel) { + if (client == NULL || client->pubsubshard_channels == NULL) { + return 0; + } + return dictFind(client->pubsubshard_channels, channel) != NULL; +} + +static int clientSubscribedToPattern(client *client, robj *pattern) { + if (client == NULL || client->pubsub_patterns == NULL) { + return 0; + } + return dictFind(client->pubsub_patterns, pattern) != NULL; +} + + static void clientCommandHelp(client *c) { const char *help[] = { "CACHING (YES|NO)", @@ -3732,9 +3894,9 @@ static void clientCommandHelp(client *c) { "KILL