From 7ccf36721836eabf31d3e756a64f928ad1f466a6 Mon Sep 17 00:00:00 2001 From: Arcadiy Ivanov Date: Tue, 17 Dec 2024 21:00:39 -0500 Subject: [PATCH] Add GETPXT (Get with millisecond expiration) command Returns null if the value not found or expired. Returns an array of length 2 as [, ]. If expiration is not set on the key, expiration returned is -1. Added tests to cover Signed-off-by: Arcadiy Ivanov --- src/commands.def | 25 ++++++++++++ src/commands/getpxt.json | 80 ++++++++++++++++++++++++++++++++++++++ src/server.h | 1 + src/t_string.c | 28 +++++++++++++ tests/unit/type/string.tcl | 17 ++++++++ 5 files changed, 151 insertions(+) create mode 100644 src/commands/getpxt.json diff --git a/src/commands.def b/src/commands.def index f03e44db9f..f09e5bd6ef 100644 --- a/src/commands.def +++ b/src/commands.def @@ -10354,6 +10354,30 @@ struct COMMAND_ARG GETEX_Args[] = { {MAKE_ARG("expiration",ARG_TYPE_ONEOF,-1,NULL,NULL,NULL,CMD_ARG_OPTIONAL,5,NULL),.subargs=GETEX_expiration_Subargs}, }; +/********** GETPXT ********************/ + +#ifndef SKIP_CMD_HISTORY_TABLE +/* GETPXT history */ +#define GETPXT_History NULL +#endif + +#ifndef SKIP_CMD_TIPS_TABLE +/* GETPXT tips */ +#define GETPXT_Tips NULL +#endif + +#ifndef SKIP_CMD_KEY_SPECS_TABLE +/* GETPXT key specs */ +keySpec GETPXT_Keyspecs[1] = { +{NULL,CMD_KEY_RO|CMD_KEY_ACCESS,KSPEC_BS_INDEX,.bs.index={1},KSPEC_FK_RANGE,.fk.range={0,1,0}} +}; +#endif + +/* GETPXT argument table */ +struct COMMAND_ARG GETPXT_Args[] = { +{MAKE_ARG("key",ARG_TYPE_KEY,0,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)}, +}; + /********** GETRANGE ********************/ #ifndef SKIP_CMD_HISTORY_TABLE @@ -11131,6 +11155,7 @@ struct COMMAND_STRUCT serverCommandTable[] = { {MAKE_CMD("get","Returns the string value of a key.","O(1)","1.0.0",CMD_DOC_NONE,NULL,NULL,"string",COMMAND_GROUP_STRING,GET_History,0,GET_Tips,0,getCommand,2,CMD_READONLY|CMD_FAST,ACL_CATEGORY_STRING,GET_Keyspecs,1,NULL,1),.args=GET_Args}, {MAKE_CMD("getdel","Returns the string value of a key after deleting the key.","O(1)","6.2.0",CMD_DOC_NONE,NULL,NULL,"string",COMMAND_GROUP_STRING,GETDEL_History,0,GETDEL_Tips,0,getdelCommand,2,CMD_WRITE|CMD_FAST,ACL_CATEGORY_STRING,GETDEL_Keyspecs,1,NULL,1),.args=GETDEL_Args}, {MAKE_CMD("getex","Returns the string value of a key after setting its expiration time.","O(1)","6.2.0",CMD_DOC_NONE,NULL,NULL,"string",COMMAND_GROUP_STRING,GETEX_History,0,GETEX_Tips,0,getexCommand,-2,CMD_WRITE|CMD_FAST,ACL_CATEGORY_STRING,GETEX_Keyspecs,1,NULL,2),.args=GETEX_Args}, +{MAKE_CMD("getpxt","Returns the string value of a key and the expiration time as a Unix milliseconds timestamp, if set.","O(1)","8.0.2",CMD_DOC_NONE,NULL,NULL,"string",COMMAND_GROUP_STRING,GETPXT_History,0,GETPXT_Tips,0,getpxtCommand,2,CMD_READONLY|CMD_FAST,ACL_CATEGORY_STRING|ACL_CATEGORY_KEYSPACE,GETPXT_Keyspecs,1,NULL,1),.args=GETPXT_Args}, {MAKE_CMD("getrange","Returns a substring of the string stored at a key.","O(N) where N is the length of the returned string. The complexity is ultimately determined by the returned length, but because creating a substring from an existing string is very cheap, it can be considered O(1) for small strings.","2.4.0",CMD_DOC_NONE,NULL,NULL,"string",COMMAND_GROUP_STRING,GETRANGE_History,0,GETRANGE_Tips,0,getrangeCommand,4,CMD_READONLY,ACL_CATEGORY_STRING,GETRANGE_Keyspecs,1,NULL,3),.args=GETRANGE_Args}, {MAKE_CMD("getset","Returns the previous string value of a key after setting it to a new value.","O(1)","1.0.0",CMD_DOC_DEPRECATED,"`SET` with the `!GET` argument","6.2.0","string",COMMAND_GROUP_STRING,GETSET_History,0,GETSET_Tips,0,getsetCommand,3,CMD_WRITE|CMD_DENYOOM|CMD_FAST,ACL_CATEGORY_STRING,GETSET_Keyspecs,1,NULL,2),.args=GETSET_Args}, {MAKE_CMD("incr","Increments the integer value of a key by one. Uses 0 as initial value if the key doesn't exist.","O(1)","1.0.0",CMD_DOC_NONE,NULL,NULL,"string",COMMAND_GROUP_STRING,INCR_History,0,INCR_Tips,0,incrCommand,2,CMD_WRITE|CMD_DENYOOM|CMD_FAST,ACL_CATEGORY_STRING,INCR_Keyspecs,1,NULL,1),.args=INCR_Args}, diff --git a/src/commands/getpxt.json b/src/commands/getpxt.json new file mode 100644 index 0000000000..4fb6a5fa9b --- /dev/null +++ b/src/commands/getpxt.json @@ -0,0 +1,80 @@ +{ + "GETPXT": { + "summary": "Returns the string value of a key and the expiration time as a Unix milliseconds timestamp, if set.", + "complexity": "O(1)", + "group": "string", + "since": "8.0.2", + "arity": 2, + "function": "getpxtCommand", + "command_flags": [ + "READONLY", + "FAST" + ], + "acl_categories": [ + "STRING", + "KEYSPACE" + ], + "key_specs": [ + { + "flags": [ + "RO", + "ACCESS" + ], + "begin_search": { + "index": { + "pos": 1 + } + }, + "find_keys": { + "range": { + "lastkey": 0, + "step": 1, + "limit": 0 + } + } + } + ], + "reply_schema": { + "oneOf": [ + { + "type": "array", + "items": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ + { + "description": "The value of the key.", + "type": "string" + }, + { + "oneOf": [ + { + "type": "integer", + "description": "Expiration Unix timestamp in milliseconds.", + "minimum": 0 + }, + { + "const": -1, + "description": "The key exists but has no associated expiration time." + } + ] + } + ] + } + }, + { + "description": "Key does not exist.", + "type": "null" + } + ] + }, + "arguments": [ + { + "name": "key", + "type": "key", + "key_spec_index": 0 + } + ] + } +} \ No newline at end of file diff --git a/src/server.h b/src/server.h index 1aafcaeb57..c39feac3b9 100644 --- a/src/server.h +++ b/src/server.h @@ -3784,6 +3784,7 @@ void psetexCommand(client *c); void getCommand(client *c); void getexCommand(client *c); void getdelCommand(client *c); +void getpxtCommand(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 da8953ee08..525ed793f6 100644 --- a/src/t_string.c +++ b/src/t_string.c @@ -393,6 +393,34 @@ void getCommand(client *c) { getGenericCommand(c); } +void getExpireGenericCommand(client *c, int output_ms) { + long long expire; + robj *o; + + if ((o = lookupKeyReadOrReply(c, c->argv[1], shared.null[c->resp])) == NULL) + return; + + if (checkType(c, o, OBJ_STRING)) { + return; + } + + addReplyArrayLen(c, 2); + addReplyBulk(c, o); + + /* The key exists. Return -1 if it has no expire, or the actual + * expire value otherwise. */ + expire = getExpire(c->db, c->argv[1]); + if (expire == -1) { + addReplyLongLong(c, -1); + } else { + addReplyLongLong(c, output_ms ? expire : ((expire + 500) / 1000)); + } +} + +void getpxtCommand(client *c) { + getExpireGenericCommand(c, 1); +} + /* * GETEX [PERSIST][EX seconds][PX milliseconds][EXAT seconds-timestamp][PXAT milliseconds-timestamp] * diff --git a/tests/unit/type/string.tcl b/tests/unit/type/string.tcl index bbfb30b60d..a9bc22b4a8 100644 --- a/tests/unit/type/string.tcl +++ b/tests/unit/type/string.tcl @@ -658,6 +658,23 @@ if {[string match {*jemalloc*} [s mem_allocator]]} { assert_range [r ttl foo] 5 10 } + test "GETPXT after SET PXAT" { + r del foo + r set foo bar pxat 17344823940230 + r getpxt foo + } {bar 17344823940230} + + test "GETPXT after SET" { + r del foo + r set foo bar + r getpxt foo + } {bar -1} + + test "GETPXT with no entry" { + r del foo + r getpxt foo + } {} + test "SET EXAT / PXAT Expiration time is expired" { r debug set-active-expire 0 set repl [attach_to_replication_stream]