diff --git a/src/expire.c b/src/expire.c index 91798b527..2dcdddaf6 100644 --- a/src/expire.c +++ b/src/expire.c @@ -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); } - diff --git a/src/server.c b/src/server.c index 4d1a9ac5c..1f162a5cd 100644 --- a/src/server.c +++ b/src/server.c @@ -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}, diff --git a/tests/unit/expire.tcl b/tests/unit/expire.tcl index 5e85fe858..5f97f638a 100644 --- a/tests/unit/expire.tcl +++ b/tests/unit/expire.tcl @@ -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 + } {} }