mirror of
https://codeberg.org/redict/redict.git
synced 2025-01-23 16:48:27 -05:00
d87a0d0286
Blocking command should not be used with MULTI, LUA, and RM_Call. This is because, the caller, who executes the command in this context, expects a reply. Today, LUA and MULTI have a special (and different) treatment to blocking commands: LUA - Most commands are marked with no-script flag which are checked when executing and command from LUA, commands that are not marked (like XREAD) verify that their blocking mode is not used inside LUA (by checking the CLIENT_LUA client flag). MULTI - Command that is going to block, first verify that the client is not inside multi (by checking the CLIENT_MULTI client flag). If the client is inside multi, they return a result which is a match to the empty key with no timeout (for example blpop inside MULTI will act as lpop) For modules that perform RM_Call with blocking command, the returned results type is REDISMODULE_REPLY_UNKNOWN and the caller can not really know what happened. Disadvantages of the current state are: No unified approach, LUA, MULTI, and RM_Call, each has a different treatment Module can not safely execute blocking command (and get reply or error). Though It is true that modules are not like LUA or MULTI and should be smarter not to execute blocking commands on RM_Call, sometimes you want to execute a command base on client input (for example if you create a module that provides a new scripting language like javascript or python). While modules (on modules command) can check for REDISMODULE_CTX_FLAGS_LUA or REDISMODULE_CTX_FLAGS_MULTI to know not to block the client, there is no way to check if the command came from another module using RM_Call. So there is no way for a module to know not to block another module RM_Call execution. This commit adds a way to unify the treatment for blocking clients by introducing a new CLIENT_DENY_BLOCKING client flag. On LUA, MULTI, and RM_Call the new flag turned on to signify that the client should not be blocked. A blocking command verifies that the flag is turned off before blocking. If a blocking command sees that the CLIENT_DENY_BLOCKING flag is on, it's not blocking and return results which are matches to empty key with no timeout (as MULTI does today). The new flag is checked on the following commands: List blocking commands: BLPOP, BRPOP, BRPOPLPUSH, BLMOVE, Zset blocking commands: BZPOPMIN, BZPOPMAX Stream blocking commands: XREAD, XREADGROUP SUBSCRIBE, PSUBSCRIBE, MONITOR In addition, the new flag is turned on inside the AOF client, we do not want to block the AOF client to prevent deadlocks and commands ordering issues (and there is also an existing assert in the code that verifies it). To keep backward compatibility on LUA, all the no-script flags on existing commands were kept untouched. In addition, a LUA special treatment on XREAD and XREADGROUP was kept. To keep backward compatibility on MULTI (which today allows SUBSCRIBE, and PSUBSCRIBE). We added a special treatment on those commands to allow executing them on MULTI. The only backward compatibility issue that this PR introduces is that now MONITOR is not allowed inside MULTI. Tests were added to verify blocking commands are not blocking the client on LUA, MULTI, or RM_Call. Tests were added to verify the module can check for CLIENT_DENY_BLOCKING flag. Co-authored-by: Oran Agra <oran@redislabs.com> Co-authored-by: Itamar Haber <itamar@redislabs.com>
399 lines
14 KiB
C
399 lines
14 KiB
C
/*
|
|
* Copyright (c) 2009-2012, Salvatore Sanfilippo <antirez at gmail dot com>
|
|
* All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions are met:
|
|
*
|
|
* * Redistributions of source code must retain the above copyright notice,
|
|
* this list of conditions and the following disclaimer.
|
|
* * Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
* * Neither the name of Redis nor the names of its contributors may be used
|
|
* to endorse or promote products derived from this software without
|
|
* specific prior written permission.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
|
|
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
* POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
#include "server.h"
|
|
|
|
/* ================================ MULTI/EXEC ============================== */
|
|
|
|
/* Client state initialization for MULTI/EXEC */
|
|
void initClientMultiState(client *c) {
|
|
c->mstate.commands = NULL;
|
|
c->mstate.count = 0;
|
|
c->mstate.cmd_flags = 0;
|
|
c->mstate.cmd_inv_flags = 0;
|
|
}
|
|
|
|
/* Release all the resources associated with MULTI/EXEC state */
|
|
void freeClientMultiState(client *c) {
|
|
int j;
|
|
|
|
for (j = 0; j < c->mstate.count; j++) {
|
|
int i;
|
|
multiCmd *mc = c->mstate.commands+j;
|
|
|
|
for (i = 0; i < mc->argc; i++)
|
|
decrRefCount(mc->argv[i]);
|
|
zfree(mc->argv);
|
|
}
|
|
zfree(c->mstate.commands);
|
|
}
|
|
|
|
/* Add a new command into the MULTI commands queue */
|
|
void queueMultiCommand(client *c) {
|
|
multiCmd *mc;
|
|
int j;
|
|
|
|
/* No sense to waste memory if the transaction is already aborted.
|
|
* this is useful in case client sends these in a pipeline, or doesn't
|
|
* bother to read previous responses and didn't notice the multi was already
|
|
* aborted. */
|
|
if (c->flags & CLIENT_DIRTY_EXEC)
|
|
return;
|
|
|
|
c->mstate.commands = zrealloc(c->mstate.commands,
|
|
sizeof(multiCmd)*(c->mstate.count+1));
|
|
mc = c->mstate.commands+c->mstate.count;
|
|
mc->cmd = c->cmd;
|
|
mc->argc = c->argc;
|
|
mc->argv = zmalloc(sizeof(robj*)*c->argc);
|
|
memcpy(mc->argv,c->argv,sizeof(robj*)*c->argc);
|
|
for (j = 0; j < c->argc; j++)
|
|
incrRefCount(mc->argv[j]);
|
|
c->mstate.count++;
|
|
c->mstate.cmd_flags |= c->cmd->flags;
|
|
c->mstate.cmd_inv_flags |= ~c->cmd->flags;
|
|
}
|
|
|
|
void discardTransaction(client *c) {
|
|
freeClientMultiState(c);
|
|
initClientMultiState(c);
|
|
c->flags &= ~(CLIENT_MULTI|CLIENT_DIRTY_CAS|CLIENT_DIRTY_EXEC);
|
|
unwatchAllKeys(c);
|
|
}
|
|
|
|
/* Flag the transaction as DIRTY_EXEC so that EXEC will fail.
|
|
* Should be called every time there is an error while queueing a command. */
|
|
void flagTransaction(client *c) {
|
|
if (c->flags & CLIENT_MULTI)
|
|
c->flags |= CLIENT_DIRTY_EXEC;
|
|
}
|
|
|
|
void multiCommand(client *c) {
|
|
if (c->flags & CLIENT_MULTI) {
|
|
addReplyError(c,"MULTI calls can not be nested");
|
|
return;
|
|
}
|
|
c->flags |= CLIENT_MULTI;
|
|
|
|
addReply(c,shared.ok);
|
|
}
|
|
|
|
void discardCommand(client *c) {
|
|
if (!(c->flags & CLIENT_MULTI)) {
|
|
addReplyError(c,"DISCARD without MULTI");
|
|
return;
|
|
}
|
|
discardTransaction(c);
|
|
addReply(c,shared.ok);
|
|
}
|
|
|
|
/* Send a MULTI command to all the slaves and AOF file. Check the execCommand
|
|
* implementation for more information. */
|
|
void execCommandPropagateMulti(client *c) {
|
|
propagate(server.multiCommand,c->db->id,&shared.multi,1,
|
|
PROPAGATE_AOF|PROPAGATE_REPL);
|
|
}
|
|
|
|
void execCommandPropagateExec(client *c) {
|
|
propagate(server.execCommand,c->db->id,&shared.exec,1,
|
|
PROPAGATE_AOF|PROPAGATE_REPL);
|
|
}
|
|
|
|
/* Aborts a transaction, with a specific error message.
|
|
* The transaction is always aboarted with -EXECABORT so that the client knows
|
|
* the server exited the multi state, but the actual reason for the abort is
|
|
* included too.
|
|
* Note: 'error' may or may not end with \r\n. see addReplyErrorFormat. */
|
|
void execCommandAbort(client *c, sds error) {
|
|
discardTransaction(c);
|
|
|
|
if (error[0] == '-') error++;
|
|
addReplyErrorFormat(c, "-EXECABORT Transaction discarded because of: %s", error);
|
|
|
|
/* Send EXEC to clients waiting data from MONITOR. We did send a MULTI
|
|
* already, and didn't send any of the queued commands, now we'll just send
|
|
* EXEC so it is clear that the transaction is over. */
|
|
if (listLength(server.monitors) && !server.loading)
|
|
replicationFeedMonitors(c,server.monitors,c->db->id,c->argv,c->argc);
|
|
}
|
|
|
|
void execCommand(client *c) {
|
|
int j;
|
|
robj **orig_argv;
|
|
int orig_argc;
|
|
struct redisCommand *orig_cmd;
|
|
int must_propagate = 0; /* Need to propagate MULTI/EXEC to AOF / slaves? */
|
|
int was_master = server.masterhost == NULL;
|
|
|
|
if (!(c->flags & CLIENT_MULTI)) {
|
|
addReplyError(c,"EXEC without MULTI");
|
|
return;
|
|
}
|
|
|
|
/* Check if we need to abort the EXEC because:
|
|
* 1) Some WATCHed key was touched.
|
|
* 2) There was a previous error while queueing commands.
|
|
* A failed EXEC in the first case returns a multi bulk nil object
|
|
* (technically it is not an error but a special behavior), while
|
|
* in the second an EXECABORT error is returned. */
|
|
if (c->flags & (CLIENT_DIRTY_CAS|CLIENT_DIRTY_EXEC)) {
|
|
addReply(c, c->flags & CLIENT_DIRTY_EXEC ? shared.execaborterr :
|
|
shared.nullarray[c->resp]);
|
|
discardTransaction(c);
|
|
goto handle_monitor;
|
|
}
|
|
|
|
uint64_t old_flags = c->flags;
|
|
|
|
/* we do not want to allow blocking commands inside multi */
|
|
c->flags |= CLIENT_DENY_BLOCKING;
|
|
|
|
/* Exec all the queued commands */
|
|
unwatchAllKeys(c); /* Unwatch ASAP otherwise we'll waste CPU cycles */
|
|
orig_argv = c->argv;
|
|
orig_argc = c->argc;
|
|
orig_cmd = c->cmd;
|
|
addReplyArrayLen(c,c->mstate.count);
|
|
for (j = 0; j < c->mstate.count; j++) {
|
|
c->argc = c->mstate.commands[j].argc;
|
|
c->argv = c->mstate.commands[j].argv;
|
|
c->cmd = c->mstate.commands[j].cmd;
|
|
|
|
/* Propagate a MULTI request once we encounter the first command which
|
|
* is not readonly nor an administrative one.
|
|
* This way we'll deliver the MULTI/..../EXEC block as a whole and
|
|
* both the AOF and the replication link will have the same consistency
|
|
* and atomicity guarantees. */
|
|
if (!must_propagate &&
|
|
!server.loading &&
|
|
!(c->cmd->flags & (CMD_READONLY|CMD_ADMIN)))
|
|
{
|
|
execCommandPropagateMulti(c);
|
|
must_propagate = 1;
|
|
}
|
|
|
|
int acl_keypos;
|
|
int acl_retval = ACLCheckCommandPerm(c,&acl_keypos);
|
|
if (acl_retval != ACL_OK) {
|
|
addACLLogEntry(c,acl_retval,acl_keypos,NULL);
|
|
addReplyErrorFormat(c,
|
|
"-NOPERM ACLs rules changed between the moment the "
|
|
"transaction was accumulated and the EXEC call. "
|
|
"This command is no longer allowed for the "
|
|
"following reason: %s",
|
|
(acl_retval == ACL_DENIED_CMD) ?
|
|
"no permission to execute the command or subcommand" :
|
|
"no permission to touch the specified keys");
|
|
} else {
|
|
call(c,server.loading ? CMD_CALL_NONE : CMD_CALL_FULL);
|
|
serverAssert((c->flags & CLIENT_BLOCKED) == 0);
|
|
}
|
|
|
|
/* Commands may alter argc/argv, restore mstate. */
|
|
c->mstate.commands[j].argc = c->argc;
|
|
c->mstate.commands[j].argv = c->argv;
|
|
c->mstate.commands[j].cmd = c->cmd;
|
|
}
|
|
|
|
// restore old DENY_BLOCKING value
|
|
if (!(old_flags & CLIENT_DENY_BLOCKING))
|
|
c->flags &= ~CLIENT_DENY_BLOCKING;
|
|
|
|
c->argv = orig_argv;
|
|
c->argc = orig_argc;
|
|
c->cmd = orig_cmd;
|
|
discardTransaction(c);
|
|
|
|
/* Make sure the EXEC command will be propagated as well if MULTI
|
|
* was already propagated. */
|
|
if (must_propagate) {
|
|
int is_master = server.masterhost == NULL;
|
|
server.dirty++;
|
|
/* If inside the MULTI/EXEC block this instance was suddenly
|
|
* switched from master to slave (using the SLAVEOF command), the
|
|
* initial MULTI was propagated into the replication backlog, but the
|
|
* rest was not. We need to make sure to at least terminate the
|
|
* backlog with the final EXEC. */
|
|
if (server.repl_backlog && was_master && !is_master) {
|
|
char *execcmd = "*1\r\n$4\r\nEXEC\r\n";
|
|
feedReplicationBacklog(execcmd,strlen(execcmd));
|
|
}
|
|
}
|
|
|
|
handle_monitor:
|
|
/* Send EXEC to clients waiting data from MONITOR. We do it here
|
|
* since the natural order of commands execution is actually:
|
|
* MUTLI, EXEC, ... commands inside transaction ...
|
|
* Instead EXEC is flagged as CMD_SKIP_MONITOR in the command
|
|
* table, and we do it here with correct ordering. */
|
|
if (listLength(server.monitors) && !server.loading)
|
|
replicationFeedMonitors(c,server.monitors,c->db->id,c->argv,c->argc);
|
|
}
|
|
|
|
/* ===================== WATCH (CAS alike for MULTI/EXEC) ===================
|
|
*
|
|
* The implementation uses a per-DB hash table mapping keys to list of clients
|
|
* WATCHing those keys, so that given a key that is going to be modified
|
|
* we can mark all the associated clients as dirty.
|
|
*
|
|
* Also every client contains a list of WATCHed keys so that's possible to
|
|
* un-watch such keys when the client is freed or when UNWATCH is called. */
|
|
|
|
/* In the client->watched_keys list we need to use watchedKey structures
|
|
* as in order to identify a key in Redis we need both the key name and the
|
|
* DB */
|
|
typedef struct watchedKey {
|
|
robj *key;
|
|
redisDb *db;
|
|
} watchedKey;
|
|
|
|
/* Watch for the specified key */
|
|
void watchForKey(client *c, robj *key) {
|
|
list *clients = NULL;
|
|
listIter li;
|
|
listNode *ln;
|
|
watchedKey *wk;
|
|
|
|
/* Check if we are already watching for this key */
|
|
listRewind(c->watched_keys,&li);
|
|
while((ln = listNext(&li))) {
|
|
wk = listNodeValue(ln);
|
|
if (wk->db == c->db && equalStringObjects(key,wk->key))
|
|
return; /* Key already watched */
|
|
}
|
|
/* This key is not already watched in this DB. Let's add it */
|
|
clients = dictFetchValue(c->db->watched_keys,key);
|
|
if (!clients) {
|
|
clients = listCreate();
|
|
dictAdd(c->db->watched_keys,key,clients);
|
|
incrRefCount(key);
|
|
}
|
|
listAddNodeTail(clients,c);
|
|
/* Add the new key to the list of keys watched by this client */
|
|
wk = zmalloc(sizeof(*wk));
|
|
wk->key = key;
|
|
wk->db = c->db;
|
|
incrRefCount(key);
|
|
listAddNodeTail(c->watched_keys,wk);
|
|
}
|
|
|
|
/* Unwatch all the keys watched by this client. To clean the EXEC dirty
|
|
* flag is up to the caller. */
|
|
void unwatchAllKeys(client *c) {
|
|
listIter li;
|
|
listNode *ln;
|
|
|
|
if (listLength(c->watched_keys) == 0) return;
|
|
listRewind(c->watched_keys,&li);
|
|
while((ln = listNext(&li))) {
|
|
list *clients;
|
|
watchedKey *wk;
|
|
|
|
/* Lookup the watched key -> clients list and remove the client
|
|
* from the list */
|
|
wk = listNodeValue(ln);
|
|
clients = dictFetchValue(wk->db->watched_keys, wk->key);
|
|
serverAssertWithInfo(c,NULL,clients != NULL);
|
|
listDelNode(clients,listSearchKey(clients,c));
|
|
/* Kill the entry at all if this was the only client */
|
|
if (listLength(clients) == 0)
|
|
dictDelete(wk->db->watched_keys, wk->key);
|
|
/* Remove this watched key from the client->watched list */
|
|
listDelNode(c->watched_keys,ln);
|
|
decrRefCount(wk->key);
|
|
zfree(wk);
|
|
}
|
|
}
|
|
|
|
/* "Touch" a key, so that if this key is being WATCHed by some client the
|
|
* next EXEC will fail. */
|
|
void touchWatchedKey(redisDb *db, robj *key) {
|
|
list *clients;
|
|
listIter li;
|
|
listNode *ln;
|
|
|
|
if (dictSize(db->watched_keys) == 0) return;
|
|
clients = dictFetchValue(db->watched_keys, key);
|
|
if (!clients) return;
|
|
|
|
/* Mark all the clients watching this key as CLIENT_DIRTY_CAS */
|
|
/* Check if we are already watching for this key */
|
|
listRewind(clients,&li);
|
|
while((ln = listNext(&li))) {
|
|
client *c = listNodeValue(ln);
|
|
|
|
c->flags |= CLIENT_DIRTY_CAS;
|
|
}
|
|
}
|
|
|
|
/* On FLUSHDB or FLUSHALL all the watched keys that are present before the
|
|
* flush but will be deleted as effect of the flushing operation should
|
|
* be touched. "dbid" is the DB that's getting the flush. -1 if it is
|
|
* a FLUSHALL operation (all the DBs flushed). */
|
|
void touchWatchedKeysOnFlush(int dbid) {
|
|
listIter li1, li2;
|
|
listNode *ln;
|
|
|
|
/* For every client, check all the waited keys */
|
|
listRewind(server.clients,&li1);
|
|
while((ln = listNext(&li1))) {
|
|
client *c = listNodeValue(ln);
|
|
listRewind(c->watched_keys,&li2);
|
|
while((ln = listNext(&li2))) {
|
|
watchedKey *wk = listNodeValue(ln);
|
|
|
|
/* For every watched key matching the specified DB, if the
|
|
* key exists, mark the client as dirty, as the key will be
|
|
* removed. */
|
|
if (dbid == -1 || wk->db->id == dbid) {
|
|
if (dictFind(wk->db->dict, wk->key->ptr) != NULL)
|
|
c->flags |= CLIENT_DIRTY_CAS;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void watchCommand(client *c) {
|
|
int j;
|
|
|
|
if (c->flags & CLIENT_MULTI) {
|
|
addReplyError(c,"WATCH inside MULTI is not allowed");
|
|
return;
|
|
}
|
|
for (j = 1; j < c->argc; j++)
|
|
watchForKey(c,c->argv[j]);
|
|
addReply(c,shared.ok);
|
|
}
|
|
|
|
void unwatchCommand(client *c) {
|
|
unwatchAllKeys(c);
|
|
c->flags &= (~CLIENT_DIRTY_CAS);
|
|
addReply(c,shared.ok);
|
|
}
|