Add NX/XX/GT/LT options to EXPIRE command group (#2795)

Add NX, XX, GT, and LT flags to EXPIRE, PEXPIRE, EXPIREAT, PEXAPIREAT.
- NX - only modify the TTL if no TTL is currently set 
- XX - only modify the TTL if there is a TTL currently set 
- GT - only increase the TTL (considering non-volatile keys as infinite expire time)
- LT - only decrease the TTL (considering non-volatile keys as infinite expire time)
return value of the command is 0 when the operation was skipped due to one of these flags.

Signed-off-by: Ning Sun <sunng@protonmail.com>
This commit is contained in:
Ning Sun 2021-08-02 13:57:49 +08:00 committed by GitHub
parent 82c3158ad5
commit f74af0e61d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 229 additions and 6 deletions

View File

@ -489,6 +489,56 @@ int checkAlreadyExpired(long long when) {
return (when <= mstime() && !server.loading && !server.masterhost);
}
#define EXPIRE_NX (1<<0)
#define EXPIRE_XX (1<<1)
#define EXPIRE_GT (1<<2)
#define EXPIRE_LT (1<<3)
/* Parse additional flags of expire commands
*
* Supported flags:
* - NX: set expiry only when the key has no expiry
* - XX: set expiry only when the key has an existing expiry
* - GT: set expiry only when the new expiry is greater than current one
* - LT: set expiry only when the new expiry is less than current one */
int parseExtendedExpireArgumentsOrReply(client *c, int *flags) {
int nx = 0, xx = 0, gt = 0, lt = 0;
int j = 3;
while (j < c->argc) {
char *opt = c->argv[j]->ptr;
if (!strcasecmp(opt,"nx")) {
*flags |= EXPIRE_NX;
nx = 1;
} else if (!strcasecmp(opt,"xx")) {
*flags |= EXPIRE_XX;
xx = 1;
} else if (!strcasecmp(opt,"gt")) {
*flags |= EXPIRE_GT;
gt = 1;
} else if (!strcasecmp(opt,"lt")) {
*flags |= EXPIRE_LT;
lt = 1;
} else {
addReplyErrorFormat(c, "Unsupported option %s", opt);
return C_ERR;
}
j++;
}
if ((nx && xx) || (nx && gt) || (nx && lt)) {
addReplyError(c, "NX and XX, GT or LT options at the same time are not compatible");
return C_ERR;
}
if (gt && lt) {
addReplyError(c, "GT and LT options at the same time are not compatible");
return C_ERR;
}
return C_OK;
}
/*-----------------------------------------------------------------------------
* Expires Commands
*----------------------------------------------------------------------------*/
@ -499,10 +549,19 @@ int checkAlreadyExpired(long long when) {
* for *AT variants of the command, or the current time for relative expires).
*
* unit is either UNIT_SECONDS or UNIT_MILLISECONDS, and is only used for
* the argv[2] parameter. The basetime is always specified in milliseconds. */
* the argv[2] parameter. The basetime is always specified in milliseconds.
*
* Additional flags are supported and parsed via parseExtendedExpireArguments */
void expireGenericCommand(client *c, long long basetime, int unit) {
robj *key = c->argv[1], *param = c->argv[2];
long long when; /* unix time in milliseconds when the key will expire. */
long long current_expire = -1;
int flag = 0;
/* checking optional flags */
if (parseExtendedExpireArgumentsOrReply(c, &flag) != C_OK) {
return;
}
if (getLongLongFromObjectOrReply(c, param, &when, NULL) != C_OK)
return;
@ -521,6 +580,50 @@ void expireGenericCommand(client *c, long long basetime, int unit) {
return;
}
if (flag) {
current_expire = getExpire(c->db, key);
/* NX option is set, check current expiry */
if (flag & EXPIRE_NX) {
if (current_expire != -1) {
addReply(c,shared.czero);
return;
}
}
/* XX option is set, check current expiry */
if (flag & EXPIRE_XX) {
if (current_expire == -1) {
/* reply 0 when the key has no expiry */
addReply(c,shared.czero);
return;
}
}
/* GT option is set, check current expiry */
if (flag & EXPIRE_GT) {
/* When current_expire is -1, we consider it as infinite TTL,
* so expire command with gt always fail the GT. */
if (when <= current_expire || current_expire == -1) {
/* reply 0 when the new expiry is not greater than current */
addReply(c,shared.czero);
return;
}
}
/* LT option is set, check current expiry */
if (flag & EXPIRE_LT) {
/* When current_expire -1, we consider it as infinite TTL,
* but 'when' can still be negative at this point, so if there is
* an expiry on the key and it's not less than current, we fail the LT. */
if (current_expire != -1 && when >= current_expire) {
/* reply 0 when the new expiry is not less than current */
addReply(c,shared.czero);
return;
}
}
}
if (checkAlreadyExpired(when)) {
robj *aux;
@ -637,4 +740,3 @@ void touchCommand(client *c) {
if (lookupKeyRead(c->db,c->argv[j]) != NULL) touched++;
addReplyLongLong(c,touched);
}

View File

@ -678,19 +678,19 @@ struct redisCommand redisCommandTable[] = {
"write fast @keyspace",
0,NULL,1,2,1,0,0,0},
{"expire",expireCommand,3,
{"expire",expireCommand,-3,
"write fast @keyspace",
0,NULL,1,1,1,0,0,0},
{"expireat",expireatCommand,3,
{"expireat",expireatCommand,-3,
"write fast @keyspace",
0,NULL,1,1,1,0,0,0},
{"pexpire",pexpireCommand,3,
{"pexpire",pexpireCommand,-3,
"write fast @keyspace",
0,NULL,1,1,1,0,0,0},
{"pexpireat",pexpireatCommand,3,
{"pexpireat",pexpireatCommand,-3,
"write fast @keyspace",
0,NULL,1,1,1,0,0,0},

View File

@ -601,4 +601,125 @@ start_server {tags {"expire"}} {
{del foo}
}
} {} {needs:repl}
test {EXPIRE with NX option on a key with ttl} {
r SET foo bar EX 100
assert_equal [r EXPIRE foo 200 NX] 0
assert_range [r TTL foo] 50 100
} {}
test {EXPIRE with NX option on a key without ttl} {
r SET foo bar
assert_equal [r EXPIRE foo 200 NX] 1
assert_range [r TTL foo] 100 200
} {}
test {EXPIRE with XX option on a key with ttl} {
r SET foo bar EX 100
assert_equal [r EXPIRE foo 200 XX] 1
assert_range [r TTL foo] 100 200
} {}
test {EXPIRE with XX option on a key without ttl} {
r SET foo bar
assert_equal [r EXPIRE foo 200 XX] 0
assert_equal [r TTL foo] -1
} {}
test {EXPIRE with GT option on a key with lower ttl} {
r SET foo bar EX 100
assert_equal [r EXPIRE foo 200 GT] 1
assert_range [r TTL foo] 100 200
} {}
test {EXPIRE with GT option on a key with higher ttl} {
r SET foo bar EX 200
assert_equal [r EXPIRE foo 100 GT] 0
assert_range [r TTL foo] 100 200
} {}
test {EXPIRE with GT option on a key without ttl} {
r SET foo bar
assert_equal [r EXPIRE foo 200 GT] 0
assert_equal [r TTL foo] -1
} {}
test {EXPIRE with LT option on a key with higher ttl} {
r SET foo bar EX 100
assert_equal [r EXPIRE foo 200 LT] 0
assert_range [r TTL foo] 50 100
} {}
test {EXPIRE with LT option on a key with lower ttl} {
r SET foo bar EX 200
assert_equal [r EXPIRE foo 100 LT] 1
assert_range [r TTL foo] 50 100
} {}
test {EXPIRE with LT option on a key without ttl} {
r SET foo bar
assert_equal [r EXPIRE foo 100 LT] 1
assert_range [r TTL foo] 50 100
} {}
test {EXPIRE with LT and XX option on a key with ttl} {
r SET foo bar EX 200
assert_equal [r EXPIRE foo 100 LT XX] 1
assert_range [r TTL foo] 50 100
} {}
test {EXPIRE with LT and XX option on a key without ttl} {
r SET foo bar
assert_equal [r EXPIRE foo 200 LT XX] 0
assert_equal [r TTL foo] -1
} {}
test {EXPIRE with conflicting options: LT GT} {
catch {r EXPIRE foo 200 LT GT} e
set e
} {ERR GT and LT options at the same time are not compatible}
test {EXPIRE with conflicting options: NX GT} {
catch {r EXPIRE foo 200 NX GT} e
set e
} {ERR NX and XX, GT or LT options at the same time are not compatible}
test {EXPIRE with conflicting options: NX LT} {
catch {r EXPIRE foo 200 NX LT} e
set e
} {ERR NX and XX, GT or LT options at the same time are not compatible}
test {EXPIRE with conflicting options: NX XX} {
catch {r EXPIRE foo 200 NX XX} e
set e
} {ERR NX and XX, GT or LT options at the same time are not compatible}
test {EXPIRE with unsupported options} {
catch {r EXPIRE foo 200 AB} e
set e
} {ERR Unsupported option AB}
test {EXPIRE with unsupported options} {
catch {r EXPIRE foo 200 XX AB} e
set e
} {ERR Unsupported option AB}
test {EXPIRE with negative expiry} {
r SET foo bar EX 100
assert_equal [r EXPIRE foo -10 LT] 1
assert_equal [r TTL foo] -2
} {}
test {EXPIRE with negative expiry on a non-valitale key} {
r SET foo bar
assert_equal [r EXPIRE foo -10 LT] 1
assert_equal [r TTL foo] -2
} {}
test {EXPIRE with non-existed key} {
assert_equal [r EXPIRE none 100 NX] 0
assert_equal [r EXPIRE none 100 XX] 0
assert_equal [r EXPIRE none 100 GT] 0
assert_equal [r EXPIRE none 100 LT] 0
} {}
}