summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/aof.c49
-rw-r--r--src/server.c14
-rw-r--r--src/server.h5
-rw-r--r--src/t_string.c340
-rw-r--r--tests/integration/aof.tcl11
-rw-r--r--tests/unit/expire.tcl71
-rw-r--r--tests/unit/type/string.tcl96
7 files changed, 476 insertions, 110 deletions
diff --git a/src/aof.c b/src/aof.c
index d3191277f..5518f0206 100644
--- a/src/aof.c
+++ b/src/aof.c
@@ -581,8 +581,6 @@ sds catAppendOnlyExpireAtCommand(sds buf, struct redisCommand *cmd, robj *key, r
void feedAppendOnlyFile(struct redisCommand *cmd, int dictid, robj **argv, int argc) {
sds buf = sdsempty();
- robj *tmpargv[3];
-
/* The DB this command was targeting is not the same as the last command
* we appended. To issue a SELECT command is needed. */
if (dictid != server.aof_selected_db) {
@@ -598,32 +596,31 @@ void feedAppendOnlyFile(struct redisCommand *cmd, int dictid, robj **argv, int a
cmd->proc == expireatCommand) {
/* Translate EXPIRE/PEXPIRE/EXPIREAT into PEXPIREAT */
buf = catAppendOnlyExpireAtCommand(buf,cmd,argv[1],argv[2]);
- } else if (cmd->proc == setexCommand || cmd->proc == psetexCommand) {
- /* Translate SETEX/PSETEX to SET and PEXPIREAT */
- tmpargv[0] = createStringObject("SET",3);
- tmpargv[1] = argv[1];
- tmpargv[2] = argv[3];
- buf = catAppendOnlyGenericCommand(buf,3,tmpargv);
- decrRefCount(tmpargv[0]);
- buf = catAppendOnlyExpireAtCommand(buf,cmd,argv[1],argv[2]);
} else if (cmd->proc == setCommand && argc > 3) {
- int i;
- robj *exarg = NULL, *pxarg = NULL;
- for (i = 3; i < argc; i ++) {
- if (!strcasecmp(argv[i]->ptr, "ex")) exarg = argv[i+1];
- if (!strcasecmp(argv[i]->ptr, "px")) pxarg = argv[i+1];
+ robj *pxarg = NULL;
+ /* When SET is used with EX/PX argument setGenericCommand propagates them with PX millisecond argument.
+ * So since the command arguments are re-written there, we can rely here on the index of PX being 3. */
+ if (!strcasecmp(argv[3]->ptr, "px")) {
+ pxarg = argv[4];
}
- serverAssert(!(exarg && pxarg));
-
- if (exarg || pxarg) {
- /* Translate SET [EX seconds][PX milliseconds] to SET and PEXPIREAT */
- buf = catAppendOnlyGenericCommand(buf,3,argv);
- if (exarg)
- buf = catAppendOnlyExpireAtCommand(buf,server.expireCommand,argv[1],
- exarg);
- if (pxarg)
- buf = catAppendOnlyExpireAtCommand(buf,server.pexpireCommand,argv[1],
- pxarg);
+ /* For AOF we convert SET key value relative time in milliseconds to SET key value absolute time in
+ * millisecond. Whenever the condition is true it implies that original SET has been transformed
+ * to SET PX with millisecond time argument so we do not need to worry about unit here.*/
+ if (pxarg) {
+ robj *millisecond = getDecodedObject(pxarg);
+ long long when = strtoll(millisecond->ptr,NULL,10);
+ when += mstime();
+
+ decrRefCount(millisecond);
+
+ robj *newargs[5];
+ newargs[0] = argv[0];
+ newargs[1] = argv[1];
+ newargs[2] = argv[2];
+ newargs[3] = shared.pxat;
+ newargs[4] = createStringObjectFromLongLong(when);
+ buf = catAppendOnlyGenericCommand(buf,5,newargs);
+ decrRefCount(newargs[4]);
} else {
buf = catAppendOnlyGenericCommand(buf,argc,argv);
}
diff --git a/src/server.c b/src/server.c
index bdedf72d0..763e3f5df 100644
--- a/src/server.c
+++ b/src/server.c
@@ -201,6 +201,14 @@ struct redisCommand redisCommandTable[] = {
"read-only fast @string",
0,NULL,1,1,1,0,0,0},
+ {"getex",getexCommand,-2,
+ "write fast @string",
+ 0,NULL,1,1,1,0,0,0},
+
+ {"getdel",getdelCommand,2,
+ "write fast @string",
+ 0,NULL,1,1,1,0,0,0},
+
/* Note that we can't flag set as fast, since it may perform an
* implicit DEL of a large key. */
{"set",setCommand,-3,
@@ -2532,6 +2540,12 @@ void createSharedObjects(void) {
/* Used in the LMOVE/BLMOVE commands */
shared.left = createStringObject("left",4);
shared.right = createStringObject("right",5);
+ shared.pexpireat = createStringObject("PEXPIREAT",9);
+ shared.pexpire = createStringObject("PEXPIRE",7);
+ shared.persist = createStringObject("PERSIST",7);
+ shared.set = createStringObject("SET",3);
+ shared.pxat = createStringObject("PXAT", 4);
+ shared.px = createStringObject("PX",2);
for (j = 0; j < OBJ_SHARED_INTEGERS; j++) {
shared.integers[j] =
makeObjectShared(createObject(OBJ_STRING,(void*)(long)j));
diff --git a/src/server.h b/src/server.h
index 5568155f9..83e007ebe 100644
--- a/src/server.h
+++ b/src/server.h
@@ -966,7 +966,8 @@ struct sharedObjectsStruct {
*busykeyerr, *oomerr, *plus, *messagebulk, *pmessagebulk, *subscribebulk,
*unsubscribebulk, *psubscribebulk, *punsubscribebulk, *del, *unlink,
*rpop, *lpop, *lpush, *rpoplpush, *lmove, *blmove, *zpopmin, *zpopmax,
- *emptyscan, *multi, *exec, *left, *right,
+ *emptyscan, *multi, *exec, *left, *right, *persist, *set, *pexpireat,
+ *pexpire, *pxat, *px,
*select[PROTO_SHARED_SELECT_CMDS],
*integers[OBJ_SHARED_INTEGERS],
*mbulkhdr[OBJ_SHARED_BULKHDR_LEN], /* "*<value>\r\n" */
@@ -2426,6 +2427,8 @@ void setnxCommand(client *c);
void setexCommand(client *c);
void psetexCommand(client *c);
void getCommand(client *c);
+void getexCommand(client *c);
+void getdelCommand(client *c);
void delCommand(client *c);
void unlinkCommand(client *c);
void existsCommand(client *c);
diff --git a/src/t_string.c b/src/t_string.c
index 2792f5557..de67484fc 100644
--- a/src/t_string.c
+++ b/src/t_string.c
@@ -61,13 +61,16 @@ static int checkStringLength(client *c, long long size) {
* If ok_reply is NULL "+OK" is used.
* If abort_reply is NULL, "$-1" is used. */
-#define OBJ_SET_NO_FLAGS 0
+#define OBJ_NO_FLAGS 0
#define OBJ_SET_NX (1<<0) /* Set if key not exists. */
#define OBJ_SET_XX (1<<1) /* Set if key exists. */
-#define OBJ_SET_EX (1<<2) /* Set if time in seconds is given */
-#define OBJ_SET_PX (1<<3) /* Set if time in ms in given */
-#define OBJ_SET_KEEPTTL (1<<4) /* Set and keep the ttl */
+#define OBJ_EX (1<<2) /* Set if time in seconds is given */
+#define OBJ_PX (1<<3) /* Set if time in ms in given */
+#define OBJ_KEEPTTL (1<<4) /* Set and keep the ttl */
#define OBJ_SET_GET (1<<5) /* Set if want to get key before set */
+#define OBJ_EXAT (1<<6) /* Set if timestamp in second is given */
+#define OBJ_PXAT (1<<7) /* Set if timestamp in ms is given */
+#define OBJ_PERSIST (1<<8) /* Set if we need to remove the ttl */
void setGenericCommand(client *c, int flags, robj *key, robj *val, robj *expire, int unit, robj *ok_reply, robj *abort_reply) {
long long milliseconds = 0; /* initialized to avoid any harmness warning */
@@ -93,91 +96,172 @@ void setGenericCommand(client *c, int flags, robj *key, robj *val, robj *expire,
if (getGenericCommand(c) == C_ERR) return;
}
- genericSetKey(c,c->db,key,val,flags & OBJ_SET_KEEPTTL,1);
+ genericSetKey(c,c->db,key, val,flags & OBJ_KEEPTTL,1);
server.dirty++;
- if (expire) setExpire(c,c->db,key,mstime()+milliseconds);
notifyKeyspaceEvent(NOTIFY_STRING,"set",key,c->db->id);
- if (expire) notifyKeyspaceEvent(NOTIFY_GENERIC,
- "expire",key,c->db->id);
+ if (expire) {
+ robj *exp = shared.pxat;
+
+ if ((flags & OBJ_PX) || (flags & OBJ_EX)) {
+ setExpire(c,c->db,key,milliseconds + mstime());
+ exp = shared.px;
+ } else {
+ setExpire(c,c->db,key,milliseconds);
+ }
+ notifyKeyspaceEvent(NOTIFY_GENERIC,"expire",key,c->db->id);
+
+ /* Propagate as SET Key Value PXAT millisecond-timestamp if there is EXAT/PXAT or
+ * propagate as SET Key Value PX millisecond if there is EX/PX flag.
+ *
+ * Additionally when we propagate the SET with PX (relative millisecond) we translate
+ * it again to SET with PXAT for the AOF.
+ *
+ * Additional care is required while modifying the argument order. AOF relies on the
+ * exp argument being at index 3. (see feedAppendOnlyFile)
+ * */
+ robj *millisecondObj = createStringObjectFromLongLong(milliseconds);
+ rewriteClientCommandVector(c,5,shared.set,key,val,exp,millisecondObj);
+ decrRefCount(millisecondObj);
+ }
if (!(flags & OBJ_SET_GET)) {
addReply(c, ok_reply ? ok_reply : shared.ok);
}
+
+ /* Propagate without the GET argument (Isn't needed if we had expire since in that case we completely re-written the command argv) */
+ if ((flags & OBJ_SET_GET) && !expire) {
+ int argc = 0;
+ int j;
+ robj **argv = zmalloc((c->argc-1)*sizeof(robj*));
+ for (j=0; j < c->argc; j++) {
+ char *a = c->argv[j]->ptr;
+ /* Skip GET which may be repeated multiple times. */
+ if (j >= 3 &&
+ (a[0] == 'g' || a[0] == 'G') &&
+ (a[1] == 'e' || a[1] == 'E') &&
+ (a[2] == 't' || a[2] == 'T') && a[3] == '\0')
+ continue;
+ argv[argc++] = c->argv[j];
+ incrRefCount(c->argv[j]);
+ }
+ replaceClientCommandVector(c, argc, argv);
+ }
}
-/* SET key value [NX] [XX] [KEEPTTL] [GET] [EX <seconds>] [PX <milliseconds>] */
-void setCommand(client *c) {
- int j;
- robj *expire = NULL;
- int unit = UNIT_SECONDS;
- int flags = OBJ_SET_NO_FLAGS;
+#define COMMAND_GET 0
+#define COMMAND_SET 1
+/*
+ * The parseExtendedStringArgumentsOrReply() function performs the common validation for extended
+ * string arguments used in SET and GET command.
+ *
+ * Get specific commands - PERSIST/DEL
+ * Set specific commands - XX/NX/GET
+ * Common commands - EX/EXAT/PX/PXAT/KEEPTTL
+ *
+ * Function takes pointers to client, flags, unit, pointer to pointer of expire obj if needed
+ * to be determined and command_type which can be COMMAND_GET or COMMAND_SET.
+ *
+ * If there are any syntax violations C_ERR is returned else C_OK is returned.
+ *
+ * Input flags are updated upon parsing the arguments. Unit and expire are updated if there are any
+ * EX/EXAT/PX/PXAT arguments. Unit is updated to millisecond if PX/PXAT is set.
+ */
+int parseExtendedStringArgumentsOrReply(client *c, int *flags, int *unit, robj **expire, int command_type) {
- for (j = 3; j < c->argc; j++) {
- char *a = c->argv[j]->ptr;
+ int j = command_type == COMMAND_GET ? 2 : 3;
+ for (; j < c->argc; j++) {
+ char *opt = c->argv[j]->ptr;
robj *next = (j == c->argc-1) ? NULL : c->argv[j+1];
- if ((a[0] == 'n' || a[0] == 'N') &&
- (a[1] == 'x' || a[1] == 'X') && a[2] == '\0' &&
- !(flags & OBJ_SET_XX) && !(flags & OBJ_SET_GET))
+ if ((opt[0] == 'n' || opt[0] == 'N') &&
+ (opt[1] == 'x' || opt[1] == 'X') && opt[2] == '\0' &&
+ !(*flags & OBJ_SET_XX) && !(*flags & OBJ_SET_GET) && (command_type == COMMAND_SET))
+ {
+ *flags |= OBJ_SET_NX;
+ } else if ((opt[0] == 'x' || opt[0] == 'X') &&
+ (opt[1] == 'x' || opt[1] == 'X') && opt[2] == '\0' &&
+ !(*flags & OBJ_SET_NX) && (command_type == COMMAND_SET))
+ {
+ *flags |= OBJ_SET_XX;
+ } else if ((opt[0] == 'g' || opt[0] == 'G') &&
+ (opt[1] == 'e' || opt[1] == 'E') &&
+ (opt[2] == 't' || opt[2] == 'T') && opt[3] == '\0' &&
+ !(*flags & OBJ_SET_NX) && (command_type == COMMAND_SET))
+ {
+ *flags |= OBJ_SET_GET;
+ } else if (!strcasecmp(opt, "KEEPTTL") && !(*flags & OBJ_PERSIST) &&
+ !(*flags & OBJ_EX) && !(*flags & OBJ_EXAT) &&
+ !(*flags & OBJ_PX) && !(*flags & OBJ_PXAT) && (command_type == COMMAND_SET))
+ {
+ *flags |= OBJ_KEEPTTL;
+ } else if (!strcasecmp(opt,"PERSIST") && (command_type == COMMAND_GET) &&
+ !(*flags & OBJ_EX) && !(*flags & OBJ_EXAT) &&
+ !(*flags & OBJ_PX) && !(*flags & OBJ_PXAT) &&
+ !(*flags & OBJ_KEEPTTL))
{
- flags |= OBJ_SET_NX;
- } else if ((a[0] == 'x' || a[0] == 'X') &&
- (a[1] == 'x' || a[1] == 'X') && a[2] == '\0' &&
- !(flags & OBJ_SET_NX))
+ *flags |= OBJ_PERSIST;
+ } else if ((opt[0] == 'e' || opt[0] == 'E') &&
+ (opt[1] == 'x' || opt[1] == 'X') && opt[2] == '\0' &&
+ !(*flags & OBJ_KEEPTTL) && !(*flags & OBJ_PERSIST) &&
+ !(*flags & OBJ_EXAT) && !(*flags & OBJ_PX) &&
+ !(*flags & OBJ_PXAT) && next)
{
- flags |= OBJ_SET_XX;
- } else if ((a[0] == 'g' || a[0] == 'G') &&
- (a[1] == 'e' || a[1] == 'E') &&
- (a[2] == 't' || a[2] == 'T') && a[3] == '\0' &&
- !(flags & OBJ_SET_NX)) {
- flags |= OBJ_SET_GET;
- } else if (!strcasecmp(c->argv[j]->ptr,"KEEPTTL") &&
- !(flags & OBJ_SET_EX) && !(flags & OBJ_SET_PX))
+ *flags |= OBJ_EX;
+ *expire = next;
+ j++;
+ } else if ((opt[0] == 'p' || opt[0] == 'P') &&
+ (opt[1] == 'x' || opt[1] == 'X') && opt[2] == '\0' &&
+ !(*flags & OBJ_KEEPTTL) && !(*flags & OBJ_PERSIST) &&
+ !(*flags & OBJ_EX) && !(*flags & OBJ_EXAT) &&
+ !(*flags & OBJ_PXAT) && next)
{
- flags |= OBJ_SET_KEEPTTL;
- } else if ((a[0] == 'e' || a[0] == 'E') &&
- (a[1] == 'x' || a[1] == 'X') && a[2] == '\0' &&
- !(flags & OBJ_SET_KEEPTTL) &&
- !(flags & OBJ_SET_PX) && next)
+ *flags |= OBJ_PX;
+ *unit = UNIT_MILLISECONDS;
+ *expire = next;
+ j++;
+ } else if ((opt[0] == 'e' || opt[0] == 'E') &&
+ (opt[1] == 'x' || opt[1] == 'X') &&
+ (opt[2] == 'a' || opt[2] == 'A') &&
+ (opt[3] == 't' || opt[3] == 'T') && opt[4] == '\0' &&
+ !(*flags & OBJ_KEEPTTL) && !(*flags & OBJ_PERSIST) &&
+ !(*flags & OBJ_EX) && !(*flags & OBJ_PX) &&
+ !(*flags & OBJ_PXAT) && next)
{
- flags |= OBJ_SET_EX;
- unit = UNIT_SECONDS;
- expire = next;
+ *flags |= OBJ_EXAT;
+ *expire = next;
j++;
- } else if ((a[0] == 'p' || a[0] == 'P') &&
- (a[1] == 'x' || a[1] == 'X') && a[2] == '\0' &&
- !(flags & OBJ_SET_KEEPTTL) &&
- !(flags & OBJ_SET_EX) && next)
+ } else if ((opt[0] == 'p' || opt[0] == 'P') &&
+ (opt[1] == 'x' || opt[1] == 'X') &&
+ (opt[2] == 'a' || opt[2] == 'A') &&
+ (opt[3] == 't' || opt[3] == 'T') && opt[4] == '\0' &&
+ !(*flags & OBJ_KEEPTTL) && !(*flags & OBJ_PERSIST) &&
+ !(*flags & OBJ_EX) && !(*flags & OBJ_EXAT) &&
+ !(*flags & OBJ_PX) && next)
{
- flags |= OBJ_SET_PX;
- unit = UNIT_MILLISECONDS;
- expire = next;
+ *flags |= OBJ_PXAT;
+ *unit = UNIT_MILLISECONDS;
+ *expire = next;
j++;
} else {
addReplyErrorObject(c,shared.syntaxerr);
- return;
+ return C_ERR;
}
}
+ return C_OK;
+}
- c->argv[2] = tryObjectEncoding(c->argv[2]);
- setGenericCommand(c,flags,c->argv[1],c->argv[2],expire,unit,NULL,NULL);
+/* SET key value [NX] [XX] [KEEPTTL] [GET] [EX <seconds>] [PX <milliseconds>]
+ * [EXAT <seconds-timestamp>][PXAT <milliseconds-timestamp>] */
+void setCommand(client *c) {
+ robj *expire = NULL;
+ int unit = UNIT_SECONDS;
+ int flags = OBJ_NO_FLAGS;
- /* Propagate without the GET argument */
- if (flags & OBJ_SET_GET) {
- int argc = 0;
- robj **argv = zmalloc((c->argc-1)*sizeof(robj*));
- for (j=0; j < c->argc; j++) {
- char *a = c->argv[j]->ptr;
- /* Skip GET which may be repeated multiple times. */
- if (j >= 3 &&
- (a[0] == 'g' || a[0] == 'G') &&
- (a[1] == 'e' || a[1] == 'E') &&
- (a[2] == 't' || a[2] == 'T') && a[3] == '\0')
- continue;
- argv[argc++] = c->argv[j];
- incrRefCount(c->argv[j]);
- }
- replaceClientCommandVector(c, argc, argv);
+ if (parseExtendedStringArgumentsOrReply(c,&flags,&unit,&expire,COMMAND_SET) != C_OK) {
+ return;
}
+
+ c->argv[2] = tryObjectEncoding(c->argv[2]);
+ setGenericCommand(c,flags,c->argv[1],c->argv[2],expire,unit,NULL,NULL);
}
void setnxCommand(client *c) {
@@ -187,12 +271,12 @@ void setnxCommand(client *c) {
void setexCommand(client *c) {
c->argv[3] = tryObjectEncoding(c->argv[3]);
- setGenericCommand(c,OBJ_SET_NO_FLAGS,c->argv[1],c->argv[3],c->argv[2],UNIT_SECONDS,NULL,NULL);
+ setGenericCommand(c,OBJ_EX,c->argv[1],c->argv[3],c->argv[2],UNIT_SECONDS,NULL,NULL);
}
void psetexCommand(client *c) {
c->argv[3] = tryObjectEncoding(c->argv[3]);
- setGenericCommand(c,OBJ_SET_NO_FLAGS,c->argv[1],c->argv[3],c->argv[2],UNIT_MILLISECONDS,NULL,NULL);
+ setGenericCommand(c,OBJ_PX,c->argv[1],c->argv[3],c->argv[2],UNIT_MILLISECONDS,NULL,NULL);
}
int getGenericCommand(client *c) {
@@ -213,6 +297,112 @@ void getCommand(client *c) {
getGenericCommand(c);
}
+/*
+ * GETEX <key> [PERSIST][EX seconds][PX milliseconds][EXAT seconds-timestamp][PXAT milliseconds-timestamp]
+ *
+ * The getexCommand() function implements extended options and variants of the GET command. Unlike GET
+ * command this command is not read-only.
+ *
+ * The default behavior when no options are specified is same as GET and does not alter any TTL.
+ *
+ * Only one of the below options can be used at a given time.
+ *
+ * 1. PERSIST removes any TTL associated with the key.
+ * 2. EX Set expiry TTL in seconds.
+ * 3. PX Set expiry TTL in milliseconds.
+ * 4. EXAT Same like EX instead of specifying the number of seconds representing the TTL
+ * (time to live), it takes an absolute Unix timestamp
+ * 5. PXAT Same like PX instead of specifying the number of milliseconds representing the TTL
+ * (time to live), it takes an absolute Unix timestamp
+ *
+ * Command would either return the bulk string, error or nil.
+ */
+void getexCommand(client *c) {
+ robj *expire = NULL;
+ int unit = UNIT_SECONDS;
+ int flags = OBJ_NO_FLAGS;
+
+ if (parseExtendedStringArgumentsOrReply(c,&flags,&unit,&expire,COMMAND_GET) != C_OK) {
+ return;
+ }
+
+ robj *o;
+
+ if ((o = lookupKeyReadOrReply(c,c->argv[1],shared.null[c->resp])) == NULL)
+ return;
+
+ if (checkType(c,o,OBJ_STRING)) {
+ return;
+ }
+
+ long long milliseconds = 0;
+
+ /* Validate the expiration time value first */
+ if (expire) {
+ if (getLongLongFromObjectOrReply(c, expire, &milliseconds, NULL) != C_OK)
+ return;
+ if (milliseconds <= 0) {
+ addReplyErrorFormat(c,"invalid expire time in %s",c->cmd->name);
+ return;
+ }
+ if (unit == UNIT_SECONDS) milliseconds *= 1000;
+ }
+
+ /* We need to do this before we expire the key or delete it */
+ addReplyBulk(c,o);
+
+ /* This command is never propagated as is. It is either propagated as PEXPIRE[AT],DEL,UNLINK or PERSIST.
+ * This why it doesn't need special handling in feedAppendOnlyFile to convert relative expire time to absolute one. */
+ if (((flags & OBJ_PXAT) || (flags & OBJ_EXAT)) && checkAlreadyExpired(milliseconds)) {
+ /* When PXAT/EXAT absolute timestamp is specified, there can be a chance that timestamp
+ * has already elapsed so delete the key in that case. */
+ int deleted = server.lazyfree_lazy_expire ? dbAsyncDelete(c->db, c->argv[1]) :
+ dbSyncDelete(c->db, c->argv[1]);
+ serverAssert(deleted);
+ robj *aux = server.lazyfree_lazy_expire ? shared.unlink : shared.del;
+ rewriteClientCommandVector(c,2,aux,c->argv[1]);
+ signalModifiedKey(c, c->db, c->argv[1]);
+ notifyKeyspaceEvent(NOTIFY_GENERIC, "del", c->argv[1], c->db->id);
+ server.dirty++;
+ } else if (expire) {
+ robj *exp = shared.pexpireat;
+ if ((flags & OBJ_PX) || (flags & OBJ_EX)) {
+ setExpire(c,c->db,c->argv[1],milliseconds + mstime());
+ exp = shared.pexpire;
+ } else {
+ setExpire(c,c->db,c->argv[1],milliseconds);
+ }
+
+ robj* millisecondObj = createStringObjectFromLongLong(milliseconds);
+ rewriteClientCommandVector(c,3,exp,c->argv[1],millisecondObj);
+ decrRefCount(millisecondObj);
+ signalModifiedKey(c, c->db, c->argv[1]);
+ notifyKeyspaceEvent(NOTIFY_GENERIC,"expire",c->argv[1],c->db->id);
+ server.dirty++;
+ } else if (flags & OBJ_PERSIST) {
+ if (removeExpire(c->db, c->argv[1])) {
+ signalModifiedKey(c, c->db, c->argv[1]);
+ rewriteClientCommandVector(c, 2, shared.persist, c->argv[1]);
+ notifyKeyspaceEvent(NOTIFY_GENERIC,"persist",c->argv[1],c->db->id);
+ server.dirty++;
+ }
+ }
+}
+
+void getdelCommand(client *c) {
+ if (getGenericCommand(c) == C_ERR) return;
+ int deleted = server.lazyfree_lazy_user_del ? dbAsyncDelete(c->db, c->argv[1]) :
+ dbSyncDelete(c->db, c->argv[1]);
+ if (deleted) {
+ /* Propagate as DEL/UNLINK command */
+ robj *aux = server.lazyfree_lazy_user_del ? shared.unlink : shared.del;
+ rewriteClientCommandVector(c,2,aux,c->argv[1]);
+ signalModifiedKey(c, c->db, c->argv[1]);
+ notifyKeyspaceEvent(NOTIFY_GENERIC, "del", c->argv[1], c->db->id);
+ server.dirty++;
+ }
+}
+
void getsetCommand(client *c) {
if (getGenericCommand(c) == C_ERR) return;
c->argv[2] = tryObjectEncoding(c->argv[2]);
@@ -221,9 +411,7 @@ void getsetCommand(client *c) {
server.dirty++;
/* Propagate as SET command */
- robj *setcmd = createStringObject("SET",3);
- rewriteClientCommandArgument(c,0,setcmd);
- decrRefCount(setcmd);
+ rewriteClientCommandArgument(c,0,shared.set);
}
void setrangeCommand(client *c) {
@@ -443,7 +631,7 @@ void decrbyCommand(client *c) {
void incrbyfloatCommand(client *c) {
long double incr, value;
- robj *o, *new, *aux1, *aux2;
+ robj *o, *new, *aux;
o = lookupKeyWrite(c->db,c->argv[1]);
if (checkType(c,o,OBJ_STRING)) return;
@@ -469,13 +657,11 @@ void incrbyfloatCommand(client *c) {
/* Always replicate INCRBYFLOAT as a SET command with the final value
* in order to make sure that differences in float precision or formatting
* will not create differences in replicas or after an AOF restart. */
- aux1 = createStringObject("SET",3);
- rewriteClientCommandArgument(c,0,aux1);
- decrRefCount(aux1);
+ rewriteClientCommandArgument(c,0,shared.set);
rewriteClientCommandArgument(c,2,new);
- aux2 = createStringObject("KEEPTTL",7);
- rewriteClientCommandArgument(c,3,aux2);
- decrRefCount(aux2);
+ aux = createStringObject("KEEPTTL",7);
+ rewriteClientCommandArgument(c,3,aux);
+ decrRefCount(aux);
}
void appendCommand(client *c) {
diff --git a/tests/integration/aof.tcl b/tests/integration/aof.tcl
index d81521374..e64e2022a 100644
--- a/tests/integration/aof.tcl
+++ b/tests/integration/aof.tcl
@@ -272,4 +272,15 @@ tags {"aof"} {
}
}
}
+
+ start_server {overrides {appendonly {yes} appendfilename {appendonly.aof}}} {
+ test {GETEX should not append to AOF} {
+ set aof [file join [lindex [r config get dir] 1] appendonly.aof]
+ r set foo bar
+ set before [file size $aof]
+ r getex foo
+ set after [file size $aof]
+ assert_equal $before $after
+ }
+ }
}
diff --git a/tests/unit/expire.tcl b/tests/unit/expire.tcl
index e71018073..9bde4809f 100644
--- a/tests/unit/expire.tcl
+++ b/tests/unit/expire.tcl
@@ -209,7 +209,7 @@ start_server {tags {"expire"}} {
set e
} {*not an integer*}
- test {EXPIRE and SET EX/PX option, TTL should not be reset after loadaof} {
+ test {EXPIRE and SET/GETEX EX/PX/EXAT/PXAT option, TTL should not be reset after loadaof} {
# This test makes sure that expire times are propagated as absolute
# times to the AOF file and not as relative time, so that when the AOF
# is reloaded the TTLs are not being shifted forward to the future.
@@ -224,6 +224,17 @@ start_server {tags {"expire"}} {
r pexpire foo4 100000
r setex foo5 100 bar
r psetex foo6 100000 bar
+ r set foo7 bar EXAT [expr [clock seconds] + 100]
+ r set foo8 bar PXAT [expr [clock milliseconds] + 100000]
+ r set foo9 bar
+ r getex foo9 EX 100
+ r set foo10 bar
+ r getex foo10 PX 100000
+ r set foo11 bar
+ r getex foo11 EXAT [expr [clock seconds] + 100]
+ r set foo12 bar
+ r getex foo12 PXAT [expr [clock milliseconds] + 100000]
+
after 2000
r debug loadaof
assert_range [r ttl foo1] 90 98
@@ -232,6 +243,12 @@ start_server {tags {"expire"}} {
assert_range [r ttl foo4] 90 98
assert_range [r ttl foo5] 90 98
assert_range [r ttl foo6] 90 98
+ assert_range [r ttl foo7] 90 98
+ assert_range [r ttl foo8] 90 98
+ assert_range [r ttl foo9] 90 98
+ assert_range [r ttl foo10] 90 98
+ assert_range [r ttl foo11] 90 98
+ assert_range [r ttl foo12] 90 98
}
test {EXPIRE relative and absolute propagation to replicas} {
@@ -248,8 +265,10 @@ start_server {tags {"expire"}} {
# https://github.com/redis/redis/pull/5171#issuecomment-409553266
set repl [attach_to_replication_stream]
- r set foo1 bar ex 100
+ r set foo1 bar ex 200
r set foo1 bar px 100000
+ r set foo1 bar exat [expr [clock seconds]+100]
+ r set foo1 bar pxat [expr [clock milliseconds]+10000]
r setex foo1 100 bar
r psetex foo1 100000 bar
r set foo2 bar
@@ -259,12 +278,19 @@ start_server {tags {"expire"}} {
r expireat foo3 [expr [clock seconds]+100]
r pexpireat foo3 [expr [clock seconds]*1000+100000]
r expireat foo3 [expr [clock seconds]-100]
+ r set foo4 bar
+ r getex foo4 ex 200
+ r getex foo4 px 200000
+ r getex foo4 exat [expr [clock seconds]+100]
+ r getex foo4 pxat [expr [clock milliseconds]+10000]
assert_replication_stream $repl {
{select *}
- {set foo1 bar ex 100}
- {set foo1 bar px 100000}
- {setex foo1 100 bar}
- {psetex foo1 100000 bar}
+ {set foo1 bar PX 200000}
+ {set foo1 bar PX 100000}
+ {set foo1 bar PXAT *}
+ {set foo1 bar PXAT *}
+ {set foo1 bar PX 100000}
+ {set foo1 bar PX 100000}
{set foo2 bar}
{expire foo2 100}
{pexpire foo2 100000}
@@ -272,6 +298,11 @@ start_server {tags {"expire"}} {
{expireat foo3 *}
{pexpireat foo3 *}
{del foo3}
+ {set foo4 bar}
+ {pexpire foo4 200000}
+ {pexpire foo4 200000}
+ {pexpireat foo4 *}
+ {pexpireat foo4 *}
}
}
@@ -297,4 +328,32 @@ start_server {tags {"expire"}} {
set ttl [r ttl foo]
assert {$ttl <= 98 && $ttl > 90}
}
+
+ test {GETEX use of PERSIST option should remove TTL} {
+ r set foo bar EX 100
+ r getex foo PERSIST
+ r ttl foo
+ } {-1}
+
+ test {GETEX use of PERSIST option should remove TTL after loadaof} {
+ r set foo bar EX 100
+ r getex foo PERSIST
+ after 2000
+ r debug loadaof
+ r ttl foo
+ } {-1}
+
+ test {GETEX propagate as to replica as PERSIST, DEL, or nothing} {
+ set repl [attach_to_replication_stream]
+ r set foo bar EX 100
+ r getex foo PERSIST
+ r getex foo
+ r getex foo exat [expr [clock seconds]-100]
+ assert_replication_stream $repl {
+ {select *}
+ {set foo bar PX 100000}
+ {persist foo}
+ {del foo}
+ }
+ }
}
diff --git a/tests/unit/type/string.tcl b/tests/unit/type/string.tcl
index 16e961623..43968b26b 100644
--- a/tests/unit/type/string.tcl
+++ b/tests/unit/type/string.tcl
@@ -102,6 +102,91 @@ start_server {tags {"string"}} {
assert_equal 20 [r get x]
}
+ test "GETEX EX option" {
+ r del foo
+ r set foo bar
+ r getex foo ex 10
+ assert_range [r ttl foo] 5 10
+ }
+
+ test "GETEX PX option" {
+ r del foo
+ r set foo bar
+ r getex foo px 10000
+ assert_range [r pttl foo] 5000 10000
+ }
+
+ test "GETEX EXAT option" {
+ r del foo
+ r set foo bar
+ r getex foo exat [expr [clock seconds] + 10]
+ assert_range [r ttl foo] 5 10
+ }
+
+ test "GETEX PXAT option" {
+ r del foo
+ r set foo bar
+ r getex foo pxat [expr [clock milliseconds] + 10000]
+ assert_range [r pttl foo] 5000 10000
+ }
+
+ test "GETEX PERSIST option" {
+ r del foo
+ r set foo bar ex 10
+ assert_range [r ttl foo] 5 10
+ r getex foo persist
+ assert_equal -1 [r ttl foo]
+ }
+
+ test "GETEX no option" {
+ r del foo
+ r set foo bar
+ r getex foo
+ assert_equal bar [r getex foo]
+ }
+
+ test "GETEX syntax errors" {
+ set ex {}
+ catch {r getex foo non-existent-option} ex
+ set ex
+ } {*syntax*}
+
+ test "GETEX no arguments" {
+ set ex {}
+ catch {r getex} ex
+ set ex
+ } {*wrong number of arguments*}
+
+ test "GETDEL command" {
+ r del foo
+ r set foo bar
+ assert_equal bar [r getdel foo ]
+ assert_equal {} [r getdel foo ]
+ }
+
+ test {GETDEL propagate as DEL command to replica} {
+ set repl [attach_to_replication_stream]
+ r set foo bar
+ r getdel foo
+ assert_replication_stream $repl {
+ {select *}
+ {set foo bar}
+ {del foo}
+ }
+ }
+
+ test {GETEX without argument does not propagate to replica} {
+ set repl [attach_to_replication_stream]
+ r set foo bar
+ r getex foo
+ r del foo
+ assert_replication_stream $repl {
+ {select *}
+ {set foo bar}
+ {del foo}
+ }
+ }
+
test {MGET} {
r flushdb
r set foo BAR
@@ -437,6 +522,17 @@ start_server {tags {"string"}} {
assert {$ttl <= 10 && $ttl > 5}
}
+ test "Extended SET EXAT option" {
+ r del foo
+ r set foo bar exat [expr [clock seconds] + 10]
+ assert_range [r ttl foo] 5 10
+ }
+
+ test "Extended SET PXAT option" {
+ r del foo
+ r set foo bar pxat [expr [clock milliseconds] + 10000]
+ assert_range [r ttl foo] 5 10
+ }
test {Extended SET using multiple options at once} {
r set foo val
assert {[r set foo bar xx px 10000] eq {OK}}