diff --git a/sentinel.conf b/sentinel.conf index c7ce1cba7..793c43735 100644 --- a/sentinel.conf +++ b/sentinel.conf @@ -72,6 +72,32 @@ logfile "" # unmounting filesystems. dir /tmp +# sentinel state-config-file +# +# By default Sentinel keeps both the static, administrator-managed configuration +# and its own runtime state (this Sentinel's id, the current epoch, per-master +# config/leader epochs, and the auto-discovered replicas and other sentinels) +# in this single configuration file, rewriting it whenever the state changes. +# +# When this file is managed by an external configuration management tool (such +# as Chef, Ansible or Puppet), those rewrites look like configuration drift and +# may trigger needless restarts. +# +# Setting "state-config-file" tells Sentinel to persist its runtime state into a +# separate file, and to never rewrite this (main) configuration file. The main +# file is then read-only from Sentinel's point of view and safe to manage +# externally, while the state file is owned exclusively by Sentinel. +# +# On startup the main config file is read first and the state file (if it +# exists) is read afterwards, so the runtime state overrides the corresponding +# static values. Runtime changes made via "SENTINEL SET" are therefore +# persisted to, and restored from, the state file. The state file is created +# automatically on the first state change if it does not exist yet. +# +# Example: +# +# sentinel state-config-file /var/lib/redis/sentinel-state.conf + # sentinel monitor # # Tells Sentinel to monitor this master, and to consider it in O_DOWN diff --git a/src/config.c b/src/config.c index e466d7ddd..c9ea7cd1e 100644 --- a/src/config.c +++ b/src/config.c @@ -1811,6 +1811,40 @@ int rewriteConfig(char *path, int force_write) { return retval; } +/* Rewrite only the Sentinel runtime state into a dedicated state file (the one + * configured via "sentinel state-config-file"), leaving the main configuration + * file - which may be owned by an external configuration management tool - + * untouched. + * + * Unlike rewriteConfig(), this does not emit the whole server configuration: + * only the "sentinel ..." directives are written, so the state file stays a + * self-contained snapshot of Sentinel's runtime state. + * + * On error -1 is returned and errno is set accordingly, otherwise 0. */ +int rewriteSentinelStateConfig(char *path) { + struct rewriteConfigState *state; + sds newcontent; + int retval; + + /* Step 1: read the old state file (if any) into our rewrite state, so we + * retain comments and unrelated lines an operator may have added. */ + if ((state = rewriteConfigReadOldFile(path)) == NULL) return -1; + + /* Step 2: rewrite only the Sentinel directives. */ + rewriteConfigSentinelOption(state); + + /* Step 3: remove orphaned Sentinel lines (state that no longer applies). */ + rewriteConfigRemoveOrphaned(state); + + /* Step 4: generate the new file content and write it out atomically. */ + newcontent = rewriteConfigGetContentFromState(state); + retval = rewriteConfigOverwriteFile(path, newcontent); + + sdsfree(newcontent); + rewriteConfigReleaseState(state); + return retval; +} + /*----------------------------------------------------------------------------- * Configs that fit one of the major types and require no special handling *----------------------------------------------------------------------------*/ diff --git a/src/sentinel.c b/src/sentinel.c index 372e4b640..f3967285f 100644 --- a/src/sentinel.c +++ b/src/sentinel.c @@ -255,8 +255,15 @@ struct sentinelState { char *sentinel_auth_user; /* Username for ACLs AUTH against other sentinel. */ int resolve_hostnames; /* Support use of hostnames, assuming DNS is well configured. */ int announce_hostnames; /* Announce hostnames instead of IPs when we have them. */ + char *state_config_file; /* Separate file used to persist Sentinel runtime state. When + NULL (the default) state is kept in the main config file. */ } sentinel; +/* Non-zero while parsing the separate Sentinel state config file, so that + * configuration entries loaded from it can be distinguished from the ones in + * the main (administrator managed) config file. See sentinelLoadStateConfigFile(). */ +static int sentinelLoadingStateFile = 0; + /* A script execution job. */ typedef struct sentinelScriptJob { int flags; /* Script job flags: SENTINEL_SCRIPT_* */ @@ -455,7 +462,8 @@ const char *preMonitorCfgName[] = { "current-epoch", "myid", "resolve-hostnames", - "announce-hostnames" + "announce-hostnames", + "state-config-file" }; /* Returns 1 if the string contains control characters (0x00-0x1F or 0x7F), @@ -505,6 +513,7 @@ void initSentinel(void) { sentinel.sentinel_auth_user = NULL; sentinel.resolve_hostnames = SENTINEL_DEFAULT_RESOLVE_HOSTNAMES; sentinel.announce_hostnames = SENTINEL_DEFAULT_ANNOUNCE_HOSTNAMES; + sentinel.state_config_file = NULL; memset(sentinel.myid,0,sizeof(sentinel.myid)); server.sentinel_config = NULL; } @@ -516,12 +525,51 @@ void sentinelCheckConfigFile(void) { serverLog(LL_WARNING, "Sentinel needs config file on disk to save state. Exiting..."); exit(1); - } else if (access(server.configfile,W_OK) == -1) { + } + + /* A state file pointing at the main config file would defeat the purpose of + * the feature: Sentinel would only ever rewrite the "sentinel ..." lines of + * that shared file and silently stop persisting other CONFIG SET changes. + * Refuse this misconfiguration up front. */ + if (sentinel.state_config_file != NULL && + !strcmp(sentinel.state_config_file,server.configfile)) + { serverLog(LL_WARNING, - "Sentinel config file %s is not writable: %s. Exiting...", - server.configfile,strerror(errno)); + "Sentinel state-config-file must differ from the main config file %s. Exiting...", + server.configfile); exit(1); } + + /* Sentinel persists its state either into the main config file or, when + * configured, into a separate state file. Make sure the file we are going + * to write is (or can be) writable. */ + char *statefile = sentinel.state_config_file ? sentinel.state_config_file : server.configfile; + if (access(statefile,F_OK) != -1) { + /* The file already exists: it must be writable. */ + if (access(statefile,W_OK) == -1) { + serverLog(LL_WARNING, + "Sentinel config file %s is not writable: %s. Exiting...", + statefile,strerror(errno)); + exit(1); + } + } else { + /* The state file does not exist yet (only possible for a separate + * state file): make sure we can create it in its directory. */ + char dirbuf[PATH_MAX]; + redis_strlcpy(dirbuf,statefile,sizeof(dirbuf)); + char *slash = strrchr(dirbuf,'/'); + char *dir = "."; + if (slash != NULL) { + *slash = '\0'; + dir = (dirbuf[0] == '\0') ? "/" : dirbuf; + } + if (access(dir,W_OK) == -1) { + serverLog(LL_WARNING, + "Sentinel state config file %s cannot be created: directory %s is not writable: %s. Exiting...", + statefile,dir,strerror(errno)); + exit(1); + } + } } /* This function gets called when the server is in Sentinel mode, started, @@ -1789,11 +1837,20 @@ void queueSentinelConfig(sds *argv, int argc, int linenum, sds line) { /* initialize sentinel_config for the first call */ if (server.sentinel_config == NULL) initializeSentinelConfig(); + /* The state config file path must be known before we load the state file + * itself, so capture it as soon as the directive is seen while parsing the + * main config file, rather than waiting for loadSentinelConfigFromQueue(). */ + if (!strcasecmp(argv[0],"state-config-file") && argc >= 2) { + sdsfree(sentinel.state_config_file); + sentinel.state_config_file = sdsdup(argv[1]); + } + entry = zmalloc(sizeof(struct sentinelLoadQueueEntry)); entry->argv = zmalloc(sizeof(char*)*argc); entry->argc = argc; entry->linenum = linenum; entry->line = sdsdup(line); + entry->from_state_file = sentinelLoadingStateFile; for (i = 0; i < argc; i++) { entry->argv[i] = sdsdup(argv[i]); } @@ -1834,7 +1891,12 @@ void loadSentinelConfigFromQueue(void) { listRewind(sentinel_configs[j],&li); while((ln = listNext(&li))) { struct sentinelLoadQueueEntry *entry = ln->value; + /* Let sentinelHandleConfiguration() know whether this entry comes + * from the state config file, so that state loaded from it can + * override the values declared in the main config file. */ + sentinelLoadingStateFile = entry->from_state_file; err = sentinelHandleConfiguration(entry->argv,entry->argc); + sentinelLoadingStateFile = 0; if (err) { linenum = entry->linenum; line = entry->line; @@ -1856,6 +1918,24 @@ loaderr: exit(1); } +/* When "sentinel state-config-file" is configured, load the Sentinel runtime + * state from that separate file. This is called after the main config file has + * been parsed (so the path is already known) but before the queued config is + * applied, so that the state file entries are queued after, and therefore + * override, the ones from the main config file. + * + * If the state file does not exist yet (e.g. the very first startup, or right + * after enabling this feature) we simply skip it: it will be created by the + * first sentinelFlushConfig(). */ +void sentinelLoadStateConfigFile(void) { + if (sentinel.state_config_file == NULL) return; + if (access(sentinel.state_config_file,F_OK) == -1) return; + + sentinelLoadingStateFile = 1; + loadServerConfig(sentinel.state_config_file,0,NULL); + sentinelLoadingStateFile = 0; +} + const char *sentinelHandleConfiguration(char **argv, int argc) { sentinelRedisInstance *ri; @@ -1865,11 +1945,28 @@ const char *sentinelHandleConfiguration(char **argv, int argc) { int quorum = atoi(argv[4]); if (quorum <= 0) return "Quorum must be 1 or greater."; - if (createSentinelRedisInstance(argv[1],SRI_MASTER,argv[2], + ri = sentinelGetMasterByName(argv[1]); + if (ri != NULL && sentinelLoadingStateFile) { + /* The master was already declared in the main config file. The + * state config file carries the current (possibly post-failover) + * address and quorum, so update the existing instance in place + * instead of failing with a duplicate master error. */ + sentinelAddr *newaddr = createSentinelAddr(argv[2],atoi(argv[3]),1); + if (newaddr == NULL) return "Wrong hostname or port for the sentinel monitor directive."; + releaseSentinelAddr(ri->addr); + ri->addr = newaddr; + ri->quorum = quorum; + } else if (createSentinelRedisInstance(argv[1],SRI_MASTER,argv[2], atoi(argv[3]),quorum,NULL) == NULL) { return sentinelCheckCreateInstanceErrors(SRI_MASTER); } + } else if (!strcasecmp(argv[0],"state-config-file") && argc == 2) { + /* state-config-file + * + * The path is captured earlier, in queueSentinelConfig(), because it + * has to be known before the state file itself is loaded. Nothing left + * to do here except accept the directive. */ } else if (!strcasecmp(argv[0],"down-after-milliseconds") && argc == 3) { /* down-after-milliseconds */ ri = sentinelGetMasterByName(argv[1]); @@ -1896,6 +1993,7 @@ const char *sentinelHandleConfiguration(char **argv, int argc) { if (!ri) return "No such master with specified name."; if (access(argv[2],X_OK) == -1) return "Notification script seems non existing or non executable."; + sdsfree(ri->notification_script); ri->notification_script = sdsnew(argv[2]); } else if (!strcasecmp(argv[0],"client-reconfig-script") && argc == 3) { /* client-reconfig-script */ @@ -1904,16 +2002,19 @@ const char *sentinelHandleConfiguration(char **argv, int argc) { if (access(argv[2],X_OK) == -1) return "Client reconfiguration script seems non existing or " "non executable."; + sdsfree(ri->client_reconfig_script); ri->client_reconfig_script = sdsnew(argv[2]); } else if (!strcasecmp(argv[0],"auth-pass") && argc == 3) { /* auth-pass */ ri = sentinelGetMasterByName(argv[1]); if (!ri) return "No such master with specified name."; + sdsfree(ri->auth_pass); ri->auth_pass = sdsnew(argv[2]); } else if (!strcasecmp(argv[0],"auth-user") && argc == 3) { /* auth-user */ ri = sentinelGetMasterByName(argv[1]); if (!ri) return "No such master with specified name."; + sdsfree(ri->auth_user); ri->auth_user = sdsnew(argv[2]); } else if (!strcasecmp(argv[0],"current-epoch") && argc == 2) { /* current-epoch */ @@ -1950,7 +2051,15 @@ const char *sentinelHandleConfiguration(char **argv, int argc) { if ((slave = createSentinelRedisInstance(NULL,SRI_SLAVE,argv[2], atoi(argv[3]), ri->quorum, ri)) == NULL) { - return sentinelCheckCreateInstanceErrors(SRI_SLAVE); + /* When loading the separate state file the same replica may already + * have been declared in the main config file (e.g. after migrating + * a combined config). A duplicate is expected in that case, so we + * just keep the existing instance instead of failing. */ + if (sentinelLoadingStateFile && errno == EBUSY) { + /* Already known: nothing to do. */ + } else { + return sentinelCheckCreateInstanceErrors(SRI_SLAVE); + } } } else if (!strcasecmp(argv[0],"known-sentinel") && (argc == 4 || argc == 5)) { @@ -1963,10 +2072,17 @@ const char *sentinelHandleConfiguration(char **argv, int argc) { if ((si = createSentinelRedisInstance(argv[4],SRI_SENTINEL,argv[2], atoi(argv[3]), ri->quorum, ri)) == NULL) { - return sentinelCheckCreateInstanceErrors(SRI_SENTINEL); + /* See the known-replica case above: a duplicate coming from the + * state file on top of the main config file is expected. */ + if (sentinelLoadingStateFile && errno == EBUSY) { + /* Already known: nothing to do. */ + } else { + return sentinelCheckCreateInstanceErrors(SRI_SENTINEL); + } + } else { + si->runid = sdsnew(argv[4]); + sentinelTryConnectionSharing(si); } - si->runid = sdsnew(argv[4]); - sentinelTryConnectionSharing(si); } } else if (!strcasecmp(argv[0],"rename-command") && argc == 4) { /* rename-command */ @@ -1974,6 +2090,9 @@ const char *sentinelHandleConfiguration(char **argv, int argc) { if (!ri) return "No such master with specified name."; sds oldcmd = sdsnew(argv[2]); sds newcmd = sdsnew(argv[3]); + /* When loading the state file, let it override an identically named + * mapping from the main config file rather than failing. */ + if (sentinelLoadingStateFile) dictDelete(ri->renamed_commands,oldcmd); if (dictAdd(ri->renamed_commands,oldcmd,newcmd) != DICT_OK) { sdsfree(oldcmd); sdsfree(newcmd); @@ -1981,8 +2100,10 @@ const char *sentinelHandleConfiguration(char **argv, int argc) { } } else if (!strcasecmp(argv[0],"announce-ip") && argc == 2) { /* announce-ip */ - if (strlen(argv[1])) + if (strlen(argv[1])) { + sdsfree(sentinel.announce_ip); sentinel.announce_ip = sdsnew(argv[1]); + } } else if (!strcasecmp(argv[0],"announce-port") && argc == 2) { /* announce-port */ sentinel.announce_port = atoi(argv[1]); @@ -1994,12 +2115,16 @@ const char *sentinelHandleConfiguration(char **argv, int argc) { } } else if (!strcasecmp(argv[0],"sentinel-user") && argc == 2) { /* sentinel-user */ - if (strlen(argv[1])) + if (strlen(argv[1])) { + sdsfree(sentinel.sentinel_auth_user); sentinel.sentinel_auth_user = sdsnew(argv[1]); + } } else if (!strcasecmp(argv[0],"sentinel-pass") && argc == 2) { /* sentinel-pass */ - if (strlen(argv[1])) + if (strlen(argv[1])) { + sdsfree(sentinel.sentinel_auth_pass); sentinel.sentinel_auth_pass = sdsnew(argv[1]); + } } else if (!strcasecmp(argv[0],"resolve-hostnames") && argc == 2) { /* resolve-hostnames */ if ((sentinel.resolve_hostnames = yesnotoi(argv[1])) == -1) { @@ -2292,7 +2417,13 @@ int sentinelFlushConfig(void) { int rewrite_status; server.hz = CONFIG_DEFAULT_HZ; - rewrite_status = rewriteConfig(server.configfile, 0); + if (sentinel.state_config_file != NULL) { + /* Persist only the Sentinel runtime state into the dedicated state + * file, leaving the main (administrator managed) config file alone. */ + rewrite_status = rewriteSentinelStateConfig(sentinel.state_config_file); + } else { + rewrite_status = rewriteConfig(server.configfile, 0); + } server.hz = saved_hz; if (rewrite_status == -1) { diff --git a/src/server.c b/src/server.c index 3d9089317..37e0d82fc 100644 --- a/src/server.c +++ b/src/server.c @@ -8177,7 +8177,12 @@ int main(int argc, char **argv) { } loadServerConfig(server.configfile, config_from_stdin, options); - if (server.sentinel_mode) loadSentinelConfigFromQueue(); + if (server.sentinel_mode) { + /* Load the separate runtime state file (if configured) after the + * main config file, so its entries override the main config. */ + sentinelLoadStateConfigFile(); + loadSentinelConfigFromQueue(); + } sdsfree(options); } if (server.sentinel_mode) sentinelCheckConfigFile(); diff --git a/src/server.h b/src/server.h index d9ffbd4fe..538f2b845 100644 --- a/src/server.h +++ b/src/server.h @@ -1718,6 +1718,8 @@ struct sentinelLoadQueueEntry { sds *argv; int linenum; sds line; + int from_state_file; /* 1 if this entry was loaded from the separate + state config file (see "sentinel state-config-file"). */ }; struct sentinelConfig { @@ -3990,6 +3992,7 @@ struct rewriteConfigState; /* Forward declaration to export API. */ int rewriteConfigRewriteLine(struct rewriteConfigState *state, const char *option, sds line, int force); void rewriteConfigMarkAsProcessed(struct rewriteConfigState *state, const char *option); int rewriteConfig(char *path, int force_write); +int rewriteSentinelStateConfig(char *path); void initConfigValues(void); void removeConfig(sds name); sds getConfigDebugInfo(void); @@ -4169,6 +4172,7 @@ void sentinelTimer(void); const char *sentinelHandleConfiguration(char **argv, int argc); void queueSentinelConfig(sds *argv, int argc, int linenum, sds line); void loadSentinelConfigFromQueue(void); +void sentinelLoadStateConfigFile(void); void sentinelIsRunning(void); void sentinelCheckConfigFile(void); void sentinelCommand(client *c); diff --git a/tests/sentinel/tests/17-state-config-file.tcl b/tests/sentinel/tests/17-state-config-file.tcl new file mode 100644 index 000000000..a5fc55bc4 --- /dev/null +++ b/tests/sentinel/tests/17-state-config-file.tcl @@ -0,0 +1,218 @@ +# Verify "sentinel state-config-file": Sentinel persists its runtime state into +# a separate file and never rewrites the (administrator managed) main config +# file. See https://github.com/redis/redis/issues/5226. +# +# This test spawns its own dedicated Sentinel process (independent from the +# shared test fleet) so it can fully control both config files. + +set ::sentinel_bin "../../../src/redis-sentinel" + +proc scf_read_file {path} { + if {![file exists $path]} { return "" } + set fd [open $path r] + set data [read $fd] + close $fd + return $data +} + +# Start a dedicated Sentinel from a config template containing a "%PORT%" +# placeholder. find_available_port() may hand back a port that a fleet instance +# already bound (the probe socket can bind over it while a real bind fails), so +# retry with a fresh port until our own process is actually up and listening. +# Returns [list pid port]. +proc scf_launch {conf_path conf_template base_dir} { + for {set attempt 0} {$attempt < 50} {incr attempt} { + set port [find_available_port $::sentinel_base_port $::redis_port_count] + set fd [open $conf_path w] + puts $fd [string map [list %PORT% $port] $conf_template] + close $fd + set pid [exec $::sentinel_bin $conf_path 2> [file join $base_dir stderr.txt] &] + for {set t 0} {$t < 30} {incr t} { + if {[catch {exec kill -0 $pid}]} break ;# process died (e.g. bind failed) + # Confirm the server answering on this port is *our* process and not + # a fleet instance that already owns the port (in which case our + # process is about to exit with a bind error). + if {![catch {set l [redis 127.0.0.1 $port 0 0]}]} { + set gotpid -1 + catch {regexp {process_id:(\d+)} [$l info server] -> gotpid} + catch {$l close} + if {$gotpid == $pid} { return [list $pid $port] } + } + after 100 + } + catch {exec kill -9 $pid} + } + fail "could not start state-config-file sentinel" + return {} +} + +if {$::tls} { + puts "Skipping state-config-file test under TLS" +} else { + set base_dir [file normalize "state-config-file-test"] + catch {exec rm -rf $base_dir} + file mkdir $base_dir + + set main_conf [file join $base_dir "sentinel.conf"] + set state_conf [file join $base_dir "sentinel-state.conf"] + + set conf_template [subst -nocommands {port %PORT% +dir $base_dir +logfile log.txt +enable-protected-configs yes +enable-debug-command yes +sentinel state-config-file $state_conf +sentinel monitor mymaster 127.0.0.1 12345 2 +sentinel down-after-milliseconds mymaster 20000}] + + lassign [scf_launch $main_conf $conf_template $base_dir] pid port + set main_conf_orig [scf_read_file $main_conf] + + # Wrap the lifecycle in a catch so that the cleanup below always runs and + # kills the spawned process, even if a non-assertion error occurs (assertion + # failures inside "test" are already handled by the test framework). + catch { + set link [redis 127.0.0.1 $port 0 0] + $link reconnect 1 + + test "state-config-file: state is written to the separate file, not the main config" { + # On startup Sentinel picks a myid and flushes state to disk. + wait_for_condition 50 100 { + [string match "*sentinel myid*" [scf_read_file $state_conf]] + } else { + fail "Sentinel did not create the state config file" + } + set state [scf_read_file $state_conf] + assert_match "*sentinel myid*" $state + assert_match "*sentinel monitor mymaster*" $state + assert_match "*sentinel current-epoch*" $state + + # The main config file must be left untouched: no runtime state added. + assert_equal $main_conf_orig [scf_read_file $main_conf] + } + + set saved_myid [$link sentinel myid] + + test "state-config-file: SENTINEL SET is persisted to the state file only" { + $link sentinel set mymaster down-after-milliseconds 12345 + wait_for_condition 50 100 { + [string match "*down-after-milliseconds mymaster 12345*" [scf_read_file $state_conf]] + } else { + fail "SENTINEL SET was not persisted to the state config file" + } + # Main config file is still untouched. + assert_equal $main_conf_orig [scf_read_file $main_conf] + } + + test "state-config-file: runtime state is restored after restart" { + catch {$link close} + exec kill $pid + wait_for_condition 50 100 { + [catch {exec kill -0 $pid}] == 1 + } else { + fail "Sentinel process did not terminate" + } + + # Restart with the same config (and port); the port is free again. + set pid [exec $::sentinel_bin $main_conf 2> [file join $base_dir stderr.txt] &] + set link "" + wait_for_condition 50 100 { + ![catch {set link [redis 127.0.0.1 $port 0 0]; $link ping}] + } else { + fail "Sentinel did not restart" + } + $link reconnect 1 + + # The Sentinel id is stable across restarts (loaded from the state file). + assert_equal $saved_myid [$link sentinel myid] + + # The SENTINEL SET override survived the restart. + set master [$link sentinel master mymaster] + assert_equal 12345 [dict get $master down-after-milliseconds] + } + } + + catch {$link close} + catch {exec kill $pid} + catch {exec rm -rf $base_dir} + + # Migration case: the same runtime directives are present in both the main + # config file (e.g. a pre-split combined config) and the state file. Sentinel + # must boot without failing on duplicates, with the state file taking + # precedence. + set base_dir [file normalize "state-config-file-dup-test"] + catch {exec rm -rf $base_dir} + file mkdir $base_dir + set main_conf [file join $base_dir "sentinel.conf"] + set state_conf [file join $base_dir "sentinel-state.conf"] + + # Pre-existing state file overlapping the main config (different current + # address and higher epoch). Written before the sentinel starts. + set fd [open $state_conf w] + puts $fd "sentinel myid 0123456789abcdef0123456789abcdef01234567" + puts $fd "sentinel monitor mymaster 127.0.0.1 12399 2" + puts $fd "sentinel known-replica mymaster 127.0.0.1 12346" + puts $fd "sentinel config-epoch mymaster 7" + puts $fd "sentinel current-epoch 7" + close $fd + + set conf_template [subst -nocommands {port %PORT% +dir $base_dir +logfile log.txt +sentinel state-config-file $state_conf +sentinel monitor mymaster 127.0.0.1 12345 2 +sentinel known-replica mymaster 127.0.0.1 12346 +sentinel config-epoch mymaster 3}] + + lassign [scf_launch $main_conf $conf_template $base_dir] pid port + + catch { + test "state-config-file: duplicate directives across files are tolerated" { + set link [redis 127.0.0.1 $port 0 0] + $link reconnect 1 + + # myid comes from the state file. + assert_equal "0123456789abcdef0123456789abcdef01234567" [$link sentinel myid] + + set master [$link sentinel master mymaster] + # Address and epoch reflect the state file (loaded last / wins). + assert_equal 12399 [dict get $master port] + assert_equal 7 [dict get $master config-epoch] + # The single replica is known exactly once. + assert_equal 1 [dict get $master num-slaves] + catch {$link close} + } + } + + catch {exec kill $pid} + catch {exec rm -rf $base_dir} + + # A state file pointing at the main config file is a misconfiguration and + # Sentinel must refuse to start. The guard runs before the port is bound, so + # a port collision cannot mask it. + test "state-config-file: refuses to point at the main config file" { + set d [file normalize "state-config-file-same-test"] + catch {exec rm -rf $d} + file mkdir $d + set conf [file join $d "sentinel.conf"] + set p [find_available_port $::sentinel_base_port $::redis_port_count] + set fd [open $conf w] + puts $fd "port $p" + puts $fd "dir $d" + puts $fd "logfile log.txt" + puts $fd "sentinel state-config-file $conf" + puts $fd "sentinel monitor mymaster 127.0.0.1 12345 2" + close $fd + + set spid [exec $::sentinel_bin $conf 2> [file join $d stderr.txt] &] + wait_for_condition 50 100 { + [catch {exec kill -0 $spid}] == 1 + } else { + fail "Sentinel did not refuse to start with state-config-file == main config" + } + assert_match "*must differ from the main config file*" \ + "[scf_read_file [file join $d log.txt]][scf_read_file [file join $d stderr.txt]]" + catch {exec kill $spid} + catch {exec rm -rf $d} + } +}