GETEX, GETDEL and SET PXAT/EXAT (#8327)
This commit introduces two new command and two options for an existing command 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. Only one of the 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 return either the bulk string, error or nil. GETDEL <key> Would delete the key after getting. SET key value [NX] [XX] [KEEPTTL] [GET] [EX <seconds>] [PX <milliseconds>] [EXAT <seconds-timestamp>][PXAT <milliseconds-timestamp>] Two new options added here are EXAT and PXAT Key implementation notes - `SET` with `PX/EX/EXAT/PXAT` is always translated to `PXAT` in `AOF`. When relative time is specified (`PX/EX`), replication will always use `PX`. - `setexCommand` and `psetexCommand` would no longer need translation in `feedAppendOnlyFile` as they are modified to invoke `setGenericCommand ` with appropriate flags which will take care of correct AOF translation. - `GETEX` without any optional argument behaves like `GET`. - `GETEX` command is never propagated, It is either propagated as `PEXPIRE[AT], or PERSIST`. - `GETDEL` command is propagated as `DEL` - Combined the validation for `SET` and `GETEX` arguments. - Test cases to validate AOF/Replication propagation
This commit is contained in:
parent
f395119ede
commit
0367a80819
47
src/aof.c
47
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);
|
||||
}
|
||||
|
14
src/server.c
14
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));
|
||||
|
@ -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);
|
||||
|
346
src/t_string.c
346
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 <seconds>] [PX <milliseconds>] */
|
||||
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 <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;
|
||||
|
||||
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 <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) {
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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}}
|
||||
|
Loading…
x
Reference in New Issue
Block a user