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)); + /* 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(); - 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); + 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], /* "*\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,77 +96,41 @@ 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); } -} -/* SET key value [NX] [XX] [KEEPTTL] [GET] [EX ] [PX ] */ -void setCommand(client *c) { - int j; - robj *expire = NULL; - int unit = UNIT_SECONDS; - int flags = OBJ_SET_NO_FLAGS; - - for (j = 3; j < c->argc; j++) { - char *a = 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)) - { - 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_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_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_SET_EX; - unit = UNIT_SECONDS; - 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) - { - flags |= OBJ_SET_PX; - unit = UNIT_MILLISECONDS; - expire = next; - j++; - } else { - addReplyErrorObject(c,shared.syntaxerr); - return; - } - } - - c->argv[2] = tryObjectEncoding(c->argv[2]); - setGenericCommand(c,flags,c->argv[1],c->argv[2],expire,unit,NULL,NULL); - - /* Propagate without the GET argument */ - if (flags & OBJ_SET_GET) { + /* 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; @@ -180,6 +147,123 @@ void setCommand(client *c) { } } +#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) { + + 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 ((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_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_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_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_EXAT; + *expire = next; + j++; + } 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_PXAT; + *unit = UNIT_MILLISECONDS; + *expire = next; + j++; + } else { + addReplyErrorObject(c,shared.syntaxerr); + return C_ERR; + } + } + return C_OK; +} + +/* SET key value [NX] [XX] [KEEPTTL] [GET] [EX ] [PX ] + * [EXAT ][PXAT ] */ +void setCommand(client *c) { + robj *expire = NULL; + int unit = UNIT_SECONDS; + int flags = OBJ_NO_FLAGS; + + 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) { c->argv[2] = tryObjectEncoding(c->argv[2]); setGenericCommand(c,OBJ_SET_NX,c->argv[1],c->argv[2],NULL,0,shared.cone,shared.czero); @@ -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 [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}}