Commit 4afc54ad authored by Josh Hershberg's avatar Josh Hershberg
Browse files

Cluster refactor: break up clusterCommand



Divide up clusterCommand into clusterCommand for shared
sub-commands and clusterCommandSpecial for implementation
specific sub-commands. So to, the cluster command help
sub-command has been divided into two implementations,
clusterCommandHelp and clusterCommandHelpSpecial. Some
common sub-subcommand implementations have been extracted
and their implemenations either made shared or else
implementation specific.
Signed-off-by: default avatarJosh Hershberg <yehoshua@redis.com>
parent 33ef6a30
...@@ -744,3 +744,177 @@ int isValidAuxString(char *s, unsigned int length) { ...@@ -744,3 +744,177 @@ int isValidAuxString(char *s, unsigned int length) {
} }
return 1; return 1;
} }
void clusterCommandMyId(client *c) {
char *name = clusterNodeGetName(getMyClusterNode());
if (name) {
addReplyBulkCBuffer(c,name, CLUSTER_NAMELEN);
} else {
addReplyError(c, "No ID yet");
}
}
void clusterCommandMyShardId(client *c) {
char *sid = clusterNodeGetShardId(getMyClusterNode());
if (sid) {
addReplyBulkCBuffer(c,sid, CLUSTER_NAMELEN);
} else {
addReplyError(c, "No shard ID yet");
}
}
/* When a cluster command is called, we need to decide whether to return TLS info or
* non-TLS info by the client's connection type. However if the command is called by
* a Lua script or RM_call, there is no connection in the fake client, so we use
* server.current_client here to get the real client if available. And if it is not
* available (modules may call commands without a real client), we return the default
* info, which is determined by server.tls_cluster. */
static int shouldReturnTlsInfo(void) {
if (server.current_client && server.current_client->conn) {
return connIsTLS(server.current_client->conn);
} else {
return server.tls_cluster;
}
}
unsigned int countKeysInSlot(unsigned int slot) {
return dictSize(server.db->dict[slot]);
}
void clusterCommandHelp(client *c) {
const char *help[] = {
"COUNTKEYSINSLOT <slot>",
" Return the number of keys in <slot>.",
"GETKEYSINSLOT <slot> <count>",
" Return key names stored by current node in a slot.",
"INFO",
" Return information about the cluster.",
"KEYSLOT <key>",
" Return the hash slot for <key>.",
"MYID",
" Return the node id.",
"MYSHARDID",
" Return the node's shard id.",
"NODES",
" Return cluster configuration seen by node. Output format:",
" <id> <ip:port@bus-port[,hostname]> <flags> <master> <pings> <pongs> <epoch> <link> <slot> ...",
"REPLICAS <node-id>",
" Return <node-id> replicas.",
"SLOTS",
" Return information about slots range mappings. Each range is made of:",
" start, end, master and replicas IP addresses, ports and ids",
"SHARDS",
" Return information about slot range mappings and the nodes associated with them.",
NULL
};
addExtendedReplyHelp(c, help, clusterCommandSpecialHelp());
}
void clusterCommand(client *c) {
if (server.cluster_enabled == 0) {
addReplyError(c,"This instance has cluster support disabled");
return;
}
if (c->argc == 2 && !strcasecmp(c->argv[1]->ptr,"help")) {
clusterCommandHelp(c);
} else if (!strcasecmp(c->argv[1]->ptr,"nodes") && c->argc == 2) {
/* CLUSTER NODES */
/* Report TLS ports to TLS client, and report non-TLS port to non-TLS client. */
sds nodes = clusterGenNodesDescription(c, 0, shouldReturnTlsInfo());
addReplyVerbatim(c,nodes,sdslen(nodes),"txt");
sdsfree(nodes);
} else if (!strcasecmp(c->argv[1]->ptr,"myid") && c->argc == 2) {
/* CLUSTER MYID */
clusterCommandMyId(c);
} else if (!strcasecmp(c->argv[1]->ptr,"myshardid") && c->argc == 2) {
/* CLUSTER MYSHARDID */
clusterCommandMyShardId(c);
} else if (!strcasecmp(c->argv[1]->ptr,"slots") && c->argc == 2) {
/* CLUSTER SLOTS */
clusterCommandSlots(c);
} else if (!strcasecmp(c->argv[1]->ptr,"shards") && c->argc == 2) {
/* CLUSTER SHARDS */
clusterCommandShards(c);
} else if (!strcasecmp(c->argv[1]->ptr,"info") && c->argc == 2) {
/* CLUSTER INFO */
sds info = genClusterInfoString();
/* Produce the reply protocol. */
addReplyVerbatim(c,info,sdslen(info),"txt");
sdsfree(info);
} else if (!strcasecmp(c->argv[1]->ptr,"keyslot") && c->argc == 3) {
/* CLUSTER KEYSLOT <key> */
sds key = c->argv[2]->ptr;
addReplyLongLong(c,keyHashSlot(key,sdslen(key)));
} else if (!strcasecmp(c->argv[1]->ptr,"countkeysinslot") && c->argc == 3) {
/* CLUSTER COUNTKEYSINSLOT <slot> */
long long slot;
if (getLongLongFromObjectOrReply(c,c->argv[2],&slot,NULL) != C_OK)
return;
if (slot < 0 || slot >= CLUSTER_SLOTS) {
addReplyError(c,"Invalid slot");
return;
}
addReplyLongLong(c,countKeysInSlot(slot));
} else if (!strcasecmp(c->argv[1]->ptr,"getkeysinslot") && c->argc == 4) {
/* CLUSTER GETKEYSINSLOT <slot> <count> */
long long maxkeys, slot;
if (getLongLongFromObjectOrReply(c,c->argv[2],&slot,NULL) != C_OK)
return;
if (getLongLongFromObjectOrReply(c,c->argv[3],&maxkeys,NULL)
!= C_OK)
return;
if (slot < 0 || slot >= CLUSTER_SLOTS || maxkeys < 0) {
addReplyError(c,"Invalid slot or number of keys");
return;
}
unsigned int keys_in_slot = countKeysInSlot(slot);
unsigned int numkeys = maxkeys > keys_in_slot ? keys_in_slot : maxkeys;
addReplyArrayLen(c,numkeys);
dictIterator *iter = NULL;
dictEntry *de = NULL;
iter = dictGetIterator(server.db->dict[slot]);
for (unsigned int i = 0; i < numkeys; i++) {
de = dictNext(iter);
serverAssert(de != NULL);
sds sdskey = dictGetKey(de);
addReplyBulkCBuffer(c, sdskey, sdslen(sdskey));
}
dictReleaseIterator(iter);
} else if ((!strcasecmp(c->argv[1]->ptr,"slaves") ||
!strcasecmp(c->argv[1]->ptr,"replicas")) && c->argc == 3) {
/* CLUSTER SLAVES <NODE ID> */
/* CLUSTER REPLICAS <NODE ID> */
clusterNode *n = clusterLookupNode(c->argv[2]->ptr, sdslen(c->argv[2]->ptr));
int j;
/* Lookup the specified node in our table. */
if (!n) {
addReplyErrorFormat(c,"Unknown node %s", (char*)c->argv[2]->ptr);
return;
}
if (clusterNodeIsSlave(n)) {
addReplyError(c,"The specified node is not a master");
return;
}
/* Report TLS ports to TLS client, and report non-TLS port to non-TLS client. */
addReplyArrayLen(c, getNumSlaves(n));
for (j = 0; j < getNumSlaves(n); j++) {
sds ni = clusterGenNodeDescription(c, getSlave(n, j), shouldReturnTlsInfo());
addReplyBulkCString(c,ni);
sdsfree(ni);
}
} else if(!clusterCommandSpecial(c)) {
addReplySubcommandSyntaxError(c);
return;
}
}
...@@ -68,7 +68,7 @@ int getClusterSize(void); ...@@ -68,7 +68,7 @@ int getClusterSize(void);
char** getClusterNodesList(size_t *numnodes); char** getClusterNodesList(size_t *numnodes);
int nodeIsMaster(clusterNode *n); int nodeIsMaster(clusterNode *n);
int handleDebugClusterCommand(client *c); int handleDebugClusterCommand(client *c);
int clusterNodeConfirmedReachable(clusterNode *node); int clusterNodePending(clusterNode *node);
char* clusterNodeIp(clusterNode *node); char* clusterNodeIp(clusterNode *node);
int clusterNodeIsSlave(clusterNode *node); int clusterNodeIsSlave(clusterNode *node);
clusterNode *clusterNodeGetSlaveof(clusterNode *node); clusterNode *clusterNodeGetSlaveof(clusterNode *node);
...@@ -76,6 +76,17 @@ char* clusterNodeGetName(clusterNode *node); ...@@ -76,6 +76,17 @@ char* clusterNodeGetName(clusterNode *node);
int clusterNodeTimedOut(clusterNode *node); int clusterNodeTimedOut(clusterNode *node);
int clusterNodeIsFailing(clusterNode *node); int clusterNodeIsFailing(clusterNode *node);
int clusterNodeIsNoFailover(clusterNode *node); int clusterNodeIsNoFailover(clusterNode *node);
void clusterCommand(client *c);
int clusterCommandSpecial(client *c);
const char** clusterCommandSpecialHelp(void);
char* clusterNodeGetShardId(clusterNode *node);
void clusterCommandSlots(client * c);
void clusterCommandMyId(client *c);
void clusterCommandMyShardId(client *c);
void clusterCommandShards(client *c);
sds clusterGenNodeDescription(client *c, clusterNode *node, int tls_primary);
int getNumSlaves(clusterNode *node);
clusterNode *getSlave(clusterNode *node, int slave_idx);
char **clusterDebugCommandHelp(void); char **clusterDebugCommandHelp(void);
ConnectionType *connTypeOfCluster(void); ConnectionType *connTypeOfCluster(void);
......
...@@ -120,20 +120,6 @@ static inline int defaultClientPort(void) { ...@@ -120,20 +120,6 @@ static inline int defaultClientPort(void) {
return server.tls_cluster ? server.tls_port : server.port; return server.tls_cluster ? server.tls_port : server.port;
} }
/* When a cluster command is called, we need to decide whether to return TLS info or
* non-TLS info by the client's connection type. However if the command is called by
* a Lua script or RM_call, there is no connection in the fake client, so we use
* server.current_client here to get the real client if available. And if it is not
* available (modules may call commands without a real client), we return the default
* info, which is determined by server.tls_cluster. */
static int shouldReturnTlsInfo(void) {
if (server.current_client && server.current_client->conn) {
return connIsTLS(server.current_client->conn);
} else {
return server.tls_cluster;
}
}
#define isSlotUnclaimed(slot) \ #define isSlotUnclaimed(slot) \
(server.cluster->slots[slot] == NULL || \ (server.cluster->slots[slot] == NULL || \
bitmapTestBit(server.cluster->owner_not_claiming_slot, slot)) bitmapTestBit(server.cluster->owner_not_claiming_slot, slot))
...@@ -5678,7 +5664,7 @@ void addShardReplyForClusterShards(client *c, list *nodes) { ...@@ -5678,7 +5664,7 @@ void addShardReplyForClusterShards(client *c, list *nodes) {
/* Add to the output buffer of the given client, an array of slot (start, end) /* Add to the output buffer of the given client, an array of slot (start, end)
* pair owned by the shard, also the primary and set of replica(s) along with * pair owned by the shard, also the primary and set of replica(s) along with
* information about each node. */ * information about each node. */
void clusterReplyShards(client *c) { void clusterCommandShards(client *c) {
addReplyArrayLen(c, dictSize(server.cluster->shards)); addReplyArrayLen(c, dictSize(server.cluster->shards));
/* This call will add slot_info_pairs to all nodes */ /* This call will add slot_info_pairs to all nodes */
clusterGenNodesSlotsInfo(0); clusterGenNodesSlotsInfo(0);
...@@ -5689,7 +5675,7 @@ void clusterReplyShards(client *c) { ...@@ -5689,7 +5675,7 @@ void clusterReplyShards(client *c) {
dictReleaseIterator(di); dictReleaseIterator(di);
} }
void clusterReplyMultiBulkSlots(client * c) { void clusterCommandSlots(client * c) {
/* Format: 1) 1) start slot /* Format: 1) 1) start slot
* 2) end slot * 2) end slot
* 3) 1) master IP * 3) 1) master IP
...@@ -5804,1228 +5790,1117 @@ sds genClusterInfoString(void) { ...@@ -5804,1228 +5790,1117 @@ sds genClusterInfoString(void) {
return info; return info;
} }
void clusterCommand(client *c) {
void removeChannelsInSlot(unsigned int slot) {
unsigned int channelcount = countChannelsInSlot(slot);
if (channelcount == 0) return;
/* Retrieve all the channels for the slot. */
robj **channels = zmalloc(sizeof(robj*)*channelcount);
raxIterator iter;
int j = 0;
unsigned char indexed[2];
indexed[0] = (slot >> 8) & 0xff;
indexed[1] = slot & 0xff;
raxStart(&iter,server.cluster->slots_to_channels);
raxSeek(&iter,">=",indexed,2);
while(raxNext(&iter)) {
if (iter.key[0] != indexed[0] || iter.key[1] != indexed[1]) break;
channels[j++] = createStringObject((char*)iter.key + 2, iter.key_len - 2);
}
raxStop(&iter);
pubsubUnsubscribeShardChannels(channels, channelcount);
zfree(channels);
}
/* -----------------------------------------------------------------------------
* Cluster functions related to serving / redirecting clients
* -------------------------------------------------------------------------- */
/* The ASKING command is required after a -ASK redirection.
* The client should issue ASKING before to actually send the command to
* the target instance. See the Redis Cluster specification for more
* information. */
void askingCommand(client *c) {
if (server.cluster_enabled == 0) { if (server.cluster_enabled == 0) {
addReplyError(c,"This instance has cluster support disabled"); addReplyError(c,"This instance has cluster support disabled");
return; return;
} }
c->flags |= CLIENT_ASKING;
addReply(c,shared.ok);
}
if (c->argc == 2 && !strcasecmp(c->argv[1]->ptr,"help")) { /* The READONLY command is used by clients to enter the read-only mode.
const char *help[] = { * In this mode slaves will not redirect clients as long as clients access
"ADDSLOTS <slot> [<slot> ...]", * with read-only commands to keys that are served by the slave's master. */
" Assign slots to current node.", void readonlyCommand(client *c) {
"ADDSLOTSRANGE <start slot> <end slot> [<start slot> <end slot> ...]", if (server.cluster_enabled == 0) {
" Assign slots which are between <start-slot> and <end-slot> to current node.", addReplyError(c,"This instance has cluster support disabled");
"BUMPEPOCH",
" Advance the cluster config epoch.",
"COUNT-FAILURE-REPORTS <node-id>",
" Return number of failure reports for <node-id>.",
"COUNTKEYSINSLOT <slot>",
" Return the number of keys in <slot>.",
"DELSLOTS <slot> [<slot> ...]",
" Delete slots information from current node.",
"DELSLOTSRANGE <start slot> <end slot> [<start slot> <end slot> ...]",
" Delete slots information which are between <start-slot> and <end-slot> from current node.",
"FAILOVER [FORCE|TAKEOVER]",
" Promote current replica node to being a master.",
"FORGET <node-id>",
" Remove a node from the cluster.",
"GETKEYSINSLOT <slot> <count>",
" Return key names stored by current node in a slot.",
"FLUSHSLOTS",
" Delete current node own slots information.",
"INFO",
" Return information about the cluster.",
"KEYSLOT <key>",
" Return the hash slot for <key>.",
"MEET <ip> <port> [<bus-port>]",
" Connect nodes into a working cluster.",
"MYID",
" Return the node id.",
"MYSHARDID",
" Return the node's shard id.",
"NODES",
" Return cluster configuration seen by node. Output format:",
" <id> <ip:port@bus-port[,hostname]> <flags> <master> <pings> <pongs> <epoch> <link> <slot> ...",
"REPLICATE <node-id>",
" Configure current node as replica to <node-id>.",
"RESET [HARD|SOFT]",
" Reset current node (default: soft).",
"SET-CONFIG-EPOCH <epoch>",
" Set config epoch of current node.",
"SETSLOT <slot> (IMPORTING <node-id>|MIGRATING <node-id>|STABLE|NODE <node-id>)",
" Set slot state.",
"REPLICAS <node-id>",
" Return <node-id> replicas.",
"SAVECONFIG",
" Force saving cluster configuration on disk.",
"SLOTS",
" Return information about slots range mappings. Each range is made of:",
" start, end, master and replicas IP addresses, ports and ids",
"SHARDS",
" Return information about slot range mappings and the nodes associated with them.",
"LINKS",
" Return information about all network links between this node and its peers.",
" Output format is an array where each array element is a map containing attributes of a link",
NULL
};
addReplyHelp(c, help);
} else if (!strcasecmp(c->argv[1]->ptr,"meet") && (c->argc == 4 || c->argc == 5)) {
/* CLUSTER MEET <ip> <port> [cport] */
long long port, cport;
if (getLongLongFromObject(c->argv[3], &port) != C_OK) {
addReplyErrorFormat(c,"Invalid base port specified: %s",
(char*)c->argv[3]->ptr);
return; return;
} }
c->flags |= CLIENT_READONLY;
addReply(c,shared.ok);
}
if (c->argc == 5) { /* The READWRITE command just clears the READONLY command state. */
if (getLongLongFromObject(c->argv[4], &cport) != C_OK) { void readwriteCommand(client *c) {
addReplyErrorFormat(c,"Invalid bus port specified: %s", if (server.cluster_enabled == 0) {
(char*)c->argv[4]->ptr); addReplyError(c,"This instance has cluster support disabled");
return; return;
} }
c->flags &= ~CLIENT_READONLY;
addReply(c,shared.ok);
}
/* Return the pointer to the cluster node that is able to serve the command.
* For the function to succeed the command should only target either:
*
* 1) A single key (even multiple times like RPOPLPUSH mylist mylist).
* 2) Multiple keys in the same hash slot, while the slot is stable (no
* resharding in progress).
*
* On success the function returns the node that is able to serve the request.
* If the node is not 'myself' a redirection must be performed. The kind of
* redirection is specified setting the integer passed by reference
* 'error_code', which will be set to CLUSTER_REDIR_ASK or
* CLUSTER_REDIR_MOVED.
*
* When the node is 'myself' 'error_code' is set to CLUSTER_REDIR_NONE.
*
* If the command fails NULL is returned, and the reason of the failure is
* provided via 'error_code', which will be set to:
*
* CLUSTER_REDIR_CROSS_SLOT if the request contains multiple keys that
* don't belong to the same hash slot.
*
* CLUSTER_REDIR_UNSTABLE if the request contains multiple keys
* belonging to the same slot, but the slot is not stable (in migration or
* importing state, likely because a resharding is in progress).
*
* CLUSTER_REDIR_DOWN_UNBOUND if the request addresses a slot which is
* not bound to any node. In this case the cluster global state should be
* 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 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;
int multiple_keys = 0;
multiState *ms, _ms;
multiCmd mc;
int i, slot = 0, migrating_slot = 0, importing_slot = 0, missing_keys = 0,
existing_keys = 0;
/* Allow any key to be set if a module disabled cluster redirections. */
if (server.cluster_module_flags & CLUSTER_MODULE_FLAG_NO_REDIRECTION)
return myself;
/* Set error code optimistically for the base case. */
if (error_code) *error_code = CLUSTER_REDIR_NONE;
/* Modules can turn off Redis Cluster redirection: this is useful
* when writing a module that implements a completely different
* distributed system. */
/* We handle all the cases as if they were EXEC commands, so we have
* a common code path for everything */
if (cmd->proc == execCommand) {
/* If CLIENT_MULTI flag is not set EXEC is just going to return an
* error. */
if (!(c->flags & CLIENT_MULTI)) return myself;
ms = &c->mstate;
} else { } else {
cport = port + CLUSTER_PORT_INCR; /* In order to have a single codepath create a fake Multi State
* structure if the client is not in MULTI/EXEC state, this way
* we have a single codepath below. */
ms = &_ms;
_ms.commands = &mc;
_ms.count = 1;
mc.argv = argv;
mc.argc = argc;
mc.cmd = cmd;
} }
if (clusterStartHandshake(c->argv[2]->ptr,port,cport) == 0 && int is_pubsubshard = cmd->proc == ssubscribeCommand ||
errno == EINVAL) cmd->proc == sunsubscribeCommand ||
cmd->proc == spublishCommand;
/* Check that all the keys are in the same hash slot, and obtain this
* slot and the node associated. */
for (i = 0; i < ms->count; i++) {
struct redisCommand *mcmd;
robj **margv;
int margc, numkeys, j;
keyReference *keyindex;
mcmd = ms->commands[i].cmd;
margc = ms->commands[i].argc;
margv = ms->commands[i].argv;
getKeysResult result = GETKEYS_RESULT_INIT;
numkeys = getKeysFromCommand(mcmd,margv,margc,&result);
keyindex = result.keys;
for (j = 0; j < numkeys; j++) {
robj *thiskey = margv[keyindex[j].pos];
int thisslot = keyHashSlot((char*)thiskey->ptr,
sdslen(thiskey->ptr));
if (firstkey == NULL) {
/* This is the first key we see. Check what is the slot
* and node. */
firstkey = thiskey;
slot = thisslot;
n = server.cluster->slots[slot];
/* Error: If a slot is not served, we are in "cluster down"
* state. However the state is yet to be updated, so this was
* not trapped earlier in processCommand(). Report the same
* error to the client. */
if (n == NULL) {
getKeysFreeResult(&result);
if (error_code)
*error_code = CLUSTER_REDIR_DOWN_UNBOUND;
return NULL;
}
/* If we are migrating or importing this slot, we need to check
* if we have all the keys in the request (the only way we
* can safely serve the request, otherwise we return a TRYAGAIN
* error). To do so we set the importing/migrating state and
* increment a counter for every missing key. */
if (n == myself &&
server.cluster->migrating_slots_to[slot] != NULL)
{ {
addReplyErrorFormat(c,"Invalid node address specified: %s:%s", migrating_slot = 1;
(char*)c->argv[2]->ptr, (char*)c->argv[3]->ptr); } else if (server.cluster->importing_slots_from[slot] != NULL) {
importing_slot = 1;
}
} else { } else {
addReply(c,shared.ok); /* If it is not the first key/channel, make sure it is exactly
* the same key/channel as the first we saw. */
if (slot != thisslot) {
/* Error: multiple keys from different slots. */
getKeysFreeResult(&result);
if (error_code)
*error_code = CLUSTER_REDIR_CROSS_SLOT;
return NULL;
} }
} else if (!strcasecmp(c->argv[1]->ptr,"nodes") && c->argc == 2) { if (importing_slot && !multiple_keys && !equalStringObjects(firstkey,thiskey)) {
/* CLUSTER NODES */ /* Flag this request as one with multiple different
/* Report TLS ports to TLS client, and report non-TLS port to non-TLS client. */ * keys/channels when the slot is in importing state. */
sds nodes = clusterGenNodesDescription(c, 0, shouldReturnTlsInfo()); multiple_keys = 1;
addReplyVerbatim(c,nodes,sdslen(nodes),"txt");
sdsfree(nodes);
} else if (!strcasecmp(c->argv[1]->ptr,"myid") && c->argc == 2) {
/* CLUSTER MYID */
addReplyBulkCBuffer(c,myself->name, CLUSTER_NAMELEN);
} else if (!strcasecmp(c->argv[1]->ptr,"myshardid") && c->argc == 2) {
/* CLUSTER MYSHARDID */
addReplyBulkCBuffer(c,myself->shard_id, CLUSTER_NAMELEN);
} else if (!strcasecmp(c->argv[1]->ptr,"slots") && c->argc == 2) {
/* CLUSTER SLOTS */
clusterReplyMultiBulkSlots(c);
} else if (!strcasecmp(c->argv[1]->ptr,"shards") && c->argc == 2) {
/* CLUSTER SHARDS */
clusterReplyShards(c);
} else if (!strcasecmp(c->argv[1]->ptr,"flushslots") && c->argc == 2) {
/* CLUSTER FLUSHSLOTS */
if (dbSize(&server.db[0], DB_MAIN) != 0) {
addReplyError(c,"DB must be empty to perform CLUSTER FLUSHSLOTS.");
return;
} }
clusterDelNodeSlots(myself);
clusterDoBeforeSleep(CLUSTER_TODO_UPDATE_STATE|CLUSTER_TODO_SAVE_CONFIG);
addReply(c,shared.ok);
} else if ((!strcasecmp(c->argv[1]->ptr,"addslots") ||
!strcasecmp(c->argv[1]->ptr,"delslots")) && c->argc >= 3)
{
/* CLUSTER ADDSLOTS <slot> [slot] ... */
/* CLUSTER DELSLOTS <slot> [slot] ... */
int j, slot;
unsigned char *slots = zmalloc(CLUSTER_SLOTS);
int del = !strcasecmp(c->argv[1]->ptr,"delslots");
memset(slots,0,CLUSTER_SLOTS);
/* Check that all the arguments are parseable.*/
for (j = 2; j < c->argc; j++) {
if ((slot = getSlotOrReply(c,c->argv[j])) == C_ERR) {
zfree(slots);
return;
} }
/* Migrating / Importing slot? Count keys we don't have.
* If it is pubsubshard command, it isn't required to check
* the channel being present or not in the node during the
* slot migration, the channel will be served from the source
* node until the migration completes with CLUSTER SETSLOT <slot>
* NODE <node-id>. */
int flags = LOOKUP_NOTOUCH | LOOKUP_NOSTATS | LOOKUP_NONOTIFY | LOOKUP_NOEXPIRE;
if ((migrating_slot || importing_slot) && !is_pubsubshard)
{
if (lookupKeyReadWithFlags(&server.db[0], thiskey, flags) == NULL) missing_keys++;
else existing_keys++;
} }
/* Check that the slots are not already busy. */
for (j = 2; j < c->argc; j++) {
slot = getSlotOrReply(c,c->argv[j]);
if (checkSlotAssignmentsOrReply(c, slots, del, slot, slot) == C_ERR) {
zfree(slots);
return;
} }
getKeysFreeResult(&result);
} }
clusterUpdateSlots(c, slots, del);
zfree(slots);
clusterDoBeforeSleep(CLUSTER_TODO_UPDATE_STATE|CLUSTER_TODO_SAVE_CONFIG);
addReply(c,shared.ok);
} else if ((!strcasecmp(c->argv[1]->ptr,"addslotsrange") ||
!strcasecmp(c->argv[1]->ptr,"delslotsrange")) && c->argc >= 4) {
if (c->argc % 2 == 1) {
addReplyErrorArity(c);
return;
}
/* CLUSTER ADDSLOTSRANGE <start slot> <end slot> [<start slot> <end slot> ...] */
/* CLUSTER DELSLOTSRANGE <start slot> <end slot> [<start slot> <end slot> ...] */
int j, startslot, endslot;
unsigned char *slots = zmalloc(CLUSTER_SLOTS);
int del = !strcasecmp(c->argv[1]->ptr,"delslotsrange");
memset(slots,0,CLUSTER_SLOTS); /* No key at all in command? then we can serve the request
/* Check that all the arguments are parseable and that all the * without redirections or errors in all the cases. */
* slots are not already busy. */ if (n == NULL) return myself;
for (j = 2; j < c->argc; j += 2) {
if ((startslot = getSlotOrReply(c,c->argv[j])) == C_ERR) {
zfree(slots);
return;
}
if ((endslot = getSlotOrReply(c,c->argv[j+1])) == C_ERR) {
zfree(slots);
return;
}
if (startslot > endslot) {
addReplyErrorFormat(c,"start slot number %d is greater than end slot number %d", startslot, endslot);
zfree(slots);
return;
}
if (checkSlotAssignmentsOrReply(c, slots, del, startslot, endslot) == C_ERR) { uint64_t cmd_flags = getCommandFlags(c);
zfree(slots); /* Cluster is globally down but we got keys? We only serve the request
return; * if it is a read command and when allow_reads_when_down is enabled. */
if (server.cluster->state != CLUSTER_OK) {
if (is_pubsubshard) {
if (!server.cluster_allow_pubsubshard_when_down) {
if (error_code) *error_code = CLUSTER_REDIR_DOWN_STATE;
return NULL;
} }
} else if (!server.cluster_allow_reads_when_down) {
/* The cluster is configured to block commands when the
* cluster is down. */
if (error_code) *error_code = CLUSTER_REDIR_DOWN_STATE;
return NULL;
} else if (cmd_flags & CMD_WRITE) {
/* The cluster is configured to allow read only commands */
if (error_code) *error_code = CLUSTER_REDIR_DOWN_RO_STATE;
return NULL;
} else {
/* Fall through and allow the command to be executed:
* this happens when server.cluster_allow_reads_when_down is
* true and the command is not a write command */
} }
clusterUpdateSlots(c, slots, del);
zfree(slots);
clusterDoBeforeSleep(CLUSTER_TODO_UPDATE_STATE|CLUSTER_TODO_SAVE_CONFIG);
addReply(c,shared.ok);
} else if (!strcasecmp(c->argv[1]->ptr,"setslot") && c->argc >= 4) {
/* SETSLOT 10 MIGRATING <node ID> */
/* SETSLOT 10 IMPORTING <node ID> */
/* SETSLOT 10 STABLE */
/* SETSLOT 10 NODE <node ID> */
int slot;
clusterNode *n;
if (nodeIsSlave(myself)) {
addReplyError(c,"Please use SETSLOT only with masters.");
return;
} }
if ((slot = getSlotOrReply(c,c->argv[2])) == -1) return; /* Return the hashslot by reference. */
if (hashslot) *hashslot = slot;
if (!strcasecmp(c->argv[3]->ptr,"migrating") && c->argc == 5) { /* MIGRATE always works in the context of the local node if the slot
if (server.cluster->slots[slot] != myself) { * is open (migrating or importing state). We need to be able to freely
addReplyErrorFormat(c,"I'm not the owner of hash slot %u",slot); * move keys among instances in this case. */
return; if ((migrating_slot || importing_slot) && cmd->proc == migrateCommand)
} return myself;
n = clusterLookupNode(c->argv[4]->ptr, sdslen(c->argv[4]->ptr));
if (n == NULL) { /* If we don't have all the keys and we are migrating the slot, send
addReplyErrorFormat(c,"I don't know about node %s", * an ASK redirection or TRYAGAIN. */
(char*)c->argv[4]->ptr); if (migrating_slot && missing_keys) {
return; /* If we have keys but we don't have all keys, we return TRYAGAIN */
} if (existing_keys) {
if (nodeIsSlave(n)) { if (error_code) *error_code = CLUSTER_REDIR_UNSTABLE;
addReplyError(c,"Target node is not a master"); return NULL;
return; } else {
} if (error_code) *error_code = CLUSTER_REDIR_ASK;
server.cluster->migrating_slots_to[slot] = n; return server.cluster->migrating_slots_to[slot];
} else if (!strcasecmp(c->argv[3]->ptr,"importing") && c->argc == 5) {
if (server.cluster->slots[slot] == myself) {
addReplyErrorFormat(c,
"I'm already the owner of hash slot %u",slot);
return;
}
n = clusterLookupNode(c->argv[4]->ptr, sdslen(c->argv[4]->ptr));
if (n == NULL) {
addReplyErrorFormat(c,"I don't know about node %s",
(char*)c->argv[4]->ptr);
return;
}
if (nodeIsSlave(n)) {
addReplyError(c,"Target node is not a master");
return;
}
server.cluster->importing_slots_from[slot] = n;
} else if (!strcasecmp(c->argv[3]->ptr,"stable") && c->argc == 4) {
/* CLUSTER SETSLOT <SLOT> STABLE */
server.cluster->importing_slots_from[slot] = NULL;
server.cluster->migrating_slots_to[slot] = NULL;
} else if (!strcasecmp(c->argv[3]->ptr,"node") && c->argc == 5) {
/* CLUSTER SETSLOT <SLOT> NODE <NODE ID> */
n = clusterLookupNode(c->argv[4]->ptr, sdslen(c->argv[4]->ptr));
if (!n) {
addReplyErrorFormat(c,"Unknown node %s",
(char*)c->argv[4]->ptr);
return;
} }
if (nodeIsSlave(n)) {
addReplyError(c,"Target node is not a master");
return;
} }
/* If this hash slot was served by 'myself' before to switch
* make sure there are no longer local keys for this hash slot. */ /* If we are receiving the slot, and the client correctly flagged the
if (server.cluster->slots[slot] == myself && n != myself) { * request as "ASKING", we can serve the request. However if the request
if (countKeysInSlot(slot) != 0) { * involves multiple keys and we don't have them all, the only option is
addReplyErrorFormat(c, * to send a TRYAGAIN error. */
"Can't assign hashslot %d to a different node " if (importing_slot &&
"while I still hold keys for this hash slot.", slot); (c->flags & CLIENT_ASKING || cmd_flags & CMD_ASKING))
return; {
if (multiple_keys && missing_keys) {
if (error_code) *error_code = CLUSTER_REDIR_UNSTABLE;
return NULL;
} else {
return myself;
} }
} }
/* If this slot is in migrating status but we have no keys
* for it assigning the slot to another node will clear
* the migrating status. */
if (countKeysInSlot(slot) == 0 &&
server.cluster->migrating_slots_to[slot])
server.cluster->migrating_slots_to[slot] = NULL;
int slot_was_mine = server.cluster->slots[slot] == myself;
clusterDelSlot(slot);
clusterAddSlot(n,slot);
/* If we are a master left without slots, we should turn into a /* Handle the read-only client case reading from a slave: if this
* replica of the new master. */ * node is a slave and the request is about a hash slot our master
if (slot_was_mine && * is serving, we can reply without redirection. */
n != myself && int is_write_command = (cmd_flags & CMD_WRITE) ||
myself->numslots == 0 && (c->cmd->proc == execCommand && (c->mstate.cmd_flags & CMD_WRITE));
server.cluster_allow_replica_migration) if (((c->flags & CLIENT_READONLY) || is_pubsubshard) &&
!is_write_command &&
nodeIsSlave(myself) &&
myself->slaveof == n)
{ {
serverLog(LL_NOTICE, return myself;
"Configuration change detected. Reconfiguring myself "
"as a replica of %.40s (%s)", n->name, n->human_nodename);
clusterSetMaster(n);
clusterDoBeforeSleep(CLUSTER_TODO_SAVE_CONFIG |
CLUSTER_TODO_UPDATE_STATE |
CLUSTER_TODO_FSYNC_CONFIG);
} }
/* If this node was importing this slot, assigning the slot to /* Base case: just return the right node. However if this node is not
* itself also clears the importing status. */ * myself, set error_code to MOVED since we need to issue a redirection. */
if (n == myself && if (n != myself && error_code) *error_code = CLUSTER_REDIR_MOVED;
server.cluster->importing_slots_from[slot]) return n;
{ }
/* This slot was manually migrated, set this node configEpoch
* to a new epoch so that the new version can be propagated /* Send the client the right redirection code, according to error_code
* by the cluster. * that should be set to one of CLUSTER_REDIR_* macros.
* *
* Note that if this ever results in a collision with another * If CLUSTER_REDIR_ASK or CLUSTER_REDIR_MOVED error codes
* node getting the same configEpoch, for example because a * are used, then the node 'n' should not be NULL, but should be the
* failover happens at the same time we close the slot, the * node we want to mention in the redirection. Moreover hashslot should
* configEpoch collision resolution will fix it assigning * be set to the hash slot that caused the redirection. */
* a different epoch to each node. */ void clusterRedirectClient(client *c, clusterNode *n, int hashslot, int error_code) {
if (clusterBumpConfigEpochWithoutConsensus() == C_OK) { if (error_code == CLUSTER_REDIR_CROSS_SLOT) {
serverLog(LL_NOTICE, addReplyError(c,"-CROSSSLOT Keys in request don't hash to the same slot");
"configEpoch updated after importing slot %d", slot); } else if (error_code == CLUSTER_REDIR_UNSTABLE) {
} /* The request spawns multiple keys in the same slot,
server.cluster->importing_slots_from[slot] = NULL; * but the slot is not "stable" currently as there is
/* After importing this slot, let the other nodes know as * a migration or import in progress. */
* soon as possible. */ addReplyError(c,"-TRYAGAIN Multiple keys request during rehashing of slot");
clusterBroadcastPong(CLUSTER_BROADCAST_ALL); } else if (error_code == CLUSTER_REDIR_DOWN_STATE) {
} addReplyError(c,"-CLUSTERDOWN The cluster is down");
} else if (error_code == CLUSTER_REDIR_DOWN_RO_STATE) {
addReplyError(c,"-CLUSTERDOWN The cluster is down and only accepts read commands");
} else if (error_code == CLUSTER_REDIR_DOWN_UNBOUND) {
addReplyError(c,"-CLUSTERDOWN Hash slot not served");
} else if (error_code == CLUSTER_REDIR_MOVED ||
error_code == CLUSTER_REDIR_ASK)
{
/* Report TLS ports to TLS client, and report non-TLS port to non-TLS client. */
int port = getNodeClientPort(n, shouldReturnTlsInfo());
addReplyErrorSds(c,sdscatprintf(sdsempty(),
"-%s %d %s:%d",
(error_code == CLUSTER_REDIR_ASK) ? "ASK" : "MOVED",
hashslot, getPreferredEndpoint(n), port));
} else { } else {
addReplyError(c, serverPanic("getNodeByQuery() unknown error.");
"Invalid CLUSTER SETSLOT action or number of arguments. Try CLUSTER HELP");
return;
} }
clusterDoBeforeSleep(CLUSTER_TODO_SAVE_CONFIG|CLUSTER_TODO_UPDATE_STATE); }
addReply(c,shared.ok);
} else if (!strcasecmp(c->argv[1]->ptr,"bumpepoch") && c->argc == 2) {
/* CLUSTER BUMPEPOCH */
int retval = clusterBumpConfigEpochWithoutConsensus();
sds reply = sdscatprintf(sdsempty(),"+%s %llu\r\n",
(retval == C_OK) ? "BUMPED" : "STILL",
(unsigned long long) myself->configEpoch);
addReplySds(c,reply);
} else if (!strcasecmp(c->argv[1]->ptr,"info") && c->argc == 2) {
/* CLUSTER INFO */
sds info = genClusterInfoString(); /* This function is called by the function processing clients incrementally
* to detect timeouts, in order to handle the following case:
*
* 1) A client blocks with BLPOP or similar blocking operation.
* 2) The master migrates the hash slot elsewhere or turns into a slave.
* 3) The client may remain blocked forever (or up to the max timeout time)
* waiting for a key change that will never happen.
*
* If the client is found to be blocked into a hash slot this node no
* longer handles, the client is sent a redirection error, and the function
* returns 1. Otherwise 0 is returned and no operation is performed. */
int clusterRedirectBlockedClientIfNeeded(client *c) {
if (c->flags & CLIENT_BLOCKED &&
(c->bstate.btype == BLOCKED_LIST ||
c->bstate.btype == BLOCKED_ZSET ||
c->bstate.btype == BLOCKED_STREAM ||
c->bstate.btype == BLOCKED_MODULE))
{
dictEntry *de;
dictIterator *di;
/* Produce the reply protocol. */ /* If the cluster is down, unblock the client with the right error.
addReplyVerbatim(c,info,sdslen(info),"txt"); * If the cluster is configured to allow reads on cluster down, we
sdsfree(info); * still want to emit this error since a write will be required
} else if (!strcasecmp(c->argv[1]->ptr,"saveconfig") && c->argc == 2) { * to unblock them which may never come. */
int retval = clusterSaveConfig(1); if (server.cluster->state == CLUSTER_FAIL) {
clusterRedirectClient(c,NULL,0,CLUSTER_REDIR_DOWN_STATE);
return 1;
}
if (retval == 0) /* If the client is blocked on module, but not on a specific key,
addReply(c,shared.ok); * don't unblock it (except for the CLUSTER_FAIL case above). */
else if (c->bstate.btype == BLOCKED_MODULE && !moduleClientIsBlockedOnKeys(c))
addReplyErrorFormat(c,"error saving the cluster node config: %s", return 0;
strerror(errno));
} else if (!strcasecmp(c->argv[1]->ptr,"keyslot") && c->argc == 3) {
/* CLUSTER KEYSLOT <key> */
sds key = c->argv[2]->ptr;
addReplyLongLong(c,keyHashSlot(key,sdslen(key))); /* All keys must belong to the same slot, so check first key only. */
} else if (!strcasecmp(c->argv[1]->ptr,"countkeysinslot") && c->argc == 3) { di = dictGetIterator(c->bstate.keys);
/* CLUSTER COUNTKEYSINSLOT <slot> */ if ((de = dictNext(di)) != NULL) {
long long slot; robj *key = dictGetKey(de);
int slot = keyHashSlot((char*)key->ptr, sdslen(key->ptr));
clusterNode *node = server.cluster->slots[slot];
if (getLongLongFromObjectOrReply(c,c->argv[2],&slot,NULL) != C_OK) /* if the client is read-only and attempting to access key that our
return; * replica can handle, allow it. */
if (slot < 0 || slot >= CLUSTER_SLOTS) { if ((c->flags & CLIENT_READONLY) &&
addReplyError(c,"Invalid slot"); !(c->lastcmd->flags & CMD_WRITE) &&
return; nodeIsSlave(myself) && myself->slaveof == node)
{
node = myself;
} }
addReplyLongLong(c,countKeysInSlot(slot));
} else if (!strcasecmp(c->argv[1]->ptr,"getkeysinslot") && c->argc == 4) {
/* CLUSTER GETKEYSINSLOT <slot> <count> */
long long maxkeys, slot;
if (getLongLongFromObjectOrReply(c,c->argv[2],&slot,NULL) != C_OK) /* We send an error and unblock the client if:
return; * 1) The slot is unassigned, emitting a cluster down error.
if (getLongLongFromObjectOrReply(c,c->argv[3],&maxkeys,NULL) * 2) The slot is not handled by this node, nor being imported. */
!= C_OK) if (node != myself &&
return; server.cluster->importing_slots_from[slot] == NULL)
if (slot < 0 || slot >= CLUSTER_SLOTS || maxkeys < 0) { {
addReplyError(c,"Invalid slot or number of keys"); if (node == NULL) {
return; clusterRedirectClient(c,NULL,0,
CLUSTER_REDIR_DOWN_UNBOUND);
} else {
clusterRedirectClient(c,node,slot,
CLUSTER_REDIR_MOVED);
}
dictReleaseIterator(di);
return 1;
}
}
dictReleaseIterator(di);
} }
return 0;
}
/* Remove all the keys in the specified hash slot.
* The number of removed items is returned. */
unsigned int delKeysInSlot(unsigned int hashslot) {
unsigned int j = 0;
unsigned int keys_in_slot = countKeysInSlot(slot);
unsigned int numkeys = maxkeys > keys_in_slot ? keys_in_slot : maxkeys;
addReplyArrayLen(c,numkeys);
dictIterator *iter = NULL; dictIterator *iter = NULL;
dictEntry *de = NULL; dictEntry *de = NULL;
iter = dictGetIterator(server.db->dict[slot]); iter = dictGetSafeIterator(server.db->dict[hashslot]);
for (unsigned int i = 0; i < numkeys; i++) { while((de = dictNext(iter)) != NULL) {
de = dictNext(iter);
serverAssert(de != NULL);
sds sdskey = dictGetKey(de); sds sdskey = dictGetKey(de);
addReplyBulkCBuffer(c, sdskey, sdslen(sdskey)); robj *key = createStringObject(sdskey, sdslen(sdskey));
dbDelete(&server.db[0], key);
propagateDeletion(&server.db[0], key, server.lazyfree_lazy_server_del);
signalModifiedKey(NULL, &server.db[0], key);
/* The keys are not actually logically deleted from the database, just moved to another node.
* The modules needs to know that these keys are no longer available locally, so just send the
* keyspace notification to the modules, but not to clients. */
moduleNotifyKeyspaceEvent(NOTIFY_GENERIC, "del", key, server.db[0].id);
postExecutionUnitOperations();
decrRefCount(key);
j++;
server.dirty++;
} }
dictReleaseIterator(iter); dictReleaseIterator(iter);
} else if (!strcasecmp(c->argv[1]->ptr,"forget") && c->argc == 3) {
/* CLUSTER FORGET <NODE ID> */
clusterNode *n = clusterLookupNode(c->argv[2]->ptr, sdslen(c->argv[2]->ptr));
if (!n) {
if (clusterBlacklistExists((char*)c->argv[2]->ptr))
/* Already forgotten. The deletion may have been gossipped by
* another node, so we pretend it succeeded. */
addReply(c,shared.ok);
else
addReplyErrorFormat(c,"Unknown node %s", (char*)c->argv[2]->ptr);
return;
} else if (n == myself) {
addReplyError(c,"I tried hard but I can't forget myself...");
return;
} else if (nodeIsSlave(myself) && myself->slaveof == n) {
addReplyError(c,"Can't forget my master!");
return;
}
clusterBlacklistAddNode(n);
clusterDelNode(n);
clusterDoBeforeSleep(CLUSTER_TODO_UPDATE_STATE|
CLUSTER_TODO_SAVE_CONFIG);
addReply(c,shared.ok);
} else if (!strcasecmp(c->argv[1]->ptr,"replicate") && c->argc == 3) {
/* CLUSTER REPLICATE <NODE ID> */
/* Lookup the specified node in our table. */
clusterNode *n = clusterLookupNode(c->argv[2]->ptr, sdslen(c->argv[2]->ptr));
if (!n) {
addReplyErrorFormat(c,"Unknown node %s", (char*)c->argv[2]->ptr);
return;
}
/* I can't replicate myself. */
if (n == myself) {
addReplyError(c,"Can't replicate myself");
return;
}
/* Can't replicate a slave. */ return j;
if (nodeIsSlave(n)) { }
addReplyError(c,"I can only replicate a master, not a replica.");
return;
}
/* If the instance is currently a master, it should have no assigned /* -----------------------------------------------------------------------------
* slots nor keys to accept to replicate some other node. * Operation(s) on channel rax tree.
* Slaves can switch to another master without issues. */ * -------------------------------------------------------------------------- */
if (nodeIsMaster(myself) &&
(myself->numslots != 0 || dbSize(&server.db[0], DB_MAIN) != 0)) {
addReplyError(c,
"To set a master the node must be empty and "
"without assigned slots.");
return;
}
/* Set the master. */ void slotToChannelUpdate(sds channel, int add) {
clusterSetMaster(n); size_t keylen = sdslen(channel);
clusterDoBeforeSleep(CLUSTER_TODO_UPDATE_STATE|CLUSTER_TODO_SAVE_CONFIG); unsigned int hashslot = keyHashSlot(channel,keylen);
addReply(c,shared.ok); unsigned char buf[64];
} else if ((!strcasecmp(c->argv[1]->ptr,"slaves") || unsigned char *indexed = buf;
!strcasecmp(c->argv[1]->ptr,"replicas")) && c->argc == 3) {
/* CLUSTER SLAVES <NODE ID> */
/* CLUSTER REPLICAS <NODE ID> */
clusterNode *n = clusterLookupNode(c->argv[2]->ptr, sdslen(c->argv[2]->ptr));
int j;
/* Lookup the specified node in our table. */ if (keylen+2 > 64) indexed = zmalloc(keylen+2);
if (!n) { indexed[0] = (hashslot >> 8) & 0xff;
addReplyErrorFormat(c,"Unknown node %s", (char*)c->argv[2]->ptr); indexed[1] = hashslot & 0xff;
return; memcpy(indexed+2,channel,keylen);
if (add) {
raxInsert(server.cluster->slots_to_channels,indexed,keylen+2,NULL,NULL);
} else {
raxRemove(server.cluster->slots_to_channels,indexed,keylen+2,NULL);
} }
if (indexed != buf) zfree(indexed);
}
if (nodeIsSlave(n)) { void slotToChannelAdd(sds channel) {
addReplyError(c,"The specified node is not a master"); slotToChannelUpdate(channel,1);
return; }
}
/* Report TLS ports to TLS client, and report non-TLS port to non-TLS client. */ void slotToChannelDel(sds channel) {
addReplyArrayLen(c,n->numslaves); slotToChannelUpdate(channel,0);
for (j = 0; j < n->numslaves; j++) { }
sds ni = clusterGenNodeDescription(c, n->slaves[j], shouldReturnTlsInfo());
addReplyBulkCString(c,ni);
sdsfree(ni);
}
} else if (!strcasecmp(c->argv[1]->ptr,"count-failure-reports") &&
c->argc == 3)
{
/* CLUSTER COUNT-FAILURE-REPORTS <NODE ID> */
clusterNode *n = clusterLookupNode(c->argv[2]->ptr, sdslen(c->argv[2]->ptr));
if (!n) { /* Get the count of the channels for a given slot. */
addReplyErrorFormat(c,"Unknown node %s", (char*)c->argv[2]->ptr); unsigned int countChannelsInSlot(unsigned int hashslot) {
return; raxIterator iter;
} else { int j = 0;
addReplyLongLong(c,clusterNodeFailureReportsCount(n)); unsigned char indexed[2];
}
} else if (!strcasecmp(c->argv[1]->ptr,"failover") &&
(c->argc == 2 || c->argc == 3))
{
/* CLUSTER FAILOVER [FORCE|TAKEOVER] */
int force = 0, takeover = 0;
if (c->argc == 3) { indexed[0] = (hashslot >> 8) & 0xff;
if (!strcasecmp(c->argv[2]->ptr,"force")) { indexed[1] = hashslot & 0xff;
force = 1; raxStart(&iter,server.cluster->slots_to_channels);
} else if (!strcasecmp(c->argv[2]->ptr,"takeover")) { raxSeek(&iter,">=",indexed,2);
takeover = 1; while(raxNext(&iter)) {
force = 1; /* Takeover also implies force. */ if (iter.key[0] != indexed[0] || iter.key[1] != indexed[1]) break;
} else { j++;
addReplyErrorObject(c,shared.syntaxerr);
return;
}
} }
raxStop(&iter);
return j;
}
/* Check preconditions. */ int clusterNodeIsMyself(clusterNode *n) {
if (nodeIsMaster(myself)) { return n == server.cluster->myself;
addReplyError(c,"You should send CLUSTER FAILOVER to a replica"); }
return;
} else if (myself->slaveof == NULL) {
addReplyError(c,"I'm a replica but my master is unknown to me");
return;
} else if (!force &&
(nodeFailed(myself->slaveof) ||
myself->slaveof->link == NULL))
{
addReplyError(c,"Master is down or failed, "
"please use CLUSTER FAILOVER FORCE");
return;
}
resetManualFailover();
server.cluster->mf_end = mstime() + CLUSTER_MF_TIMEOUT;
if (takeover) { clusterNode* getMyClusterNode(void) {
/* A takeover does not perform any initial check. It just return server.cluster->myself;
* generates a new configuration epoch for this node without }
* consensus, claims the master's slots, and broadcast the new
* configuration. */
serverLog(LL_NOTICE,"Taking over the master (user request).");
clusterBumpConfigEpochWithoutConsensus();
clusterFailoverReplaceYourMaster();
} else if (force) {
/* If this is a forced failover, we don't need to talk with our
* master to agree about the offset. We just failover taking over
* it without coordination. */
serverLog(LL_NOTICE,"Forced failover user request accepted.");
server.cluster->mf_can_start = 1;
} else {
serverLog(LL_NOTICE,"Manual failover user request accepted.");
clusterSendMFStart(myself->slaveof);
}
addReply(c,shared.ok);
} else if (!strcasecmp(c->argv[1]->ptr,"set-config-epoch") && c->argc == 3)
{
/* CLUSTER SET-CONFIG-EPOCH <epoch>
*
* The user is allowed to set the config epoch only when a node is
* totally fresh: no config epoch, no other known node, and so forth.
* This happens at cluster creation time to start with a cluster where
* every node has a different node ID, without to rely on the conflicts
* resolution system which is too slow when a big cluster is created. */
long long epoch;
if (getLongLongFromObjectOrReply(c,c->argv[2],&epoch,NULL) != C_OK) int clusterManualFailoverTimeLimit(void) {
return; return server.cluster->mf_end;
}
if (epoch < 0) { char* getMyClusterId(void) {
addReplyErrorFormat(c,"Invalid config epoch specified: %lld",epoch); return server.cluster->myself->name;
} else if (dictSize(server.cluster->nodes) > 1) { }
addReplyError(c,"The user can assign a config epoch only when the "
"node does not know any other node.");
} else if (myself->configEpoch != 0) {
addReplyError(c,"Node config epoch is already non-zero");
} else {
myself->configEpoch = epoch;
serverLog(LL_NOTICE,
"configEpoch set to %llu via CLUSTER SET-CONFIG-EPOCH",
(unsigned long long) myself->configEpoch);
if (server.cluster->currentEpoch < (uint64_t)epoch) int getClusterSize(void) {
server.cluster->currentEpoch = epoch; return dictSize(server.cluster->nodes);
/* No need to fsync the config here since in the unlucky event }
* of a failure to persist the config, the conflict resolution code
* will assign a unique config to this node. */ char** getClusterNodesList(size_t *numnodes) {
clusterDoBeforeSleep(CLUSTER_TODO_UPDATE_STATE| size_t count = dictSize(server.cluster->nodes);
CLUSTER_TODO_SAVE_CONFIG); char **ids = zmalloc((count+1)*CLUSTER_NAMELEN);
addReply(c,shared.ok); dictIterator *di = dictGetIterator(server.cluster->nodes);
dictEntry *de;
int j = 0;
while((de = dictNext(di)) != NULL) {
clusterNode *node = dictGetVal(de);
if (node->flags & (CLUSTER_NODE_NOADDR|CLUSTER_NODE_HANDSHAKE)) continue;
ids[j] = zmalloc(CLUSTER_NAMELEN);
memcpy(ids[j],node->name,CLUSTER_NAMELEN);
j++;
} }
} else if (!strcasecmp(c->argv[1]->ptr,"reset") && *numnodes = j;
(c->argc == 2 || c->argc == 3)) ids[j] = NULL; /* Null term so that FreeClusterNodesList does not need
{ * to also get the count argument. */
/* CLUSTER RESET [SOFT|HARD] */ dictReleaseIterator(di);
int hard = 0; return ids;
}
/* Parse soft/hard argument. Default is soft. */ int nodeIsMaster(clusterNode *n) {
if (c->argc == 3) { return n->flags & CLUSTER_NODE_MASTER;
if (!strcasecmp(c->argv[2]->ptr,"hard")) { }
hard = 1;
} else if (!strcasecmp(c->argv[2]->ptr,"soft")) { int handleDebugClusterCommand(client *c) {
hard = 0; if (strcasecmp(c->argv[1]->ptr, "CLUSTERLINK") ||
} else { strcasecmp(c->argv[2]->ptr, "KILL") ||
addReplyErrorObject(c,shared.syntaxerr); c->argc != 5) {
return; return 0;
} }
if (!server.cluster_enabled) {
addReplyError(c, "Debug option only available for cluster mode enabled setup!");
return 1;
} }
/* Slaves can be reset while containing data, but not master nodes /* Find the node. */
* that must be empty. */ clusterNode *n = clusterLookupNode(c->argv[4]->ptr, sdslen(c->argv[4]->ptr));
if (nodeIsMaster(myself) && dbSize(c->db, DB_MAIN) != 0) { if (!n) {
addReplyError(c,"CLUSTER RESET can't be called with " addReplyErrorFormat(c, "Unknown node %s", (char *) c->argv[4]->ptr);
"master nodes containing keys"); return 1;
return;
} }
clusterReset(hard);
addReply(c,shared.ok); /* Terminate the link based on the direction or all. */
} else if (!strcasecmp(c->argv[1]->ptr,"links") && c->argc == 2) { if (!strcasecmp(c->argv[3]->ptr, "from")) {
/* CLUSTER LINKS */ freeClusterLink(n->inbound_link);
addReplyClusterLinksDescription(c); } else if (!strcasecmp(c->argv[3]->ptr, "to")) {
freeClusterLink(n->link);
} else if (!strcasecmp(c->argv[3]->ptr, "all")) {
freeClusterLink(n->link);
freeClusterLink(n->inbound_link);
} else { } else {
addReplySubcommandSyntaxError(c); addReplyErrorFormat(c, "Unknown direction %s", (char *) c->argv[3]->ptr);
return;
} }
} addReply(c, shared.ok);
void removeChannelsInSlot(unsigned int slot) { return 1;
unsigned int channelcount = countChannelsInSlot(slot); }
if (channelcount == 0) return;
/* Retrieve all the channels for the slot. */ int clusterNodePending(clusterNode *node) {
robj **channels = zmalloc(sizeof(robj*)*channelcount); return node->flags & (CLUSTER_NODE_NOADDR|CLUSTER_NODE_HANDSHAKE);
raxIterator iter; }
int j = 0;
unsigned char indexed[2];
indexed[0] = (slot >> 8) & 0xff; char* clusterNodeIp(clusterNode *node) {
indexed[1] = slot & 0xff; return node->ip;
raxStart(&iter,server.cluster->slots_to_channels); }
raxSeek(&iter,">=",indexed,2);
while(raxNext(&iter)) {
if (iter.key[0] != indexed[0] || iter.key[1] != indexed[1]) break;
channels[j++] = createStringObject((char*)iter.key + 2, iter.key_len - 2);
}
raxStop(&iter);
pubsubUnsubscribeShardChannels(channels, channelcount); int clusterNodeIsSlave(clusterNode *node) {
zfree(channels); return node->flags & CLUSTER_NODE_SLAVE;
} }
clusterNode *clusterNodeGetSlaveof(clusterNode *node) {
return node->slaveof;
}
/* ----------------------------------------------------------------------------- char* clusterNodeGetName(clusterNode *node) {
* Cluster functions related to serving / redirecting clients return node->name;
* -------------------------------------------------------------------------- */ }
/* The ASKING command is required after a -ASK redirection. int clusterNodeTimedOut(clusterNode *node) {
* The client should issue ASKING before to actually send the command to return nodeTimedOut(node);
* the target instance. See the Redis Cluster specification for more
* information. */
void askingCommand(client *c) {
if (server.cluster_enabled == 0) {
addReplyError(c,"This instance has cluster support disabled");
return;
}
c->flags |= CLIENT_ASKING;
addReply(c,shared.ok);
} }
/* The READONLY command is used by clients to enter the read-only mode. int clusterNodeIsFailing(clusterNode *node) {
* In this mode slaves will not redirect clients as long as clients access return nodeFailed(node);
* with read-only commands to keys that are served by the slave's master. */
void readonlyCommand(client *c) {
if (server.cluster_enabled == 0) {
addReplyError(c,"This instance has cluster support disabled");
return;
}
c->flags |= CLIENT_READONLY;
addReply(c,shared.ok);
} }
/* The READWRITE command just clears the READONLY command state. */ int clusterNodeIsNoFailover(clusterNode *node) {
void readwriteCommand(client *c) { return node->flags & CLUSTER_NODE_NOFAILOVER;
if (server.cluster_enabled == 0) {
addReplyError(c,"This instance has cluster support disabled");
return;
}
c->flags &= ~CLIENT_READONLY;
addReply(c,shared.ok);
} }
/* Return the pointer to the cluster node that is able to serve the command. const char **clusterDebugCommandHelp(void) {
* For the function to succeed the command should only target either: static const char *help[] = {
* "CLUSTERLINK KILL <to|from|all> <node-id>",
* 1) A single key (even multiple times like RPOPLPUSH mylist mylist). " Kills the link based on the direction to/from (both) with the provided node.",
* 2) Multiple keys in the same hash slot, while the slot is stable (no NULL
* resharding in progress). };
*
* On success the function returns the node that is able to serve the request.
* If the node is not 'myself' a redirection must be performed. The kind of
* redirection is specified setting the integer passed by reference
* 'error_code', which will be set to CLUSTER_REDIR_ASK or
* CLUSTER_REDIR_MOVED.
*
* When the node is 'myself' 'error_code' is set to CLUSTER_REDIR_NONE.
*
* If the command fails NULL is returned, and the reason of the failure is
* provided via 'error_code', which will be set to:
*
* CLUSTER_REDIR_CROSS_SLOT if the request contains multiple keys that
* don't belong to the same hash slot.
*
* CLUSTER_REDIR_UNSTABLE if the request contains multiple keys
* belonging to the same slot, but the slot is not stable (in migration or
* importing state, likely because a resharding is in progress).
*
* CLUSTER_REDIR_DOWN_UNBOUND if the request addresses a slot which is
* not bound to any node. In this case the cluster global state should be
* 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 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;
int multiple_keys = 0;
multiState *ms, _ms;
multiCmd mc;
int i, slot = 0, migrating_slot = 0, importing_slot = 0, missing_keys = 0,
existing_keys = 0;
/* Allow any key to be set if a module disabled cluster redirections. */ return help;
if (server.cluster_module_flags & CLUSTER_MODULE_FLAG_NO_REDIRECTION) }
return myself;
/* Set error code optimistically for the base case. */ char* clusterNodeGetShardId(clusterNode *node) {
if (error_code) *error_code = CLUSTER_REDIR_NONE; return node->shard_id;
}
/* Modules can turn off Redis Cluster redirection: this is useful int clusterCommandSpecial(client *c) {
* when writing a module that implements a completely different if (!strcasecmp(c->argv[1]->ptr,"meet") && (c->argc == 4 || c->argc == 5)) {
* distributed system. */ /* CLUSTER MEET <ip> <port> [cport] */
long long port, cport;
/* We handle all the cases as if they were EXEC commands, so we have if (getLongLongFromObject(c->argv[3], &port) != C_OK) {
* a common code path for everything */ addReplyErrorFormat(c,"Invalid base port specified: %s",
if (cmd->proc == execCommand) { (char*)c->argv[3]->ptr);
/* If CLIENT_MULTI flag is not set EXEC is just going to return an return 1;
* error. */
if (!(c->flags & CLIENT_MULTI)) return myself;
ms = &c->mstate;
} else {
/* In order to have a single codepath create a fake Multi State
* structure if the client is not in MULTI/EXEC state, this way
* we have a single codepath below. */
ms = &_ms;
_ms.commands = &mc;
_ms.count = 1;
mc.argv = argv;
mc.argc = argc;
mc.cmd = cmd;
} }
int is_pubsubshard = cmd->proc == ssubscribeCommand || if (c->argc == 5) {
cmd->proc == sunsubscribeCommand || if (getLongLongFromObject(c->argv[4], &cport) != C_OK) {
cmd->proc == spublishCommand; addReplyErrorFormat(c,"Invalid bus port specified: %s",
(char*)c->argv[4]->ptr);
/* Check that all the keys are in the same hash slot, and obtain this return 1;
* slot and the node associated. */ }
for (i = 0; i < ms->count; i++) { } else {
struct redisCommand *mcmd; cport = port + CLUSTER_PORT_INCR;
robj **margv;
int margc, numkeys, j;
keyReference *keyindex;
mcmd = ms->commands[i].cmd;
margc = ms->commands[i].argc;
margv = ms->commands[i].argv;
getKeysResult result = GETKEYS_RESULT_INIT;
numkeys = getKeysFromCommand(mcmd,margv,margc,&result);
keyindex = result.keys;
for (j = 0; j < numkeys; j++) {
robj *thiskey = margv[keyindex[j].pos];
int thisslot = keyHashSlot((char*)thiskey->ptr,
sdslen(thiskey->ptr));
if (firstkey == NULL) {
/* This is the first key we see. Check what is the slot
* and node. */
firstkey = thiskey;
slot = thisslot;
n = server.cluster->slots[slot];
/* Error: If a slot is not served, we are in "cluster down"
* state. However the state is yet to be updated, so this was
* not trapped earlier in processCommand(). Report the same
* error to the client. */
if (n == NULL) {
getKeysFreeResult(&result);
if (error_code)
*error_code = CLUSTER_REDIR_DOWN_UNBOUND;
return NULL;
} }
/* If we are migrating or importing this slot, we need to check if (clusterStartHandshake(c->argv[2]->ptr,port,cport) == 0 &&
* if we have all the keys in the request (the only way we errno == EINVAL)
* can safely serve the request, otherwise we return a TRYAGAIN
* error). To do so we set the importing/migrating state and
* increment a counter for every missing key. */
if (n == myself &&
server.cluster->migrating_slots_to[slot] != NULL)
{ {
migrating_slot = 1; addReplyErrorFormat(c,"Invalid node address specified: %s:%s",
} else if (server.cluster->importing_slots_from[slot] != NULL) { (char*)c->argv[2]->ptr, (char*)c->argv[3]->ptr);
importing_slot = 1;
}
} else { } else {
/* If it is not the first key/channel, make sure it is exactly addReply(c,shared.ok);
* the same key/channel as the first we saw. */
if (slot != thisslot) {
/* Error: multiple keys from different slots. */
getKeysFreeResult(&result);
if (error_code)
*error_code = CLUSTER_REDIR_CROSS_SLOT;
return NULL;
}
if (importing_slot && !multiple_keys && !equalStringObjects(firstkey,thiskey)) {
/* Flag this request as one with multiple different
* keys/channels when the slot is in importing state. */
multiple_keys = 1;
} }
} else if (!strcasecmp(c->argv[1]->ptr,"flushslots") && c->argc == 2) {
/* CLUSTER FLUSHSLOTS */
if (dbSize(&server.db[0], DB_MAIN) != 0) {
addReplyError(c,"DB must be empty to perform CLUSTER FLUSHSLOTS.");
return 1;
} }
clusterDelNodeSlots(myself);
clusterDoBeforeSleep(CLUSTER_TODO_UPDATE_STATE|CLUSTER_TODO_SAVE_CONFIG);
addReply(c,shared.ok);
} else if ((!strcasecmp(c->argv[1]->ptr,"addslots") ||
!strcasecmp(c->argv[1]->ptr,"delslots")) && c->argc >= 3) {
/* CLUSTER ADDSLOTS <slot> [slot] ... */
/* CLUSTER DELSLOTS <slot> [slot] ... */
int j, slot;
unsigned char *slots = zmalloc(CLUSTER_SLOTS);
int del = !strcasecmp(c->argv[1]->ptr,"delslots");
/* Migrating / Importing slot? Count keys we don't have. memset(slots,0,CLUSTER_SLOTS);
* If it is pubsubshard command, it isn't required to check /* Check that all the arguments are parseable.*/
* the channel being present or not in the node during the for (j = 2; j < c->argc; j++) {
* slot migration, the channel will be served from the source if ((slot = getSlotOrReply(c,c->argv[j])) == C_ERR) {
* node until the migration completes with CLUSTER SETSLOT <slot> zfree(slots);
* NODE <node-id>. */ return 1;
int flags = LOOKUP_NOTOUCH | LOOKUP_NOSTATS | LOOKUP_NONOTIFY | LOOKUP_NOEXPIRE;
if ((migrating_slot || importing_slot) && !is_pubsubshard)
{
if (lookupKeyReadWithFlags(&server.db[0], thiskey, flags) == NULL) missing_keys++;
else existing_keys++;
} }
} }
getKeysFreeResult(&result); /* Check that the slots are not already busy. */
for (j = 2; j < c->argc; j++) {
slot = getSlotOrReply(c,c->argv[j]);
if (checkSlotAssignmentsOrReply(c, slots, del, slot, slot) == C_ERR) {
zfree(slots);
return 1;
} }
}
clusterUpdateSlots(c, slots, del);
zfree(slots);
clusterDoBeforeSleep(CLUSTER_TODO_UPDATE_STATE|CLUSTER_TODO_SAVE_CONFIG);
addReply(c,shared.ok);
} else if ((!strcasecmp(c->argv[1]->ptr,"addslotsrange") ||
!strcasecmp(c->argv[1]->ptr,"delslotsrange")) && c->argc >= 4) {
if (c->argc % 2 == 1) {
addReplyErrorArity(c);
return 1;
}
/* CLUSTER ADDSLOTSRANGE <start slot> <end slot> [<start slot> <end slot> ...] */
/* CLUSTER DELSLOTSRANGE <start slot> <end slot> [<start slot> <end slot> ...] */
int j, startslot, endslot;
unsigned char *slots = zmalloc(CLUSTER_SLOTS);
int del = !strcasecmp(c->argv[1]->ptr,"delslotsrange");
/* No key at all in command? then we can serve the request memset(slots,0,CLUSTER_SLOTS);
* without redirections or errors in all the cases. */ /* Check that all the arguments are parseable and that all the
if (n == NULL) return myself; * slots are not already busy. */
for (j = 2; j < c->argc; j += 2) {
uint64_t cmd_flags = getCommandFlags(c); if ((startslot = getSlotOrReply(c,c->argv[j])) == C_ERR) {
/* Cluster is globally down but we got keys? We only serve the request zfree(slots);
* if it is a read command and when allow_reads_when_down is enabled. */ return 1;
if (server.cluster->state != CLUSTER_OK) {
if (is_pubsubshard) {
if (!server.cluster_allow_pubsubshard_when_down) {
if (error_code) *error_code = CLUSTER_REDIR_DOWN_STATE;
return NULL;
} }
} else if (!server.cluster_allow_reads_when_down) { if ((endslot = getSlotOrReply(c,c->argv[j+1])) == C_ERR) {
/* The cluster is configured to block commands when the zfree(slots);
* cluster is down. */ return 1;
if (error_code) *error_code = CLUSTER_REDIR_DOWN_STATE;
return NULL;
} else if (cmd_flags & CMD_WRITE) {
/* The cluster is configured to allow read only commands */
if (error_code) *error_code = CLUSTER_REDIR_DOWN_RO_STATE;
return NULL;
} else {
/* Fall through and allow the command to be executed:
* this happens when server.cluster_allow_reads_when_down is
* true and the command is not a write command */
} }
if (startslot > endslot) {
addReplyErrorFormat(c,"start slot number %d is greater than end slot number %d", startslot, endslot);
zfree(slots);
return 1;
} }
/* Return the hashslot by reference. */ if (checkSlotAssignmentsOrReply(c, slots, del, startslot, endslot) == C_ERR) {
if (hashslot) *hashslot = slot; zfree(slots);
return 1;
/* MIGRATE always works in the context of the local node if the slot
* is open (migrating or importing state). We need to be able to freely
* move keys among instances in this case. */
if ((migrating_slot || importing_slot) && cmd->proc == migrateCommand)
return myself;
/* If we don't have all the keys and we are migrating the slot, send
* an ASK redirection or TRYAGAIN. */
if (migrating_slot && missing_keys) {
/* If we have keys but we don't have all keys, we return TRYAGAIN */
if (existing_keys) {
if (error_code) *error_code = CLUSTER_REDIR_UNSTABLE;
return NULL;
} else {
if (error_code) *error_code = CLUSTER_REDIR_ASK;
return server.cluster->migrating_slots_to[slot];
} }
} }
clusterUpdateSlots(c, slots, del);
zfree(slots);
clusterDoBeforeSleep(CLUSTER_TODO_UPDATE_STATE|CLUSTER_TODO_SAVE_CONFIG);
addReply(c,shared.ok);
} else if (!strcasecmp(c->argv[1]->ptr,"setslot") && c->argc >= 4) {
/* SETSLOT 10 MIGRATING <node ID> */
/* SETSLOT 10 IMPORTING <node ID> */
/* SETSLOT 10 STABLE */
/* SETSLOT 10 NODE <node ID> */
int slot;
clusterNode *n;
/* If we are receiving the slot, and the client correctly flagged the if (nodeIsSlave(myself)) {
* request as "ASKING", we can serve the request. However if the request addReplyError(c,"Please use SETSLOT only with masters.");
* involves multiple keys and we don't have them all, the only option is return 1;
* to send a TRYAGAIN error. */
if (importing_slot &&
(c->flags & CLIENT_ASKING || cmd_flags & CMD_ASKING))
{
if (multiple_keys && missing_keys) {
if (error_code) *error_code = CLUSTER_REDIR_UNSTABLE;
return NULL;
} else {
return myself;
}
} }
/* Handle the read-only client case reading from a slave: if this if ((slot = getSlotOrReply(c, c->argv[2])) == -1) return 1;
* node is a slave and the request is about a hash slot our master
* is serving, we can reply without redirection. */
int is_write_command = (cmd_flags & CMD_WRITE) ||
(c->cmd->proc == execCommand && (c->mstate.cmd_flags & CMD_WRITE));
if (((c->flags & CLIENT_READONLY) || is_pubsubshard) &&
!is_write_command &&
nodeIsSlave(myself) &&
myself->slaveof == n)
{
return myself;
}
/* Base case: just return the right node. However if this node is not if (!strcasecmp(c->argv[3]->ptr,"migrating") && c->argc == 5) {
* myself, set error_code to MOVED since we need to issue a redirection. */ if (server.cluster->slots[slot] != myself) {
if (n != myself && error_code) *error_code = CLUSTER_REDIR_MOVED; addReplyErrorFormat(c,"I'm not the owner of hash slot %u",slot);
return n; return 1;
}
/* Send the client the right redirection code, according to error_code
* that should be set to one of CLUSTER_REDIR_* macros.
*
* If CLUSTER_REDIR_ASK or CLUSTER_REDIR_MOVED error codes
* are used, then the node 'n' should not be NULL, but should be the
* node we want to mention in the redirection. Moreover hashslot should
* be set to the hash slot that caused the redirection. */
void clusterRedirectClient(client *c, clusterNode *n, int hashslot, int error_code) {
if (error_code == CLUSTER_REDIR_CROSS_SLOT) {
addReplyError(c,"-CROSSSLOT Keys in request don't hash to the same slot");
} else if (error_code == CLUSTER_REDIR_UNSTABLE) {
/* The request spawns multiple keys in the same slot,
* but the slot is not "stable" currently as there is
* a migration or import in progress. */
addReplyError(c,"-TRYAGAIN Multiple keys request during rehashing of slot");
} else if (error_code == CLUSTER_REDIR_DOWN_STATE) {
addReplyError(c,"-CLUSTERDOWN The cluster is down");
} else if (error_code == CLUSTER_REDIR_DOWN_RO_STATE) {
addReplyError(c,"-CLUSTERDOWN The cluster is down and only accepts read commands");
} else if (error_code == CLUSTER_REDIR_DOWN_UNBOUND) {
addReplyError(c,"-CLUSTERDOWN Hash slot not served");
} else if (error_code == CLUSTER_REDIR_MOVED ||
error_code == CLUSTER_REDIR_ASK)
{
/* Report TLS ports to TLS client, and report non-TLS port to non-TLS client. */
int port = getNodeClientPort(n, shouldReturnTlsInfo());
addReplyErrorSds(c,sdscatprintf(sdsempty(),
"-%s %d %s:%d",
(error_code == CLUSTER_REDIR_ASK) ? "ASK" : "MOVED",
hashslot, getPreferredEndpoint(n), port));
} else {
serverPanic("getNodeByQuery() unknown error.");
} }
} n = clusterLookupNode(c->argv[4]->ptr, sdslen(c->argv[4]->ptr));
if (n == NULL) {
/* This function is called by the function processing clients incrementally addReplyErrorFormat(c,"I don't know about node %s",
* to detect timeouts, in order to handle the following case: (char*)c->argv[4]->ptr);
*
* 1) A client blocks with BLPOP or similar blocking operation.
* 2) The master migrates the hash slot elsewhere or turns into a slave.
* 3) The client may remain blocked forever (or up to the max timeout time)
* waiting for a key change that will never happen.
*
* If the client is found to be blocked into a hash slot this node no
* longer handles, the client is sent a redirection error, and the function
* returns 1. Otherwise 0 is returned and no operation is performed. */
int clusterRedirectBlockedClientIfNeeded(client *c) {
if (c->flags & CLIENT_BLOCKED &&
(c->bstate.btype == BLOCKED_LIST ||
c->bstate.btype == BLOCKED_ZSET ||
c->bstate.btype == BLOCKED_STREAM ||
c->bstate.btype == BLOCKED_MODULE))
{
dictEntry *de;
dictIterator *di;
/* 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; return 1;
} }
if (nodeIsSlave(n)) {
/* If the client is blocked on module, but not on a specific key, addReplyError(c,"Target node is not a master");
* don't unblock it (except for the CLUSTER_FAIL case above). */ return 1;
if (c->bstate.btype == BLOCKED_MODULE && !moduleClientIsBlockedOnKeys(c))
return 0;
/* All keys must belong to the same slot, so check first key only. */
di = dictGetIterator(c->bstate.keys);
if ((de = dictNext(di)) != NULL) {
robj *key = dictGetKey(de);
int slot = keyHashSlot((char*)key->ptr, sdslen(key->ptr));
clusterNode *node = server.cluster->slots[slot];
/* if the client is read-only and attempting to access key that our
* replica can handle, allow it. */
if ((c->flags & CLIENT_READONLY) &&
!(c->lastcmd->flags & CMD_WRITE) &&
nodeIsSlave(myself) && myself->slaveof == node)
{
node = myself;
} }
server.cluster->migrating_slots_to[slot] = n;
/* We send an error and unblock the client if: } else if (!strcasecmp(c->argv[3]->ptr,"importing") && c->argc == 5) {
* 1) The slot is unassigned, emitting a cluster down error. if (server.cluster->slots[slot] == myself) {
* 2) The slot is not handled by this node, nor being imported. */ addReplyErrorFormat(c,
if (node != myself && "I'm already the owner of hash slot %u",slot);
server.cluster->importing_slots_from[slot] == NULL) return 1;
{
if (node == NULL) {
clusterRedirectClient(c,NULL,0,
CLUSTER_REDIR_DOWN_UNBOUND);
} else {
clusterRedirectClient(c,node,slot,
CLUSTER_REDIR_MOVED);
} }
dictReleaseIterator(di); n = clusterLookupNode(c->argv[4]->ptr, sdslen(c->argv[4]->ptr));
if (n == NULL) {
addReplyErrorFormat(c,"I don't know about node %s",
(char*)c->argv[4]->ptr);
return 1; return 1;
} }
if (nodeIsSlave(n)) {
addReplyError(c,"Target node is not a master");
return 1;
} }
dictReleaseIterator(di); server.cluster->importing_slots_from[slot] = n;
} else if (!strcasecmp(c->argv[3]->ptr,"stable") && c->argc == 4) {
/* CLUSTER SETSLOT <SLOT> STABLE */
server.cluster->importing_slots_from[slot] = NULL;
server.cluster->migrating_slots_to[slot] = NULL;
} else if (!strcasecmp(c->argv[3]->ptr,"node") && c->argc == 5) {
/* CLUSTER SETSLOT <SLOT> NODE <NODE ID> */
n = clusterLookupNode(c->argv[4]->ptr, sdslen(c->argv[4]->ptr));
if (!n) {
addReplyErrorFormat(c,"Unknown node %s",
(char*)c->argv[4]->ptr);
return 1;
} }
return 0; if (nodeIsSlave(n)) {
} addReplyError(c,"Target node is not a master");
return 1;
/* Remove all the keys in the specified hash slot.
* The number of removed items is returned. */
unsigned int delKeysInSlot(unsigned int hashslot) {
unsigned int j = 0;
dictIterator *iter = NULL;
dictEntry *de = NULL;
iter = dictGetSafeIterator(server.db->dict[hashslot]);
while((de = dictNext(iter)) != NULL) {
sds sdskey = dictGetKey(de);
robj *key = createStringObject(sdskey, sdslen(sdskey));
dbDelete(&server.db[0], key);
propagateDeletion(&server.db[0], key, server.lazyfree_lazy_server_del);
signalModifiedKey(NULL, &server.db[0], key);
/* The keys are not actually logically deleted from the database, just moved to another node.
* The modules needs to know that these keys are no longer available locally, so just send the
* keyspace notification to the modules, but not to clients. */
moduleNotifyKeyspaceEvent(NOTIFY_GENERIC, "del", key, server.db[0].id);
postExecutionUnitOperations();
decrRefCount(key);
j++;
server.dirty++;
} }
dictReleaseIterator(iter); /* If this hash slot was served by 'myself' before to switch
* make sure there are no longer local keys for this hash slot. */
return j; if (server.cluster->slots[slot] == myself && n != myself) {
} if (countKeysInSlot(slot) != 0) {
addReplyErrorFormat(c,
unsigned int countKeysInSlot(unsigned int slot) { "Can't assign hashslot %d to a different node "
return dictSize(server.db->dict[slot]); "while I still hold keys for this hash slot.", slot);
} return 1;
}
}
/* If this slot is in migrating status but we have no keys
* for it assigning the slot to another node will clear
* the migrating status. */
if (countKeysInSlot(slot) == 0 &&
server.cluster->migrating_slots_to[slot])
server.cluster->migrating_slots_to[slot] = NULL;
/* ----------------------------------------------------------------------------- int slot_was_mine = server.cluster->slots[slot] == myself;
* Operation(s) on channel rax tree. clusterDelSlot(slot);
* -------------------------------------------------------------------------- */ clusterAddSlot(n,slot);
void slotToChannelUpdate(sds channel, int add) { /* If we are a master left without slots, we should turn into a
size_t keylen = sdslen(channel); * replica of the new master. */
unsigned int hashslot = keyHashSlot(channel,keylen); if (slot_was_mine &&
unsigned char buf[64]; n != myself &&
unsigned char *indexed = buf; myself->numslots == 0 &&
server.cluster_allow_replica_migration) {
serverLog(LL_NOTICE,
"Configuration change detected. Reconfiguring myself "
"as a replica of %.40s (%s)", n->name, n->human_nodename);
clusterSetMaster(n);
clusterDoBeforeSleep(CLUSTER_TODO_SAVE_CONFIG |
CLUSTER_TODO_UPDATE_STATE |
CLUSTER_TODO_FSYNC_CONFIG);
}
if (keylen+2 > 64) indexed = zmalloc(keylen+2); /* If this node was importing this slot, assigning the slot to
indexed[0] = (hashslot >> 8) & 0xff; * itself also clears the importing status. */
indexed[1] = hashslot & 0xff; if (n == myself &&
memcpy(indexed+2,channel,keylen); server.cluster->importing_slots_from[slot]) {
if (add) { /* This slot was manually migrated, set this node configEpoch
raxInsert(server.cluster->slots_to_channels,indexed,keylen+2,NULL,NULL); * to a new epoch so that the new version can be propagated
* by the cluster.
*
* Note that if this ever results in a collision with another
* node getting the same configEpoch, for example because a
* failover happens at the same time we close the slot, the
* configEpoch collision resolution will fix it assigning
* a different epoch to each node. */
if (clusterBumpConfigEpochWithoutConsensus() == C_OK) {
serverLog(LL_NOTICE,
"configEpoch updated after importing slot %d", slot);
}
server.cluster->importing_slots_from[slot] = NULL;
/* After importing this slot, let the other nodes know as
* soon as possible. */
clusterBroadcastPong(CLUSTER_BROADCAST_ALL);
}
} else { } else {
raxRemove(server.cluster->slots_to_channels,indexed,keylen+2,NULL); addReplyError(c,
"Invalid CLUSTER SETSLOT action or number of arguments. Try CLUSTER HELP");
return 1;
}
clusterDoBeforeSleep(CLUSTER_TODO_SAVE_CONFIG|CLUSTER_TODO_UPDATE_STATE);
addReply(c,shared.ok);
} else if (!strcasecmp(c->argv[1]->ptr,"bumpepoch") && c->argc == 2) {
/* CLUSTER BUMPEPOCH */
int retval = clusterBumpConfigEpochWithoutConsensus();
sds reply = sdscatprintf(sdsempty(),"+%s %llu\r\n",
(retval == C_OK) ? "BUMPED" : "STILL",
(unsigned long long) myself->configEpoch);
addReplySds(c,reply);
} else if (!strcasecmp(c->argv[1]->ptr,"saveconfig") && c->argc == 2) {
int retval = clusterSaveConfig(1);
if (retval == 0)
addReply(c,shared.ok);
else
addReplyErrorFormat(c,"error saving the cluster node config: %s",
strerror(errno));
} else if (!strcasecmp(c->argv[1]->ptr,"forget") && c->argc == 3) {
/* CLUSTER FORGET <NODE ID> */
clusterNode *n = clusterLookupNode(c->argv[2]->ptr, sdslen(c->argv[2]->ptr));
if (!n) {
if (clusterBlacklistExists((char*)c->argv[2]->ptr))
/* Already forgotten. The deletion may have been gossipped by
* another node, so we pretend it succeeded. */
addReply(c,shared.ok);
else
addReplyErrorFormat(c,"Unknown node %s", (char*)c->argv[2]->ptr);
return 1;
} else if (n == myself) {
addReplyError(c,"I tried hard but I can't forget myself...");
return 1;
} else if (nodeIsSlave(myself) && myself->slaveof == n) {
addReplyError(c,"Can't forget my master!");
return 1;
}
clusterBlacklistAddNode(n);
clusterDelNode(n);
clusterDoBeforeSleep(CLUSTER_TODO_UPDATE_STATE|
CLUSTER_TODO_SAVE_CONFIG);
addReply(c,shared.ok);
} else if (!strcasecmp(c->argv[1]->ptr,"replicate") && c->argc == 3) {
/* CLUSTER REPLICATE <NODE ID> */
/* Lookup the specified node in our table. */
clusterNode *n = clusterLookupNode(c->argv[2]->ptr, sdslen(c->argv[2]->ptr));
if (!n) {
addReplyErrorFormat(c,"Unknown node %s", (char*)c->argv[2]->ptr);
return 1;
} }
if (indexed != buf) zfree(indexed);
}
void slotToChannelAdd(sds channel) {
slotToChannelUpdate(channel,1);
}
void slotToChannelDel(sds channel) {
slotToChannelUpdate(channel,0);
}
/* Get the count of the channels for a given slot. */
unsigned int countChannelsInSlot(unsigned int hashslot) {
raxIterator iter;
int j = 0;
unsigned char indexed[2];
indexed[0] = (hashslot >> 8) & 0xff; /* I can't replicate myself. */
indexed[1] = hashslot & 0xff; if (n == myself) {
raxStart(&iter,server.cluster->slots_to_channels); addReplyError(c,"Can't replicate myself");
raxSeek(&iter,">=",indexed,2); return 1;
while(raxNext(&iter)) {
if (iter.key[0] != indexed[0] || iter.key[1] != indexed[1]) break;
j++;
} }
raxStop(&iter);
return j;
}
int clusterNodeIsMyself(clusterNode *n) {
return n == server.cluster->myself;
}
clusterNode* getMyClusterNode(void) {
return server.cluster->myself;
}
int clusterManualFailoverTimeLimit(void) {
return server.cluster->mf_end;
}
char* getMyClusterId(void) {
return server.cluster->myself->name;
}
int getClusterSize(void) { /* Can't replicate a slave. */
return dictSize(server.cluster->nodes); if (nodeIsSlave(n)) {
} addReplyError(c,"I can only replicate a master, not a replica.");
return 1;
}
char** getClusterNodesList(size_t *numnodes) { /* If the instance is currently a master, it should have no assigned
size_t count = dictSize(server.cluster->nodes); * slots nor keys to accept to replicate some other node.
char **ids = zmalloc((count+1)*CLUSTER_NAMELEN); * Slaves can switch to another master without issues. */
dictIterator *di = dictGetIterator(server.cluster->nodes); if (nodeIsMaster(myself) &&
dictEntry *de; (myself->numslots != 0 || dbSize(&server.db[0], DB_MAIN) != 0)) {
int j = 0; addReplyError(c,
while((de = dictNext(di)) != NULL) { "To set a master the node must be empty and "
clusterNode *node = dictGetVal(de); "without assigned slots.");
if (node->flags & (CLUSTER_NODE_NOADDR|CLUSTER_NODE_HANDSHAKE)) continue; return 1;
ids[j] = zmalloc(CLUSTER_NAMELEN);
memcpy(ids[j],node->name,CLUSTER_NAMELEN);
j++;
} }
*numnodes = j;
ids[j] = NULL; /* Null term so that FreeClusterNodesList does not need
* to also get the count argument. */
dictReleaseIterator(di);
return ids;
}
int nodeIsMaster(clusterNode *n) { /* Set the master. */
return n->flags & CLUSTER_NODE_MASTER; clusterSetMaster(n);
} clusterDoBeforeSleep(CLUSTER_TODO_UPDATE_STATE|CLUSTER_TODO_SAVE_CONFIG);
addReply(c,shared.ok);
} else if (!strcasecmp(c->argv[1]->ptr,"count-failure-reports") &&
c->argc == 3)
{
/* CLUSTER COUNT-FAILURE-REPORTS <NODE ID> */
clusterNode *n = clusterLookupNode(c->argv[2]->ptr, sdslen(c->argv[2]->ptr));
int handleDebugClusterCommand(client *c) { if (!n) {
if (strcasecmp(c->argv[1]->ptr, "CLUSTERLINK") || addReplyErrorFormat(c,"Unknown node %s", (char*)c->argv[2]->ptr);
strcasecmp(c->argv[2]->ptr, "KILL") || return 1;
c->argc != 5) { } else {
return 0; addReplyLongLong(c,clusterNodeFailureReportsCount(n));
} }
} else if (!strcasecmp(c->argv[1]->ptr,"failover") &&
(c->argc == 2 || c->argc == 3))
{
/* CLUSTER FAILOVER [FORCE|TAKEOVER] */
int force = 0, takeover = 0;
if (!server.cluster_enabled) { if (c->argc == 3) {
addReplyError(c, "Debug option only available for cluster mode enabled setup!"); if (!strcasecmp(c->argv[2]->ptr,"force")) {
force = 1;
} else if (!strcasecmp(c->argv[2]->ptr,"takeover")) {
takeover = 1;
force = 1; /* Takeover also implies force. */
} else {
addReplyErrorObject(c,shared.syntaxerr);
return 1; return 1;
} }
}
/* Find the node. */ /* Check preconditions. */
clusterNode *n = clusterLookupNode(c->argv[4]->ptr, sdslen(c->argv[4]->ptr)); if (nodeIsMaster(myself)) {
if (!n) { addReplyError(c,"You should send CLUSTER FAILOVER to a replica");
addReplyErrorFormat(c, "Unknown node %s", (char *) c->argv[4]->ptr); return 1;
} else if (myself->slaveof == NULL) {
addReplyError(c,"I'm a replica but my master is unknown to me");
return 1;
} else if (!force &&
(nodeFailed(myself->slaveof) ||
myself->slaveof->link == NULL))
{
addReplyError(c,"Master is down or failed, "
"please use CLUSTER FAILOVER FORCE");
return 1; return 1;
} }
resetManualFailover();
server.cluster->mf_end = mstime() + CLUSTER_MF_TIMEOUT;
/* Terminate the link based on the direction or all. */ if (takeover) {
if (!strcasecmp(c->argv[3]->ptr, "from")) { /* A takeover does not perform any initial check. It just
freeClusterLink(n->inbound_link); * generates a new configuration epoch for this node without
} else if (!strcasecmp(c->argv[3]->ptr, "to")) { * consensus, claims the master's slots, and broadcast the new
freeClusterLink(n->link); * configuration. */
} else if (!strcasecmp(c->argv[3]->ptr, "all")) { serverLog(LL_NOTICE,"Taking over the master (user request).");
freeClusterLink(n->link); clusterBumpConfigEpochWithoutConsensus();
freeClusterLink(n->inbound_link); clusterFailoverReplaceYourMaster();
} else if (force) {
/* If this is a forced failover, we don't need to talk with our
* master to agree about the offset. We just failover taking over
* it without coordination. */
serverLog(LL_NOTICE,"Forced failover user request accepted.");
server.cluster->mf_can_start = 1;
} else { } else {
addReplyErrorFormat(c, "Unknown direction %s", (char *) c->argv[3]->ptr); serverLog(LL_NOTICE,"Manual failover user request accepted.");
clusterSendMFStart(myself->slaveof);
} }
addReply(c, shared.ok); addReply(c,shared.ok);
} else if (!strcasecmp(c->argv[1]->ptr,"set-config-epoch") && c->argc == 3)
{
/* CLUSTER SET-CONFIG-EPOCH <epoch>
*
* The user is allowed to set the config epoch only when a node is
* totally fresh: no config epoch, no other known node, and so forth.
* This happens at cluster creation time to start with a cluster where
* every node has a different node ID, without to rely on the conflicts
* resolution system which is too slow when a big cluster is created. */
long long epoch;
if (getLongLongFromObjectOrReply(c,c->argv[2],&epoch,NULL) != C_OK)
return 1; return 1;
}
int clusterNodeConfirmedReachable(clusterNode *node) { if (epoch < 0) {
return !(node->flags & (CLUSTER_NODE_NOADDR|CLUSTER_NODE_HANDSHAKE)); addReplyErrorFormat(c,"Invalid config epoch specified: %lld",epoch);
} } else if (dictSize(server.cluster->nodes) > 1) {
addReplyError(c,"The user can assign a config epoch only when the "
"node does not know any other node.");
} else if (myself->configEpoch != 0) {
addReplyError(c,"Node config epoch is already non-zero");
} else {
myself->configEpoch = epoch;
serverLog(LL_NOTICE,
"configEpoch set to %llu via CLUSTER SET-CONFIG-EPOCH",
(unsigned long long) myself->configEpoch);
char* clusterNodeIp(clusterNode *node) { if (server.cluster->currentEpoch < (uint64_t)epoch)
return node->ip; server.cluster->currentEpoch = epoch;
} /* No need to fsync the config here since in the unlucky event
* of a failure to persist the config, the conflict resolution code
* will assign a unique config to this node. */
clusterDoBeforeSleep(CLUSTER_TODO_UPDATE_STATE|
CLUSTER_TODO_SAVE_CONFIG);
addReply(c,shared.ok);
}
} else if (!strcasecmp(c->argv[1]->ptr,"reset") &&
(c->argc == 2 || c->argc == 3))
{
/* CLUSTER RESET [SOFT|HARD] */
int hard = 0;
int clusterNodeIsSlave(clusterNode *node) { /* Parse soft/hard argument. Default is soft. */
return !nodeIsMaster(node); if (c->argc == 3) {
} if (!strcasecmp(c->argv[2]->ptr,"hard")) {
hard = 1;
} else if (!strcasecmp(c->argv[2]->ptr,"soft")) {
hard = 0;
} else {
addReplyErrorObject(c,shared.syntaxerr);
return 1;
}
}
clusterNode *clusterNodeGetSlaveof(clusterNode *node) { /* Slaves can be reset while containing data, but not master nodes
return node->slaveof; * that must be empty. */
} if (nodeIsMaster(myself) && dbSize(c->db, DB_MAIN) != 0) {
addReplyError(c,"CLUSTER RESET can't be called with "
"master nodes containing keys");
return 1;
}
clusterReset(hard);
addReply(c,shared.ok);
} else if (!strcasecmp(c->argv[1]->ptr,"links") && c->argc == 2) {
/* CLUSTER LINKS */
addReplyClusterLinksDescription(c);
} else {
return 0;
}
char* clusterNodeGetName(clusterNode *node) { return 1;
return node->name;
} }
int clusterNodeTimedOut(clusterNode *node) { const char** clusterCommandSpecialHelp(void) {
return nodeTimedOut(node); static const char *help[] = {
} "ADDSLOTS <slot> [<slot> ...]",
" Assign slots to current node.",
"ADDSLOTSRANGE <start slot> <end slot> [<start slot> <end slot> ...]",
" Assign slots which are between <start-slot> and <end-slot> to current node.",
"BUMPEPOCH",
" Advance the cluster config epoch.",
"COUNT-FAILURE-REPORTS <node-id>",
" Return number of failure reports for <node-id>.",
"DELSLOTS <slot> [<slot> ...]",
" Delete slots information from current node.",
"DELSLOTSRANGE <start slot> <end slot> [<start slot> <end slot> ...]",
" Delete slots information which are between <start-slot> and <end-slot> from current node.",
"FAILOVER [FORCE|TAKEOVER]",
" Promote current replica node to being a master.",
"FORGET <node-id>",
" Remove a node from the cluster.",
"FLUSHSLOTS",
" Delete current node own slots information.",
"MEET <ip> <port> [<bus-port>]",
" Connect nodes into a working cluster.",
"REPLICATE <node-id>",
" Configure current node as replica to <node-id>.",
"RESET [HARD|SOFT]",
" Reset current node (default: soft).",
"SET-CONFIG-EPOCH <epoch>",
" Set config epoch of current node.",
"SETSLOT <slot> (IMPORTING <node-id>|MIGRATING <node-id>|STABLE|NODE <node-id>)",
" Set slot state.",
"SAVECONFIG",
" Force saving cluster configuration on disk.",
"LINKS",
" Return information about all network links between this node and its peers.",
" Output format is an array where each array element is a map containing attributes of a link",
NULL
};
int clusterNodeIsFailing(clusterNode *node) { return help;
return nodeFailed(node);
} }
int clusterNodeIsNoFailover(clusterNode *node) { int getNumSlaves(clusterNode *node) {
return node->flags & CLUSTER_NODE_NOFAILOVER; return node->numslaves;
} }
char **clusterDebugCommandHelp(void) { clusterNode *getSlave(clusterNode *node, int slave_idx) {
const char *help[] = { return node->slaves[slave_idx];
"CLUSTERLINK KILL <to|from|all> <node-id>",
" Kills the link based on the direction to/from (both) with the provided node." ,
NULL
};
return help;
} }
...@@ -8967,7 +8967,7 @@ int RM_GetClusterNodeInfo(RedisModuleCtx *ctx, const char *id, char *ip, char *m ...@@ -8967,7 +8967,7 @@ int RM_GetClusterNodeInfo(RedisModuleCtx *ctx, const char *id, char *ip, char *m
UNUSED(ctx); UNUSED(ctx);
   
clusterNode *node = clusterLookupNode(id, strlen(id)); clusterNode *node = clusterLookupNode(id, strlen(id));
if (node == NULL || !clusterNodeConfirmedReachable(node)) if (node == NULL || clusterNodePending(node))
{ {
return REDISMODULE_ERR; return REDISMODULE_ERR;
} }
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment