mirror of
https://github.com/redis/redis.git
synced 2026-07-14 20:40:53 -04:00
sentinel: add state-config-file to separate runtime state from static config
Sentinel rewrites its config file on every state change, mixing administrator-managed static settings with runtime state (myid, epochs, known replicas/sentinels, post-failover addresses). When the file is owned by a configuration management tool (Chef, Ansible, ...) these rewrites look like drift and trigger needless restarts (redis#5226). Add an optional "sentinel state-config-file <path>" directive. When set, Sentinel persists its runtime state into that separate file and never rewrites the main config file, which becomes read-only from Sentinel's point of view and safe to manage externally. The main file is read first and the state file (if present) is read afterwards, so runtime state overrides the static values. Runtime changes made via SENTINEL SET are persisted to, and restored from, the state file. The state file is created on the first state change if it does not exist. Because runtime-tunable directives may now legitimately appear in both files, the state-file load path overrides instead of failing: monitor updates an existing master's address/quorum, known-replica/known-sentinel tolerate duplicates, rename-command overrides, and sds fields are freed before reassignment. The main-file duplicate-detection checks are preserved. Sentinel refuses to start if the state file path equals the main config file. When unset (the default) behavior is unchanged. Signed-off-by: Stan Hu <stanhu@gmail.com>
This commit is contained in:
parent
5b22a09918
commit
2c42bbbf92
6 changed files with 432 additions and 14 deletions
|
|
@ -72,6 +72,32 @@ logfile ""
|
|||
# unmounting filesystems.
|
||||
dir /tmp
|
||||
|
||||
# sentinel state-config-file <path>
|
||||
#
|
||||
# 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 <master-name> <ip> <redis-port> <quorum>
|
||||
#
|
||||
# Tells Sentinel to monitor this master, and to consider it in O_DOWN
|
||||
|
|
|
|||
34
src/config.c
34
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
|
||||
*----------------------------------------------------------------------------*/
|
||||
|
|
|
|||
157
src/sentinel.c
157
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 <path>
|
||||
*
|
||||
* 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 <name> <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 <name> <path> */
|
||||
|
|
@ -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 <name> <password> */
|
||||
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 <name> <username> */
|
||||
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 <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 <name> <command> <renamed-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 <ip-address> */
|
||||
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 <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 <user-name> */
|
||||
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 <password> */
|
||||
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 <yes|no> */
|
||||
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) {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
218
tests/sentinel/tests/17-state-config-file.tcl
Normal file
218
tests/sentinel/tests/17-state-config-file.tcl
Normal file
|
|
@ -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}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue