c->reply = listCreate();
listSetFreeMethod(c->reply,decrRefCount);
listSetDupMethod(c->reply,dupClientReplyValue);
- c->blocking_keys = NULL;
- c->blocking_keys_num = 0;
+ c->bpop.keys = NULL;
+ c->bpop.count = 0;
+ c->bpop.timeout = 0;
+ c->bpop.target = NULL;
c->io_keys = listCreate();
c->watched_keys = listCreate();
listSetFreeMethod(c->io_keys,decrRefCount);
redisLog(REDIS_VERBOSE,"Closing idle client");
freeClient(c);
} else if (c->flags & REDIS_BLOCKED) {
- if (c->blockingto != 0 && c->blockingto < now) {
+ if (c->bpop.timeout != 0 && c->bpop.timeout < now) {
addReply(c,shared.nullmultibulk);
unblockClientWaitingData(c);
}
{"rpop",rpopCommand,2,0,NULL,1,1,1},
{"lpop",lpopCommand,2,0,NULL,1,1,1},
{"brpop",brpopCommand,-3,0,NULL,1,1,1},
+ {"brpoplpush",brpoplpushCommand,4,REDIS_CMD_DENYOOM,NULL,1,2,1},
{"blpop",blpopCommand,-3,0,NULL,1,1,1},
{"llen",llenCommand,2,0,NULL,1,1,1},
{"lindex",lindexCommand,3,0,NULL,1,1,1},
{"lrange",lrangeCommand,4,0,NULL,1,1,1},
{"ltrim",ltrimCommand,4,0,NULL,1,1,1},
{"lrem",lremCommand,4,0,NULL,1,1,1},
- {"rpoplpush",rpoplpushcommand,3,REDIS_CMD_DENYOOM,NULL,1,2,1},
+ {"rpoplpush",rpoplpushCommand,3,REDIS_CMD_DENYOOM,NULL,1,2,1},
{"sadd",saddCommand,3,REDIS_CMD_DENYOOM,NULL,1,1,1},
{"srem",sremCommand,3,0,NULL,1,1,1},
{"smove",smoveCommand,4,0,NULL,1,2,1},
int count; /* Total number of MULTI commands */
} multiState;
+typedef struct blockingState {
+ robj **keys; /* The key we are waiting to terminate a blocking
+ * operation such as BLPOP. Otherwise NULL. */
+ int count; /* Number of blocking keys */
+ time_t timeout; /* Blocking operation timeout. If UNIX current time
+ * is >= timeout then the operation timed out. */
+ robj *target; /* The key that should receive the element,
+ * for BRPOPLPUSH. */
+} blockingState;
+
/* With multiplexing we need to take per-clinet state.
* Clients are taken in a liked list. */
typedef struct redisClient {
long repldboff; /* replication DB file offset */
off_t repldbsize; /* replication DB file size */
multiState mstate; /* MULTI/EXEC state */
- robj **blocking_keys; /* The key we are waiting to terminate a blocking
- * operation such as BLPOP. Otherwise NULL. */
- int blocking_keys_num; /* Number of blocking keys */
- time_t blockingto; /* Blocking operation timeout. If UNIX current time
- * is >= blockingto then the operation timed out. */
+ blockingState bpop; /* blocking state */
list *io_keys; /* Keys this client is waiting to be loaded from the
* swap file in order to continue. */
list *watched_keys; /* Keys WATCHED for MULTI/EXEC CAS */
void flushallCommand(redisClient *c);
void sortCommand(redisClient *c);
void lremCommand(redisClient *c);
-void rpoplpushcommand(redisClient *c);
+void rpoplpushCommand(redisClient *c);
void infoCommand(redisClient *c);
void mgetCommand(redisClient *c);
void monitorCommand(redisClient *c);
void discardCommand(redisClient *c);
void blpopCommand(redisClient *c);
void brpopCommand(redisClient *c);
+void brpoplpushCommand(redisClient *c);
void appendCommand(redisClient *c);
void substrCommand(redisClient *c);
void strlenCommand(redisClient *c);
* since the element is not just returned but pushed against another list
* as well. This command was originally proposed by Ezra Zygmuntowicz.
*/
-void rpoplpushcommand(redisClient *c) {
+void rpoplpushCommand(redisClient *c) {
robj *sobj, *value;
if ((sobj = lookupKeyWriteOrReply(c,c->argv[1],shared.nullbulk)) == NULL ||
checkType(c,sobj,REDIS_LIST)) return;
/* Set a client in blocking mode for the specified key, with the specified
* timeout */
-void blockForKeys(redisClient *c, robj **keys, int numkeys, time_t timeout) {
+void blockForKeys(redisClient *c, robj **keys, int numkeys, time_t timeout, robj *target) {
dictEntry *de;
list *l;
int j;
- c->blocking_keys = zmalloc(sizeof(robj*)*numkeys);
- c->blocking_keys_num = numkeys;
- c->blockingto = timeout;
+ c->bpop.keys = zmalloc(sizeof(robj*)*numkeys);
+ c->bpop.count = numkeys;
+ c->bpop.timeout = timeout;
+ c->bpop.target = target;
+
+ if (target != NULL) {
+ incrRefCount(target);
+ }
+
for (j = 0; j < numkeys; j++) {
/* Add the key in the client structure, to map clients -> keys */
- c->blocking_keys[j] = keys[j];
+ c->bpop.keys[j] = keys[j];
incrRefCount(keys[j]);
/* And in the other "side", to map keys -> clients */
list *l;
int j;
- redisAssert(c->blocking_keys != NULL);
+ redisAssert(c->bpop.keys != NULL);
/* The client may wait for multiple keys, so unblock it for every key. */
- for (j = 0; j < c->blocking_keys_num; j++) {
+ for (j = 0; j < c->bpop.count; j++) {
/* Remove this client from the list of clients waiting for this key. */
- de = dictFind(c->db->blocking_keys,c->blocking_keys[j]);
+ de = dictFind(c->db->blocking_keys,c->bpop.keys[j]);
redisAssert(de != NULL);
l = dictGetEntryVal(de);
listDelNode(l,listSearchKey(l,c));
/* If the list is empty we need to remove it to avoid wasting memory */
if (listLength(l) == 0)
- dictDelete(c->db->blocking_keys,c->blocking_keys[j]);
- decrRefCount(c->blocking_keys[j]);
+ dictDelete(c->db->blocking_keys,c->bpop.keys[j]);
+ decrRefCount(c->bpop.keys[j]);
}
+
/* Cleanup the client structure */
- zfree(c->blocking_keys);
- c->blocking_keys = NULL;
+ zfree(c->bpop.keys);
+ c->bpop.keys = NULL;
+ c->bpop.target = NULL;
c->flags &= (~REDIS_BLOCKED);
server.blpop_blocked_clients--;
/* We want to process data if there is some command waiting
redisAssert(ln != NULL);
receiver = ln->value;
- addReplyMultiBulkLen(receiver,2);
- addReplyBulk(receiver,key);
- addReplyBulk(receiver,ele);
+ robj *target = receiver->bpop.target;
+
unblockClientWaitingData(receiver);
+
+ if (target == NULL) {
+ /* BRPOP/BLPOP return a multi-bulk with the name
+ * of the popped list */
+ addReplyMultiBulkLen(receiver,2);
+ addReplyBulk(receiver,key);
+ addReplyBulk(receiver,ele);
+ } else {
+ /* BRPOPLPUSH */
+ robj *dobj = lookupKeyWrite(receiver->db,target);
+ if (dobj && checkType(receiver,dobj,REDIS_LIST)) return 0;
+
+ addReplyBulk(receiver,ele);
+
+ if (!handleClientsWaitingListPush(receiver, target, ele)) {
+ /* Create the list if the key does not exist */
+ if (!dobj) {
+ dobj = createZiplistObject();
+ dbAdd(receiver->db, target, dobj);
+ }
+ listTypePush(dobj, ele, REDIS_HEAD);
+ }
+
+ decrRefCount(target);
+ }
+
return 1;
}
+int checkTimeout(redisClient *c, robj *object, time_t *timeout) {
+ long long lltimeout;
+
+ if (getLongLongFromObject(object, &lltimeout) != REDIS_OK) {
+ addReplyError(c, "timeout is not an integer");
+ return REDIS_ERR;
+ }
+
+ if (lltimeout < 0) {
+ addReplyError(c, "timeout is negative");
+ return REDIS_ERR;
+ }
+
+ *timeout = lltimeout;
+
+ return REDIS_OK;
+}
+
/* Blocking RPOP/LPOP */
void blockingPopGenericCommand(redisClient *c, int where) {
robj *o;
- long long lltimeout;
time_t timeout;
int j;
- /* Make sure timeout is an integer value */
- if (getLongLongFromObjectOrReply(c,c->argv[c->argc-1],&lltimeout,
- "timeout is not an integer") != REDIS_OK) return;
-
- /* Make sure the timeout is not negative */
- if (lltimeout < 0) {
- addReplyError(c,"timeout is negative");
+ if (checkTimeout(c, c->argv[c->argc - 1], &timeout) != REDIS_OK) {
return;
}
* because it is... */
addReplyMultiBulkLen(c,2);
addReplyBulk(c,argv[1]);
+
popGenericCommand(c,where);
/* Fix the client structure with the original stuff */
c->argv = orig_argv;
c->argc = orig_argc;
+
return;
}
}
}
/* If the list is empty or the key does not exists we must block */
- timeout = lltimeout;
if (timeout > 0) timeout += time(NULL);
- blockForKeys(c,c->argv+1,c->argc-2,timeout);
+ blockForKeys(c, c->argv + 1, c->argc - 2, timeout, NULL);
}
void blpopCommand(redisClient *c) {
void brpopCommand(redisClient *c) {
blockingPopGenericCommand(c,REDIS_TAIL);
}
+
+void brpoplpushCommand(redisClient *c) {
+ time_t timeout;
+
+ if (checkTimeout(c, c->argv[3], &timeout) != REDIS_OK) {
+ return;
+ }
+
+ robj *key = lookupKeyWrite(c->db, c->argv[1]);
+
+ if (key == NULL) {
+ if (c->flags & REDIS_MULTI) {
+
+ /* Blocking against an empty list in a multi state
+ * returns immediately. */
+ addReply(c, shared.nullmultibulk);
+ } else {
+ if (timeout > 0) timeout += time(NULL);
+
+ /* The list is empty and the client blocks. */
+ blockForKeys(c, c->argv + 1, 1, timeout, c->argv[2]);
+ }
+ } else {
+ if (key->type != REDIS_LIST) {
+ addReply(c, shared.wrongtypeerr);
+ } else {
+
+ /* The list exists and has elements, so
+ * the regular rpoplpushCommand is executed. */
+ redisAssert(listTypeLength(key) > 0);
+ rpoplpushCommand(c);
+ }
+ }
+}
assert_equal 0 [r llen blist1]
assert_equal 1 [r llen blist2]
}
+
+ test "BRPOPLPUSH - $type" {
+ r del target
+
+ set rd [redis_deferring_client]
+ create_$type blist "a b $large c d"
+
+ $rd brpoplpush blist target 1
+ assert_equal d [$rd read]
+
+ assert_equal d [r rpop target]
+ assert_equal "a b $large c" [r lrange blist 0 -1]
+ }
+ }
+
+ test "BRPOPLPUSH with zero timeout should block indefinitely" {
+ set rd [redis_deferring_client]
+ r del blist target
+ $rd brpoplpush blist target 0
+ after 1000
+ r rpush blist foo
+ assert_equal foo [$rd read]
+ assert_equal {foo} [r lrange target 0 -1]
+ }
+
+ test "BRPOPLPUSH with a client BLPOPing the target list" {
+ set rd [redis_deferring_client]
+ set rd2 [redis_deferring_client]
+ r del blist target
+ $rd2 blpop target 0
+ $rd brpoplpush blist target 0
+ after 1000
+ r rpush blist foo
+ assert_equal foo [$rd read]
+ assert_equal {target foo} [$rd2 read]
+ assert_equal 0 [r exists target]
+ }
+
+ test "BRPOPLPUSH with wrong source type" {
+ set rd [redis_deferring_client]
+ r del blist target
+ r set blist nolist
+ $rd brpoplpush blist target 1
+ assert_error "ERR*wrong kind*" {$rd read}
+ }
+
+ test "BRPOPLPUSH with wrong destination type" {
+ set rd [redis_deferring_client]
+ r del blist target
+ r set target nolist
+ r lpush blist foo
+ $rd brpoplpush blist target 1
+ assert_error "ERR*wrong kind*" {$rd read}
+
+ set rd [redis_deferring_client]
+ r del blist target
+ r set target nolist
+ $rd brpoplpush blist target 0
+ after 1000
+ r rpush blist foo
+ assert_error "ERR*wrong kind*" {$rd read}
+ assert_equal {foo} [r lrange blist 0 -1]
+ }
+
+ test "linked BRPOPLPUSH" {
+ set rd1 [redis_deferring_client]
+ set rd2 [redis_deferring_client]
+
+ r del list1 list2 list3
+
+ $rd1 brpoplpush list1 list2 0
+ $rd2 brpoplpush list2 list3 0
+
+ r rpush list1 foo
+
+ assert_equal {} [r lrange list1 0 -1]
+ assert_equal {} [r lrange list2 0 -1]
+ assert_equal {foo} [r lrange list3 0 -1]
+ }
+
+ test "circular BRPOPLPUSH" {
+ set rd1 [redis_deferring_client]
+ set rd2 [redis_deferring_client]
+
+ r del list1 list2
+
+ $rd1 brpoplpush list1 list2 0
+ $rd2 brpoplpush list2 list1 0
+
+ r rpush list1 foo
+
+ assert_equal {foo} [r lrange list1 0 -1]
+ assert_equal {} [r lrange list2 0 -1]
+ }
+
+ test "self-referential BRPOPLPUSH" {
+ set rd [redis_deferring_client]
+
+ r del blist
+
+ $rd brpoplpush blist blist 0
+
+ r rpush blist foo
+
+ assert_equal {foo} [r lrange blist 0 -1]
}
+ test "BRPOPLPUSH inside a transaction" {
+ r del xlist target
+ r lpush xlist foo
+ r lpush xlist bar
+
+ r multi
+ r brpoplpush xlist target 0
+ r brpoplpush xlist target 0
+ r brpoplpush xlist target 0
+ r lrange xlist 0 -1
+ r lrange target 0 -1
+ r exec
+ } {foo bar {} {} {bar foo}}
+
foreach {pop} {BLPOP BRPOP} {
test "$pop: with single empty list argument" {
set rd [redis_deferring_client]