diff options
-rw-r--r-- | src/commands/sentinel-config.json | 14 | ||||
-rw-r--r-- | src/sentinel.c | 262 | ||||
-rw-r--r-- | tests/sentinel/tests/15-config-set-config-get.tcl | 58 |
3 files changed, 239 insertions, 95 deletions
diff --git a/src/commands/sentinel-config.json b/src/commands/sentinel-config.json index ecc317603..6e3075f7c 100644 --- a/src/commands/sentinel-config.json +++ b/src/commands/sentinel-config.json @@ -1,12 +1,18 @@ { "CONFIG": { "summary": "Configures Redis Sentinel.", - "complexity": "O(1)", + "complexity": "O(N) when N is the number of configuration parameters provided", "group": "sentinel", "since": "6.2.0", "arity": -4, "container": "SENTINEL", "function": "sentinelCommand", + "history": [ + [ + "7.2.0", + "Added the ability to set and get multiple parameters in one call." + ] + ], "command_flags": [ "ADMIN", "SENTINEL", @@ -42,7 +48,7 @@ "type": "string" }, "announce-port": { - "type": "integer" + "type": "string" }, "sentinel-user": { "type": "string" @@ -87,6 +93,7 @@ "name":"set", "token":"SET", "type":"block", + "multiple": true, "arguments":[ { "name":"parameter", @@ -101,7 +108,8 @@ { "token":"GET", "name":"parameter", - "type":"string" + "type":"string", + "multiple": true } ] } diff --git a/src/sentinel.c b/src/sentinel.c index 326def135..6a1cd1777 100644 --- a/src/sentinel.c +++ b/src/sentinel.c @@ -3178,6 +3178,13 @@ void sentinelSendPeriodicCommands(sentinelRedisInstance *ri) { } /* =========================== SENTINEL command ============================= */ +static void populateDict(dict *options_dict, char **options) { + for (int i=0; options[i]; i++) { + sds option = sdsnew(options[i]); + if (dictAdd(options_dict, option, NULL)==DICT_ERR) + sdsfree(option); + } +} const char* getLogLevel(void) { switch (server.verbosity) { @@ -3189,52 +3196,112 @@ const char* getLogLevel(void) { return "unknown"; } -/* SENTINEL CONFIG SET <option> <value>*/ +/* SENTINEL CONFIG SET option value [option value ...] */ void sentinelConfigSetCommand(client *c) { - robj *o = c->argv[3]; - robj *val = c->argv[4]; long long numval; int drop_conns = 0; + char *option; + robj *val; + char *options[] = { + "announce-ip", + "sentinel-user", + "sentinel-pass", + "resolve-hostnames", + "announce-port", + "announce-hostnames", + "loglevel", + NULL}; + static dict *options_dict = NULL; + if (!options_dict) { + options_dict = dictCreate(&stringSetDictType); + populateDict(options_dict, options); + } + dict *set_configs = dictCreate(&stringSetDictType); + + /* Validate arguments are valid */ + for (int i = 3; i < c->argc; i++) { + option = c->argv[i]->ptr; + + /* Validate option is valid */ + if (dictFind(options_dict, option) == NULL) { + addReplyErrorFormat(c, "Invalid argument '%s' to SENTINEL CONFIG SET", option); + goto exit; + } - if (!strcasecmp(o->ptr, "resolve-hostnames")) { - if ((numval = yesnotoi(val->ptr)) == -1) goto badfmt; - sentinel.resolve_hostnames = numval; - } else if (!strcasecmp(o->ptr, "announce-hostnames")) { - if ((numval = yesnotoi(val->ptr)) == -1) goto badfmt; - sentinel.announce_hostnames = numval; - } else if (!strcasecmp(o->ptr, "announce-ip")) { - if (sentinel.announce_ip) sdsfree(sentinel.announce_ip); - sentinel.announce_ip = sdsnew(val->ptr); - } else if (!strcasecmp(o->ptr, "announce-port")) { - if (getLongLongFromObject(val, &numval) == C_ERR || - numval < 0 || numval > 65535) - goto badfmt; - sentinel.announce_port = numval; - } else if (!strcasecmp(o->ptr, "sentinel-user")) { - sdsfree(sentinel.sentinel_auth_user); - sentinel.sentinel_auth_user = sdslen(val->ptr) == 0 ? - NULL : sdsdup(val->ptr); - drop_conns = 1; - } else if (!strcasecmp(o->ptr, "sentinel-pass")) { - sdsfree(sentinel.sentinel_auth_pass); - sentinel.sentinel_auth_pass = sdslen(val->ptr) == 0 ? - NULL : sdsdup(val->ptr); - drop_conns = 1; - } else if (!strcasecmp(o->ptr, "loglevel")) { - if (!strcasecmp(val->ptr, "debug")) - server.verbosity = LL_DEBUG; - else if (!strcasecmp(val->ptr, "verbose")) - server.verbosity = LL_VERBOSE; - else if (!strcasecmp(val->ptr, "notice")) - server.verbosity = LL_NOTICE; - else if (!strcasecmp(val->ptr, "warning")) - server.verbosity = LL_WARNING; - else - goto badfmt; - } else { - addReplyErrorFormat(c, "Invalid argument '%s' to SENTINEL CONFIG SET", - (char *) o->ptr); - return; + /* Check duplicates */ + if (dictFind(set_configs, option) != NULL) { + addReplyErrorFormat(c, "Duplicate argument '%s' to SENTINEL CONFIG SET", option); + goto exit; + } + + serverAssert(dictAdd(set_configs, sdsnew(option), NULL) == C_OK); + + /* Validate argument */ + if (i + 1 == c->argc) { + addReplyErrorFormat(c, "Missing argument '%s' value", option); + goto exit; + } + val = c->argv[++i]; + + if (!strcasecmp(option, "resolve-hostnames")) { + if ((yesnotoi(val->ptr)) == -1) goto badfmt; + } else if (!strcasecmp(option, "announce-hostnames")) { + if ((yesnotoi(val->ptr)) == -1) goto badfmt; + } else if (!strcasecmp(option, "announce-port")) { + if (getLongLongFromObject(val, &numval) == C_ERR || + numval < 0 || numval > 65535) goto badfmt; + } else if (!strcasecmp(option, "loglevel")) { + if (!(!strcasecmp(val->ptr, "debug") || !strcasecmp(val->ptr, "verbose") || + !strcasecmp(val->ptr, "notice") || !strcasecmp(val->ptr, "warning"))) goto badfmt; + } + } + + /* Apply changes */ + for (int i = 3; i < c->argc; i++) { + int moreargs = (c->argc-1) - i; + option = c->argv[i]->ptr; + if (!strcasecmp(option, "loglevel") && moreargs > 0) { + val = c->argv[++i]; + if (!strcasecmp(val->ptr, "debug")) + server.verbosity = LL_DEBUG; + else if (!strcasecmp(val->ptr, "verbose")) + server.verbosity = LL_VERBOSE; + else if (!strcasecmp(val->ptr, "notice")) + server.verbosity = LL_NOTICE; + else if (!strcasecmp(val->ptr, "warning")) + server.verbosity = LL_WARNING; + } else if (!strcasecmp(option, "resolve-hostnames") && moreargs > 0) { + val = c->argv[++i]; + numval = yesnotoi(val->ptr); + sentinel.resolve_hostnames = numval; + } else if (!strcasecmp(option, "announce-hostnames") && moreargs > 0) { + val = c->argv[++i]; + numval = yesnotoi(val->ptr); + sentinel.announce_hostnames = numval; + } else if (!strcasecmp(option, "announce-ip") && moreargs > 0) { + val = c->argv[++i]; + if (sentinel.announce_ip) sdsfree(sentinel.announce_ip); + sentinel.announce_ip = sdsnew(val->ptr); + } else if (!strcasecmp(option, "announce-port") && moreargs > 0) { + val = c->argv[++i]; + getLongLongFromObject(val, &numval); + sentinel.announce_port = numval; + } else if (!strcasecmp(option, "sentinel-user") && moreargs > 0) { + val = c->argv[++i]; + sdsfree(sentinel.sentinel_auth_user); + sentinel.sentinel_auth_user = sdslen(val->ptr) == 0 ? + NULL : sdsdup(val->ptr); + drop_conns = 1; + } else if (!strcasecmp(option, "sentinel-pass") && moreargs > 0) { + val = c->argv[++i]; + sdsfree(sentinel.sentinel_auth_pass); + sentinel.sentinel_auth_pass = sdslen(val->ptr) == 0 ? + NULL : sdsdup(val->ptr); + drop_conns = 1; + } else { + /* Should never reach here */ + serverAssert(0); + } } sentinelFlushConfigAndReply(c); @@ -3243,61 +3310,72 @@ void sentinelConfigSetCommand(client *c) { if (drop_conns) sentinelDropConnections(); +exit: + dictRelease(set_configs); return; badfmt: addReplyErrorFormat(c, "Invalid value '%s' to SENTINEL CONFIG SET '%s'", - (char *) val->ptr, (char *) o->ptr); + (char *) val->ptr, option); + dictRelease(set_configs); } -/* SENTINEL CONFIG GET <option> */ +/* SENTINEL CONFIG GET <option> [<option> ...] */ void sentinelConfigGetCommand(client *c) { - robj *o = c->argv[3]; - const char *pattern = o->ptr; + char *pattern; void *replylen = addReplyDeferredLen(c); int matches = 0; - - if (stringmatch(pattern,"resolve-hostnames",1)) { - addReplyBulkCString(c,"resolve-hostnames"); - addReplyBulkCString(c,sentinel.resolve_hostnames ? "yes" : "no"); - matches++; - } - - if (stringmatch(pattern, "announce-hostnames", 1)) { - addReplyBulkCString(c,"announce-hostnames"); - addReplyBulkCString(c,sentinel.announce_hostnames ? "yes" : "no"); - matches++; - } - - if (stringmatch(pattern, "announce-ip", 1)) { - addReplyBulkCString(c,"announce-ip"); - addReplyBulkCString(c,sentinel.announce_ip ? sentinel.announce_ip : ""); - matches++; - } - - if (stringmatch(pattern, "announce-port", 1)) { - addReplyBulkCString(c, "announce-port"); - addReplyBulkLongLong(c, sentinel.announce_port); - matches++; - } - - if (stringmatch(pattern, "sentinel-user", 1)) { - addReplyBulkCString(c, "sentinel-user"); - addReplyBulkCString(c, sentinel.sentinel_auth_user ? sentinel.sentinel_auth_user : ""); - matches++; - } - - if (stringmatch(pattern, "sentinel-pass", 1)) { - addReplyBulkCString(c, "sentinel-pass"); - addReplyBulkCString(c, sentinel.sentinel_auth_pass ? sentinel.sentinel_auth_pass : ""); - matches++; - } - - if (stringmatch(pattern, "loglevel", 1)) { - addReplyBulkCString(c, "loglevel"); - addReplyBulkCString(c, getLogLevel()); - matches++; + /* Create a dictionary to store the input configs,to avoid adding duplicate twice */ + dict *d = dictCreate(&externalStringType); + for (int i = 3; i < c->argc; i++) { + pattern = c->argv[i]->ptr; + /* If the string doesn't contain glob patterns and available in dictionary, don't look further, just continue. */ + if (!strpbrk(pattern, "[*?") && dictFind(d, pattern)) continue; + /* we want to print all the matched patterns and avoid printing duplicates twice */ + if (stringmatch(pattern,"resolve-hostnames",1) && !dictFind(d, "resolve-hostnames")) { + addReplyBulkCString(c,"resolve-hostnames"); + addReplyBulkCString(c,sentinel.resolve_hostnames ? "yes" : "no"); + dictAdd(d, "resolve-hostnames", NULL); + matches++; + } + if (stringmatch(pattern, "announce-hostnames", 1) && !dictFind(d, "announce-hostnames")) { + addReplyBulkCString(c,"announce-hostnames"); + addReplyBulkCString(c,sentinel.announce_hostnames ? "yes" : "no"); + dictAdd(d, "announce-hostnames", NULL); + matches++; + } + if (stringmatch(pattern, "announce-ip", 1) && !dictFind(d, "announce-ip")) { + addReplyBulkCString(c,"announce-ip"); + addReplyBulkCString(c,sentinel.announce_ip ? sentinel.announce_ip : ""); + dictAdd(d, "announce-ip", NULL); + matches++; + } + if (stringmatch(pattern, "announce-port", 1) && !dictFind(d, "announce-port")) { + addReplyBulkCString(c, "announce-port"); + addReplyBulkLongLong(c, sentinel.announce_port); + dictAdd(d, "announce-port", NULL); + matches++; + } + if (stringmatch(pattern, "sentinel-user", 1) && !dictFind(d, "sentinel-user")) { + addReplyBulkCString(c, "sentinel-user"); + addReplyBulkCString(c, sentinel.sentinel_auth_user ? sentinel.sentinel_auth_user : ""); + dictAdd(d, "sentinel-user", NULL); + matches++; + } + if (stringmatch(pattern, "sentinel-pass", 1) && !dictFind(d, "sentinel-pass")) { + addReplyBulkCString(c, "sentinel-pass"); + addReplyBulkCString(c, sentinel.sentinel_auth_pass ? sentinel.sentinel_auth_pass : ""); + dictAdd(d, "sentinel-pass", NULL); + matches++; + } + if (stringmatch(pattern, "loglevel", 1) && !dictFind(d, "loglevel")) { + addReplyBulkCString(c, "loglevel"); + addReplyBulkCString(c, getLogLevel()); + dictAdd(d, "loglevel", NULL); + matches++; + } } + dictRelease(d); setDeferredMapLen(c, replylen, matches); } @@ -3787,9 +3865,9 @@ void sentinelCommand(client *c) { " Check if the current Sentinel configuration is able to reach the quorum", " needed to failover a master and the majority needed to authorize the", " failover.", -"CONFIG SET <param> <value>", +"CONFIG SET param value [param value ...]", " Set a global Sentinel configuration parameter.", -"CONFIG GET <param>", +"CONFIG GET <param> [param param param ...]", " Get global Sentinel configuration parameter.", "DEBUG [<param> <value> ...]", " Show a list of configurable time parameters and their values (milliseconds).", @@ -4042,12 +4120,12 @@ NULL sentinelSetCommand(c); } else if (!strcasecmp(c->argv[1]->ptr,"config")) { if (c->argc < 4) goto numargserr; - if (!strcasecmp(c->argv[2]->ptr,"set") && c->argc == 5) + if (!strcasecmp(c->argv[2]->ptr,"set") && c->argc >= 5) sentinelConfigSetCommand(c); - else if (!strcasecmp(c->argv[2]->ptr,"get") && c->argc == 4) + else if (!strcasecmp(c->argv[2]->ptr,"get") && c->argc >= 4) sentinelConfigGetCommand(c); else - addReplyError(c, "Only SENTINEL CONFIG GET <option> / SET <option> <value> are supported."); + addReplyError(c, "Only SENTINEL CONFIG GET <param> [<param> <param> ...]/ SET <param> <value> [<param> <value> ...] are supported."); } else if (!strcasecmp(c->argv[1]->ptr,"info-cache")) { /* SENTINEL INFO-CACHE <name> */ if (c->argc < 2) goto numargserr; diff --git a/tests/sentinel/tests/15-config-set-config-get.tcl b/tests/sentinel/tests/15-config-set-config-get.tcl new file mode 100644 index 000000000..f9831f8e8 --- /dev/null +++ b/tests/sentinel/tests/15-config-set-config-get.tcl @@ -0,0 +1,58 @@ +source "../tests/includes/init-tests.tcl" + +test "SENTINEL CONFIG SET and SENTINEL CONFIG GET handles multiple variables" { + foreach_sentinel_id id { + assert_equal {OK} [S $id SENTINEL CONFIG SET resolve-hostnames yes announce-port 1234] + } + assert_match {*yes*1234*} [S 1 SENTINEL CONFIG GET resolve-hostnames announce-port] + assert_match {announce-port 1234} [S 1 SENTINEL CONFIG GET announce-port] +} + +test "SENTINEL CONFIG GET for duplicate and unknown variables" { + assert_equal {OK} [S 1 SENTINEL CONFIG SET resolve-hostnames yes announce-port 1234] + assert_match {resolve-hostnames yes} [S 1 SENTINEL CONFIG GET resolve-hostnames resolve-hostnames does-not-exist] +} + +test "SENTINEL CONFIG GET for patterns" { + assert_equal {OK} [S 1 SENTINEL CONFIG SET loglevel notice announce-port 1234 announce-hostnames yes ] + assert_match {loglevel notice} [S 1 SENTINEL CONFIG GET log* *level loglevel] + assert_match {announce-hostnames yes announce-ip*announce-port 1234} [S 1 SENTINEL CONFIG GET announce*] +} + +test "SENTINEL CONFIG SET duplicate variables" { + catch {[S 1 SENTINEL CONFIG SET resolve-hostnames yes announce-port 1234 announce-port 100]} e + if {![string match "*Duplicate argument*" $e]} { + fail "Should give wrong arity error" + } +} + +test "SENTINEL CONFIG SET, one option does not exist" { + foreach_sentinel_id id { + assert_equal {OK} [S $id SENTINEL CONFIG SET announce-port 111] + catch {[S $id SENTINEL CONFIG SET does-not-exist yes announce-port 1234]} e + if {![string match "*Invalid argument*" $e]} { + fail "Should give Invalid argument error" + } + } + # The announce-port should not be set to 1234 as it was called with a wrong argument + assert_match {*111*} [S 1 SENTINEL CONFIG GET announce-port] +} + +test "SENTINEL CONFIG SET, one option with wrong value" { + foreach_sentinel_id id { + assert_equal {OK} [S $id SENTINEL CONFIG SET resolve-hostnames no] + catch {[S $id SENTINEL CONFIG SET announce-port -1234 resolve-hostnames yes]} e + if {![string match "*Invalid value*" $e]} { + fail "Expected to return Invalid value error" + } + } + # The resolve-hostnames should not be set to yes as it was called after an argument with an invalid value + assert_match {*no*} [S 1 SENTINEL CONFIG GET resolve-hostnames] +} + +test "SENTINEL CONFIG SET, wrong number of arguments" { + catch {[S 1 SENTINEL CONFIG SET resolve-hostnames yes announce-port 1234 announce-ip]} e + if {![string match "*Missing argument*" $e]} { + fail "Expected to return Missing argument error" + } +} |