diff --git a/redis.conf b/redis.conf index 62062b397..7635c14b4 100644 --- a/redis.conf +++ b/redis.conf @@ -1527,6 +1527,22 @@ aof-timestamp-enabled no # # shutdown-timeout 10 +# When Redis receives a SIGINT or SIGTERM, shutdown is initiated and by default +# an RDB snapshot is written to disk in a blocking operation if save points are configured. +# The options used on signaled shutdown can include the following values: +# default: Saves RDB snapshot only if save points are configured. +# Waits for lagging replicas to catch up. +# save: Forces a DB saving operation even if no save points are configured. +# nosave: Prevents DB saving operation even if one or more save points are configured. +# now: Skips waiting for lagging replicas. +# force: Ignores any errors that would normally prevent the server from exiting. +# +# Any combination of values is allowed as long as "save" and "nosave" are not set simultaneously. +# Example: "nosave force now" +# +# shutdown-on-sigint default +# shutdown-on-sigterm default + ################ NON-DETERMINISTIC LONG BLOCKING COMMANDS ##################### # Maximum time in milliseconds for EVAL scripts, functions and in some cases diff --git a/src/config.c b/src/config.c index 61841cb62..805aececb 100644 --- a/src/config.c +++ b/src/config.c @@ -94,6 +94,15 @@ configEnum aof_fsync_enum[] = { {NULL, 0} }; +configEnum shutdown_on_sig_enum[] = { + {"default", 0}, + {"save", SHUTDOWN_SAVE}, + {"nosave", SHUTDOWN_NOSAVE}, + {"now", SHUTDOWN_NOW}, + {"force", SHUTDOWN_FORCE}, + {NULL, 0} +}; + configEnum repl_diskless_load_enum[] = { {"disabled", REPL_DISKLESS_LOAD_DISABLED}, {"on-empty-db", REPL_DISKLESS_LOAD_WHEN_DB_EMPTY}, @@ -283,33 +292,50 @@ static standardConfig *lookupConfig(sds name) { *----------------------------------------------------------------------------*/ /* Get enum value from name. If there is no match INT_MIN is returned. */ -int configEnumGetValue(configEnum *ce, char *name) { - while(ce->name != NULL) { - if (!strcasecmp(ce->name,name)) return ce->val; - ce++; +int configEnumGetValue(configEnum *ce, sds *argv, int argc, int bitflags) { + if (argc == 0 || (!bitflags && argc != 1)) return INT_MIN; + int values = 0; + for (int i = 0; i < argc; i++) { + int matched = 0; + for (configEnum *ceItem = ce; ceItem->name != NULL; ceItem++) { + if (!strcasecmp(argv[i],ceItem->name)) { + values |= ceItem->val; + matched = 1; + } + } + if (!matched) return INT_MIN; } - return INT_MIN; + return values; } -/* Get enum name from value. If no match is found NULL is returned. */ -const char *configEnumGetName(configEnum *ce, int val) { - while(ce->name != NULL) { - if (ce->val == val) return ce->name; - ce++; +/* Get enum name/s from value. If no matches are found "unknown" is returned. */ +static sds configEnumGetName(configEnum *ce, int values, int bitflags) { + sds names = NULL; + int matches = 0; + for( ; ce->name != NULL; ce++) { + if (values == ce->val) { /* Short path for perfect match */ + sdsfree(names); + return sdsnew(ce->name); + } + if (bitflags && (values & ce->val)) { + names = names ? sdscatfmt(names, " %s", ce->name) : sdsnew(ce->name); + matches |= ce->val; + } } - return NULL; -} - -/* Wrapper for configEnumGetName() returning "unknown" instead of NULL if - * there is no match. */ -const char *configEnumGetNameOrUnknown(configEnum *ce, int val) { - const char *name = configEnumGetName(ce,val); - return name ? name : "unknown"; + if (!names || values != matches) { + sdsfree(names); + return sdsnew("unknown"); + } + return names; } /* Used for INFO generation. */ const char *evictPolicyToString(void) { - return configEnumGetNameOrUnknown(maxmemory_policy_enum,server.maxmemory_policy); + for (configEnum *ce = maxmemory_policy_enum; ce->name != NULL; ce++) { + if (server.maxmemory_policy == ce->val) + return ce->name; + } + serverPanic("unknown eviction policy"); } /*----------------------------------------------------------------------------- @@ -1304,12 +1330,13 @@ void rewriteConfigOctalOption(struct rewriteConfigState *state, const char *opti /* Rewrite an enumeration option. It takes as usually state and option name, * and in addition the enumeration array and the default value for the * option. */ -void rewriteConfigEnumOption(struct rewriteConfigState *state, const char *option, int value, configEnum *ce, int defval) { - sds line; - const char *name = configEnumGetNameOrUnknown(ce,value); - int force = value != defval; +void rewriteConfigEnumOption(struct rewriteConfigState *state, const char *option, int value, standardConfig *config) { + int multiarg = config->flags & MULTI_ARG_CONFIG; + sds names = configEnumGetName(config->data.enumd.enum_value,value,multiarg); + sds line = sdscatfmt(sdsempty(),"%s %s",option,names); + sdsfree(names); + int force = value != config->data.enumd.default_value; - line = sdscatprintf(sdsempty(),"%s %s",option,name); rewriteConfigRewriteLine(state,option,line,force); } @@ -1898,10 +1925,12 @@ static void enumConfigInit(standardConfig *config) { } static int enumConfigSet(standardConfig *config, sds *argv, int argc, const char **err) { - UNUSED(argc); - int enumval = configEnumGetValue(config->data.enumd.enum_value, argv[0]); + int enumval; + int bitflags = !!(config->flags & MULTI_ARG_CONFIG); + enumval = configEnumGetValue(config->data.enumd.enum_value, argv, argc, bitflags); + if (enumval == INT_MIN) { - sds enumerr = sdsnew("argument must be one of the following: "); + sds enumerr = sdsnew("argument(s) must be one of the following: "); configEnum *enumNode = config->data.enumd.enum_value; while(enumNode->name != NULL) { enumerr = sdscatlen(enumerr, enumNode->name, @@ -1932,12 +1961,13 @@ static int enumConfigSet(standardConfig *config, sds *argv, int argc, const char static sds enumConfigGet(standardConfig *config) { int val = config->flags & MODULE_CONFIG ? getModuleEnumConfig(config->privdata) : *(config->data.enumd.config); - return sdsnew(configEnumGetNameOrUnknown(config->data.enumd.enum_value,val)); + int bitflags = !!(config->flags & MULTI_ARG_CONFIG); + return configEnumGetName(config->data.enumd.enum_value,val,bitflags); } static void enumConfigRewrite(standardConfig *config, const char *name, struct rewriteConfigState *state) { int val = config->flags & MODULE_CONFIG ? getModuleEnumConfig(config->privdata) : *(config->data.enumd.config); - rewriteConfigEnumOption(state, name, val, config->data.enumd.enum_value, config->data.enumd.default_value); + rewriteConfigEnumOption(state, name, val, config); } #define createEnumConfig(name, alias, flags, enum, config_addr, default, is_valid, apply) { \ @@ -2297,6 +2327,16 @@ static int isValidAOFdirname(char *val, const char **err) { return 1; } +static int isValidShutdownOnSigFlags(int val, const char **err) { + /* Individual arguments are validated by createEnumConfig logic. + * We just need to ensure valid combinations here. */ + if (val & SHUTDOWN_NOSAVE && val & SHUTDOWN_SAVE) { + *err = "shutdown options SAVE and NOSAVE can't be used simultaneously"; + return 0; + } + return 1; +} + static int isValidAnnouncedHostname(char *val, const char **err) { if (strlen(val) >= NET_HOST_STR_LEN) { *err = "Hostnames must be less than " @@ -2943,6 +2983,8 @@ standardConfig static_configs[] = { createEnumConfig("enable-module-command", NULL, IMMUTABLE_CONFIG, protected_action_enum, server.enable_module_cmd, PROTECTED_ACTION_ALLOWED_NO, NULL, NULL), createEnumConfig("cluster-preferred-endpoint-type", NULL, MODIFIABLE_CONFIG, cluster_preferred_endpoint_type_enum, server.cluster_preferred_endpoint_type, CLUSTER_ENDPOINT_TYPE_IP, NULL, NULL), createEnumConfig("propagation-error-behavior", NULL, MODIFIABLE_CONFIG, propagation_error_behavior_enum, server.propagation_error_behavior, PROPAGATION_ERR_BEHAVIOR_IGNORE, NULL, NULL), + createEnumConfig("shutdown-on-sigint", NULL, MODIFIABLE_CONFIG | MULTI_ARG_CONFIG, shutdown_on_sig_enum, server.shutdown_on_sigint, 0, isValidShutdownOnSigFlags, NULL), + createEnumConfig("shutdown-on-sigterm", NULL, MODIFIABLE_CONFIG | MULTI_ARG_CONFIG, shutdown_on_sig_enum, server.shutdown_on_sigterm, 0, isValidShutdownOnSigFlags, NULL), /* Integer configs */ createIntConfig("databases", NULL, IMMUTABLE_CONFIG, 1, INT_MAX, server.dbnum, 16, INTEGER_CONFIG, NULL, NULL), diff --git a/src/server.c b/src/server.c index dbfe5aaea..629368cf4 100644 --- a/src/server.c +++ b/src/server.c @@ -1213,10 +1213,16 @@ int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) { cronUpdateMemoryStats(); - /* We received a SIGTERM, shutting down here in a safe way, as it is + /* We received a SIGTERM or SIGINT, shutting down here in a safe way, as it is * not ok doing so inside the signal handler. */ if (server.shutdown_asap && !isShutdownInitiated()) { - if (prepareForShutdown(SHUTDOWN_NOFLAGS) == C_OK) exit(0); + int shutdownFlags = SHUTDOWN_NOFLAGS; + if (server.last_sig_received == SIGINT && server.shutdown_on_sigint) + shutdownFlags = server.shutdown_on_sigint; + else if (server.last_sig_received == SIGTERM && server.shutdown_on_sigterm) + shutdownFlags = server.shutdown_on_sigterm; + + if (prepareForShutdown(shutdownFlags) == C_OK) exit(0); } else if (isShutdownInitiated()) { if (server.mstime >= server.shutdown_mstime || isReadyToShutdown()) { if (finishShutdown() == C_OK) exit(0); @@ -1469,6 +1475,7 @@ void whileBlockedCron() { if (prepareForShutdown(SHUTDOWN_NOSAVE) == C_OK) exit(0); serverLog(LL_WARNING,"SIGTERM received but errors trying to shut down the server, check the logs for more information"); server.shutdown_asap = 0; + server.last_sig_received = 0; } } @@ -2542,6 +2549,7 @@ void initServer(void) { server.aof_last_write_status = C_OK; server.aof_last_write_errno = 0; server.repl_good_slaves_count = 0; + server.last_sig_received = 0; /* Create the timer callback, this is our way to process many background * operations incrementally, like clients timeout, eviction of unaccessed @@ -4012,6 +4020,7 @@ static void cancelShutdown(void) { server.shutdown_asap = 0; server.shutdown_flags = 0; server.shutdown_mstime = 0; + server.last_sig_received = 0; replyToClientsBlockedOnShutdown(); unpauseClients(PAUSE_DURING_SHUTDOWN); } @@ -6302,6 +6311,7 @@ static void sigShutdownHandler(int sig) { serverLogFromHandler(LL_WARNING, msg); server.shutdown_asap = 1; + server.last_sig_received = sig; } void setupSignalHandlers(void) { diff --git a/src/server.h b/src/server.h index 7d284cb04..36128aac9 100644 --- a/src/server.h +++ b/src/server.h @@ -1462,6 +1462,7 @@ struct redisServer { redisAtomic unsigned int lruclock; /* Clock for LRU eviction */ volatile sig_atomic_t shutdown_asap; /* Shutdown ordered by signal handler. */ mstime_t shutdown_mstime; /* Timestamp to limit graceful shutdown. */ + int last_sig_received; /* Indicates the last SIGNAL received, if any (e.g., SIGINT or SIGTERM). */ int shutdown_flags; /* Flags passed to prepareForShutdown(). */ int activerehashing; /* Incremental rehash in serverCron() */ int active_defrag_running; /* Active defragmentation running (holds current scan aggressiveness) */ @@ -1729,6 +1730,8 @@ struct redisServer { * abort(). useful for Valgrind. */ /* Shutdown */ int shutdown_timeout; /* Graceful shutdown time limit in seconds. */ + int shutdown_on_sigint; /* Shutdown flags configured for SIGINT. */ + int shutdown_on_sigterm; /* Shutdown flags configured for SIGTERM. */ /* Replication (master) */ char replid[CONFIG_RUN_ID_SIZE+1]; /* My current replication ID. */ diff --git a/tests/modules/moduleconfigs.c b/tests/modules/moduleconfigs.c index a9e434a7b..af30bda34 100644 --- a/tests/modules/moduleconfigs.c +++ b/tests/modules/moduleconfigs.c @@ -139,4 +139,4 @@ int RedisModule_OnUnload(RedisModuleCtx *ctx) { strval = NULL; } return REDISMODULE_OK; -} \ No newline at end of file +} diff --git a/tests/unit/introspection.tcl b/tests/unit/introspection.tcl index 89d8f170b..4b4b8f56b 100644 --- a/tests/unit/introspection.tcl +++ b/tests/unit/introspection.tcl @@ -286,16 +286,22 @@ start_server {tags {"introspection"}} { } } {} {external:skip} - test {CONFIG REWRITE handles save properly} { + test {CONFIG REWRITE handles save and shutdown properly} { r config set save "3600 1 300 100 60 10000" + r config set shutdown-on-sigterm "nosave now" + r config set shutdown-on-sigint "save" r config rewrite restart_server 0 true false assert_equal [r config get save] {save {3600 1 300 100 60 10000}} + assert_equal [r config get shutdown-on-sigterm] {shutdown-on-sigterm {nosave now}} + assert_equal [r config get shutdown-on-sigint] {shutdown-on-sigint save} r config set save "" + r config set shutdown-on-sigterm "default" r config rewrite restart_server 0 true false assert_equal [r config get save] {save {}} + assert_equal [r config get shutdown-on-sigterm] {shutdown-on-sigterm default} start_server {config "minimal.conf"} { assert_equal [r config get save] {save {3600 1 300 100 60 10000}} diff --git a/tests/unit/moduleapi/moduleconfigs.tcl b/tests/unit/moduleapi/moduleconfigs.tcl index 01aa1e88e..29682d2e2 100644 --- a/tests/unit/moduleapi/moduleconfigs.tcl +++ b/tests/unit/moduleapi/moduleconfigs.tcl @@ -54,7 +54,7 @@ start_server {tags {"modules"}} { test {Enums only able to be set to passed in values} { # Module authors specify what values are valid for enums, check that only those values are ok on a set catch {[r config set moduleconfigs.enum four]} e - assert_match {*argument must be one of the following*} $e + assert_match {*must be one of the following*} $e } test {Unload removes module configs} {