Skip to content

Commit 3d13912

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 7536991 commit 3d13912

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
@@ -1070,6 +1070,8 @@ void createSharedObjects(void) {
10701070
"-READONLY You can't write against a read only slave.\r\n"));
10711071
shared.oomerr = createObject(REDIS_STRING,sdsnew(
10721072
"-OOM command not allowed when used memory > 'maxmemory'.\r\n"));
1073+
shared.execaborterr = createObject(REDIS_STRING,sdsnew(
1074+
"-EXECABORT Transaction discarded because of previous errors.\r\n"));
10731075
shared.space = createObject(REDIS_STRING,sdsnew(" "));
10741076
shared.colon = createObject(REDIS_STRING,sdsnew(":"));
10751077
shared.plus = createObject(REDIS_STRING,sdsnew("+"));
@@ -1590,11 +1592,13 @@ int processCommand(redisClient *c) {
15901592
* such as wrong arity, bad command name and so forth. */
15911593
c->cmd = c->lastcmd = lookupCommand(c->argv[0]->ptr);
15921594
if (!c->cmd) {
1595+
flagTransaction(c);
15931596
addReplyErrorFormat(c,"unknown command '%s'",
15941597
(char*)c->argv[0]->ptr);
15951598
return REDIS_OK;
15961599
} else if ((c->cmd->arity > 0 && c->cmd->arity != c->argc) ||
15971600
(c->argc < -c->cmd->arity)) {
1601+
flagTransaction(c);
15981602
addReplyErrorFormat(c,"wrong number of arguments for '%s' command",
15991603
c->cmd->name);
16001604
return REDIS_OK;
@@ -1603,6 +1607,7 @@ int processCommand(redisClient *c) {
16031607
/* Check if the user is authenticated */
16041608
if (server.requirepass && !c->authenticated && c->cmd->proc != authCommand)
16051609
{
1610+
flagTransaction(c);
16061611
addReplyError(c,"operation not permitted");
16071612
return REDIS_OK;
16081613
}
@@ -1638,6 +1643,7 @@ int processCommand(redisClient *c) {
16381643
if (server.maxmemory) {
16391644
int retval = freeMemoryIfNeeded();
16401645
if ((c->cmd->flags & REDIS_CMD_DENYOOM) && retval == REDIS_ERR) {
1646+
flagTransaction(c);
16411647
addReply(c, shared.oomerr);
16421648
return REDIS_OK;
16431649
}
@@ -1649,6 +1655,7 @@ int processCommand(redisClient *c) {
16491655
&& server.lastbgsave_status == REDIS_ERR &&
16501656
c->cmd->flags & REDIS_CMD_WRITE)
16511657
{
1658+
flagTransaction(c);
16521659
addReply(c, shared.bgsaveerr);
16531660
return REDIS_OK;
16541661
}
@@ -1680,6 +1687,7 @@ int processCommand(redisClient *c) {
16801687
server.repl_serve_stale_data == 0 &&
16811688
!(c->cmd->flags & REDIS_CMD_STALE))
16821689
{
1690+
flagTransaction(c);
16831691
addReply(c, shared.masterdownerr);
16841692
return REDIS_OK;
16851693
}
@@ -1701,6 +1709,7 @@ int processCommand(redisClient *c) {
17011709
c->argc == 2 &&
17021710
tolower(((char*)c->argv[1]->ptr)[0]) == 'k'))
17031711
{
1712+
flagTransaction(c);
17041713
addReply(c, shared.slowscripterr);
17051714
return REDIS_OK;
17061715
}

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,
@@ -980,6 +981,7 @@ void queueMultiCommand(redisClient *c);
980981
void touchWatchedKey(redisDb *db, robj *key);
981982
void touchWatchedKeysOnFlush(int dbid);
982983
void discardTransaction(redisClient *c);
984+
void flagTransaction(redisClient *c);
983985

984986
/* Redis object implementation */
985987
void decrRefCount(void *o);

0 commit comments

Comments
 (0)