diff --git a/src/commands.def b/src/commands.def index 3f56b4e0fe..6e4a1db48f 100644 --- a/src/commands.def +++ b/src/commands.def @@ -10541,6 +10541,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 @@ -10721,6 +10745,32 @@ struct COMMAND_ARG MGET_Args[] = { {MAKE_ARG("key",ARG_TYPE_KEY,0,NULL,NULL,NULL,CMD_ARG_MULTIPLE,0,NULL)}, }; +/********** MGETPXT ********************/ + +#ifndef SKIP_CMD_HISTORY_TABLE +/* MGETPXT history */ +#define MGETPXT_History NULL +#endif + +#ifndef SKIP_CMD_TIPS_TABLE +/* MGETPXT tips */ +const char *MGETPXT_Tips[] = { +"request_policy:multi_shard", +}; +#endif + +#ifndef SKIP_CMD_KEY_SPECS_TABLE +/* MGETPXT key specs */ +keySpec MGETPXT_Keyspecs[1] = { +{NULL,CMD_KEY_RO|CMD_KEY_ACCESS,KSPEC_BS_INDEX,.bs.index={1},KSPEC_FK_RANGE,.fk.range={-1,1,0}} +}; +#endif + +/* MGETPXT argument table */ +struct COMMAND_ARG MGETPXT_Args[] = { +{MAKE_ARG("key",ARG_TYPE_KEY,0,NULL,NULL,NULL,CMD_ARG_MULTIPLE,0,NULL)}, +}; + /********** MSET ********************/ #ifndef SKIP_CMD_HISTORY_TABLE @@ -11319,6 +11369,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}, @@ -11326,6 +11377,7 @@ struct COMMAND_STRUCT serverCommandTable[] = { {MAKE_CMD("incrbyfloat","Increment the floating point value of a key by a number. Uses 0 as initial value if the key doesn't exist.","O(1)","2.6.0",CMD_DOC_NONE,NULL,NULL,"string",COMMAND_GROUP_STRING,INCRBYFLOAT_History,0,INCRBYFLOAT_Tips,0,incrbyfloatCommand,3,CMD_WRITE|CMD_DENYOOM|CMD_FAST,ACL_CATEGORY_STRING,INCRBYFLOAT_Keyspecs,1,NULL,2),.args=INCRBYFLOAT_Args}, {MAKE_CMD("lcs","Finds the longest common substring.","O(N*M) where N and M are the lengths of s1 and s2, respectively","7.0.0",CMD_DOC_NONE,NULL,NULL,"string",COMMAND_GROUP_STRING,LCS_History,0,LCS_Tips,0,lcsCommand,-3,CMD_READONLY,ACL_CATEGORY_STRING,LCS_Keyspecs,1,NULL,6),.args=LCS_Args}, {MAKE_CMD("mget","Atomically returns the string values of one or more keys.","O(N) where N is the number of keys to retrieve.","1.0.0",CMD_DOC_NONE,NULL,NULL,"string",COMMAND_GROUP_STRING,MGET_History,0,MGET_Tips,1,mgetCommand,-2,CMD_READONLY|CMD_FAST,ACL_CATEGORY_STRING,MGET_Keyspecs,1,NULL,1),.args=MGET_Args}, +{MAKE_CMD("mgetpxt","Atomically returns the string values of one or more keys and their millisecond expiration, if available.","O(N) where N is the number of keys to retrieve.","1.0.0",CMD_DOC_NONE,NULL,NULL,"string",COMMAND_GROUP_STRING,MGETPXT_History,0,MGETPXT_Tips,1,mgetpxtCommand,-2,CMD_READONLY|CMD_FAST,ACL_CATEGORY_STRING|ACL_CATEGORY_KEYSPACE,MGETPXT_Keyspecs,1,NULL,1),.args=MGETPXT_Args}, {MAKE_CMD("mset","Atomically creates or modifies the string values of one or more keys.","O(N) where N is the number of keys to set.","1.0.1",CMD_DOC_NONE,NULL,NULL,"string",COMMAND_GROUP_STRING,MSET_History,0,MSET_Tips,2,msetCommand,-3,CMD_WRITE|CMD_DENYOOM,ACL_CATEGORY_STRING,MSET_Keyspecs,1,NULL,1),.args=MSET_Args}, {MAKE_CMD("msetnx","Atomically modifies the string values of one or more keys only when all keys don't exist.","O(N) where N is the number of keys to set.","1.0.1",CMD_DOC_NONE,NULL,NULL,"string",COMMAND_GROUP_STRING,MSETNX_History,0,MSETNX_Tips,0,msetnxCommand,-3,CMD_WRITE|CMD_DENYOOM,ACL_CATEGORY_STRING,MSETNX_Keyspecs,1,NULL,1),.args=MSETNX_Args}, {MAKE_CMD("psetex","Sets both string value and expiration time in milliseconds of a key. The key is created if it doesn't exist.","O(1)","2.6.0",CMD_DOC_DEPRECATED,"`SET` with the `PX` argument","2.6.12","string",COMMAND_GROUP_STRING,PSETEX_History,0,PSETEX_Tips,0,psetexCommand,4,CMD_WRITE|CMD_DENYOOM,ACL_CATEGORY_STRING,PSETEX_Keyspecs,1,NULL,3),.args=PSETEX_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/commands/mgetpxt.json b/src/commands/mgetpxt.json new file mode 100644 index 0000000000..2c5ed43b98 --- /dev/null +++ b/src/commands/mgetpxt.json @@ -0,0 +1,88 @@ +{ + "MGETPXT": { + "summary": "Atomically returns the string values of one or more keys and their millisecond expiration, if available.", + "complexity": "O(N) where N is the number of keys to retrieve.", + "group": "string", + "since": "1.0.0", + "arity": -2, + "function": "mgetpxtCommand", + "command_flags": [ + "READONLY", + "FAST" + ], + "acl_categories": [ + "STRING", + "KEYSPACE" + ], + "command_tips": [ + "REQUEST_POLICY:MULTI_SHARD" + ], + "key_specs": [ + { + "flags": [ + "RO", + "ACCESS" + ], + "begin_search": { + "index": { + "pos": 1 + } + }, + "find_keys": { + "range": { + "lastkey": -1, + "step": 1, + "limit": 0 + } + } + } + ], + "reply_schema": { + "description": "List of values at the specified keys.", + "type": "array", + "minItems": 1, + "items": { + "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." + } + ] + } + ] + } + }, + { + "type": "null" + } + ] + } + }, + "arguments": [ + { + "name": "key", + "type": "key", + "key_spec_index": 0, + "multiple": true + } + ] + } +} diff --git a/src/server.h b/src/server.h index 8c1b517276..c95537f23a 100644 --- a/src/server.h +++ b/src/server.h @@ -3622,6 +3622,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); @@ -3694,6 +3695,7 @@ void rpoplpushCommand(client *c); void lmoveCommand(client *c); void infoCommand(client *c); void mgetCommand(client *c); +void mgetpxtCommand(client *c); void monitorCommand(client *c); void expireCommand(client *c); void expireatCommand(client *c); diff --git a/src/t_string.c b/src/t_string.c index 31feb74b4e..a9177be0f5 100644 --- a/src/t_string.c +++ b/src/t_string.c @@ -402,6 +402,30 @@ void getCommand(client *c) { getGenericCommand(c); } +void getpxtCommand(client *c) { + 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, expire); + } +} + /* * GETEX [PERSIST][EX seconds][PX milliseconds][EXAT seconds-timestamp][PXAT milliseconds-timestamp] * @@ -623,6 +647,34 @@ void mgetCommand(client *c) { } } +void mgetpxtCommand(client *c) { + int j; + + addReplyArrayLen(c, c->argc - 1); + for (j = 1; j < c->argc; j++) { + robj *o = lookupKeyRead(c->db, c->argv[j]); + if (o == NULL) { + addReplyNull(c); + } else { + if (o->type != OBJ_STRING) { + addReplyNull(c); + } else { + addReplyArrayLen(c, 2); + addReplyBulk(c, o); + + /* The key exists. Return -1 if it has no expire, or the actual + * expire value otherwise. */ + long long expire = getExpire(c->db, c->argv[j]); + if (expire == -1) { + addReplyLongLong(c, -1); + } else { + addReplyLongLong(c, expire); + } + } + } + } +} + void msetGenericCommand(client *c, int nx) { int j; diff --git a/src/valkey-benchmark.c b/src/valkey-benchmark.c index cd20d04750..f3bd2a5bab 100644 --- a/src/valkey-benchmark.c +++ b/src/valkey-benchmark.c @@ -141,6 +141,7 @@ typedef struct _client { redisContext *context; sds obuf; char **randptr; /* Pointers to :rand: strings inside the command buf */ + bool *randlast; /* Pointer to flag indicating whether :rand: should be last used or new */ size_t randlen; /* Number of pointers in client->randptr */ size_t randfree; /* Number of unused pointers in client->randptr */ char **stagptr; /* Pointers to slot hashtags (cluster mode only) */ @@ -365,6 +366,7 @@ static void freeClient(client c) { redisFree(c->context); sdsfree(c->obuf); zfree(c->randptr); + zfree(c->randlast); zfree(c->stagptr); zfree(c); if (config.num_threads) pthread_mutex_lock(&(config.liveclients_mutex)); @@ -396,16 +398,21 @@ static void resetClient(client c) { static void generateClientKey(client c) { static _Atomic size_t seq_key = 0; + size_t last_rand = 0; for (size_t i = 0; i < c->randlen; i++) { char *p = c->randptr[i] + 11; size_t key = 0; - if (config.keyspacelen != 0) { + if(c->randlast[i]) { + key = last_rand; + } + else if (config.keyspacelen != 0) { if (config.sequential_replacement) { key = atomic_fetch_add_explicit(&seq_key, 1, memory_order_relaxed); } else { key = random(); } key %= config.keyspacelen; + last_rand = key; } for (size_t j = 0; j < 12; j++) { @@ -751,6 +758,7 @@ static client createClient(char *cmd, size_t len, client from, int thread_id) { c->written = 0; c->pending = config.pipeline + c->prefix_pending; c->randptr = NULL; + c->randlast = NULL; c->randlen = 0; c->stagptr = NULL; c->staglen = 0; @@ -761,11 +769,13 @@ static client createClient(char *cmd, size_t len, client from, int thread_id) { c->randlen = from->randlen; c->randfree = 0; c->randptr = zmalloc(sizeof(char *) * c->randlen); + c->randlast = zmalloc(sizeof(bool) * c->randlen); /* copy the offsets. */ for (j = 0; j < (int)c->randlen; j++) { c->randptr[j] = c->obuf + (from->randptr[j] - from->obuf); /* Adjust for the different select prefix length. */ c->randptr[j] += c->prefixlen - from->prefixlen; + c->randlast[j] = from->randlast[j]; } } else { char *p = c->obuf; @@ -773,14 +783,23 @@ static client createClient(char *cmd, size_t len, client from, int thread_id) { c->randlen = 0; c->randfree = RANDPTR_INITIAL_SIZE; c->randptr = zmalloc(sizeof(char *) * c->randfree); - while ((p = strstr(p, "__rand_int__")) != NULL) { + c->randlast = zmalloc(sizeof(bool) * c->randfree); + while ((p = strstr(p, "__rand_")) != NULL) { + bool rand_int = !(bool)strncmp("__rand_int__", p, 12); + bool rand_last = !(bool)strncmp("__rand_lst__", p, 12); + if(!rand_int && ! rand_last) + continue; + if (c->randfree == 0) { c->randptr = zrealloc(c->randptr, sizeof(char *) * c->randlen * 2); + c->randlast = zrealloc(c->randlast, sizeof(bool) * c->randlen * 2); c->randfree += c->randlen; } - c->randptr[c->randlen++] = p; + c->randptr[c->randlen] = p; + c->randlast[c->randlen] = rand_last; + c->randlen++; c->randfree--; - p += 12; /* 12 is strlen("__rand_int__). */ + p += 12; /* 12 is strlen("__rand_int__), same for "__rand_lst__". */ } } } @@ -1593,8 +1612,10 @@ int parseOptions(int argc, char **argv) { " $ valkey-benchmark -r 10000 -n 10000 eval 'return redis.call(\"ping\")' 0\n\n" " Fill a list with 10000 random elements:\n" " $ valkey-benchmark -r 10000 -n 10000 lpush mylist __rand_int__\n\n" - " On user specified command lines __rand_int__ is replaced with a random integer\n" - " with a range of values selected by the -r option.\n"); + " On user-specified command lines __rand_int__ is replaced with a random integer\n" + " with a range of values selected by the -r option.\n" + " On user-specified command lines __rand_lst__ is replaced with the last value\n" + " of __rand_int__ or zero if no such value is available.\n"); exit(exit_status); } @@ -2022,6 +2043,39 @@ int main(int argc, char **argv) { free(cmd); } + if (test_is_selected("set_pxat")) { + len = redisFormatCommand(&cmd, "SET key%s:__rand_int__ %s PXAT 17344823940230", tag, data); + benchmark("SET w/ PXAT", cmd, len); + free(cmd); + } + + if (test_is_selected("getpxt")) { + len = redisFormatCommand(&cmd, "GETPXT key%s:__rand_int__", tag); + benchmark("GETPXT", cmd, len); + free(cmd); + } + + if (test_is_selected("getpxt_simulated")) { + redisFormatCommand(&cmd, "MULTI"); + sds pipeline = sdscatprintf(sdsempty(), "%s", cmd); + free(cmd); + + redisFormatCommand(&cmd, "GET key%s:__rand_int__", tag); + pipeline = sdscatprintf(pipeline, "%s", cmd); + free(cmd); + + redisFormatCommand(&cmd, "PEXPIRETIME key%s:__rand_lst__", tag); + pipeline = sdscatprintf(pipeline, "%s", cmd); + free(cmd); + + redisFormatCommand(&cmd, "EXEC"); + pipeline = sdscatprintf(pipeline, "%s", cmd); + free(cmd); + + benchmark("GETPXT Simulated", pipeline, sdslen(pipeline)); + sdsfree(pipeline); + } + if (test_is_selected("function_load")) { char *script = generateFunctionScript(config.num_functions, 0); len = redisFormatCommand(&cmd, "function load replace %s", script); diff --git a/tests/unit/type/string.tcl b/tests/unit/type/string.tcl index 9bd04fdd66..b69f5f7200 100644 --- a/tests/unit/type/string.tcl +++ b/tests/unit/type/string.tcl @@ -214,6 +214,28 @@ start_server {tags {"string"}} { r mget foo{t} baazz{t} bar{t} myset{t} } {BAR {} FOO {}} + test {MGETPXT} { + r flushdb + r set foo{t} BAR pxat 17344823940230 + r set bar{t} FOO pxat 17344823940231 + r mgetpxt foo{t} bar{t} + } {{BAR 17344823940230} {FOO 17344823940231}} + + test {MGETPXT against non existing key} { + r mgetpxt foo{t} baazz{t} bar{t} + } {{BAR 17344823940230} {} {FOO 17344823940231}} + + test {MGETPXT against non-string key} { + r sadd myset{t} ciao + r sadd myset{t} bau + r mgetpxt foo{t} baazz{t} bar{t} myset{t} + } {{BAR 17344823940230} {} {FOO 17344823940231} {}} + + test {MGETPXT against a key with no expiration} { + r set baz{t} BAZ + r mgetpxt foo{t} baz{t} bar{t} + } {{BAR 17344823940230} {BAZ -1} {FOO 17344823940231}} + test {GETSET (set new value)} { r del foo list [r getset foo xyz] [r get foo] @@ -658,6 +680,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 with no expiration" { + 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]