From c95a582a749a83b117bdd51f4a7a29a616a0bf3c Mon Sep 17 00:00:00 2001 From: Madelyn Olson <matolson@amazon.com> Date: Thu, 30 May 2019 11:50:31 -0700 Subject: [PATCH 1/5] Add configuration option for allowing reads on cluster down --- redis.conf | 16 ++++++++++++++++ src/cluster.c | 13 ++++++++++--- src/config.c | 2 ++ src/server.h | 2 ++ 4 files changed, 30 insertions(+), 3 deletions(-) diff --git a/redis.conf b/redis.conf index d37760176..870849a79 100644 --- a/redis.conf +++ b/redis.conf @@ -1194,6 +1194,22 @@ lua-time-limit 5000 # # cluster-replica-no-failover no +# This option, when set to yes, allows nodes to serve read traffic while the +# the cluster is in a down state, as long as it believes it owns the slots. +# +# This is useful for two cases. The first case is for when an application +# doesn't require consistency of data during node failures or network partitions. +# One example of this is a cache, where as long as the node has the data it +# should be able to serve it. +# +# The second use case is for configurations that don't meet the recommended +# three shards but want to enable cluster mode and scale later. A +# master outage in a 1 or 2 shard configuration causes a read/write outage to the +# entire cluster without this option set, with it set there is only a write outage. +# Without a quorum of masters, slot ownership will not change automatically. +# +# cluster-allow-reads-when-down no + # In order to setup your cluster make sure to read the documentation # available at http://redis.io web site. diff --git a/src/cluster.c b/src/cluster.c index 9e6ddb2c4..4024035e9 100644 --- a/src/cluster.c +++ b/src/cluster.c @@ -5595,8 +5595,12 @@ clusterNode *getNodeByQuery(client *c, struct redisCommand *cmd, robj **argv, in * without redirections or errors in all the cases. */ if (n == NULL) return myself; - /* Cluster is globally down but we got keys? We can't serve the request. */ - if (server.cluster->state != CLUSTER_OK) { + /* Cluster is globally down but we got keys? We only serve the request + * if it is a read command and when allow_reads_when_down is enabled. */ + if ((server.cluster->state != CLUSTER_OK) && + !(server.cluster_allow_reads_when_down && ((cmd->flags & CMD_READONLY) + || (cmd->proc == evalCommand) || (cmd->proc == evalShaCommand)))) + { if (error_code) *error_code = CLUSTER_REDIR_DOWN_STATE; return NULL; } @@ -5701,7 +5705,10 @@ int clusterRedirectBlockedClientIfNeeded(client *c) { dictEntry *de; dictIterator *di; - /* If the cluster is down, unblock the client with the right error. */ + /* If the cluster is down, unblock the client with the right error. + * If the cluster is configured to allow reads on cluster down, we + * still want to emit this error since a write will be required + * to unblock them which may never come. */ if (server.cluster->state == CLUSTER_FAIL) { clusterRedirectClient(c,NULL,0,CLUSTER_REDIR_DOWN_STATE); return 1; diff --git a/src/config.c b/src/config.c index 5bfb0de5c..5ff333fec 100644 --- a/src/config.c +++ b/src/config.c @@ -2166,6 +2166,8 @@ standardConfig configs[] = { createBoolConfig("syslog-enabled", NULL, IMMUTABLE_CONFIG, server.syslog_enabled, 0, NULL, NULL), createBoolConfig("cluster-enabled", NULL, IMMUTABLE_CONFIG, server.cluster_enabled, 0, NULL, NULL), createBoolConfig("appendonly", NULL, MODIFIABLE_CONFIG, server.aof_enabled, 0, NULL, updateAppendonly), + createBoolConfig("cluster-allow-reads-when-down", NULL, MODIFIABLE_CONFIG, server.cluster_allow_reads_when_down, 0, NULL, NULL}, + /* String Configs */ createStringConfig("aclfile", NULL, IMMUTABLE_CONFIG, ALLOW_EMPTY_STRING, server.acl_filename, "", NULL, NULL), diff --git a/src/server.h b/src/server.h index f2c93241c..b5e51002b 100644 --- a/src/server.h +++ b/src/server.h @@ -1334,6 +1334,8 @@ struct redisServer { to set in order to suppress certain native Redis Cluster features. Check the REDISMODULE_CLUSTER_FLAG_*. */ + int cluster_allow_reads_when_down; /* Are reads allowed when the cluster + is down? */ /* Scripting */ lua_State *lua; /* The Lua interpreter. We use just one for all clients */ client *lua_client; /* The "fake client" to query Redis from Lua */ From 44aa22c63553d6be2879c51161feb50c1b31eab8 Mon Sep 17 00:00:00 2001 From: Madelyn Olson <matolson@amazon.com> Date: Mon, 28 Oct 2019 12:58:03 -0700 Subject: [PATCH 2/5] Added better exception handling around scripting and module --- src/module.c | 11 +++++++++-- src/scripting.c | 16 ++++++++++++---- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/src/module.c b/src/module.c index 5d9a39387..31d337b14 100644 --- a/src/module.c +++ b/src/module.c @@ -3174,6 +3174,8 @@ fmterr: * EINVAL: wrong command arity. * ENOENT: command does not exist. * EPERM: operation in Cluster instance with key in non local slot. + * EROFS: operation in Cluster instance when a write command is sent + * in a readonly state. * * This API is documented here: https://redis.io/topics/modules-intro */ @@ -3231,13 +3233,18 @@ RedisModuleCallReply *RM_Call(RedisModuleCtx *ctx, const char *cmdname, const ch * trying to access non-local keys, with the exception of commands * received from our master. */ if (server.cluster_enabled && !(ctx->client->flags & CLIENT_MASTER)) { + int error_code; /* Duplicate relevant flags in the module client. */ c->flags &= ~(CLIENT_READONLY|CLIENT_ASKING); c->flags |= ctx->client->flags & (CLIENT_READONLY|CLIENT_ASKING); - if (getNodeByQuery(c,c->cmd,c->argv,c->argc,NULL,NULL) != + if (getNodeByQuery(c,c->cmd,c->argv,c->argc,NULL,&error_code) != server.cluster->myself) { - errno = EPERM; + if (error_code == CLUSTER_REDIR_DOWN_STATE) { + errno = EROFS; + } else { + errno = EPERM; + } goto cleanup; } } diff --git a/src/scripting.c b/src/scripting.c index 7cf21f408..c627207d5 100644 --- a/src/scripting.c +++ b/src/scripting.c @@ -679,15 +679,23 @@ int luaRedisGenericCommand(lua_State *lua, int raise_error) { if (server.cluster_enabled && !server.loading && !(server.lua_caller->flags & CLIENT_MASTER)) { + int error_code; /* Duplicate relevant flags in the lua client. */ c->flags &= ~(CLIENT_READONLY|CLIENT_ASKING); c->flags |= server.lua_caller->flags & (CLIENT_READONLY|CLIENT_ASKING); - if (getNodeByQuery(c,c->cmd,c->argv,c->argc,NULL,NULL) != + if (getNodeByQuery(c,c->cmd,c->argv,c->argc,NULL,&error_code) != server.cluster->myself) { - luaPushError(lua, - "Lua script attempted to access a non local key in a " - "cluster node"); + if (error_code == CLUSTER_REDIR_DOWN_STATE) { + luaPushError(lua, + "Lua script attempted execute a write command while " + "cluster is down"); + } else { + luaPushError(lua, + "Lua script attempted to access a non local key in a " + "cluster node"); + } + goto cleanup; } } From 576a08908b0fcf85a01803507238c4cc14bd85d1 Mon Sep 17 00:00:00 2001 From: Madelyn Olson <matolson@amazon.com> Date: Wed, 30 Oct 2019 00:11:17 -0700 Subject: [PATCH 3/5] Split error message so dependandent callers give a useful result --- src/cluster.c | 23 +++++++++++++++-------- src/cluster.h | 1 + src/module.c | 5 ++++- src/scripting.c | 10 +++++++--- 4 files changed, 27 insertions(+), 12 deletions(-) diff --git a/src/cluster.c b/src/cluster.c index 4024035e9..a18543e0b 100644 --- a/src/cluster.c +++ b/src/cluster.c @@ -5476,8 +5476,8 @@ void readwriteCommand(client *c) { * already "down" but it is fragile to rely on the update of the global state, * so we also handle it here. * - * CLUSTER_REDIR_DOWN_STATE if the cluster is down but the user attempts to - * execute a command that addresses one or more keys. */ + * CLUSTER_REDIR_DOWN_STATE and CLUSTER_REDIR_DOWN_RO_STATE if the cluster is + * down but the user attempts to execute a command that addresses one or more keys. */ clusterNode *getNodeByQuery(client *c, struct redisCommand *cmd, robj **argv, int argc, int *hashslot, int *error_code) { clusterNode *n = NULL; robj *firstkey = NULL; @@ -5597,12 +5597,17 @@ clusterNode *getNodeByQuery(client *c, struct redisCommand *cmd, robj **argv, in /* Cluster is globally down but we got keys? We only serve the request * if it is a read command and when allow_reads_when_down is enabled. */ - if ((server.cluster->state != CLUSTER_OK) && - !(server.cluster_allow_reads_when_down && ((cmd->flags & CMD_READONLY) - || (cmd->proc == evalCommand) || (cmd->proc == evalShaCommand)))) - { - if (error_code) *error_code = CLUSTER_REDIR_DOWN_STATE; - return NULL; + if (server.cluster->state != CLUSTER_OK) { + if (!server.cluster_allow_reads_when_down) { + if (error_code) *error_code = CLUSTER_REDIR_DOWN_STATE; + return NULL; + } + + if (!(cmd->flags & CMD_READONLY) && !(cmd->proc == evalCommand) + && !(cmd->proc == evalShaCommand)) { + if (error_code) *error_code = CLUSTER_REDIR_DOWN_RO_STATE; + return NULL; + } } /* Return the hashslot by reference. */ @@ -5671,6 +5676,8 @@ void clusterRedirectClient(client *c, clusterNode *n, int hashslot, int error_co addReplySds(c,sdsnew("-TRYAGAIN Multiple keys request during rehashing of slot\r\n")); } else if (error_code == CLUSTER_REDIR_DOWN_STATE) { addReplySds(c,sdsnew("-CLUSTERDOWN The cluster is down\r\n")); + } else if (error_code == CLUSTER_REDIR_DOWN_RO_STATE) { + addReplySds(c,sdsnew("-CLUSTERDOWN The cluster is down and only accepts read commands\r\n")); } else if (error_code == CLUSTER_REDIR_DOWN_UNBOUND) { addReplySds(c,sdsnew("-CLUSTERDOWN Hash slot not served\r\n")); } else if (error_code == CLUSTER_REDIR_MOVED || diff --git a/src/cluster.h b/src/cluster.h index 3ba60df6e..35fc0cbfa 100644 --- a/src/cluster.h +++ b/src/cluster.h @@ -29,6 +29,7 @@ #define CLUSTER_REDIR_MOVED 4 /* -MOVED redirection required. */ #define CLUSTER_REDIR_DOWN_STATE 5 /* -CLUSTERDOWN, global state. */ #define CLUSTER_REDIR_DOWN_UNBOUND 6 /* -CLUSTERDOWN, unbound slot. */ +#define CLUSTER_REDIR_DOWN_RO_STATE 7 /* -CLUSTERDOWN, allow reads. */ struct clusterNode; diff --git a/src/module.c b/src/module.c index 31d337b14..a4e7cbe2c 100644 --- a/src/module.c +++ b/src/module.c @@ -3176,6 +3176,7 @@ fmterr: * EPERM: operation in Cluster instance with key in non local slot. * EROFS: operation in Cluster instance when a write command is sent * in a readonly state. + * ENETDOWN: operation in Cluster instance when cluster is down. * * This API is documented here: https://redis.io/topics/modules-intro */ @@ -3240,8 +3241,10 @@ RedisModuleCallReply *RM_Call(RedisModuleCtx *ctx, const char *cmdname, const ch if (getNodeByQuery(c,c->cmd,c->argv,c->argc,NULL,&error_code) != server.cluster->myself) { - if (error_code == CLUSTER_REDIR_DOWN_STATE) { + if (error_code == CLUSTER_REDIR_DOWN_RO_STATE) { errno = EROFS; + } else if (error_code == CLUSTER_REDIR_DOWN_STATE) { + errno = ENETDOWN; } else { errno = EPERM; } diff --git a/src/scripting.c b/src/scripting.c index c627207d5..96eb9681d 100644 --- a/src/scripting.c +++ b/src/scripting.c @@ -686,11 +686,15 @@ int luaRedisGenericCommand(lua_State *lua, int raise_error) { if (getNodeByQuery(c,c->cmd,c->argv,c->argc,NULL,&error_code) != server.cluster->myself) { - if (error_code == CLUSTER_REDIR_DOWN_STATE) { + if (error_code == CLUSTER_REDIR_DOWN_RO_STATE) { luaPushError(lua, - "Lua script attempted execute a write command while " + "Lua script attempted to execute a write command while the" + "cluster is down and readonly"); + } else if (error_code == CLUSTER_REDIR_DOWN_STATE) { + luaPushError(lua, + "Lua script attempted to execute a command while the" "cluster is down"); - } else { + } else {} luaPushError(lua, "Lua script attempted to access a non local key in a " "cluster node"); From 12caffee6159c6fc2bdf226a224a6c5ed67a42cb Mon Sep 17 00:00:00 2001 From: Madelyn Olson <matolson@amazon.com> Date: Mon, 16 Dec 2019 23:38:31 -0800 Subject: [PATCH 4/5] Added a missed space in lua errors --- src/scripting.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/scripting.c b/src/scripting.c index 96eb9681d..98d4b2b7e 100644 --- a/src/scripting.c +++ b/src/scripting.c @@ -688,13 +688,13 @@ int luaRedisGenericCommand(lua_State *lua, int raise_error) { { if (error_code == CLUSTER_REDIR_DOWN_RO_STATE) { luaPushError(lua, - "Lua script attempted to execute a write command while the" + "Lua script attempted to execute a write command while the " "cluster is down and readonly"); } else if (error_code == CLUSTER_REDIR_DOWN_STATE) { luaPushError(lua, - "Lua script attempted to execute a command while the" + "Lua script attempted to execute a command while the " "cluster is down"); - } else {} + } else { luaPushError(lua, "Lua script attempted to access a non local key in a " "cluster node"); From 7b3e3d6a13db7bfe0b93056d9a004185e18d6ae3 Mon Sep 17 00:00:00 2001 From: Madelyn Olson <matolson@amazon.com> Date: Mon, 16 Dec 2019 23:40:19 -0800 Subject: [PATCH 5/5] Resolved merge miss --- src/config.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config.c b/src/config.c index 5ff333fec..3d53e656b 100644 --- a/src/config.c +++ b/src/config.c @@ -2166,7 +2166,7 @@ standardConfig configs[] = { createBoolConfig("syslog-enabled", NULL, IMMUTABLE_CONFIG, server.syslog_enabled, 0, NULL, NULL), createBoolConfig("cluster-enabled", NULL, IMMUTABLE_CONFIG, server.cluster_enabled, 0, NULL, NULL), createBoolConfig("appendonly", NULL, MODIFIABLE_CONFIG, server.aof_enabled, 0, NULL, updateAppendonly), - createBoolConfig("cluster-allow-reads-when-down", NULL, MODIFIABLE_CONFIG, server.cluster_allow_reads_when_down, 0, NULL, NULL}, + createBoolConfig("cluster-allow-reads-when-down", NULL, MODIFIABLE_CONFIG, server.cluster_allow_reads_when_down, 0, NULL, NULL), /* String Configs */