Skip to content

Commit 41f0f92

Browse files
committed
Safer handling of MULTI/EXEC on errors.
After the transcation starts with a MULIT, the previous behavior was to return an error on problems such as maxmemory limit reached. But still to execute the transaction with the subset of queued commands on EXEC. While it is true that the client was able to check for errors distinguish QUEUED by an error reply, MULTI/EXEC in most client implementations uses pipelining for speed, so all the commands and EXEC are sent without caring about replies. With this change: 1) EXEC fails if at least one command was not queued because of an error. The EXECABORT error is used. 2) A generic error is always reported on EXEC. 3) The client DISCARDs the MULTI state after a failed EXEC, otherwise pipelining multiple transactions would be basically impossible: After a failed EXEC the next transaction would be simply queued as the tail of the previous transaction.
1 parent 5ab4151 commit 41f0f92

File tree

3 files changed

+44
-21
lines changed

3 files changed

+44
-21
lines changed

src/multi.c

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -72,10 +72,17 @@ void queueMultiCommand(redisClient *c) {
7272
void discardTransaction(redisClient *c) {
7373
freeClientMultiState(c);
7474
initClientMultiState(c);
75-
c->flags &= ~(REDIS_MULTI|REDIS_DIRTY_CAS);;
75+
c->flags &= ~(REDIS_MULTI|REDIS_DIRTY_CAS|REDIS_DIRTY_EXEC);;
7676
unwatchAllKeys(c);
7777
}
7878

79+
/* Flag the transacation as DIRTY_EXEC so that EXEC will fail.
80+
* Should be called every time there is an error while queueing a command. */
81+
void flagTransaction(redisClient *c) {
82+
if (c->flags & REDIS_MULTI)
83+
c->flags |= REDIS_DIRTY_EXEC;
84+
}
85+
7986
void multiCommand(redisClient *c) {
8087
if (c->flags & REDIS_MULTI) {
8188
addReplyError(c,"MULTI calls can not be nested");
@@ -117,14 +124,19 @@ void execCommand(redisClient *c) {
117124
return;
118125
}
119126

120-
/* Check if we need to abort the EXEC if some WATCHed key was touched.
121-
* A failed EXEC will return a multi bulk nil object. */
122-
if (c->flags & REDIS_DIRTY_CAS) {
127+
/* Check if we need to abort the EXEC because:
128+
* 1) Some WATCHed key was touched.
129+
* 2) There was a previous error while queueing commands.
130+
* A failed EXEC in the first case returns a multi bulk nil object
131+
* (technically it is not an error but a special behavior), while
132+
* in the second an EXECABORT error is returned. */
133+
if (c->flags & (REDIS_DIRTY_CAS|REDIS_DIRTY_EXEC)) {
134+
addReply(c, c->flags & REDIS_DIRTY_EXEC ? shared.execaborterr :
135+
shared.nullmultibulk);
123136
freeClientMultiState(c);
124137
initClientMultiState(c);
125-
c->flags &= ~(REDIS_MULTI|REDIS_DIRTY_CAS);
138+
c->flags &= ~(REDIS_MULTI|REDIS_DIRTY_CAS|REDIS_DIRTY_EXEC);
126139
unwatchAllKeys(c);
127-
addReply(c,shared.nullmultibulk);
128140
goto handle_monitor;
129141
}
130142

@@ -156,7 +168,7 @@ void execCommand(redisClient *c) {
156168
c->cmd = orig_cmd;
157169
freeClientMultiState(c);
158170
initClientMultiState(c);
159-
c->flags &= ~(REDIS_MULTI|REDIS_DIRTY_CAS);
171+
c->flags &= ~(REDIS_MULTI|REDIS_DIRTY_CAS|REDIS_DIRTY_EXEC);
160172
/* Make sure the EXEC command is always replicated / AOF, since we
161173
* always send the MULTI command (we can't know beforehand if the
162174
* next operations will contain at least a modification to the DB). */

src/redis.c

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1037,6 +1037,8 @@ void createSharedObjects(void) {
10371037
"-READONLY You can't write against a read only slave.\r\n"));
10381038
shared.oomerr = createObject(REDIS_STRING,sdsnew(
10391039
"-OOM command not allowed when used memory > 'maxmemory'.\r\n"));
1040+
shared.execaborterr = createObject(REDIS_STRING,sdsnew(
1041+
"-EXECABORT Transaction discarded because of previous errors.\r\n"));
10401042
shared.space = createObject(REDIS_STRING,sdsnew(" "));
10411043
shared.colon = createObject(REDIS_STRING,sdsnew(":"));
10421044
shared.plus = createObject(REDIS_STRING,sdsnew("+"));
@@ -1553,11 +1555,13 @@ int processCommand(redisClient *c) {
15531555
* such as wrong arity, bad command name and so forth. */
15541556
c->cmd = c->lastcmd = lookupCommand(c->argv[0]->ptr);
15551557
if (!c->cmd) {
1558+
flagTransaction(c);
15561559
addReplyErrorFormat(c,"unknown command '%s'",
15571560
(char*)c->argv[0]->ptr);
15581561
return REDIS_OK;
15591562
} else if ((c->cmd->arity > 0 && c->cmd->arity != c->argc) ||
15601563
(c->argc < -c->cmd->arity)) {
1564+
flagTransaction(c);
15611565
addReplyErrorFormat(c,"wrong number of arguments for '%s' command",
15621566
c->cmd->name);
15631567
return REDIS_OK;
@@ -1566,6 +1570,7 @@ int processCommand(redisClient *c) {
15661570
/* Check if the user is authenticated */
15671571
if (server.requirepass && !c->authenticated && c->cmd->proc != authCommand)
15681572
{
1573+
flagTransaction(c);
15691574
addReplyError(c,"operation not permitted");
15701575
return REDIS_OK;
15711576
}
@@ -1578,6 +1583,7 @@ int processCommand(redisClient *c) {
15781583
if (server.maxmemory) {
15791584
int retval = freeMemoryIfNeeded();
15801585
if ((c->cmd->flags & REDIS_CMD_DENYOOM) && retval == REDIS_ERR) {
1586+
flagTransaction(c);
15811587
addReply(c, shared.oomerr);
15821588
return REDIS_OK;
15831589
}
@@ -1589,6 +1595,7 @@ int processCommand(redisClient *c) {
15891595
&& server.lastbgsave_status == REDIS_ERR &&
15901596
c->cmd->flags & REDIS_CMD_WRITE)
15911597
{
1598+
flagTransaction(c);
15921599
addReply(c, shared.bgsaveerr);
15931600
return REDIS_OK;
15941601
}
@@ -1620,6 +1627,7 @@ int processCommand(redisClient *c) {
16201627
server.repl_serve_stale_data == 0 &&
16211628
!(c->cmd->flags & REDIS_CMD_STALE))
16221629
{
1630+
flagTransaction(c);
16231631
addReply(c, shared.masterdownerr);
16241632
return REDIS_OK;
16251633
}
@@ -1641,6 +1649,7 @@ int processCommand(redisClient *c) {
16411649
c->argc == 2 &&
16421650
tolower(((char*)c->argv[1]->ptr)[0]) == 'k'))
16431651
{
1652+
flagTransaction(c);
16441653
addReply(c, shared.slowscripterr);
16451654
return REDIS_OK;
16461655
}

src/redis.h

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -169,19 +169,20 @@
169169
#define REDIS_AOF_WAIT_REWRITE 2 /* AOF waits rewrite to start appending */
170170

171171
/* Client flags */
172-
#define REDIS_SLAVE 1 /* This client is a slave server */
173-
#define REDIS_MASTER 2 /* This client is a master server */
174-
#define REDIS_MONITOR 4 /* This client is a slave monitor, see MONITOR */
175-
#define REDIS_MULTI 8 /* This client is in a MULTI context */
176-
#define REDIS_BLOCKED 16 /* The client is waiting in a blocking operation */
177-
#define REDIS_DIRTY_CAS 64 /* Watched keys modified. EXEC will fail. */
178-
#define REDIS_CLOSE_AFTER_REPLY 128 /* Close after writing entire reply. */
179-
#define REDIS_UNBLOCKED 256 /* This client was unblocked and is stored in
180-
server.unblocked_clients */
181-
#define REDIS_LUA_CLIENT 512 /* This is a non connected client used by Lua */
182-
#define REDIS_ASKING 1024 /* Client issued the ASKING command */
183-
#define REDIS_CLOSE_ASAP 2048 /* Close this client ASAP */
184-
#define REDIS_UNIX_SOCKET 4096 /* Client connected via Unix domain socket */
172+
#define REDIS_SLAVE (1<<0) /* This client is a slave server */
173+
#define REDIS_MASTER (1<<1) /* This client is a master server */
174+
#define REDIS_MONITOR (1<<2) /* This client is a slave monitor, see MONITOR */
175+
#define REDIS_MULTI (1<<3) /* This client is in a MULTI context */
176+
#define REDIS_BLOCKED (1<<4) /* The client is waiting in a blocking operation */
177+
#define REDIS_DIRTY_CAS (1<<5) /* Watched keys modified. EXEC will fail. */
178+
#define REDIS_CLOSE_AFTER_REPLY (1<<6) /* Close after writing entire reply. */
179+
#define REDIS_UNBLOCKED (1<<7) /* This client was unblocked and is stored in
180+
server.unblocked_clients */
181+
#define REDIS_LUA_CLIENT (1<<8) /* This is a non connected client used by Lua */
182+
#define REDIS_ASKING (1<<9) /* Client issued the ASKING command */
183+
#define REDIS_CLOSE_ASAP (1<<10)/* Close this client ASAP */
184+
#define REDIS_UNIX_SOCKET (1<<11) /* Client connected via Unix domain socket */
185+
#define REDIS_DIRTY_EXEC (1<<12) /* EXEC will fail for errors while queueing */
185186

186187
/* Client request types */
187188
#define REDIS_REQ_INLINE 1
@@ -425,7 +426,7 @@ struct sharedObjectsStruct {
425426
*colon, *nullbulk, *nullmultibulk, *queued,
426427
*emptymultibulk, *wrongtypeerr, *nokeyerr, *syntaxerr, *sameobjecterr,
427428
*outofrangeerr, *noscripterr, *loadingerr, *slowscripterr, *bgsaveerr,
428-
*masterdownerr, *roslaveerr,
429+
*masterdownerr, *roslaveerr, *execaborterr,
429430
*oomerr, *plus, *messagebulk, *pmessagebulk, *subscribebulk,
430431
*unsubscribebulk, *psubscribebulk, *punsubscribebulk, *del, *rpop, *lpop,
431432
*lpush,
@@ -845,6 +846,7 @@ void queueMultiCommand(redisClient *c);
845846
void touchWatchedKey(redisDb *db, robj *key);
846847
void touchWatchedKeysOnFlush(int dbid);
847848
void discardTransaction(redisClient *c);
849+
void flagTransaction(redisClient *c);
848850

849851
/* Redis object implementation */
850852
void decrRefCount(void *o);

0 commit comments

Comments
 (0)