From 60e1582ddb4247195d2fbe1b52968e393103d865 Mon Sep 17 00:00:00 2001 From: Moti Cohen Date: Sun, 26 May 2024 03:34:15 +0300 Subject: [PATCH 1/2] Fix statistics test (#13293) --- tests/unit/type/hash-field-expire.tcl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/type/hash-field-expire.tcl b/tests/unit/type/hash-field-expire.tcl index 00722483251..59b47110c45 100644 --- a/tests/unit/type/hash-field-expire.tcl +++ b/tests/unit/type/hash-field-expire.tcl @@ -1278,12 +1278,12 @@ start_server {tags {"external:skip needs:debug"}} { r config resetstat r del myhash r hset myhash f1 v1 f2 v2 f3 v3 f4 v4 f5 v5 - r hpexpire myhash 100 1 f1 f2 f3 + r hpexpire myhash 100 FIELDS 3 f1 f2 f3 assert_match [get_hashes_with_expiry_fields r] 1 r hset myhash2 f1 v1 f2 v2 f3 v3 f4 v4 f5 v5 assert_match [get_hashes_with_expiry_fields r] 1 - r hpexpire myhash2 100 1 f1 f2 f3 + r hpexpire myhash2 100 FIELDS 3 f1 f2 f3 assert_match [get_hashes_with_expiry_fields r] 2 wait_for_condition 50 50 { From 2f34f6f0b98cc7de1a9b4fafe06e62703be5b0af Mon Sep 17 00:00:00 2001 From: Ozan Tezcan Date: Sun, 26 May 2024 13:30:45 +0300 Subject: [PATCH 2/2] Delete hsetf and hgetf (#13291) Changes: - Delete hsetf and hgetf commands - Hfe commands will return empty array instead of nil. --------- Co-authored-by: Moti Cohen --- src/commands.def | 115 ---- src/commands/hgetf.json | 136 ---- src/commands/hsetf.json | 216 ------ src/server.h | 2 - src/t_hash.c | 921 +------------------------- tests/unit/type/hash-field-expire.tcl | 511 ++------------ 6 files changed, 76 insertions(+), 1825 deletions(-) delete mode 100644 src/commands/hgetf.json delete mode 100644 src/commands/hsetf.json diff --git a/src/commands.def b/src/commands.def index 54a5322a59f..499fbb78f89 100644 --- a/src/commands.def +++ b/src/commands.def @@ -3455,52 +3455,6 @@ struct COMMAND_ARG HGETALL_Args[] = { {MAKE_ARG("key",ARG_TYPE_KEY,0,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)}, }; -/********** HGETF ********************/ - -#ifndef SKIP_CMD_HISTORY_TABLE -/* HGETF history */ -#define HGETF_History NULL -#endif - -#ifndef SKIP_CMD_TIPS_TABLE -/* HGETF tips */ -#define HGETF_Tips NULL -#endif - -#ifndef SKIP_CMD_KEY_SPECS_TABLE -/* HGETF key specs */ -keySpec HGETF_Keyspecs[1] = { -{NULL,CMD_KEY_RW|CMD_KEY_UPDATE,KSPEC_BS_INDEX,.bs.index={1},KSPEC_FK_RANGE,.fk.range={0,1,0}} -}; -#endif - -/* HGETF condition argument table */ -struct COMMAND_ARG HGETF_condition_Subargs[] = { -{MAKE_ARG("nx",ARG_TYPE_PURE_TOKEN,-1,"NX",NULL,NULL,CMD_ARG_NONE,0,NULL)}, -{MAKE_ARG("xx",ARG_TYPE_PURE_TOKEN,-1,"XX",NULL,NULL,CMD_ARG_NONE,0,NULL)}, -{MAKE_ARG("gt",ARG_TYPE_PURE_TOKEN,-1,"GT",NULL,NULL,CMD_ARG_NONE,0,NULL)}, -{MAKE_ARG("lt",ARG_TYPE_PURE_TOKEN,-1,"LT",NULL,NULL,CMD_ARG_NONE,0,NULL)}, -}; - -/* HGETF expiration argument table */ -struct COMMAND_ARG HGETF_expiration_Subargs[] = { -{MAKE_ARG("seconds",ARG_TYPE_INTEGER,-1,"EX",NULL,NULL,CMD_ARG_NONE,0,NULL)}, -{MAKE_ARG("milliseconds",ARG_TYPE_INTEGER,-1,"PX",NULL,NULL,CMD_ARG_NONE,0,NULL)}, -{MAKE_ARG("unix-time-seconds",ARG_TYPE_UNIX_TIME,-1,"EXAT",NULL,NULL,CMD_ARG_NONE,0,NULL)}, -{MAKE_ARG("unix-time-milliseconds",ARG_TYPE_UNIX_TIME,-1,"PXAT",NULL,NULL,CMD_ARG_NONE,0,NULL)}, -{MAKE_ARG("persist",ARG_TYPE_PURE_TOKEN,-1,"PERSIST",NULL,NULL,CMD_ARG_NONE,0,NULL)}, -}; - -/* HGETF argument table */ -struct COMMAND_ARG HGETF_Args[] = { -{MAKE_ARG("key",ARG_TYPE_KEY,0,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)}, -{MAKE_ARG("condition",ARG_TYPE_ONEOF,-1,NULL,NULL,NULL,CMD_ARG_OPTIONAL,4,NULL),.subargs=HGETF_condition_Subargs}, -{MAKE_ARG("expiration",ARG_TYPE_ONEOF,-1,NULL,NULL,NULL,CMD_ARG_OPTIONAL,5,NULL),.subargs=HGETF_expiration_Subargs}, -{MAKE_ARG("fields",ARG_TYPE_STRING,-1,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)}, -{MAKE_ARG("count",ARG_TYPE_INTEGER,-1,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)}, -{MAKE_ARG("field",ARG_TYPE_STRING,-1,NULL,NULL,NULL,CMD_ARG_MULTIPLE,0,NULL)}, -}; - /********** HINCRBY ********************/ #ifndef SKIP_CMD_HISTORY_TABLE @@ -3910,73 +3864,6 @@ struct COMMAND_ARG HSET_Args[] = { {MAKE_ARG("data",ARG_TYPE_BLOCK,-1,NULL,NULL,NULL,CMD_ARG_MULTIPLE,2,NULL),.subargs=HSET_data_Subargs}, }; -/********** HSETF ********************/ - -#ifndef SKIP_CMD_HISTORY_TABLE -/* HSETF history */ -#define HSETF_History NULL -#endif - -#ifndef SKIP_CMD_TIPS_TABLE -/* HSETF tips */ -#define HSETF_Tips NULL -#endif - -#ifndef SKIP_CMD_KEY_SPECS_TABLE -/* HSETF key specs */ -keySpec HSETF_Keyspecs[1] = { -{NULL,CMD_KEY_RW|CMD_KEY_UPDATE,KSPEC_BS_INDEX,.bs.index={1},KSPEC_FK_RANGE,.fk.range={0,1,0}} -}; -#endif - -/* HSETF create option argument table */ -struct COMMAND_ARG HSETF_create_option_Subargs[] = { -{MAKE_ARG("dcf",ARG_TYPE_PURE_TOKEN,-1,"DCF",NULL,NULL,CMD_ARG_NONE,0,NULL)}, -{MAKE_ARG("dof",ARG_TYPE_PURE_TOKEN,-1,"DOF",NULL,NULL,CMD_ARG_NONE,0,NULL)}, -}; - -/* HSETF return option argument table */ -struct COMMAND_ARG HSETF_return_option_Subargs[] = { -{MAKE_ARG("getnew",ARG_TYPE_PURE_TOKEN,-1,"GETNEW",NULL,NULL,CMD_ARG_NONE,0,NULL)}, -{MAKE_ARG("getold",ARG_TYPE_PURE_TOKEN,-1,"GETOLD",NULL,NULL,CMD_ARG_NONE,0,NULL)}, -}; - -/* HSETF condition argument table */ -struct COMMAND_ARG HSETF_condition_Subargs[] = { -{MAKE_ARG("nx",ARG_TYPE_PURE_TOKEN,-1,"NX",NULL,NULL,CMD_ARG_NONE,0,NULL)}, -{MAKE_ARG("xx",ARG_TYPE_PURE_TOKEN,-1,"XX",NULL,NULL,CMD_ARG_NONE,0,NULL)}, -{MAKE_ARG("gt",ARG_TYPE_PURE_TOKEN,-1,"GT",NULL,NULL,CMD_ARG_NONE,0,NULL)}, -{MAKE_ARG("lt",ARG_TYPE_PURE_TOKEN,-1,"LT",NULL,NULL,CMD_ARG_NONE,0,NULL)}, -}; - -/* HSETF expiration argument table */ -struct COMMAND_ARG HSETF_expiration_Subargs[] = { -{MAKE_ARG("seconds",ARG_TYPE_INTEGER,-1,"EX",NULL,NULL,CMD_ARG_NONE,0,NULL)}, -{MAKE_ARG("milliseconds",ARG_TYPE_INTEGER,-1,"PX",NULL,NULL,CMD_ARG_NONE,0,NULL)}, -{MAKE_ARG("unix-time-seconds",ARG_TYPE_UNIX_TIME,-1,"EXAT",NULL,NULL,CMD_ARG_NONE,0,NULL)}, -{MAKE_ARG("unix-time-milliseconds",ARG_TYPE_UNIX_TIME,-1,"PXAT",NULL,NULL,CMD_ARG_NONE,0,NULL)}, -{MAKE_ARG("keepttl",ARG_TYPE_PURE_TOKEN,-1,"KEEPTTL",NULL,NULL,CMD_ARG_NONE,0,NULL)}, -}; - -/* HSETF data argument table */ -struct COMMAND_ARG HSETF_data_Subargs[] = { -{MAKE_ARG("field",ARG_TYPE_STRING,-1,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)}, -{MAKE_ARG("value",ARG_TYPE_STRING,-1,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)}, -}; - -/* HSETF argument table */ -struct COMMAND_ARG HSETF_Args[] = { -{MAKE_ARG("key",ARG_TYPE_KEY,0,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)}, -{MAKE_ARG("create key option",ARG_TYPE_PURE_TOKEN,-1,"DC",NULL,NULL,CMD_ARG_OPTIONAL,0,NULL)}, -{MAKE_ARG("create option",ARG_TYPE_ONEOF,-1,NULL,NULL,NULL,CMD_ARG_OPTIONAL,2,NULL),.subargs=HSETF_create_option_Subargs}, -{MAKE_ARG("return option",ARG_TYPE_ONEOF,-1,NULL,NULL,NULL,CMD_ARG_OPTIONAL,2,NULL),.subargs=HSETF_return_option_Subargs}, -{MAKE_ARG("condition",ARG_TYPE_ONEOF,-1,NULL,NULL,NULL,CMD_ARG_OPTIONAL,4,NULL),.subargs=HSETF_condition_Subargs}, -{MAKE_ARG("expiration",ARG_TYPE_ONEOF,-1,NULL,NULL,NULL,CMD_ARG_OPTIONAL,5,NULL),.subargs=HSETF_expiration_Subargs}, -{MAKE_ARG("fvs",ARG_TYPE_STRING,-1,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)}, -{MAKE_ARG("count",ARG_TYPE_INTEGER,-1,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)}, -{MAKE_ARG("data",ARG_TYPE_BLOCK,-1,NULL,NULL,NULL,CMD_ARG_MULTIPLE,2,NULL),.subargs=HSETF_data_Subargs}, -}; - /********** HSETNX ********************/ #ifndef SKIP_CMD_HISTORY_TABLE @@ -11111,7 +10998,6 @@ struct COMMAND_STRUCT redisCommandTable[] = { {MAKE_CMD("hexpiretime","Returns the expiration time of a hash field as a Unix timestamp, in seconds.","O(N) where N is the number of specified fields","7.4.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HEXPIRETIME_History,0,HEXPIRETIME_Tips,0,hexpiretimeCommand,-5,CMD_READONLY|CMD_FAST,ACL_CATEGORY_HASH,HEXPIRETIME_Keyspecs,1,NULL,4),.args=HEXPIRETIME_Args}, {MAKE_CMD("hget","Returns the value of a field in a hash.","O(1)","2.0.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HGET_History,0,HGET_Tips,0,hgetCommand,3,CMD_READONLY|CMD_FAST,ACL_CATEGORY_HASH,HGET_Keyspecs,1,NULL,2),.args=HGET_Args}, {MAKE_CMD("hgetall","Returns all fields and values in a hash.","O(N) where N is the size of the hash.","2.0.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HGETALL_History,0,HGETALL_Tips,1,hgetallCommand,2,CMD_READONLY,ACL_CATEGORY_HASH,HGETALL_Keyspecs,1,NULL,1),.args=HGETALL_Args}, -{MAKE_CMD("hgetf","For each specified field: get its value and optionally set the field's remaining time to live / UNIX expiration timestamp in seconds / milliseconds","O(N) where N is the number of specified fields","7.4.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HGETF_History,0,HGETF_Tips,0,hgetfCommand,-5,CMD_WRITE|CMD_DENYOOM|CMD_FAST,ACL_CATEGORY_HASH,HGETF_Keyspecs,1,NULL,6),.args=HGETF_Args}, {MAKE_CMD("hincrby","Increments the integer value of a field in a hash by a number. Uses 0 as initial value if the field doesn't exist.","O(1)","2.0.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HINCRBY_History,0,HINCRBY_Tips,0,hincrbyCommand,4,CMD_WRITE|CMD_DENYOOM|CMD_FAST,ACL_CATEGORY_HASH,HINCRBY_Keyspecs,1,NULL,3),.args=HINCRBY_Args}, {MAKE_CMD("hincrbyfloat","Increments the floating point value of a field by a number. Uses 0 as initial value if the field doesn't exist.","O(1)","2.6.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HINCRBYFLOAT_History,0,HINCRBYFLOAT_Tips,0,hincrbyfloatCommand,4,CMD_WRITE|CMD_DENYOOM|CMD_FAST,ACL_CATEGORY_HASH,HINCRBYFLOAT_Keyspecs,1,NULL,3),.args=HINCRBYFLOAT_Args}, {MAKE_CMD("hkeys","Returns all fields in a hash.","O(N) where N is the size of the hash.","2.0.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HKEYS_History,0,HKEYS_Tips,1,hkeysCommand,2,CMD_READONLY,ACL_CATEGORY_HASH,HKEYS_Keyspecs,1,NULL,1),.args=HKEYS_Args}, @@ -11126,7 +11012,6 @@ struct COMMAND_STRUCT redisCommandTable[] = { {MAKE_CMD("hrandfield","Returns one or more random fields from a hash.","O(N) where N is the number of fields returned","6.2.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HRANDFIELD_History,0,HRANDFIELD_Tips,1,hrandfieldCommand,-2,CMD_READONLY,ACL_CATEGORY_HASH,HRANDFIELD_Keyspecs,1,NULL,2),.args=HRANDFIELD_Args}, {MAKE_CMD("hscan","Iterates over fields and values of a hash.","O(1) for every call. O(N) for a complete iteration, including enough command calls for the cursor to return back to 0. N is the number of elements inside the collection.","2.8.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HSCAN_History,0,HSCAN_Tips,1,hscanCommand,-3,CMD_READONLY,ACL_CATEGORY_HASH,HSCAN_Keyspecs,1,NULL,5),.args=HSCAN_Args}, {MAKE_CMD("hset","Creates or modifies the value of a field in a hash.","O(1) for each field/value pair added, so O(N) to add N field/value pairs when the command is called with multiple field/value pairs.","2.0.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HSET_History,1,HSET_Tips,0,hsetCommand,-4,CMD_WRITE|CMD_DENYOOM|CMD_FAST,ACL_CATEGORY_HASH,HSET_Keyspecs,1,NULL,2),.args=HSET_Args}, -{MAKE_CMD("hsetf","For each specified field value pair: set field to value and optionally set the field's remaining time to live / UNIX expiration timestamp in seconds / milliseconds","O(N) where N is the number of specified fields","7.4.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HSETF_History,0,HSETF_Tips,0,hsetfCommand,-6,CMD_WRITE|CMD_DENYOOM|CMD_FAST,ACL_CATEGORY_HASH,HSETF_Keyspecs,1,NULL,9),.args=HSETF_Args}, {MAKE_CMD("hsetnx","Sets the value of a field in a hash only when the field doesn't exist.","O(1)","2.0.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HSETNX_History,0,HSETNX_Tips,0,hsetnxCommand,4,CMD_WRITE|CMD_DENYOOM|CMD_FAST,ACL_CATEGORY_HASH,HSETNX_Keyspecs,1,NULL,3),.args=HSETNX_Args}, {MAKE_CMD("hstrlen","Returns the length of the value of a field.","O(1)","3.2.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HSTRLEN_History,0,HSTRLEN_Tips,0,hstrlenCommand,3,CMD_READONLY|CMD_FAST,ACL_CATEGORY_HASH,HSTRLEN_Keyspecs,1,NULL,2),.args=HSTRLEN_Args}, {MAKE_CMD("httl","Returns the TTL in seconds of a hash field.","O(N) where N is the number of specified fields","7.4.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HTTL_History,0,HTTL_Tips,0,httlCommand,-5,CMD_READONLY|CMD_FAST,ACL_CATEGORY_HASH,HTTL_Keyspecs,1,NULL,4),.args=HTTL_Args}, diff --git a/src/commands/hgetf.json b/src/commands/hgetf.json deleted file mode 100644 index 9124da8a8ee..00000000000 --- a/src/commands/hgetf.json +++ /dev/null @@ -1,136 +0,0 @@ -{ - "HGETF": { - "summary": "For each specified field: get its value and optionally set the field's remaining time to live / UNIX expiration timestamp in seconds / milliseconds", - "complexity": "O(N) where N is the number of specified fields", - "group": "hash", - "since": "7.4.0", - "arity": -5, - "function": "hgetfCommand", - "history": [], - "command_flags": [ - "WRITE", - "DENYOOM", - "FAST" - ], - "acl_categories": [ - "HASH" - ], - "key_specs": [ - { - "flags": [ - "RW", - "UPDATE" - ], - "begin_search": { - "index": { - "pos": 1 - } - }, - "find_keys": { - "range": { - "lastkey": 0, - "step": 1, - "limit": 0 - } - } - } - ], - "reply_schema": { - "oneOf": [ - { - "description": "Key does not exist.", - "type": "null" - }, - { - "description": "Array of results", - "type": "array", - "minItems": 1, - "maxItems": 4294967295, - "items": { - "description": "Field value", - "type": "string" - } - } - ] - }, - "arguments": [ - { - "name": "key", - "type": "key", - "key_spec_index": 0 - }, - { - "name": "condition", - "type": "oneof", - "optional": true, - "arguments": [ - { - "name": "nx", - "type": "pure-token", - "token": "NX" - }, - { - "name": "xx", - "type": "pure-token", - "token": "XX" - }, - { - "name": "gt", - "type": "pure-token", - "token": "GT" - }, - { - "name": "lt", - "type": "pure-token", - "token": "LT" - } - ] - }, - { - "name": "expiration", - "type": "oneof", - "optional": true, - "arguments": [ - { - "name": "seconds", - "type": "integer", - "token": "EX" - }, - { - "name": "milliseconds", - "type": "integer", - "token": "PX" - }, - { - "name": "unix-time-seconds", - "type": "unix-time", - "token": "EXAT" - }, - { - "name": "unix-time-milliseconds", - "type": "unix-time", - "token": "PXAT" - }, - { - "name": "persist", - "type": "pure-token", - "token": "PERSIST" - } - ] - }, - { - "name": "FIELDS", - "type": "string" - }, - { - "name": "count", - "type": "integer" - }, - { - "name": "field", - "type": "string", - "multiple": true - } - ] - } -} diff --git a/src/commands/hsetf.json b/src/commands/hsetf.json deleted file mode 100644 index d7e42f9673e..00000000000 --- a/src/commands/hsetf.json +++ /dev/null @@ -1,216 +0,0 @@ -{ - "HSETF": { - "summary": "For each specified field value pair: set field to value and optionally set the field's remaining time to live / UNIX expiration timestamp in seconds / milliseconds", - "complexity": "O(N) where N is the number of specified fields", - "group": "hash", - "since": "7.4.0", - "arity": -6, - "function": "hsetfCommand", - "history": [], - "command_flags": [ - "WRITE", - "DENYOOM", - "FAST" - ], - "acl_categories": [ - "HASH" - ], - "key_specs": [ - { - "flags": [ - "RW", - "UPDATE" - ], - "begin_search": { - "index": { - "pos": 1 - } - }, - "find_keys": { - "range": { - "lastkey": 0, - "step": 1, - "limit": 0 - } - } - } - ], - "reply_schema": { - "oneOf": [ - { - "description": "Key does not exist and DC condition was given", - "type": "null" - }, - { - "description": "Array of field values when GETNEW/GETOLD is used", - "type": "array", - "minItems": 1, - "maxItems": 4294967295, - "items": { - "oneOf": [ - { - "description": "Field value", - "type": "string" - }, - { - "description": "Field does not exist and couldn't create it (DCF not met)", - "type": "null" - } - ] - } - }, - { - "description": "Array of results", - "type": "array", - "minItems": 1, - "maxItems": 4294967295, - "items": { - "oneOf": [ - { - "description": "Cannot set field value. DOC/DOF condition not met.", - "const": 0 - }, - { - "description": "Set field value without updating TTL", - "const": 1 - }, - { - "description": "Set field value and updated TTL", - "const": 3 - } - ] - } - } - ] - }, - "arguments": [ - { - "name": "key", - "type": "key", - "key_spec_index": 0 - }, - { - "name": "create key option", - "optional": true, - "type": "pure-token", - "token": "DC" - }, - { - "name": "create option", - "type": "oneof", - "optional": true, - "arguments": [ - { - "name": "dcf", - "type": "pure-token", - "token": "DCF" - }, - { - "name": "dof", - "type": "pure-token", - "token": "DOF" - } - ] - }, - { - "name": "return option", - "type": "oneof", - "optional": true, - "arguments": [ - { - "name": "getnew", - "type": "pure-token", - "token": "GETNEW" - }, - { - "name": "getold", - "type": "pure-token", - "token": "GETOLD" - } - ] - }, - { - "name": "condition", - "type": "oneof", - "optional": true, - "arguments": [ - { - "name": "nx", - "type": "pure-token", - "token": "NX" - }, - { - "name": "xx", - "type": "pure-token", - "token": "XX" - }, - { - "name": "gt", - "type": "pure-token", - "token": "GT" - }, - { - "name": "lt", - "type": "pure-token", - "token": "LT" - } - ] - }, - { - "name": "expiration", - "type": "oneof", - "optional": true, - "arguments": [ - { - "name": "seconds", - "type": "integer", - "token": "EX" - }, - { - "name": "milliseconds", - "type": "integer", - "token": "PX" - }, - { - "name": "unix-time-seconds", - "type": "unix-time", - "token": "EXAT" - }, - { - "name": "unix-time-milliseconds", - "type": "unix-time", - "token": "PXAT" - }, - { - "name": "keepttl", - "type": "pure-token", - "token": "KEEPTTL" - } - ] - }, - { - "name": "FVS", - "type": "string" - }, - { - "name": "count", - "type": "integer" - }, - { - "name": "data", - "type": "block", - "multiple": true, - "arguments": [ - { - "name": "field", - "type": "string" - }, - { - "name": "value", - "type": "string" - } - ] - } - ] - } -} diff --git a/src/server.h b/src/server.h index 70a778c2532..c433f5dab37 100644 --- a/src/server.h +++ b/src/server.h @@ -3669,7 +3669,6 @@ void strlenCommand(client *c); void zrankCommand(client *c); void zrevrankCommand(client *c); void hsetCommand(client *c); -void hsetfCommand(client *c); void hpexpireCommand(client *c); void hexpireCommand(client *c); void hpexpireatCommand(client *c); @@ -3681,7 +3680,6 @@ void hpexpiretimeCommand(client *c); void hpersistCommand(client *c); void hsetnxCommand(client *c); void hgetCommand(client *c); -void hgetfCommand(client *c); void hmgetCommand(client *c); void hdelCommand(client *c); void hlenCommand(client *c); diff --git a/src/t_hash.c b/src/t_hash.c index 299bea8aa78..8f5f0ee3f65 100644 --- a/src/t_hash.c +++ b/src/t_hash.c @@ -21,7 +21,6 @@ static ExpireMeta *hashGetExpireMeta(const eItem hash); static void hexpireGenericCommand(client *c, const char *cmd, long long basetime, int unit); static ExpireAction hashTypeActiveExpire(eItem hashObj, void *ctx); static void hfieldPersist(robj *hashObj, hfield field); -static void updateGlobalHfeDs(redisDb *db, robj *o, uint64_t minExpire, uint64_t minExpireFields); /* hash dictType funcs */ static int dictHfieldKeyCompare(dict *d, const void *key1, const void *key2); @@ -1076,7 +1075,31 @@ void hashTypeSetExDone(HashTypeSetEx *ex) { dbDelete(ex->db,ex->key); if (ex->c) notifyKeyspaceEvent(NOTIFY_GENERIC,"del",ex->key, ex->db->id); } else { - updateGlobalHfeDs(ex->db, ex->hashObj, ex->minExpire, ex->minExpireFields); + /* If minimum HFE of the hash is smaller than expiration time of the + * specified fields in the command as well as it is smaller or equal + * than expiration time provided in the command, then the minimum + * HFE of the hash won't change following this command. */ + if ((ex->minExpire < ex->minExpireFields)) + return; + + /* retrieve new expired time. It might have changed. */ + uint64_t newMinExpire = hashTypeGetNextTimeToExpire(ex->hashObj); + + /* Calculate the diff between old minExpire and newMinExpire. If it is + * only few seconds, then don't have to update global HFE DS. At the worst + * case fields of hash will be active-expired up to few seconds later. + * + * In any case, active-expire operation will know to update global + * HFE DS more efficiently than here for a single item. + */ + uint64_t diff = (ex->minExpire > newMinExpire) ? + (ex->minExpire - newMinExpire) : (newMinExpire - ex->minExpire); + if (diff < HASH_NEW_EXPIRE_DIFF_THRESHOLD) return; + + if (ex->minExpire != EB_EXPIRE_TIME_INVALID) + ebRemove(&ex->db->hexpires, &hashExpireBucketsType, ex->hashObj); + if (newMinExpire != EB_EXPIRE_TIME_INVALID) + ebAdd(&ex->db->hexpires, &hashExpireBucketsType, ex->hashObj, newMinExpire); } } } @@ -2679,7 +2702,7 @@ static void httlGenericCommand(client *c, const char *cmd, long long basetime, i long numFields = 0, numFieldsAt = 3; /* Read the hash object */ - if ((hashObj = lookupKeyReadOrReply(c, c->argv[1], shared.null[c->resp])) == NULL || + if ((hashObj = lookupKeyReadOrReply(c, c->argv[1], shared.emptyarray)) == NULL || checkType(c, hashObj, OBJ_HASH)) return; if (strcasecmp(c->argv[numFieldsAt-1]->ptr, "FIELDS")) { @@ -2802,7 +2825,7 @@ static void hexpireGenericCommand(client *c, const char *cmd, long long basetime robj *hashObj, *keyArg = c->argv[1], *expireArg = c->argv[2]; /* Read the hash object */ - if ((hashObj = lookupKeyWriteOrReply(c, keyArg, shared.null[c->resp])) == NULL || + if ((hashObj = lookupKeyWriteOrReply(c, keyArg, shared.emptyarray)) == NULL || checkType(c, hashObj, OBJ_HASH)) return; /* Read the expiry time from command */ @@ -2926,7 +2949,7 @@ void hpersistCommand(client *c) { int changed = 0; /* Used to determine whether to send a notification. */ /* Read the hash object */ - if ((hashObj = lookupKeyReadOrReply(c, c->argv[1], shared.null[c->resp])) == NULL || + if ((hashObj = lookupKeyReadOrReply(c, c->argv[1], shared.emptyarray)) == NULL || checkType(c, hashObj, OBJ_HASH)) return; if (strcasecmp(c->argv[numFieldsAt-1]->ptr, "FIELDS")) { @@ -3035,891 +3058,3 @@ void hpersistCommand(client *c) { * has been successfully deleted. */ if (changed) notifyKeyspaceEvent(NOTIFY_HASH,"hpersist",c->argv[1],c->db->id); } - -/** - * TODO: Move top of the file - * HGETF - HSETF command arguments - */ -#define HFE_CMD_NX (1<<0) /* If not exist */ -#define HFE_CMD_XX (1<<1) /* If exists */ -#define HFE_CMD_GT (1<<2) /* Greater than */ -#define HFE_CMD_LT (1<<3) /* Less than */ -#define HFE_CMD_COND_MASK (0x0F) - -#define HFE_CMD_PX (1<<4) /* Milliseconds */ -#define HFE_CMD_EX (1<<5) /* Seconds */ -#define HFE_CMD_PXAT (1<<6) /* Unix timestamp milliseconds */ -#define HFE_CMD_EXAT (1<<7) /* Unix timestamp seconds */ -#define HFE_CMD_PERSIST (1<<8) /* Delete TTL */ -#define HFE_CMD_KEEPTTL (1<<9) /* Keep TTL */ -#define HFE_CMD_EXPIRY_MASK (0x3F0) - -#define HFE_CMD_DC (1<<10) /* Don't create key */ -#define HFE_CMD_DCF (1<<11) /* Don't create field */ -#define HFE_CMD_DOF (1<<12) /* Don't overwrite field */ -#define HFE_CMD_GETNEW (1<<13) /* Get new value */ -#define HFE_CMD_GETOLD (1<<14) /* Get old value */ - -#define HSETF_FAIL 0 /* Failed to set value (DCF/DOF not met) */ -#define HSETF_FIELD 1 /* Field value is set without TTL */ -#define HSETF_FIELD_AND_TTL 3 /* Both field value and TTL is set */ - -/* Validate expire time is not more than EB_EXPIRE_TIME_MAX, - * or it does not overflow */ -static int validateExpire(client *c, int unit, robj *o, long long basetime, - uint64_t *expire) -{ - long long val; - /* Read the expiry time from command */ - if (getLongLongFromObjectOrReply(c, o, &val, NULL) != C_OK) - return C_ERR; - - if (val < 0 || val > (long long) EB_EXPIRE_TIME_MAX) { - addReplyErrorExpireTime(c); - return C_ERR; - } - - if (unit == UNIT_SECONDS) { - if (val > (long long) EB_EXPIRE_TIME_MAX / 1000) { - addReplyErrorExpireTime(c); - return C_ERR; - } - val *= 1000; - } else { - if (val > (long long) EB_EXPIRE_TIME_MAX) { - addReplyErrorExpireTime(c); - return C_ERR; - } - } - - if (val > (long long) EB_EXPIRE_TIME_MAX - basetime) { - addReplyErrorExpireTime(c); - return C_ERR; - } - val += basetime; - *expire = val; - return C_OK; -} - -/* Convert listpack to listpackEx encoding or attach hfe meta to dict */ -static void attachHfeMeta(redisDb *db, robj *o, robj *keyArg) { - if (o->encoding == OBJ_ENCODING_LISTPACK) { - hashTypeConvert(o, OBJ_ENCODING_LISTPACK_EX, &db->hexpires); - - listpackEx *lpt = o->ptr; - dictEntry *de = dbFind(db, keyArg->ptr); - serverAssert(de != NULL); - lpt->key = dictGetKey(de); - } else if (o->encoding == OBJ_ENCODING_HT) { - dictExpireMetadata *dictExpireMeta; - dict *d = o->ptr; - - /* If dict doesn't have metadata attached */ - if (!isDictWithMetaHFE(d)) { - /* Realloc (only header of dict) with metadata for hash-field expiration */ - dictTypeAddMeta(&d, &mstrHashDictTypeWithHFE); - dictExpireMeta = (dictExpireMetadata *) dictMetadata(d); - o->ptr = d; - - /* Find the key in the keyspace. Need to keep reference to the key for - * notifications or even removal of the hash */ - dictEntry *de = dbFind(db, keyArg->ptr); - serverAssert(de != NULL); - sds key = dictGetKey(de); - - /* Fillup dict HFE metadata */ - dictExpireMeta->key = key; /* reference key in keyspace */ - dictExpireMeta->hfe = ebCreate(); /* Allocate HFE DS */ - dictExpireMeta->expireMeta.trash = 1; /* mark as trash (as long it wasn't ebAdd()) */ - } - } -} - -/* - * Called after modifying fields to update global hfe DS if necessary - * - * minExpire: minimum expiry time of the key before modification - * minExpireFields: minimum expiry time of the modified fields - */ -static void updateGlobalHfeDs(redisDb *db, robj *o,uint64_t minExpire, uint64_t minExpireFields) -{ - /* If minimum HFE of the hash is smaller than expiration time of the - * specified fields in the command as well as it is smaller or equal - * than expiration time provided in the command, then the minimum - * HFE of the hash won't change following this command. */ - if ((minExpire < minExpireFields)) - return; - - /* retrieve new expired time. It might have changed. */ - uint64_t newMinExpire = hashTypeGetNextTimeToExpire(o); - - /* Calculate the diff between old minExpire and newMinExpire. If it is - * only few seconds, then don't have to update global HFE DS. At the worst - * case fields of hash will be active-expired up to few seconds later. - * - * In any case, active-expire operation will know to update global - * HFE DS more efficiently than here for a single item. - */ - uint64_t diff = (minExpire > newMinExpire) ? - (minExpire - newMinExpire) : (newMinExpire - minExpire); - if (diff < HASH_NEW_EXPIRE_DIFF_THRESHOLD) return; - - if (minExpire != EB_EXPIRE_TIME_INVALID) - ebRemove(&db->hexpires, &hashExpireBucketsType, o); - if (newMinExpire != EB_EXPIRE_TIME_INVALID) - ebAdd(&db->hexpires, &hashExpireBucketsType, o, newMinExpire); -} - -/* Parse hgetf command arguments. */ -static int hgetfParseArgs(client *c, int *flags, uint64_t *expireAt, - int *firstFieldPos, int *fieldCount) -{ - *flags = 0; - *firstFieldPos = -1; - *fieldCount = -1; - - for (int i = 2; i < c->argc; i++) { - if (!strcasecmp(c->argv[i]->ptr, "fields")) { - long val; - - if (*firstFieldPos != -1) { - addReplyErrorFormat(c, "multiple FIELDS argument"); - return C_ERR; - } - - if (i >= c->argc - 2) { - addReplyErrorArity(c); - return C_ERR; - } - - if (getRangeLongFromObjectOrReply(c, c->argv[i + 1], 1, INT_MAX, &val, - "invalid number of fields") != C_OK) - return C_ERR; - - if (val > c->argc - i - 2) { - addReplyErrorArity(c); - return C_ERR; - } - - *firstFieldPos = i + 2; - *fieldCount = (int) val; - i = *firstFieldPos + *fieldCount - 1; - } else if (!strcasecmp(c->argv[i]->ptr, "NX")) { - if (*flags & (HFE_CMD_XX | HFE_CMD_GT | HFE_CMD_LT)) - goto err_condition; - *flags |= HFE_CMD_NX; - } else if (!strcasecmp(c->argv[i]->ptr, "XX")) { - if (*flags & (HFE_CMD_NX | HFE_CMD_GT | HFE_CMD_LT)) - goto err_condition; - *flags |= HFE_CMD_XX; - } else if (!strcasecmp(c->argv[i]->ptr, "GT")) { - if (*flags & (HFE_CMD_NX | HFE_CMD_XX | HFE_CMD_LT)) - goto err_condition; - *flags |= HFE_CMD_GT; - } else if (!strcasecmp(c->argv[i]->ptr, "LT")) { - if (*flags & (HFE_CMD_NX | HFE_CMD_XX | HFE_CMD_GT)) - goto err_condition; - *flags |= HFE_CMD_LT; - } else if (!strcasecmp(c->argv[i]->ptr, "EX")) { - if (*flags & (HFE_CMD_EXAT | HFE_CMD_PX | HFE_CMD_PXAT | HFE_CMD_PERSIST)) - goto err_expiration; - - if (i >= c->argc - 1) - goto err_missing_expire; - - *flags |= HFE_CMD_EX; - i++; - if (validateExpire(c, UNIT_SECONDS, c->argv[i], - commandTimeSnapshot(), expireAt) != C_OK) - return C_ERR; - - } else if (!strcasecmp(c->argv[i]->ptr, "PX")) { - if (*flags & (HFE_CMD_EX | HFE_CMD_EXAT | HFE_CMD_PXAT | HFE_CMD_PERSIST)) - goto err_expiration; - - if (i >= c->argc - 1) - goto err_missing_expire; - - *flags |= HFE_CMD_PX; - i++; - if (validateExpire(c, UNIT_MILLISECONDS, c->argv[i], - commandTimeSnapshot(), expireAt) != C_OK) - return C_ERR; - } else if (!strcasecmp(c->argv[i]->ptr, "EXAT")) { - if (*flags & (HFE_CMD_EX | HFE_CMD_PX | HFE_CMD_PXAT | HFE_CMD_PERSIST)) - goto err_expiration; - - if (i >= c->argc - 1) - goto err_missing_expire; - - *flags |= HFE_CMD_EXAT; - i++; - if (validateExpire(c, UNIT_SECONDS, c->argv[i], 0, expireAt) != C_OK) - return C_ERR; - } else if (!strcasecmp(c->argv[i]->ptr, "PXAT")) { - if (*flags & (HFE_CMD_EX | HFE_CMD_EXAT | HFE_CMD_PX | HFE_CMD_PERSIST)) - goto err_expiration; - - if (i >= c->argc - 1) - goto err_missing_expire; - - *flags |= HFE_CMD_PXAT; - i++; - if (validateExpire(c, UNIT_MILLISECONDS, c->argv[i], 0, expireAt) != C_OK) - return C_ERR; - } else if (!strcasecmp(c->argv[i]->ptr, "PERSIST")) { - if (*flags & (HFE_CMD_EX | HFE_CMD_EXAT | HFE_CMD_PX | HFE_CMD_PXAT)) - goto err_expiration; - *flags |= HFE_CMD_PERSIST; - } else { - addReplyErrorFormat(c, "unknown argument: %s", (char*) c->argv[i]->ptr); - return C_ERR; - } - } - - /* FIELDS argument is mandatory. */ - if (*firstFieldPos < 0) { - addReplyError(c, "missing FIELDS argument"); - return C_ERR; - } - - if (*flags & HFE_CMD_COND_MASK && - (!(*flags & (HFE_CMD_EX | HFE_CMD_EXAT | HFE_CMD_PX | HFE_CMD_PXAT)))) - { - addReplyError(c, "NX, XX, GT, and LT can be specified only when EX, PX, EXAT, or PXAT is specified"); - return C_ERR; - } - - return C_OK; - -err_missing_expire: - addReplyError(c, "missing expire time"); - return C_ERR; -err_condition: - addReplyError(c, "Only one of NX, XX, GT, and LT arguments can be specified"); - return C_ERR; -err_expiration: - addReplyError(c, "Only one of EX, PX, EXAT, PXAT or PERSIST arguments can be specified"); - return C_ERR; -} - -/* Reply with field value and optionally set expire time according to 'flag'. - * Return 1 if expire time is updated. */ -static int hgetfReplyValueAndSetExpiry(client *c, robj *o, sds field, int flag, - uint64_t expireAt, uint64_t *minPrevExp) -{ - unsigned char *fptr = NULL, *vptr = NULL, *tptr; - hfield hf = NULL; - dict *d = NULL; - dictEntry *de = NULL; - uint64_t prevExpire = EB_EXPIRE_TIME_INVALID; - - if (o->encoding == OBJ_ENCODING_HT) { - d = o->ptr; - /* First retrieve the field to check if it exists */ - de = dictFind(d, field); - if (de == NULL) { - addReplyNull(c); - return 0; - } - - hf = dictGetKey(de); - if (hfieldIsExpired(hf)) { - addReplyNull(c); - return 0; - } - prevExpire = hfieldGetExpireTime(hf); - - /* Reply with value */ - sds val = dictGetVal(de); - addReplyBulkCBuffer(c, val, sdslen(val)); - } else if (o->encoding == OBJ_ENCODING_LISTPACK_EX) { - long long expire; - unsigned char *vstr = NULL; - unsigned int vlen = UINT_MAX; - long long vll = LLONG_MAX; - listpackEx *lpt = o->ptr; - - fptr = lpFirst(lpt->lp); - if (fptr != NULL) { - fptr = lpFind(lpt->lp, fptr, (unsigned char *) field, sdslen(field), 2); - if (fptr != NULL) { - vptr = lpNext(lpt->lp, fptr); - serverAssert(vptr != NULL); - - tptr = lpNext(lpt->lp, vptr); - serverAssert(tptr && lpGetIntegerValue(tptr, &expire)); - - if (expire != HASH_LP_NO_TTL) - prevExpire = expire; - } - } - - /* Return null if field does not exist */ - if (fptr == NULL || hashTypeIsExpired(o, expire)) { - addReplyNull(c); - return 0; - } - - /* Reply with value */ - vstr = lpGetValue(vptr, &vlen, &vll); - if (vstr) - addReplyBulkCBuffer(c, vstr, vlen); - else - addReplyBulkLongLong(c, vll); - } else { - serverPanic("Unknown encoding: %d", o->encoding); - } - - if (!(flag & HFE_CMD_EXPIRY_MASK) || /* Check if any of EX, EXAT, PX, PXAT, PERSIST flags is set */ - ((flag & HFE_CMD_GT) && (expireAt <= prevExpire)) || - ((flag & HFE_CMD_LT) && (expireAt >= prevExpire)) || - ((flag & HFE_CMD_XX) && (prevExpire == EB_EXPIRE_TIME_INVALID)) || - ((flag & HFE_CMD_NX) && (prevExpire != EB_EXPIRE_TIME_INVALID)) || - ((flag & HFE_CMD_PERSIST) && (prevExpire == EB_EXPIRE_TIME_INVALID))) { - return 0; - } - - if (*minPrevExp > prevExpire) - *minPrevExp = prevExpire; - - /* if expiration time is in the past */ - if (checkAlreadyExpired(expireAt)) { - hashTypeDelete(o, field); - return 1; - } - - if (o->encoding == OBJ_ENCODING_HT) { - if (flag & HFE_CMD_PERSIST) { - hfieldPersist(o, hf); - } else { - if (!hfieldIsExpireAttached(hf)) { - /* allocate new field with expire metadata */ - hfield hfNew = hfieldNew(hf, hfieldlen(hf), 1 /*withExpireMeta*/); - /* Replace the old field with the new one with metadata */ - dictSetKey(d, de, hfNew); - hfieldFree(hf); - hf = hfNew; - } - - dictExpireMetadata *meta = (dictExpireMetadata *) dictMetadata(d); - if (prevExpire != EB_EXPIRE_TIME_INVALID) - ebRemove(&meta->hfe, &hashFieldExpireBucketsType, hf); - - ebAdd(&meta->hfe, &hashFieldExpireBucketsType, hf, expireAt); - } - } else { - uint64_t exp = flag & HFE_CMD_PERSIST ? HASH_LP_NO_TTL : expireAt; - listpackExUpdateExpiry(o, field, fptr, vptr, exp); - } - - return 1; -} - -/* - * For each specified field: get its value and optionally set the field's - * remaining time to live. - * - * HGETF key - * [NX | XX | GT | LT] - * [EX seconds | PX milliseconds | EXAT unix-time-seconds | PXAT unix-time-milliseconds | PERSIST] - * - **/ -void hgetfCommand(client *c) { - int flags = 0; - robj *hashObj, *keyArg = c->argv[1]; - - int firstFieldPos = 0; - int numFields = 0; - uint64_t expireAt = EB_EXPIRE_TIME_INVALID; - - if (hgetfParseArgs(c, &flags, &expireAt, &firstFieldPos, &numFields) != C_OK) - return; - - /* Read the hash object */ - if ((hashObj = lookupKeyWriteOrReply(c, c->argv[1], shared.null[c->resp])) == NULL || - checkType(c, hashObj, OBJ_HASH)) return; - - attachHfeMeta(c->db, hashObj, keyArg); - uint64_t minExpire = hashTypeGetMinExpire(hashObj); - - /* Figure out from provided set of fields in command, which one has the minimum - * expiration time, before the modification (Will be used for optimization below) */ - uint64_t minExpireFields = EB_EXPIRE_TIME_INVALID; - - int updated = 0; - addReplyArrayLen(c, numFields); - for (int i = 0; i < numFields ; i++) { - sds field = c->argv[firstFieldPos + i]->ptr; - updated += hgetfReplyValueAndSetExpiry(c, hashObj, field, flags, - expireAt, &minExpireFields); - } - - /* Notify keyspace event, update dirty count and update global HFE DS */ - if (updated > 0) { - server.dirty += updated; - signalModifiedKey(c,c->db,keyArg); - notifyKeyspaceEvent(NOTIFY_HASH,"hgetf",keyArg,c->db->id); - if (hashTypeLength(hashObj, 0) == 0) { - dbDelete(c->db,keyArg); - notifyKeyspaceEvent(NOTIFY_GENERIC,"del",keyArg, c->db->id); - } else { - updateGlobalHfeDs(c->db, hashObj, minExpire, minExpireFields); - } - } -} - -/* Check hsetf command args and return 1 if TTL will be updated/discarded. */ -static int hsetfCheckTTLCondition(int flag, uint64_t prevExpire, uint64_t expireAt) { - /* When none of EX, PX, EXAT, PXAT, KEEPTTL are specified: - * any previous expiration time associated with field is discarded. */ - if (!(flag & HFE_CMD_EXPIRY_MASK) && prevExpire != EB_EXPIRE_TIME_INVALID) - return 1; - - if ((flag & (HFE_CMD_PX | HFE_CMD_PXAT | HFE_CMD_EX | HFE_CMD_EXAT))) { - if (((flag & HFE_CMD_COND_MASK) == 0) || /* None of NX, PX, GT, LT is set */ - ((flag & HFE_CMD_GT) && (expireAt > prevExpire)) || - ((flag & HFE_CMD_LT) && (expireAt < prevExpire)) || - (flag & HFE_CMD_XX && prevExpire != EB_EXPIRE_TIME_INVALID) || - (flag & HFE_CMD_NX && prevExpire == EB_EXPIRE_TIME_INVALID)) { - return 1; - } - } - return 0; -} - -/* For hsetf command, add reply from listpack item */ -static void hsetfReplyFromListpack(client *c, unsigned char *vptr) { - unsigned int vlen = UINT_MAX; - long long vll = LLONG_MAX; - unsigned char *vstr = NULL; - - if (!vptr) { - addReplyNull(c); - } else { - vstr = lpGetValue(vptr, &vlen, &vll); - if (vstr) - addReplyBulkCBuffer(c, vstr, vlen); - else - addReplyBulkLongLong(c, vll); - } -} - -/* For hsetf command, add reply to client according to flag argument. */ -static void hsetfAddReply(client *c, int flag, sds prevval, sds newval, int ret) { - if (flag & HFE_CMD_GETOLD) { - if (!prevval) { - addReplyNull(c); - } else { - addReplyBulkCBuffer(c, prevval, sdslen(prevval)); - } - } else if (flag & HFE_CMD_GETNEW) { - if (!newval) { - addReplyNull(c); - } else { - addReplyBulkCBuffer(c, newval, sdslen(newval)); - } - } else { - addReplyLongLong(c, ret); - } -} - -/* Set field and expire time according to 'flag'. - * Return 1 if field and/or expire time is updated. */ -static int hsetfSetFieldAndReply(client *c, robj *o, sds field, sds value, - int flag, uint64_t expireAt, uint64_t *minPrevExp) -{ - int ret = HSETF_FAIL; - uint64_t prevExpire = EB_EXPIRE_TIME_INVALID; - - if (o->encoding == OBJ_ENCODING_LISTPACK_EX) { - long long expire; - unsigned char *fptr, *vptr = NULL, *tptr; - listpackEx *lpt = o->ptr; - - fptr = lpFirst(lpt->lp); - if (fptr != NULL) { - fptr = lpFind(lpt->lp, fptr, (unsigned char *) field, sdslen(field), 2); - if (fptr != NULL) { - vptr = lpNext(lpt->lp, fptr); - tptr = lpNext(lpt->lp, vptr); - serverAssert(tptr && lpGetIntegerValue(tptr, &expire)); - - if (expire != HASH_LP_NO_TTL) - prevExpire = expire; - } - } - - /* Check DCF (don't create fields) and DOF (don't override fields) arg. */ - if ((!fptr && (flag & HFE_CMD_DCF)) || (fptr && (flag & HFE_CMD_DOF))) { - /* When GETNEW or GETOLD is specified, regardless if a set operation - * was actually performed, we return value / old value of field or - * nil if there is no field. One corner case, if GETNEW and DOF - * (don't override fields) arguments are given and field exists, we - * won't override the field and return the existing value. - */ - if (flag & (HFE_CMD_GETNEW | HFE_CMD_GETOLD)) - hsetfReplyFromListpack(c, vptr); - else - addReplyLongLong(c, ret); - - return 0; - } - - /* Field value will be updated. */ - ret = HSETF_FIELD; - - /* Decide if we are going to set TTL */ - if (hsetfCheckTTLCondition(flag, prevExpire, expireAt)) - ret = HSETF_FIELD_AND_TTL; - - if (flag & HFE_CMD_GETOLD) - hsetfReplyFromListpack(c, vptr); - else if (flag & HFE_CMD_GETNEW) - addReplyBulkCBuffer(c, (char*)value, sdslen(value)); - else - addReplyLongLong(c, ret); - - if (!fptr) { - if (ret != HSETF_FIELD_AND_TTL) { - listpackExAddNew(o, field, value, HASH_LP_NO_TTL); - } else { - /* If expiration time is in the past, no need to create the field */ - if (!checkAlreadyExpired(expireAt)) { - if (*minPrevExp > expireAt) - *minPrevExp = expireAt; - - listpackExAddNew(o, field, value, expireAt); - } - } - } else { - if (ret != HSETF_FIELD_AND_TTL) { - /* We just set the field value without updating the TTL */ - lpt->lp = lpReplace(lpt->lp, &vptr, (unsigned char *) value, sdslen(value)); - } else { - /* We are going to update TTL. Delete the field first and then - * insert again according to new TTL if necessary. */ - lpt->lp = lpDeleteRangeWithEntry(lpt->lp, &fptr, 3); - - if (*minPrevExp > prevExpire) - *minPrevExp = prevExpire; - - if (!(flag & HFE_CMD_EXPIRY_MASK)) { - /* If none of EX,EXAT,PX,PXAT,KEEPTTL is specified, TTL is - * discarded. */ - listpackExAddNew(o, field, value, HASH_LP_NO_TTL); - } else if (!checkAlreadyExpired(expireAt)){ - if (*minPrevExp > expireAt) - *minPrevExp = expireAt; - - listpackExAddNew(o, field, value, expireAt); - } - } - } - - return 1; - } else if (o->encoding == OBJ_ENCODING_HT) { - hfield hf = NULL; - dictEntry *de = NULL; - dict *d = o->ptr; - dictExpireMetadata *meta = (dictExpireMetadata *) dictMetadata(d); - sds prevVal = NULL; - - /* First retrieve the field to check if it exists */ - de = dictFind(d, field); - if (de) { - hf = dictGetKey(de); - prevExpire = hfieldGetExpireTime(hf); - prevVal = dictGetVal(de); - } - - /* Check DCF (don't create fields) and DOF (don't override fields) arg. */ - if ((!de && (flag & HFE_CMD_DCF)) || (de && (flag & HFE_CMD_DOF))) { - hsetfAddReply(c, flag, prevVal, prevVal, ret); - return 0; - } - - /* Field value will be updated. */ - ret = HSETF_FIELD; - - /* Decide if we are going to set/discard TTL */ - if (hsetfCheckTTLCondition(flag, prevExpire, expireAt)) - ret = HSETF_FIELD_AND_TTL; - - hsetfAddReply(c, flag, prevVal, value, ret); - - if (!hf || !hfieldIsExpireAttached(hf)) { - hfieldFree(hf); - - int withExpireMeta = (ret == HSETF_FIELD_AND_TTL) ? 1 : 0; - hf = hfieldNew(field, sdslen(field), withExpireMeta); - - if (!de) { - dictUseStoredKeyApi(d, 1); - de = dictAddRaw(d, hf, NULL); - dictUseStoredKeyApi(d, 0); - } - dictSetKey(d, de, hf); - } - - dictSetVal(d, de, sdsdup(value)); - sdsfree(prevVal); - - if (ret == HSETF_FIELD_AND_TTL) { - if (*minPrevExp > prevExpire) - *minPrevExp = prevExpire; - - if (!(flag & HFE_CMD_EXPIRY_MASK)) { - /* If none of EX,EXAT,PX,PXAT,KEEPTTL is specified, TTL is - * discarded. */ - hfieldPersist(o, hf); - } else if (checkAlreadyExpired(expireAt)) { - /* if expiration time is in the past */ - hashTypeDelete(o, field); - } else { - if (*minPrevExp > expireAt) - *minPrevExp = expireAt; - - if (prevExpire != EB_EXPIRE_TIME_INVALID) - ebRemove(&meta->hfe, &hashFieldExpireBucketsType, hf); - - ebAdd(&meta->hfe, &hashFieldExpireBucketsType, hf, expireAt); - } - } - - return 1; - } else { - serverPanic("Unknown encoding: %d", o->encoding); - } -} - -/* Parse hsetf command arguments. */ -static int hsetfParseArgs(client *c, int *flags, uint64_t *expireAt, - int *firstFieldPos, int *fieldCount) -{ - long val; - - *flags = 0; - *firstFieldPos = -1; - *fieldCount = -1; - - for (int i = 2; i < c->argc; i++) { - if (!strcasecmp(c->argv[i]->ptr, "fvs")) { - if (*firstFieldPos != -1) { - addReplyErrorFormat(c, "multiple FVS argument"); - return C_ERR; - } - - if (i >= c->argc - 3) { - addReplyErrorArity(c); - return C_ERR; - } - - if (getRangeLongFromObjectOrReply(c, c->argv[i + 1], 1, INT_MAX, &val, - "invalid number of fvs count") != C_OK) - return C_ERR; - - if (val > ((c->argc - i - 2) / 2)) { - addReplyErrorArity(c); - return C_ERR; - } - - *firstFieldPos = i + 2; - *fieldCount = (int) val; - i = *firstFieldPos + (*fieldCount) * 2 - 1; - } else if (!strcasecmp(c->argv[i]->ptr, "NX")) { - if (*flags & (HFE_CMD_XX | HFE_CMD_GT | HFE_CMD_LT)) - goto err_condition; - *flags |= HFE_CMD_NX; - } else if (!strcasecmp(c->argv[i]->ptr, "XX")) { - if (*flags & (HFE_CMD_NX | HFE_CMD_GT | HFE_CMD_LT)) - goto err_condition; - *flags |= HFE_CMD_XX; - } else if (!strcasecmp(c->argv[i]->ptr, "GT")) { - if (*flags & (HFE_CMD_NX | HFE_CMD_XX | HFE_CMD_LT)) - goto err_condition; - *flags |= HFE_CMD_GT; - } else if (!strcasecmp(c->argv[i]->ptr, "LT")) { - if (*flags & (HFE_CMD_NX | HFE_CMD_XX | HFE_CMD_GT)) - goto err_condition; - *flags |= HFE_CMD_LT; - } else if (!strcasecmp(c->argv[i]->ptr, "EX")) { - if (*flags & (HFE_CMD_EXAT | HFE_CMD_PX | HFE_CMD_PXAT | HFE_CMD_KEEPTTL)) - goto err_expiration; - - if (i >= c->argc - 1) - goto err_missing_expire; - - *flags |= HFE_CMD_EX; - i++; - if (validateExpire(c, UNIT_SECONDS, c->argv[i], - commandTimeSnapshot(), expireAt) != C_OK) - return C_ERR; - - } else if (!strcasecmp(c->argv[i]->ptr, "PX")) { - if (*flags & (HFE_CMD_EX | HFE_CMD_EXAT | HFE_CMD_PXAT | HFE_CMD_KEEPTTL)) - goto err_expiration; - - if (i >= c->argc - 1) - goto err_missing_expire; - - *flags |= HFE_CMD_PX; - i++; - if (validateExpire(c, UNIT_MILLISECONDS, c->argv[i], - commandTimeSnapshot(), expireAt) != C_OK) - return C_ERR; - } else if (!strcasecmp(c->argv[i]->ptr, "EXAT")) { - if (*flags & (HFE_CMD_EX | HFE_CMD_PX | HFE_CMD_PXAT | HFE_CMD_KEEPTTL)) - goto err_expiration; - - if (i >= c->argc - 1) - goto err_missing_expire; - - *flags |= HFE_CMD_EXAT; - i++; - if (validateExpire(c, UNIT_SECONDS, c->argv[i], 0, expireAt) != C_OK) - return C_ERR; - } else if (!strcasecmp(c->argv[i]->ptr, "PXAT")) { - if (*flags & (HFE_CMD_EX | HFE_CMD_EXAT | HFE_CMD_PX | HFE_CMD_KEEPTTL)) - goto err_expiration; - - if (i >= c->argc - 1) - goto err_missing_expire; - - *flags |= HFE_CMD_PXAT; - i++; - if (validateExpire(c, UNIT_MILLISECONDS, c->argv[i], 0, expireAt) != C_OK) - return C_ERR; - } else if (!strcasecmp(c->argv[i]->ptr, "KEEPTTL")) { - if (*flags & (HFE_CMD_EX | HFE_CMD_EXAT | HFE_CMD_PX | HFE_CMD_PXAT)) - goto err_expiration; - *flags |= HFE_CMD_KEEPTTL; - } else if (!strcasecmp(c->argv[i]->ptr, "DC")) { - *flags |= HFE_CMD_DC; - } else if (!strcasecmp(c->argv[i]->ptr, "DCF")) { - if (*flags & HFE_CMD_DOF) - goto err_field_condition; - *flags |= HFE_CMD_DCF; - } else if (!strcasecmp(c->argv[i]->ptr, "DOF")) { - if (*flags & HFE_CMD_DCF) - goto err_field_condition; - *flags |= HFE_CMD_DOF; - } else if (!strcasecmp(c->argv[i]->ptr, "GETNEW")) { - if (*flags & HFE_CMD_GETOLD) - goto err_return_condition; - *flags |= HFE_CMD_GETNEW; - } else if (!strcasecmp(c->argv[i]->ptr, "GETOLD")) { - if (*flags & HFE_CMD_GETNEW) - goto err_return_condition; - *flags |= HFE_CMD_GETOLD; - } else { - addReplyErrorFormat(c, "unknown argument: %s", (char*) c->argv[i]->ptr); - return C_ERR; - } - } - - /* FVS argument is mandatory. */ - if (*firstFieldPos <= 0) { - addReplyError(c, "missing FVS argument"); - return C_ERR; - } - - if (*flags & HFE_CMD_COND_MASK && - (!(*flags & (HFE_CMD_EX | HFE_CMD_EXAT | HFE_CMD_PX | HFE_CMD_PXAT)))) - { - addReplyError(c, "NX, XX, GT, and LT can be specified only when EX, PX, EXAT, or PXAT is specified"); - return C_ERR; - } - - return C_OK; - -err_missing_expire: - addReplyError(c, "missing expire time"); - return C_ERR; -err_condition: - addReplyError(c, "Only one of NX, XX, GT, and LT arguments can be specified"); - return C_ERR; -err_expiration: - addReplyError(c, "Only one of EX, PX, EXAT, PXAT or KEEPTTL arguments can be specified"); - return C_ERR; -err_field_condition: - addReplyError(c, "Only one of DCF or DOF arguments can be specified"); - return C_ERR; -err_return_condition: - addReplyError(c, "Only one of GETOLD or GETNEW arguments can be specified"); - return C_ERR; -} - -/* - * Set field value and optionally set the field's remaining time to live. - * Optionally it creates the key/fields. - * - * HSETF key - * [DC] [DCF | DOF] - * [NX | XX | GT | LT] - * [GETNEW | GETOLD] - * [EX seconds | PX milliseconds | EXAT unix-time-seconds | PXAT unix-time-milliseconds | KEEPTTL] - * - */ -void hsetfCommand(client *c) { - int flags = 0; - robj *hashObj, *keyArg = c->argv[1]; - - int firstFieldPos = 0; - int numFields = 0; - uint64_t expireAt = EB_EXPIRE_TIME_INVALID; - - if (hsetfParseArgs(c, &flags, &expireAt, &firstFieldPos, &numFields) != C_OK) - return; - - hashObj = lookupKeyWrite(c->db, c->argv[1]); - if (!hashObj) { - /* Don't create the object if command has DC or DCF arguments */ - if (flags & HFE_CMD_DC || flags & HFE_CMD_DCF) { - addReplyOrErrorObject(c, shared.null[c->resp]); - return; - } - - hashObj = createHashObject(); - dbAdd(c->db,c->argv[1],hashObj); - } - - hashTypeTryConversion(c->db,hashObj,c->argv, - firstFieldPos, - firstFieldPos + (numFields * 2) - 1); - - attachHfeMeta(c->db, hashObj, keyArg); - uint64_t minExpire = hashTypeGetMinExpire(hashObj); - - /* Figure out from provided set of fields in command, which one has the minimum - * expiration time, before the modification (Will be used for optimization below) */ - uint64_t minExpireFields = EB_EXPIRE_TIME_INVALID; - - int updated = 0; - addReplyArrayLen(c, numFields); - for (int i = 0; i < numFields ; i++) { - sds field = c->argv[firstFieldPos + (i * 2)]->ptr; - sds value = c->argv[firstFieldPos + (i * 2) + 1]->ptr; - updated += hsetfSetFieldAndReply(c, hashObj, field, value, flags, - expireAt, &minExpireFields); - } - - if (updated == 0) { - /* If we didn't update anything and object is empty, it means we just - * created the object above and leaving it empty. If this is the case, - * we should avoid creating the object in the first place. - * See above DC / DCF flags check when object does not exist. */ - serverAssert(hashTypeLength(hashObj, 0) != 0); - } else { - /* Notify keyspace event, update dirty count and update global HFE DS */ - server.dirty += updated; - signalModifiedKey(c,c->db,keyArg); - notifyKeyspaceEvent(NOTIFY_HASH,"hsetf",keyArg,c->db->id); - if (hashTypeLength(hashObj, 0) == 0) { - dbDelete(c->db,keyArg); - notifyKeyspaceEvent(NOTIFY_GENERIC,"del",keyArg, c->db->id); - } else { - updateGlobalHfeDs(c->db, hashObj, minExpire, minExpireFields); - } - } -} diff --git a/tests/unit/type/hash-field-expire.tcl b/tests/unit/type/hash-field-expire.tcl index 59b47110c45..08d0203ea27 100644 --- a/tests/unit/type/hash-field-expire.tcl +++ b/tests/unit/type/hash-field-expire.tcl @@ -17,11 +17,6 @@ set P_NO_FIELD -2 set P_NO_EXPIRY -1 set P_OK 1 -######## HSETF -set S_FAIL 0 -set S_FIELD 1 -set S_FIELD_AND_TTL 3 - ############################### AUX FUNCS ###################################### proc get_hashes_with_expiry_fields {r} { @@ -109,6 +104,17 @@ start_server {tags {"external:skip needs:debug"}} { r config set hash-max-listpack-entries 512 } + test "HEXPIRE/HEXPIREAT/HPEXPIRE/HPEXPIREAT - Returns empty array if key does not exist" { + r del myhash + # Make sure we can distinguish between an empty array and a null response + r readraw 1 + assert_equal {*0} [r HEXPIRE myhash 1000 FIELDS 1 a] + assert_equal {*0} [r HEXPIREAT myhash 1000 FIELDS 1 a] + assert_equal {*0} [r HPEXPIRE myhash 1000 FIELDS 1 a] + assert_equal {*0} [r HPEXPIREAT myhash 1000 FIELDS 1 a] + r readraw 0 + } + test "HPEXPIRE(AT) - Test 'NX' flag ($type)" { r del myhash r hset myhash field1 value1 field2 value2 field3 value3 @@ -278,6 +284,15 @@ start_server {tags {"external:skip needs:debug"}} { r flushall async } + test "HTTL/HPTTL - Returns empty array if key does not exist" { + r del myhash + # Make sure we can distinguish between an empty array and a null response + r readraw 1 + assert_equal {*0} [r HTTL myhash FIELDS 1 a] + assert_equal {*0} [r HPTTL myhash FIELDS 1 a] + r readraw 0 + } + test "HTTL/HPTTL - Input validation gets failed on nonexists field or field without expire ($type)" { r del myhash r HSET myhash field1 value1 field2 value2 @@ -301,6 +316,15 @@ start_server {tags {"external:skip needs:debug"}} { assert_range $ttl 1000 2000 } + test "HEXPIRETIME/HPEXPIRETIME - Returns empty array if key does not exist" { + r del myhash + # Make sure we can distinguish between an empty array and a null response + r readraw 1 + assert_equal {*0} [r HEXPIRETIME myhash FIELDS 1 a] + assert_equal {*0} [r HPEXPIRETIME myhash FIELDS 1 a] + r readraw 0 + } + test "HEXPIRETIME - returns TTL in Unix timestamp ($type)" { r del myhash r HSET myhash field1 value1 @@ -652,6 +676,14 @@ start_server {tags {"external:skip needs:debug"}} { wait_for_condition 20 10 { [r exists myhash] == 0 } else { fail "'myhash' should be expired" } } {} {singledb:skip} + test "HPERSIST - Returns empty array if key does not exist" { + r del myhash + # Make sure we can distinguish between an empty array and a null response + r readraw 1 + assert_equal {*0} [r HPERSIST myhash FIELDS 1 a] + r readraw 0 + } + test "HPERSIST - input validation ($type)" { # HPERSIST key r del myhash @@ -685,462 +717,9 @@ start_server {tags {"external:skip needs:debug"}} { assert_equal [r hpersist myhash FIELDS 1 fieldnonexist] $P_NO_FIELD } - test "HGETF - input validation ($type)" { - assert_error {*wrong number of arguments*} {r hgetf myhash} - assert_error {*wrong number of arguments*} {r hgetf myhash fields} - assert_error {*wrong number of arguments*} {r hgetf myhash fields 1} - assert_error {*wrong number of arguments*} {r hgetf myhash fields 2 a} - assert_error {*wrong number of arguments*} {r hgetf myhash fields 3 a b} - assert_error {*wrong number of arguments*} {r hgetf myhash fields 3 a b} - assert_error {*unknown argument*} {r hgetf myhash fields 1 a unknown} - assert_error {*missing FIELDS argument*} {r hgetf myhash nx ex 100} - assert_error {*multiple FIELDS argument*} {r hgetf myhash fields 1 a fields 1 b} - - r hset myhash f1 v1 f2 v2 f3 v3 - # NX, XX, GT, and LT can be specified only when EX, PX, EXAT, or PXAT is specified - assert_error {*only when EX, PX, EXAT, or PXAT is specified*} {r hgetf myhash nx fields 1 a} - assert_error {*only when EX, PX, EXAT, or PXAT is specified*} {r hgetf myhash xx fields 1 a} - assert_error {*only when EX, PX, EXAT, or PXAT is specified*} {r hgetf myhash gt fields 1 a} - assert_error {*only when EX, PX, EXAT, or PXAT is specified*} {r hgetf myhash lt fields 1 a} - - # Only one of NX, XX, GT, and LT can be specified - assert_error {*Only one of NX, XX, GT, and LT arguments*} {r hgetf myhash nx xx EX 100 fields 1 a} - assert_error {*Only one of NX, XX, GT, and LT arguments*} {r hgetf myhash xx nx EX 100 fields 1 a} - assert_error {*Only one of NX, XX, GT, and LT arguments*} {r hgetf myhash gt nx EX 100 fields 1 a} - assert_error {*Only one of NX, XX, GT, and LT arguments*} {r hgetf myhash gt lt EX 100 fields 1 a} - assert_error {*Only one of NX, XX, GT, and LT arguments*} {r hgetf myhash xx gt EX 100 fields 1 a} - assert_error {*Only one of NX, XX, GT, and LT arguments*} {r hgetf myhash lt gt EX 100 fields 1 a} - - # Only one of EX, PX, EXAT, PXAT or PERSIST can be specified - assert_error {*Only one of EX, PX, EXAT, PXAT or PERSIST arguments*} {r hgetf myhash EX 100 PX 1000 fields 1 a} - assert_error {*Only one of EX, PX, EXAT, PXAT or PERSIST arguments*} {r hgetf myhash EX 100 EXAT 100 fields 1 a} - assert_error {*Only one of EX, PX, EXAT, PXAT or PERSIST arguments*} {r hgetf myhash EXAT 100 EX 1000 fields 1 a} - assert_error {*Only one of EX, PX, EXAT, PXAT or PERSIST arguments*} {r hgetf myhash EXAT 100 PX 1000 fields 1 a} - assert_error {*Only one of EX, PX, EXAT, PXAT or PERSIST arguments*} {r hgetf myhash PX 100 EXAT 100 fields 1 a} - assert_error {*Only one of EX, PX, EXAT, PXAT or PERSIST arguments*} {r hgetf myhash PX 100 PXAT 100 fields 1 a} - assert_error {*Only one of EX, PX, EXAT, PXAT or PERSIST arguments*} {r hgetf myhash PXAT 100 EX 100 fields 1 a} - assert_error {*Only one of EX, PX, EXAT, PXAT or PERSIST arguments*} {r hgetf myhash PXAT 100 EXAT 100 fields 1 a} - assert_error {*Only one of EX, PX, EXAT, PXAT or PERSIST arguments*} {r hgetf myhash EX 100 PERSIST fields 1 a} - assert_error {*Only one of EX, PX, EXAT, PXAT or PERSIST arguments*} {r hgetf myhash PERSIST EX 100 fields 1 a} - - # missing expire time - assert_error {*not an integer or out of range*} {r hgetf myhash ex fields 1 a} - assert_error {*not an integer or out of range*} {r hgetf myhash px fields 1 a} - assert_error {*not an integer or out of range*} {r hgetf myhash exat fields 1 a} - assert_error {*not an integer or out of range*} {r hgetf myhash pxat fields 1 a} - - # expire time more than 2 ^ 48 - assert_error {*invalid expire time*} {r hgetf myhash EXAT [expr (1<<48)] 1 f1} - assert_error {*invalid expire time*} {r hgetf myhash PXAT [expr (1<<48)] 1 f1} - assert_error {*invalid expire time*} {r hgetf myhash EX [expr (1<<48) - [clock seconds] + 1000 ] 1 f1} - assert_error {*invalid expire time*} {r hgetf myhash PX [expr (1<<48) - [clock milliseconds] + 1000 ] 1 f1} - - # negative expire time - assert_error {*invalid expire time*} {r hgetf myhash EXAT -10 1 f1} - - # negative field value count - assert_error {*invalid number of fields*} {r hgetf myhash fields -1 a} - } - - test "HGETF - Verify field value reply type is string ($type)" { - r del myhash - r hsetf myhash FVS 1 f1 1 - - r readraw 1 - assert_equal [r hgetf myhash FIELDS 1 f1] {*1} - assert_equal [r read] {$1} - assert_equal [r read] {1} - r readraw 0 - } - - test "HGETF - Test 'NX' flag ($type)" { - r del myhash - r hset myhash field1 value1 field2 value2 field3 value3 - assert_equal [r hgetf myhash EX 1000 NX FIELDS 1 field1] [list "value1"] - assert_equal [r hgetf myhash EX 10000 NX FIELDS 2 field1 field2] [list "value1" "value2"] - assert_range [r httl myhash FIELDS 1 field1] 1 1000 - assert_range [r httl myhash FIELDS 1 field2] 5000 10000 - - # A field with no expiration is treated as an infinite expiration. - # LT should set the expire time if field has no TTL. - r del myhash - r hset myhash field1 value1 - assert_equal [r hgetf myhash EX 1500 LT FIELDS 1 field1] [list "value1"] - assert_not_equal [r httl myhash FIELDS 1 field1] "$T_NO_EXPIRY" - } - - test "HGETF - Test 'XX' flag ($type)" { - r del myhash - r hset myhash field1 value1 field2 value2 field3 value3 - assert_equal [r hgetf myhash EX 1000 NX FIELDS 1 field1] [list "value1"] - assert_equal [r hgetf myhash EX 10000 XX FIELDS 2 field1 field2] [list "value1" "value2"] - assert_range [r httl myhash FIELDS 1 field1] 9900 10000 - assert_equal [r httl myhash FIELDS 1 field2] "$T_NO_EXPIRY" - } - - test "HGETF - Test 'GT' flag ($type)" { - r del myhash - r hset myhash field1 value1 field2 value2 field3 value3 - assert_equal [r hgetf myhash EX 1000 NX FIELDS 1 field1] [list "value1"] - assert_equal [r hgetf myhash EX 2000 NX FIELDS 1 field2] [list "value2"] - assert_equal [r hgetf myhash EX 1500 GT FIELDS 2 field1 field2] [list "value1" "value2"] - assert_range [r httl myhash FIELDS 1 field1] 1400 1500 - assert_range [r httl myhash FIELDS 1 field2] 1900 2000 - } - - test "HGETF - Test 'LT' flag ($type)" { - r del myhash - r hset myhash field1 value1 field2 value2 field3 value3 - assert_equal [r hgetf myhash EX 1000 NX FIELDS 1 field1] [list "value1"] - assert_equal [r hgetf myhash EX 2000 NX FIELDS 1 field2] [list "value2"] - assert_equal [r hgetf myhash EX 1500 LT FIELDS 2 field1 field2] [list "value1" "value2"] - assert_range [r httl myhash FIELDS 1 field1] 1 1000 - assert_range [r httl myhash FIELDS 1 field2] 1000 1500 - } - - test "HGETF - Test 'EX' flag ($type)" { - r del myhash - r hset myhash field1 value1 field2 value2 field3 value3 - assert_equal [r hgetf myhash EX 1000 FIELDS 1 field3] [list "value3"] - assert_range [r httl myhash FIELDS 1 field3] 1 1000 - } - - test "HGETF - Test 'EXAT' flag ($type)" { - r del myhash - r hset myhash field1 value1 field2 value2 field3 value3 - assert_equal [r hgetf myhash EXAT 4000000000 FIELDS 1 field3] [list "value3"] - assert_range [expr [r httl myhash FIELDS 1 field3] + [clock seconds]] 3900000000 4000000000 - } - - test "HGETF - Test 'PX' flag ($type)" { - r del myhash - r hset myhash field1 value1 field2 value2 field3 value3 - assert_equal [r hgetf myhash PX 1000000 FIELDS 1 field3] [list "value3"] - assert_range [r httl myhash FIELDS 1 field3] 900 1000 - } - - test "HGETF - Test 'PXAT' flag ($type)" { - r del myhash - r hset myhash field1 value1 field2 value2 field3 value3 - assert_equal [r hgetf myhash PXAT 4000000000000 FIELDS 1 field3] [list "value3"] - assert_range [expr [r httl myhash FIELDS 1 field3] + [clock seconds]] 3900000000 4000000000 - } - - test "HGETF - Test 'PERSIST' flag ($type)" { - r del myhash - r debug set-active-expire 0 - - r hset myhash f1 v1 f2 v2 f3 v3 - r hgetf myhash PX 5000 FIELDS 3 f1 f2 f3 - assert_not_equal [r httl myhash FIELDS 1 f1] "$T_NO_EXPIRY" - assert_not_equal [r httl myhash FIELDS 1 f2] "$T_NO_EXPIRY" - assert_not_equal [r httl myhash FIELDS 1 f3] "$T_NO_EXPIRY" - - assert_equal [r hgetf myhash PERSIST FIELDS 1 f1] "v1" - assert_equal [r httl myhash FIELDS 1 f1] "$T_NO_EXPIRY" - - assert_equal [r hgetf myhash PERSIST FIELDS 2 f2 f3] "v2 v3" - assert_equal [r httl myhash FIELDS 2 f2 f3] "$T_NO_EXPIRY $T_NO_EXPIRY" - } - - test "HGETF - Test setting expired ttl deletes key ($type)" { - r del myhash - r hset myhash f1 v1 f2 v2 f3 v3 - - # hgetf without setting ttl - assert_equal [lsort [r hgetf myhash fields 3 f1 f2 f3]] [lsort "v1 v2 v3"] - assert_equal [r httl myhash FIELDS 3 f1 f2 f3] "$T_NO_EXPIRY $T_NO_EXPIRY $T_NO_EXPIRY" - - # set expired ttl and verify key is deleted - r hgetf myhash PXAT 1 fields 3 f1 f2 f3 - assert_equal [r exists myhash] 0 - } - - test "HGETF - Test active expiry ($type)" { - r del myhash - r debug set-active-expire 0 - - r hset myhash f1 v1 f2 v2 f3 v3 f4 v4 f5 v5 - r hgetf myhash PXAT 1 FIELDS 5 f1 f2 f3 f4 f5 - - r debug set-active-expire 1 - wait_for_condition 50 20 { [r EXISTS myhash] == 0 } else { fail "'myhash' should be expired" } - } - - test "HGETF - A field with TTL overridden with another value (TTL discarded) ($type)" { - r del myhash - r hset myhash field1 value1 field2 value2 field3 value3 - r hgetf myhash PX 10000 NX FIELDS 1 field1 - r hgetf myhash EX 100 NX FIELDS 1 field2 - - # field2 TTL will be discarded - r hset myhash field2 value4 - - # Expected TTL will be discarded - assert_equal [r hget myhash field2] "value4" - assert_equal [r httl myhash FIELDS 2 field2 field3] "$T_NO_EXPIRY $T_NO_EXPIRY" - - # Other field is not affected. - assert_not_equal [r httl myhash FIELDS 1 field1] "$T_NO_EXPIRY" - } - - test "HSETF - input validation ($type)" { - assert_error {*wrong number of arguments*} {r hsetf myhash} - assert_error {*wrong number of arguments*} {r hsetf myhash fvs} - assert_error {*wrong number of arguments*} {r hsetf myhash fvs 1} - assert_error {*wrong number of arguments*} {r hsetf myhash fvs 2 a b} - assert_error {*wrong number of arguments*} {r hsetf myhash fvs 3 a b c d} - assert_error {*wrong number of arguments*} {r hsetf myhash fvs 3 a b} - assert_error {*unknown argument*} {r hsetf myhash fvs 1 a b unknown} - assert_error {*missing FVS argument*} {r hsetf myhash nx nx ex 100} - assert_error {*multiple FVS argument*} {r hsetf myhash DC fvs 1 a b fvs 1 a b} - - r hset myhash f1 v1 f2 v2 f3 v3 - # NX, XX, GT, and LT can be specified only when EX, PX, EXAT, or PXAT is specified - assert_error {*only when EX, PX, EXAT, or PXAT is specified*} {r hsetf myhash nx fvs 1 a b} - assert_error {*only when EX, PX, EXAT, or PXAT is specified*} {r hsetf myhash xx fvs 1 a b} - assert_error {*only when EX, PX, EXAT, or PXAT is specified*} {r hsetf myhash gt fvs 1 a b} - assert_error {*only when EX, PX, EXAT, or PXAT is specified*} {r hsetf myhash lt fvs 1 a b} - - # Only one of NX, XX, GT, and LT can be specified - assert_error {*Only one of NX, XX, GT, and LT arguments*} {r hsetf myhash nx xx EX 100 fvs 1 a b} - assert_error {*Only one of NX, XX, GT, and LT arguments*} {r hsetf myhash xx nx EX 100 fvs 1 a b} - assert_error {*Only one of NX, XX, GT, and LT arguments*} {r hsetf myhash gt nx EX 100 fvs 1 a b} - assert_error {*Only one of NX, XX, GT, and LT arguments*} {r hsetf myhash gt lt EX 100 fvs 1 a b} - assert_error {*Only one of NX, XX, GT, and LT arguments*} {r hsetf myhash xx gt EX 100 fvs 1 a b} - assert_error {*Only one of NX, XX, GT, and LT arguments*} {r hsetf myhash lt gt EX 100 fvs 1 a b} - - # Only one of EX, PX, EXAT, PXAT or KEEPTTL can be specified - assert_error {*Only one of EX, PX, EXAT, PXAT or KEEPTTL arguments*} {r hsetf myhash EX 100 PX 1000 fvs 1 a b} - assert_error {*Only one of EX, PX, EXAT, PXAT or KEEPTTL arguments*} {r hsetf myhash EX 100 EXAT 100 fvs 1 a b} - assert_error {*Only one of EX, PX, EXAT, PXAT or KEEPTTL arguments*} {r hsetf myhash EXAT 100 EX 1000 fvs 1 a b} - assert_error {*Only one of EX, PX, EXAT, PXAT or KEEPTTL arguments*} {r hsetf myhash EXAT 100 PX 1000 fvs 1 a b} - assert_error {*Only one of EX, PX, EXAT, PXAT or KEEPTTL arguments*} {r hsetf myhash PX 100 EXAT 100 fvs 1 a b} - assert_error {*Only one of EX, PX, EXAT, PXAT or KEEPTTL arguments*} {r hsetf myhash PX 100 PXAT 100 fvs 1 a b} - assert_error {*Only one of EX, PX, EXAT, PXAT or KEEPTTL arguments*} {r hsetf myhash PXAT 100 EX 100 fvs 1 a b} - assert_error {*Only one of EX, PX, EXAT, PXAT or KEEPTTL arguments*} {r hsetf myhash PXAT 100 EXAT 100 fvs 1 a b} - assert_error {*Only one of EX, PX, EXAT, PXAT or KEEPTTL arguments*} {r hsetf myhash EX 100 KEEPTTL fvs 1 a b} - assert_error {*Only one of EX, PX, EXAT, PXAT or KEEPTTL arguments*} {r hsetf myhash KEEPTTL EX 100 fvs 1 a b} - - # Only one of DCF, DOF can be specified - assert_error {*Only one of DCF or DOF arguments can be specified*} {r hsetf myhash DCF DOF fvs 1 a b} - assert_error {*Only one of DCF or DOF arguments can be specified*} {r hsetf myhash DOF DCF fvs 1 a b} - - # Only one of GETNEW, GETOLD can be specified - assert_error {*Only one of GETOLD or GETNEW arguments can be specified*} {r hsetf myhash GETNEW GETOLD fvs 1 a b} - assert_error {*Only one of GETOLD or GETNEW arguments can be specified*} {r hsetf myhash GETOLD GETNEW fvs 1 a b} - - # missing expire time - assert_error {*not an integer or out of range*} {r hsetf myhash ex fvs 1 a b} - assert_error {*not an integer or out of range*} {r hsetf myhash px fvs 1 a b} - assert_error {*not an integer or out of range*} {r hsetf myhash exat fvs 1 a b} - assert_error {*not an integer or out of range*} {r hsetf myhash pxat fvs 1 a b} - - # expire time more than 2 ^ 48 - assert_error {*invalid expire time*} {r hsetf myhash EXAT [expr (1<<48)] 1 a b} - assert_error {*invalid expire time*} {r hsetf myhash PXAT [expr (1<<48)] 1 a b} - assert_error {*invalid expire time*} {r hsetf myhash EX [expr (1<<48) - [clock seconds] + 1000 ] 1 a b} - assert_error {*invalid expire time*} {r hsetf myhash PX [expr (1<<48) - [clock milliseconds] + 1000 ] 1 a b} - - # negative ttl - assert_error {*invalid expire time*} {r hsetf myhash EXAT -1 1 a b} - - # negative field value count - assert_error {*invalid number of fvs count*} {r hsetf myhash fvs -1 a b} - } - - test "HSETF - Verify field value reply type is string ($type)" { - r del myhash - r hsetf myhash FVS 1 field 1 - r readraw 1 - - # Test with GETOLD - assert_equal [r hsetf myhash GETOLD FVS 1 field 200] {*1} - assert_equal [r read] {$1} - assert_equal [r read] {1} - - # Test with GETNEW. - assert_equal [r hsetf myhash DOF GETNEW FVS 1 field 300] {*1} - assert_equal [r read] {$3} - assert_equal [r read] {200} - - r readraw 0 - } - - test "HSETF - Test DC flag ($type)" { - r del myhash - # don't create key - assert_equal "" [r hsetf myhash DC fvs 1 a b] - } - - test "HSETF - Test DCF/DOF flag ($type)" { - r del myhash - r hset myhash f1 v1 f2 v2 f3 v3 - - # Don't overwrite fields - assert_equal [r hsetf myhash DOF fvs 2 f1 n1 f2 n2] "$S_FAIL $S_FAIL" - assert_equal [r hsetf myhash DOF fvs 3 f1 n1 f2 b2 f4 v4] "$S_FAIL $S_FAIL $S_FIELD" - assert_equal [lsort [r hgetall myhash]] [lsort "f1 v1 f2 v2 f3 v3 f4 v4"] - - # Don't create fields - assert_equal [r hsetf myhash DCF fvs 3 f1 n1 f2 b2 f5 v5] "$S_FIELD $S_FIELD $S_FAIL" - assert_equal [lsort [r hgetall myhash]] [lsort "f1 n1 f2 b2 f3 v3 f4 v4"] - } - - test "HSETF - Test 'NX' flag ($type)" { - r del myhash - r hset myhash f1 v1 f2 v2 f3 v3 - assert_equal [r hsetf myhash EX 1000 NX FVS 1 f1 n1] "$S_FIELD_AND_TTL" - assert_equal [r hsetf myhash EX 10000 NX FVS 2 f1 n1 f2 n2] "$S_FIELD $S_FIELD_AND_TTL" - assert_range [r httl myhash FIELDS 1 f1] 990 1000 - assert_range [r httl myhash FIELDS 1 f2] 9990 10000 - } - - test "HSETF - Test 'XX' flag ($type)" { - r del myhash - r hset myhash f1 v1 f2 v2 f3 v3 - assert_equal [r hsetf myhash EX 1000 NX FVS 1 f1 n1] "$S_FIELD_AND_TTL" - assert_equal [r hsetf myhash EX 10000 XX FVS 2 f1 n1 f2 n2] "$S_FIELD_AND_TTL $S_FIELD" - assert_range [r httl myhash FIELDS 1 f1] 9900 10000 - assert_equal [r httl myhash FIELDS 1 f2] "$T_NO_EXPIRY" - } - - test "HSETF - Test 'GT' flag ($type)" { - r del myhash - r hset myhash f1 v1 f2 v2 f3 v3 - assert_equal [r hsetf myhash EX 1000 NX FVS 1 f1 n1] "$S_FIELD_AND_TTL" - assert_equal [r hsetf myhash EX 2000 NX FVS 1 f2 n2] "$S_FIELD_AND_TTL" - assert_equal [r hsetf myhash EX 1500 GT FVS 2 f1 n1 f2 n2] "$S_FIELD_AND_TTL $S_FIELD" - assert_range [r httl myhash FIELDS 1 f1] 1400 1500 - assert_range [r httl myhash FIELDS 1 f2] 1600 2000 - } - - test "HSETF - Test 'LT' flag ($type)" { - r del myhash - r hset myhash f1 v1 f2 v2 f3 v3 - assert_equal [r hsetf myhash EX 1000 NX FVS 1 f1 v1] "$S_FIELD_AND_TTL" - assert_equal [r hsetf myhash EX 2000 NX FVS 1 f2 v2] "$S_FIELD_AND_TTL" - assert_equal [r hsetf myhash EX 1500 LT FVS 2 f1 v1 f2 v2] "$S_FIELD $S_FIELD_AND_TTL" - assert_range [r httl myhash FIELDS 1 f1] 900 1000 - assert_range [r httl myhash FIELDS 1 f2] 1400 1500 - - # A field with no expiration is treated as an infinite expiration. - # LT should set the expire time if field has no TTL. - r del myhash - r hset myhash f1 v1 - assert_equal [r hsetf myhash EX 1500 LT FVS 1 f1 v1] "$S_FIELD_AND_TTL" - } - - test "HSETF - Test 'EX' flag ($type)" { - r del myhash - r hset myhash f1 v1 f2 v2 - assert_equal [r hsetf myhash EX 1000 FVS 1 f3 v3 ] "$S_FIELD_AND_TTL" - assert_range [r httl myhash FIELDS 1 f3] 900 1000 - } - - test "HSETF - Test 'EXAT' flag ($type)" { - r del myhash - r hset myhash f1 v1 f2 v2 - assert_equal [r hsetf myhash EXAT 4000000000 FVS 1 f3 v3] "$S_FIELD_AND_TTL" - assert_range [expr [r httl myhash FIELDS 1 f3] + [clock seconds]] 3900000000 4000000000 - } - - test "HSETF - Test 'PX' flag ($type)" { - r del myhash - assert_equal [r hsetf myhash PX 1000000 FVS 1 f3 v3] "$S_FIELD_AND_TTL" - assert_range [r httl myhash FIELDS 1 f3] 990 1000 - } - - test "HSETF - Test 'PXAT' flag ($type)" { - r del myhash - r hset myhash f1 v2 f2 v2 f3 v3 - assert_equal [r hsetf myhash PXAT 4000000000000 FVS 1 f2 v2] "$S_FIELD_AND_TTL" - assert_range [expr [r httl myhash FIELDS 1 f2] + [clock seconds]] 3900000000 4000000000 - } - - test "HSETF - Test 'KEEPTTL' flag ($type)" { - r del myhash - - r hsetf myhash FVS 2 f1 v1 f2 v2 - r hsetf myhash PX 5000 FVS 1 f2 v2 - - # f1 does not have ttl - assert_equal [r httl myhash FIELDS 1 f1] "$T_NO_EXPIRY" - - # f2 has ttl - assert_not_equal [r httl myhash FIELDS 1 f2] "$T_NO_EXPIRY" - - # Validate KEEPTTL preserve TTL - assert_equal [r hsetf myhash KEEPTTL FVS 1 f2 n2] "$S_FIELD" - assert_not_equal [r httl myhash FIELDS 1 f2] "$T_NO_EXPIRY" - assert_equal [r hget myhash f2] "n2" - } - - test "HSETF - Test no expiry flag discards TTL ($type)" { - r del myhash - - r hsetf myhash FVS 1 f1 v1 - r hsetf myhash PX 5000 FVS 1 f2 v2 - - assert_equal [r hsetf myhash FVS 2 f1 v1 f2 v2] "$S_FIELD $S_FIELD_AND_TTL" - assert_not_equal [r httl myhash FIELDS 1 f1 f2] "$T_NO_EXPIRY $T_NO_EXPIRY" - } - - test "HSETF - Test 'GETNEW/GETOLD' flag ($type)" { - r del myhash - - assert_equal [r hsetf myhash GETOLD fvs 2 f1 v1 f2 v2] "{} {}" - assert_equal [r hsetf myhash GETNEW fvs 2 f1 v1 f2 v2] "v1 v2" - assert_equal [r hsetf myhash GETOLD fvs 2 f1 n1 f2 n2] "v1 v2" - assert_equal [r hsetf myhash GETOLD DOF fvs 2 f1 n1 f2 n2] "n1 n2" - assert_equal [r hsetf myhash GETNEW DOF fvs 2 f1 n1 f2 n2] "n1 n2" - assert_equal [r hsetf myhash GETNEW DCF fvs 2 f1 x1 f2 x2] "x1 x2" - assert_equal [r hsetf myhash GETNEW DCF fvs 2 f4 x4 f5 x5] "{} {}" - - r del myhash - assert_equal [r hsetf myhash GETOLD fvs 2 f1 v1 f2 v2] "{} {}" - - # DOF check will prevent override and GETNEW should return old value - assert_equal [r hsetf myhash DOF GETNEW fvs 2 f1 v12 f2 v22] "v1 v2" - } - - test "HSETF - Test with active expiry" { - r del myhash - r debug set-active-expire 0 - - r hsetf myhash PX 10 FVS 5 f1 v1 f2 v2 f3 v3 f4 v4 f5 v5 - r debug set-active-expire 1 - wait_for_condition 50 20 { [r EXISTS myhash] == 0 } else { fail "'myhash' should be expired" } - } - - test "HSETF - Set time in the past ($type)" { - r del myhash - assert_equal [r hsetf myhash EXAT [expr {[clock seconds] - 1}] FVS 2 f1 v1 f2 v2] "$S_FIELD_AND_TTL $S_FIELD_AND_TTL" - assert_equal [r hexists myhash field1] 0 - - # Try with override - r hset myhash fvs 2 f1 v1 f2 v2 - assert_equal [r hsetf myhash EXAT [expr {[clock seconds] - 1}] FVS 2 f1 v1 f2 v2] "$S_FIELD_AND_TTL $S_FIELD_AND_TTL" - assert_equal [r hexists myhash field1] 0 - } - - test "HSETF - Test failed hsetf call should not leave empty key ($type)" { - r del myhash - # This should not create the field as DCF flag is given - assert_equal [r hsetf myhash DCF FVS 1 a b] "" - - # Key should not exist - assert_equal [r exists myhash] 0 - - # Try with GETNEW/GETOLD - assert_equal [r hsetf myhash GETNEW DCF FVS 1 a b] "" - assert_equal [r exists myhash] 0 - assert_equal [r hsetf myhash GETOLD DCF FVS 1 a b] "" - assert_equal [r exists myhash] 0 - } - test {DUMP / RESTORE are able to serialize / unserialize a hash} { r config set sanitize-dump-payload yes + r del myhash r hmset myhash a 1 b 2 c 3 r hexpireat myhash 2524600800 fields 1 a r hexpireat myhash 2524600801 fields 1 b @@ -1153,6 +732,7 @@ start_server {tags {"external:skip needs:debug"}} { test {DUMP / RESTORE are able to serialize / unserialize a hash with TTL 0 for all fields} { r config set sanitize-dump-payload yes + r del myhash r hmset myhash a 1 b 2 c 3 r hexpire myhash 9999999 fields 1 a ;# make all TTLs of fields to 0 r hpersist myhash fields 1 a @@ -1231,15 +811,19 @@ start_server {tags {"external:skip needs:debug"}} { wait_for_condition 50 20 { [r EXISTS myhash] == 0 } else { fail "'myhash' should be expired" } } - test "HSETF - Test listpack converts to ht" { + test "Test listpack converts to ht and active expiry works" { r del myhash r debug set-active-expire 0 - # Check expiry works after listpack converts ht by using hsetf + # Check expiry works after listpack converts to ht for {set i 0} {$i < 1024} {incr i} { - r hsetf myhash PX 10 FVS 3 a$i b$i c$i d$i e$i f$i + r hset myhash f1_$i v1_$i f2_$i v2_$i f3_$i v3_$i f4_$i v4_$i + r hpexpire myhash 10 FIELDS 4 f1_$i f2_$i f3_$i f4_$i } + assert_encoding hashtable myhash + assert_equal [r hlen myhash] 4096 + r debug set-active-expire 1 wait_for_condition 50 20 { [r EXISTS myhash] == 0 } else { fail "'myhash' should be expired" } } @@ -1258,7 +842,8 @@ start_server {tags {"external:skip needs:debug"}} { # Test with single item list r hset myhash f1 $payload1 - assert_equal [r hgetf myhash EX 2000 FIELDS 1 f1] $payload1 + r hexpire myhash 2000 FIELDS 1 f1 + assert_equal [r hget myhash f1] $payload1 r del myhash # Test with multiple items