From e2608478b6a46998bb3196b9b3ef4a24cbbae8a7 Mon Sep 17 00:00:00 2001 From: Ozan Tezcan Date: Fri, 14 Feb 2025 17:13:35 +0300 Subject: [PATCH] Add HGETDEL, HGETEX and HSETEX hash commands (#13798) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR adds three new hash commands: HGETDEL, HGETEX and HSETEX. These commands enable user to do multiple operations in one step atomically e.g. set a hash field and update its TTL with a single command. Previously, it was only possible to do it by calling hset and hexpire commands subsequently. - **HGETDEL command** ``` HGETDEL FIELDS field [field ...] ``` **Description** Get and delete the value of one or more fields of a given hash key **Reply** Array reply: list of the value associated with each field or nil if the field doesn’t exist. - **HGETEX command** ``` HGETEX [EX seconds | PX milliseconds | EXAT unix-time-seconds | PXAT unix-time-milliseconds | PERSIST] FIELDS field [field ...] ``` **Description** Get the value of one or more fields of a given hash key, and optionally set their expiration **Options:** EX seconds: Set the specified expiration time, in seconds. PX milliseconds: Set the specified expiration time, in milliseconds. EXAT timestamp-seconds: Set the specified Unix time at which the field will expire, in seconds. PXAT timestamp-milliseconds: Set the specified Unix time at which the field will expire, in milliseconds. PERSIST: Remove the time to live associated with the field. **Reply** Array reply: list of the value associated with each field or nil if the field doesn’t exist. - **HSETEX command** ``` HSETEX [FNX | FXX] [EX seconds | PX milliseconds | EXAT unix-time-seconds | PXAT unix-time-milliseconds | KEEPTTL] FIELDS field value [field value...] ``` **Description** Set the value of one or more fields of a given hash key, and optionally set their expiration **Options:** FNX: Only set the fields if all do not already exist. FXX: Only set the fields if all already exist. EX seconds: Set the specified expiration time, in seconds. PX milliseconds: Set the specified expiration time, in milliseconds. EXAT timestamp-seconds: Set the specified Unix time at which the field will expire, in seconds. PXAT timestamp-milliseconds: Set the specified Unix time at which the field will expire, in milliseconds. KEEPTTL: Retain the time to live associated with the field. Note: If no option is provided, any associated expiration time will be discarded similar to how SET command behaves. **Reply** Integer reply: 0 if no fields were set Integer reply: 1 if all the fields were set --- src/commands.def | 129 +++++ src/commands/hgetdel.json | 78 +++ src/commands/hgetex.json | 111 ++++ src/commands/hsetex.json | 132 +++++ src/server.c | 1 + src/server.h | 9 +- src/t_hash.c | 770 ++++++++++++++++++++++---- tests/unit/info-keysizes.tcl | 25 + tests/unit/pubsub.tcl | 52 ++ tests/unit/type/hash-field-expire.tcl | 653 ++++++++++++++++++++++ tests/unit/type/hash.tcl | 84 +++ 11 files changed, 1920 insertions(+), 124 deletions(-) create mode 100644 src/commands/hgetdel.json create mode 100644 src/commands/hgetex.json create mode 100644 src/commands/hsetex.json diff --git a/src/commands.def b/src/commands.def index d8f496342..dd55c8162 100644 --- a/src/commands.def +++ b/src/commands.def @@ -3472,6 +3472,78 @@ struct COMMAND_ARG HGETALL_Args[] = { {MAKE_ARG("key",ARG_TYPE_KEY,0,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)}, }; +/********** HGETDEL ********************/ + +#ifndef SKIP_CMD_HISTORY_TABLE +/* HGETDEL history */ +#define HGETDEL_History NULL +#endif + +#ifndef SKIP_CMD_TIPS_TABLE +/* HGETDEL tips */ +#define HGETDEL_Tips NULL +#endif + +#ifndef SKIP_CMD_KEY_SPECS_TABLE +/* HGETDEL key specs */ +keySpec HGETDEL_Keyspecs[1] = { +{NULL,CMD_KEY_RW|CMD_KEY_ACCESS|CMD_KEY_DELETE,KSPEC_BS_INDEX,.bs.index={1},KSPEC_FK_RANGE,.fk.range={0,1,0}} +}; +#endif + +/* HGETDEL fields argument table */ +struct COMMAND_ARG HGETDEL_fields_Subargs[] = { +{MAKE_ARG("numfields",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)}, +}; + +/* HGETDEL argument table */ +struct COMMAND_ARG HGETDEL_Args[] = { +{MAKE_ARG("key",ARG_TYPE_KEY,0,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)}, +{MAKE_ARG("fields",ARG_TYPE_BLOCK,-1,"FIELDS",NULL,NULL,CMD_ARG_NONE,2,NULL),.subargs=HGETDEL_fields_Subargs}, +}; + +/********** HGETEX ********************/ + +#ifndef SKIP_CMD_HISTORY_TABLE +/* HGETEX history */ +#define HGETEX_History NULL +#endif + +#ifndef SKIP_CMD_TIPS_TABLE +/* HGETEX tips */ +#define HGETEX_Tips NULL +#endif + +#ifndef SKIP_CMD_KEY_SPECS_TABLE +/* HGETEX key specs */ +keySpec HGETEX_Keyspecs[1] = { +{"RW and UPDATE because it changes the TTL",CMD_KEY_RW|CMD_KEY_ACCESS|CMD_KEY_UPDATE,KSPEC_BS_INDEX,.bs.index={1},KSPEC_FK_RANGE,.fk.range={0,1,0}} +}; +#endif + +/* HGETEX expiration argument table */ +struct COMMAND_ARG HGETEX_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)}, +}; + +/* HGETEX fields argument table */ +struct COMMAND_ARG HGETEX_fields_Subargs[] = { +{MAKE_ARG("numfields",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)}, +}; + +/* HGETEX argument table */ +struct COMMAND_ARG HGETEX_Args[] = { +{MAKE_ARG("key",ARG_TYPE_KEY,0,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)}, +{MAKE_ARG("expiration",ARG_TYPE_ONEOF,-1,NULL,NULL,NULL,CMD_ARG_OPTIONAL,5,NULL),.subargs=HGETEX_expiration_Subargs}, +{MAKE_ARG("fields",ARG_TYPE_BLOCK,-1,"FIELDS",NULL,NULL,CMD_ARG_NONE,2,NULL),.subargs=HGETEX_fields_Subargs}, +}; + /********** HINCRBY ********************/ #ifndef SKIP_CMD_HISTORY_TABLE @@ -3903,6 +3975,60 @@ struct COMMAND_ARG HSET_Args[] = { {MAKE_ARG("data",ARG_TYPE_BLOCK,-1,NULL,NULL,NULL,CMD_ARG_MULTIPLE,2,NULL),.subargs=HSET_data_Subargs}, }; +/********** HSETEX ********************/ + +#ifndef SKIP_CMD_HISTORY_TABLE +/* HSETEX history */ +#define HSETEX_History NULL +#endif + +#ifndef SKIP_CMD_TIPS_TABLE +/* HSETEX tips */ +#define HSETEX_Tips NULL +#endif + +#ifndef SKIP_CMD_KEY_SPECS_TABLE +/* HSETEX key specs */ +keySpec HSETEX_Keyspecs[1] = { +{NULL,CMD_KEY_RW|CMD_KEY_UPDATE,KSPEC_BS_INDEX,.bs.index={1},KSPEC_FK_RANGE,.fk.range={0,1,0}} +}; +#endif + +/* HSETEX condition argument table */ +struct COMMAND_ARG HSETEX_condition_Subargs[] = { +{MAKE_ARG("fnx",ARG_TYPE_PURE_TOKEN,-1,"FNX",NULL,NULL,CMD_ARG_NONE,0,NULL)}, +{MAKE_ARG("fxx",ARG_TYPE_PURE_TOKEN,-1,"FXX",NULL,NULL,CMD_ARG_NONE,0,NULL)}, +}; + +/* HSETEX expiration argument table */ +struct COMMAND_ARG HSETEX_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)}, +}; + +/* HSETEX fields data argument table */ +struct COMMAND_ARG HSETEX_fields_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)}, +}; + +/* HSETEX fields argument table */ +struct COMMAND_ARG HSETEX_fields_Subargs[] = { +{MAKE_ARG("numfields",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=HSETEX_fields_data_Subargs}, +}; + +/* HSETEX argument table */ +struct COMMAND_ARG HSETEX_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,2,NULL),.subargs=HSETEX_condition_Subargs}, +{MAKE_ARG("expiration",ARG_TYPE_ONEOF,-1,NULL,NULL,NULL,CMD_ARG_OPTIONAL,5,NULL),.subargs=HSETEX_expiration_Subargs}, +{MAKE_ARG("fields",ARG_TYPE_BLOCK,-1,"FIELDS",NULL,NULL,CMD_ARG_NONE,2,NULL),.subargs=HSETEX_fields_Subargs}, +}; + /********** HSETNX ********************/ #ifndef SKIP_CMD_HISTORY_TABLE @@ -11043,6 +11169,8 @@ 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,2),.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("hgetdel","Returns the value of a field and deletes it from the hash.","O(N) where N is the number of specified fields","8.0.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HGETDEL_History,0,HGETDEL_Tips,0,hgetdelCommand,-5,CMD_WRITE|CMD_FAST,ACL_CATEGORY_HASH,HGETDEL_Keyspecs,1,NULL,2),.args=HGETDEL_Args}, +{MAKE_CMD("hgetex","Get the value of one or more fields of a given hash key, and optionally set their expiration.","O(N) where N is the number of specified fields","8.0.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HGETEX_History,0,HGETEX_Tips,0,hgetexCommand,-5,CMD_WRITE|CMD_FAST,ACL_CATEGORY_HASH,HGETEX_Keyspecs,1,NULL,3),.args=HGETEX_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}, @@ -11057,6 +11185,7 @@ 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("hsetex","Set the value of one or more fields of a given hash key, and optionally set their expiration.","O(N) where N is the number of fields being set.","8.0.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HSETEX_History,0,HSETEX_Tips,0,hsetexCommand,-6,CMD_WRITE|CMD_DENYOOM|CMD_FAST,ACL_CATEGORY_HASH,HSETEX_Keyspecs,1,NULL,4),.args=HSETEX_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,1,httlCommand,-5,CMD_READONLY|CMD_FAST,ACL_CATEGORY_HASH,HTTL_Keyspecs,1,NULL,2),.args=HTTL_Args}, diff --git a/src/commands/hgetdel.json b/src/commands/hgetdel.json new file mode 100644 index 000000000..af748fb52 --- /dev/null +++ b/src/commands/hgetdel.json @@ -0,0 +1,78 @@ +{ + "HGETDEL": { + "summary": "Returns the value of a field and deletes it from the hash.", + "complexity": "O(N) where N is the number of specified fields", + "group": "hash", + "since": "8.0.0", + "arity": -5, + "function": "hgetdelCommand", + "history": [], + "command_flags": [ + "WRITE", + "FAST" + ], + "acl_categories": [ + "HASH" + ], + "key_specs": [ + { + "flags": [ + "RW", + "ACCESS", + "DELETE" + ], + "begin_search": { + "index": { + "pos": 1 + } + }, + "find_keys": { + "range": { + "lastkey": 0, + "step": 1, + "limit": 0 + } + } + } + ], + "reply_schema": { + "description": "List of values associated with the given fields, in the same order as they are requested.", + "type": "array", + "minItems": 1, + "items": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "arguments": [ + { + "name": "key", + "type": "key", + "key_spec_index": 0 + }, + { + "name": "fields", + "token": "FIELDS", + "type": "block", + "arguments": [ + { + "name": "numfields", + "type": "integer" + }, + { + "name": "field", + "type": "string", + "multiple": true + } + ] + } + ] + } +} + diff --git a/src/commands/hgetex.json b/src/commands/hgetex.json new file mode 100644 index 000000000..02889ca83 --- /dev/null +++ b/src/commands/hgetex.json @@ -0,0 +1,111 @@ +{ + "HGETEX": { + "summary": "Get the value of one or more fields of a given hash key, and optionally set their expiration.", + "complexity": "O(N) where N is the number of specified fields", + "group": "hash", + "since": "8.0.0", + "arity": -5, + "function": "hgetexCommand", + "history": [], + "command_flags": [ + "WRITE", + "FAST" + ], + "acl_categories": [ + "HASH" + ], + "key_specs": [ + { + "notes": "RW and UPDATE because it changes the TTL", + "flags": [ + "RW", + "ACCESS", + "UPDATE" + ], + "begin_search": { + "index": { + "pos": 1 + } + }, + "find_keys": { + "range": { + "lastkey": 0, + "step": 1, + "limit": 0 + } + } + } + ], + "reply_schema": { + "description": "List of values associated with the given fields, in the same order as they are requested.", + "type": "array", + "minItems": 1, + "items": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "arguments": [ + { + "name": "key", + "type": "key", + "key_spec_index": 0 + }, + { + "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", + "token": "FIELDS", + "type": "block", + "arguments": [ + { + "name": "numfields", + "type": "integer" + }, + { + "name": "field", + "type": "string", + "multiple": true + } + ] + } + ] + } +} + diff --git a/src/commands/hsetex.json b/src/commands/hsetex.json new file mode 100644 index 000000000..6f6a6c600 --- /dev/null +++ b/src/commands/hsetex.json @@ -0,0 +1,132 @@ +{ + "HSETEX": { + "summary": "Set the value of one or more fields of a given hash key, and optionally set their expiration.", + "complexity": "O(N) where N is the number of fields being set.", + "group": "hash", + "since": "8.0.0", + "arity": -6, + "function": "hsetexCommand", + "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": "No field was set (due to FXX or FNX flags).", + "const": 0 + }, + { + "description": "All the fields were set.", + "const": 1 + } + ] + }, + "arguments": [ + { + "name": "key", + "type": "key", + "key_spec_index": 0 + }, + { + "name": "condition", + "type": "oneof", + "optional": true, + "arguments": [ + { + "name": "fnx", + "type": "pure-token", + "token": "FNX" + }, + { + "name": "fxx", + "type": "pure-token", + "token": "FXX" + } + ] + }, + { + "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": "fields", + "token": "FIELDS", + "type": "block", + "arguments": [ + { + "name": "numfields", + "type": "integer" + }, + { + "name": "data", + "type": "block", + "multiple": true, + "arguments": [ + { + "name": "field", + "type": "string" + }, + { + "name": "value", + "type": "string" + } + ] + } + ] + } + ] + } +} diff --git a/src/server.c b/src/server.c index 0e056c8be..48936ac53 100644 --- a/src/server.c +++ b/src/server.c @@ -2034,6 +2034,7 @@ void createSharedObjects(void) { shared.set = createStringObject("SET",3); shared.eval = createStringObject("EVAL",4); shared.hpexpireat = createStringObject("HPEXPIREAT",10); + shared.hpersist = createStringObject("HPERSIST",8); shared.hdel = createStringObject("HDEL",4); /* Shared command argument */ diff --git a/src/server.h b/src/server.h index e04035c28..d65392b8c 100644 --- a/src/server.h +++ b/src/server.h @@ -1434,7 +1434,7 @@ struct sharedObjectsStruct { *rpop, *lpop, *lpush, *rpoplpush, *lmove, *blmove, *zpopmin, *zpopmax, *emptyscan, *multi, *exec, *left, *right, *hset, *srem, *xgroup, *xclaim, *script, *replconf, *eval, *persist, *set, *pexpireat, *pexpire, - *hdel, *hpexpireat, + *hdel, *hpexpireat, *hpersist, *time, *pxat, *absttl, *retrycount, *force, *justid, *entriesread, *lastid, *ping, *setid, *keepttl, *load, *createconsumer, *getack, *special_asterick, *special_equals, *default_username, *redacted, @@ -3361,7 +3361,9 @@ typedef struct dictExpireMetadata { #define HFE_LAZY_AVOID_HASH_DEL (1<<1) /* Avoid deleting hash if the field is the last one */ #define HFE_LAZY_NO_NOTIFICATION (1<<2) /* Do not send notification, used when multiple fields * may expire and only one notification is desired. */ -#define HFE_LAZY_ACCESS_EXPIRED (1<<3) /* Avoid lazy expire and allow access to expired fields */ +#define HFE_LAZY_NO_SIGNAL (1<<3) /* Do not send signal, used when multiple fields + * may expire and only one signal is desired. */ +#define HFE_LAZY_ACCESS_EXPIRED (1<<4) /* Avoid lazy expire and allow access to expired fields */ void hashTypeConvert(robj *o, int enc, ebuckets *hexpires); void hashTypeTryConversion(redisDb *db, robj *subject, robj **argv, int start, int end); @@ -3881,6 +3883,7 @@ void strlenCommand(client *c); void zrankCommand(client *c); void zrevrankCommand(client *c); void hsetCommand(client *c); +void hsetexCommand(client *c); void hpexpireCommand(client *c); void hexpireCommand(client *c); void hpexpireatCommand(client *c); @@ -3893,6 +3896,8 @@ void hpersistCommand(client *c); void hsetnxCommand(client *c); void hgetCommand(client *c); void hmgetCommand(client *c); +void hgetexCommand(client *c); +void hgetdelCommand(client *c); void hdelCommand(client *c); void hlenCommand(client *c); void hstrlenCommand(client *c); diff --git a/src/t_hash.c b/src/t_hash.c index c6e48b77a..b4d31bd0e 100644 --- a/src/t_hash.c +++ b/src/t_hash.c @@ -48,7 +48,7 @@ typedef listpackEntry CommonEntry; /* extend usage beyond lp */ static ExpireAction onFieldExpire(eItem item, void *ctx); static ExpireMeta* hfieldGetExpireMeta(const eItem field); static ExpireMeta *hashGetExpireMeta(const eItem hash); -static void hexpireGenericCommand(client *c, const char *cmd, long long basetime, int unit); +static void hexpireGenericCommand(client *c, long long basetime, int unit); static ExpireAction hashTypeActiveExpire(eItem hashObj, void *ctx); static uint64_t hashTypeExpire(robj *o, ExpireCtx *expireCtx, int updateGlobalHFE); static void hfieldPersist(robj *hashObj, hfield field); @@ -214,15 +214,13 @@ typedef struct HashTypeSetEx { * minimum expiration time. If minimum recorded * is above minExpire of the hash, then we don't * have to update global HFE DS */ - int fieldDeleted; /* Number of fields deleted */ - int fieldUpdated; /* Number of fields updated */ /* Optionally provide client for notification */ client *c; const char *cmd; } HashTypeSetEx; -int hashTypeSetExInit(robj *key, robj *o, client *c, redisDb *db, const char *cmd, +int hashTypeSetExInit(robj *key, robj *o, client *c, redisDb *db, ExpireSetCond expireSetCond, HashTypeSetEx *ex); SetExRes hashTypeSetEx(robj *o, sds field, uint64_t expireAt, HashTypeSetEx *exInfo); @@ -531,6 +529,15 @@ SetExRes hashTypeSetExpiryListpack(HashTypeSetEx *ex, sds field, prevExpire = (uint64_t) expireTime; } + /* Special value of EXPIRE_TIME_INVALID indicates field should be persisted.*/ + if (expireAt == EB_EXPIRE_TIME_INVALID) { + /* Return error if already there is no ttl. */ + if (prevExpire == EB_EXPIRE_TIME_INVALID) + return HSETEX_NO_CONDITION_MET; + listpackExUpdateExpiry(ex->hashObj, field, fptr, vptr, HASH_LP_NO_TTL); + return HSETEX_OK; + } + if (prevExpire == EB_EXPIRE_TIME_INVALID) { /* For fields without expiry, LT condition is considered valid */ if (ex->expireSetCond & (HFE_XX | HFE_GT)) @@ -551,13 +558,7 @@ SetExRes hashTypeSetExpiryListpack(HashTypeSetEx *ex, sds field, if (unlikely(checkAlreadyExpired(expireAt))) { propagateHashFieldDeletion(ex->db, ex->key->ptr, field, sdslen(field)); hashTypeDelete(ex->hashObj, field, 1); - - /* get listpack length */ - listpackEx *lpt = ((listpackEx *) ex->hashObj->ptr); - unsigned long length = lpLength(lpt->lp) / 3; - updateKeysizesHist(ex->db, getKeySlot(ex->key->ptr), OBJ_HASH, length+1, length); server.stat_expired_subkeys++; - ex->fieldDeleted++; return HSETEX_DELETED; } @@ -565,7 +566,6 @@ SetExRes hashTypeSetExpiryListpack(HashTypeSetEx *ex, sds field, ex->minExpireFields = expireAt; listpackExUpdateExpiry(ex->hashObj, field, fptr, vptr, expireAt); - ex->fieldUpdated++; return HSETEX_OK; } @@ -788,7 +788,8 @@ GetFieldRes hashTypeGetValue(redisDb *db, robj *o, sds field, unsigned char **vs dbDelete(db,keyObj); res = GETF_EXPIRED_HASH; } - signalModifiedKey(NULL, db, keyObj); + if (!(hfeFlags & HFE_LAZY_NO_SIGNAL)) + signalModifiedKey(NULL, db, keyObj); decrRefCount(keyObj); return res; } @@ -1010,34 +1011,33 @@ int hashTypeSet(redisDb *db, robj *o, sds field, sds value, int flags) { SetExRes hashTypeSetExpiryHT(HashTypeSetEx *exInfo, sds field, uint64_t expireAt) { dict *ht = exInfo->hashObj->ptr; dictEntry *existingEntry = NULL; + hfield hfNew = NULL; - /* New field with expiration metadata */ - hfield hfNew = hfieldNew(field, sdslen(field), 1 /*withExpireMeta*/); - - if ((existingEntry = dictFind(ht, field)) == NULL) { - hfieldFree(hfNew); + if ((existingEntry = dictFind(ht, field)) == NULL) return HSETEX_NO_FIELD; - } hfield hfOld = dictGetKey(existingEntry); + /* Special value of EXPIRE_TIME_INVALID indicates field should be persisted.*/ + if (expireAt == EB_EXPIRE_TIME_INVALID) { + /* Return error if already there is no ttl. */ + if (hfieldGetExpireTime(hfOld) == EB_EXPIRE_TIME_INVALID) + return HSETEX_NO_CONDITION_MET; + + hfieldPersist(exInfo->hashObj, hfOld); + return HSETEX_OK; + } /* If field doesn't have expiry metadata attached */ if (!hfieldIsExpireAttached(hfOld)) { - /* For fields without expiry, LT condition is considered valid */ - if (exInfo->expireSetCond & (HFE_XX | HFE_GT)) { - hfieldFree(hfNew); + if (exInfo->expireSetCond & (HFE_XX | HFE_GT)) return HSETEX_NO_CONDITION_MET; - } /* Delete old field. Below goanna dictSetKey(..,hfNew) */ hfieldFree(hfOld); - + /* New field with expiration metadata */ + hfNew = hfieldNew(field, sdslen(field), 1); } else { /* field has ExpireMeta struct attached */ - - /* No need for hfNew (Just modify expire-time of existing field) */ - hfieldFree(hfNew); - uint64_t prevExpire = hfieldGetExpireTime(hfOld); /* If field has valid expiration time, then check GT|LT|NX */ @@ -1073,13 +1073,10 @@ SetExRes hashTypeSetExpiryHT(HashTypeSetEx *exInfo, sds field, uint64_t expireAt /* If expired, then delete the field and propagate the deletion. * If replica, continue like the field is valid */ if (unlikely(checkAlreadyExpired(expireAt))) { - unsigned long length = dictSize(ht); - updateKeysizesHist(exInfo->db, getKeySlot(exInfo->key->ptr), OBJ_HASH, length, length-1); /* replicas should not initiate deletion of fields */ propagateHashFieldDeletion(exInfo->db, exInfo->key->ptr, field, sdslen(field)); hashTypeDelete(exInfo->hashObj, field, 1); server.stat_expired_subkeys++; - exInfo->fieldDeleted++; return HSETEX_DELETED; } @@ -1088,7 +1085,6 @@ SetExRes hashTypeSetExpiryHT(HashTypeSetEx *exInfo, sds field, uint64_t expireAt dictExpireMetadata *dm = (dictExpireMetadata *) dictMetadata(ht); ebAdd(&dm->hfe, &hashFieldExpireBucketsType, hfNew, expireAt); - exInfo->fieldUpdated++; return HSETEX_OK; } @@ -1097,20 +1093,18 @@ SetExRes hashTypeSetExpiryHT(HashTypeSetEx *exInfo, sds field, uint64_t expireAt * * Take care to call first hashTypeSetExInit() and then call this function. * Finally, call hashTypeSetExDone() to notify and update global HFE DS. + * + * Special value of EB_EXPIRE_TIME_INVALID for 'expireAt' argument will persist + * the field. */ -SetExRes hashTypeSetEx(robj *o, sds field, uint64_t expireAt, HashTypeSetEx *exInfo) -{ - if (o->encoding == OBJ_ENCODING_LISTPACK_EX) - { +SetExRes hashTypeSetEx(robj *o, sds field, uint64_t expireAt, HashTypeSetEx *exInfo) { + if (o->encoding == OBJ_ENCODING_LISTPACK_EX) { unsigned char *fptr = NULL, *vptr = NULL, *tptr = NULL; - listpackEx *lpt = o->ptr; - long long expireTime = HASH_LP_NO_TTL; - if ((fptr = lpFirst(lpt->lp)) == NULL) - return HSETEX_NO_FIELD; - - fptr = lpFind(lpt->lp, fptr, (unsigned char*)field, sdslen(field), 2); + fptr = lpFirst(lpt->lp); + if (fptr) + fptr = lpFind(lpt->lp, fptr, (unsigned char*)field, sdslen(field), 2); if (!fptr) return HSETEX_NO_FIELD; @@ -1120,7 +1114,7 @@ SetExRes hashTypeSetEx(robj *o, sds field, uint64_t expireAt, HashTypeSetEx *exI serverAssert(vptr != NULL); tptr = lpNext(lpt->lp, vptr); - serverAssert(tptr && lpGetIntegerValue(tptr, &expireTime)); + serverAssert(tptr); /* update TTL */ return hashTypeSetExpiryListpack(exInfo, field, fptr, vptr, tptr, expireAt); @@ -1144,19 +1138,16 @@ void initDictExpireMetadata(sds key, robj *o) { } /* Init HashTypeSetEx struct before calling hashTypeSetEx() */ -int hashTypeSetExInit(robj *key, robj *o, client *c, redisDb *db, const char *cmd, +int hashTypeSetExInit(robj *key, robj *o, client *c, redisDb *db, ExpireSetCond expireSetCond, HashTypeSetEx *ex) { dict *ht = o->ptr; ex->expireSetCond = expireSetCond; ex->minExpire = EB_EXPIRE_TIME_INVALID; ex->c = c; - ex->cmd = cmd; ex->db = db; ex->key = key; ex->hashObj = o; - ex->fieldDeleted = 0; - ex->fieldUpdated = 0; ex->minExpireFields = EB_EXPIRE_TIME_INVALID; /* Take care that HASH support expiration */ @@ -1220,50 +1211,38 @@ int hashTypeSetExInit(robj *key, robj *o, client *c, redisDb *db, const char *cm /* * After calling hashTypeSetEx() for setting fields or their expiry, call this - * function to notify and update global HFE DS. + * function to update global HFE DS. */ void hashTypeSetExDone(HashTypeSetEx *ex) { - /* Notify keyspace event, update dirty count and update global HFE DS */ - if (ex->fieldDeleted + ex->fieldUpdated > 0) { - server.dirty += ex->fieldDeleted + ex->fieldUpdated; - if (ex->fieldDeleted && hashTypeLength(ex->hashObj, 0) == 0) { - dbDelete(ex->db,ex->key); - signalModifiedKey(ex->c, ex->db, ex->key); - notifyKeyspaceEvent(NOTIFY_HASH, "hdel", ex->key, ex->db->id); - notifyKeyspaceEvent(NOTIFY_GENERIC,"del",ex->key, ex->db->id); - } else { - signalModifiedKey(ex->c, ex->db, ex->key); - notifyKeyspaceEvent(NOTIFY_HASH, ex->fieldDeleted ? "hdel" : "hexpire", - ex->key, ex->db->id); + if (hashTypeLength(ex->hashObj, 0) == 0) + return; - /* 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; + /* 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 = hashTypeGetMinExpire(ex->hashObj, 1 /*accurate*/); + /* Retrieve new expired time. It might have changed. */ + uint64_t newMinExpire = hashTypeGetMinExpire(ex->hashObj, 1 /*accurate*/); - /* 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; + /* 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); - } - } + 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); } /* Delete an element from a hash. @@ -2222,6 +2201,303 @@ void hsetCommand(client *c) { server.dirty += (c->argc - 2)/2; } +/* Parse expire time from argument and do boundary checks. */ +static int parseExpireTime(client *c, robj *o, int unit, long long basetime, + long long *expire) +{ + long long val; + + /* Read the expiry time from command */ + if (getLongLongFromObjectOrReply(c, o, &val, NULL) != C_OK) + return C_ERR; + + if (val < 0) { + addReplyError(c,"invalid expire time, must be >= 0"); + return C_ERR; + } + + if (unit == UNIT_SECONDS) { + if (val > (long long) HFE_MAX_ABS_TIME_MSEC / 1000) { + addReplyErrorExpireTime(c); + return C_ERR; + } + val *= 1000; + } + + if (val > (long long) HFE_MAX_ABS_TIME_MSEC - basetime) { + addReplyErrorExpireTime(c); + return C_ERR; + } + val += basetime; + *expire = val; + return C_OK; +} + +/* Flags that are used as part of HGETEX and HSETEX commands. */ +#define HFE_EX (1<<0) /* Expiration time in seconds */ +#define HFE_PX (1<<1) /* Expiration time in milliseconds */ +#define HFE_EXAT (1<<2) /* Expiration time in unix seconds */ +#define HFE_PXAT (1<<3) /* Expiration time in unix milliseconds */ +#define HFE_PERSIST (1<<4) /* Persist fields */ +#define HFE_KEEPTTL (1<<5) /* Do not discard field ttl on set op */ +#define HFE_FXX (1<<6) /* Set fields if all the fields already exist */ +#define HFE_FNX (1<<7) /* Set fields if none of the fields exist */ + +/* Parse hsetex command arguments. + * HSETEX + * [FNX|FXX] + * [EX seconds|PX milliseconds|EXAT unix-time-seconds|PXAT unix-time-milliseconds|KEEPTTL] + * FIELDS field value [field value ...] +*/ +static int hsetexParseArgs(client *c, int *flags, + long long *expire_time, int *expire_time_pos, + int *first_field_pos, int *field_count) { + *flags = 0; + *first_field_pos = -1; + *field_count = -1; + *expire_time_pos = -1; + + for (int i = 2; i < c->argc; i++) { + if (!strcasecmp(c->argv[i]->ptr, "fields")) { + long val; + + if (i >= c->argc - 3) { + 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; + + int remaining = (c->argc - i - 2); + if (remaining % 2 != 0 || val != remaining / 2) { + addReplyErrorArity(c); + return C_ERR; + } + + *first_field_pos = i + 2; + *field_count = (int) val; + return C_OK; + } else if (!strcasecmp(c->argv[i]->ptr, "EX")) { + if (*flags & (HFE_EX | HFE_EXAT | HFE_PX | HFE_PXAT | HFE_KEEPTTL)) + goto err_expiration; + + if (i >= c->argc - 1) + goto err_missing_expire; + + *flags |= HFE_EX; + i++; + + if (parseExpireTime(c, c->argv[i], UNIT_SECONDS, + commandTimeSnapshot(), expire_time) != C_OK) + return C_ERR; + + *expire_time_pos = i; + } else if (!strcasecmp(c->argv[i]->ptr, "PX")) { + if (*flags & (HFE_EX | HFE_EXAT | HFE_PX | HFE_PXAT | HFE_KEEPTTL)) + goto err_expiration; + + if (i >= c->argc - 1) + goto err_missing_expire; + + *flags |= HFE_PX; + i++; + if (parseExpireTime(c, c->argv[i], UNIT_MILLISECONDS, + commandTimeSnapshot(), expire_time) != C_OK) + return C_ERR; + + *expire_time_pos = i; + } else if (!strcasecmp(c->argv[i]->ptr, "EXAT")) { + if (*flags & (HFE_EX | HFE_EXAT | HFE_PX | HFE_PXAT | HFE_KEEPTTL)) + goto err_expiration; + + if (i >= c->argc - 1) + goto err_missing_expire; + + *flags |= HFE_EXAT; + i++; + if (parseExpireTime(c, c->argv[i], UNIT_SECONDS, 0, expire_time) != C_OK) + return C_ERR; + + *expire_time_pos = i; + } else if (!strcasecmp(c->argv[i]->ptr, "PXAT")) { + if (*flags & (HFE_EX | HFE_EXAT | HFE_PX | HFE_PXAT | HFE_KEEPTTL)) + goto err_expiration; + + if (i >= c->argc - 1) + goto err_missing_expire; + + *flags |= HFE_PXAT; + i++; + if (parseExpireTime(c, c->argv[i], UNIT_MILLISECONDS, 0, + expire_time) != C_OK) + return C_ERR; + + *expire_time_pos = i; + } else if (!strcasecmp(c->argv[i]->ptr, "KEEPTTL")) { + if (*flags & (HFE_EX | HFE_EXAT | HFE_PX | HFE_PXAT | HFE_KEEPTTL)) + goto err_expiration; + *flags |= HFE_KEEPTTL; + } else if (!strcasecmp(c->argv[i]->ptr, "FXX")) { + if (*flags & (HFE_FXX | HFE_FNX)) + goto err_condition; + *flags |= HFE_FXX; + } else if (!strcasecmp(c->argv[i]->ptr, "FNX")) { + if (*flags & (HFE_FXX | HFE_FNX)) + goto err_condition; + *flags |= HFE_FNX; + } else { + addReplyErrorFormat(c, "unknown argument: %s", (char*) c->argv[i]->ptr); + return C_ERR; + } + } + + serverAssert(0); + +err_missing_expire: + addReplyError(c, "missing expire time"); + return C_ERR; +err_condition: + addReplyError(c, "Only one of FXX or FNX 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; +} + +/* Set the value of one or more fields of a given hash key, and optionally set + * their expiration. + * + * HSETEX key + * [FNX | FXX] + * [EX seconds | PX milliseconds | EXAT unix-time-seconds | PXAT unix-time-milliseconds | KEEPTTL] + * FIELDS field value [field value...] + * + * Reply: + * Integer reply: 0 if no fields were set (due to FXX/FNX args) + * Integer reply: 1 if all the fields were set + */ +void hsetexCommand(client *c) { + int flags = 0, first_field_pos = 0, field_count = 0, expire_time_pos = -1; + int updated = 0, deleted = 0, set_expiry; + long long expire_time = EB_EXPIRE_TIME_INVALID; + unsigned long oldlen, newlen; + robj *o; + HashTypeSetEx setex; + + if (hsetexParseArgs(c, &flags, &expire_time, &expire_time_pos, + &first_field_pos, &field_count) != C_OK) + return; + + o = lookupKeyWrite(c->db, c->argv[1]); + if (checkType(c, o, OBJ_HASH)) + return; + + if (!o) { + if (flags & HFE_FXX) { + addReplyLongLong(c, 0); + return; + } + o = createHashObject(); + dbAdd(c->db, c->argv[1], o); + } + oldlen = hashTypeLength(o, 0); + + if (flags & (HFE_FXX | HFE_FNX)) { + int found = 0; + for (int i = 0; i < field_count; i++) { + sds field = c->argv[first_field_pos + (i * 2)]->ptr; + const int opt = HFE_LAZY_NO_NOTIFICATION | + HFE_LAZY_NO_SIGNAL | + HFE_LAZY_AVOID_HASH_DEL; + int exists = hashTypeExists(c->db, o, field, opt, NULL); + found += (exists != 0); + + /* Check for early exit if the condition is already invalid. */ + if (((flags & HFE_FXX) && !exists) || + ((flags & HFE_FNX) && exists)) + break; + } + + int all_exists = (found == field_count); + int non_exists = (found == 0); + + if (((flags & HFE_FNX) && !non_exists) || + ((flags & HFE_FXX) && !all_exists)) + { + addReplyLongLong(c, 0); + goto out; + } + } + hashTypeTryConversion(c->db, o,c->argv, first_field_pos, c->argc - 1); + + /* Check if we will set the expiration time. */ + set_expiry = flags & (HFE_EX | HFE_PX | HFE_EXAT | HFE_PXAT); + if (set_expiry) + hashTypeSetExInit(c->argv[1], o, c, c->db, 0, &setex); + + + for (int i = 0; i < field_count; i++) { + sds field = c->argv[first_field_pos + (i * 2)]->ptr; + sds value = c->argv[first_field_pos + (i * 2) + 1]->ptr; + + int opt = HASH_SET_COPY; + /* If we are going to set the expiration time later, no need to discard + * it as part of set operation now. */ + if (flags & (HFE_EX | HFE_PX | HFE_EXAT | HFE_PXAT | HFE_KEEPTTL)) + opt |= HASH_SET_KEEP_TTL; + + hashTypeSet(c->db, o, field, value, opt); + + /* Update the expiration time. */ + if (set_expiry) { + int ret = hashTypeSetEx(o, field, expire_time, &setex); + updated += (ret == HSETEX_OK); + deleted += (ret == HSETEX_DELETED); + } + } + + if (set_expiry) + hashTypeSetExDone(&setex); + + server.dirty += field_count; + signalModifiedKey(c, c->db, c->argv[1]); + notifyKeyspaceEvent(NOTIFY_HASH, "hset", c->argv[1], c->db->id); + if (deleted || updated) + notifyKeyspaceEvent(NOTIFY_HASH, deleted ? "hdel": "hexpire", + c->argv[1], c->db->id); + + if (deleted) { + /* If fields are deleted due to timestamp is being in the past, hdel's + * are already propagated. No need to propagate the command itself. */ + preventCommandPropagation(c); + } else if (set_expiry && !(flags & HFE_PXAT)) { + /* Propagate as 'HSETEX PXAT ..' if there is EX/EXAT/PX flag*/ + + /* Replace EX/EXAT/PX with PXAT */ + rewriteClientCommandArgument(c, expire_time_pos - 1, shared.pxat); + /* Replace timestamp with unix timestamp milliseconds. */ + robj *expire = createStringObjectFromLongLong(expire_time); + rewriteClientCommandArgument(c, expire_time_pos, expire); + decrRefCount(expire); + } + + addReplyLongLong(c, 1); + +out: + /* Key may become empty due to lazy expiry in hashTypeExists() + * or the new expiration time is in the past.*/ + newlen = hashTypeLength(o, 0); + if (newlen == 0) { + dbDelete(c->db, c->argv[1]); + notifyKeyspaceEvent(NOTIFY_GENERIC, "del", c->argv[1], c->db->id); + } + if (oldlen != newlen) + updateKeysizesHist(c->db, getKeySlot(c->argv[1]->ptr), OBJ_HASH, + oldlen, newlen); +} + void hincrbyCommand(client *c) { long long value, incr, oldvalue; robj *o; @@ -2393,6 +2669,254 @@ void hmgetCommand(client *c) { } } +/* Get and delete the value of one or more fields of a given hash key. + * HGETDEL FIELDS field1 field2 ... + * Reply: list of the value associated with each field or nil if the field + * doesn’t exist. + */ +void hgetdelCommand(client *c) { + int res = 0, hfe = 0, deleted = 0, expired = 0; + unsigned long oldlen = 0, newlen= 0; + long num_fields = 0; + robj *o; + + o = lookupKeyWrite(c->db, c->argv[1]); + if (checkType(c, o, OBJ_HASH)) + return; + + if (strcasecmp(c->argv[2]->ptr, "FIELDS") != 0) { + addReplyError(c, "Mandatory argument FIELDS is missing or not at the right position"); + return; + } + + /* Read number of fields */ + if (getRangeLongFromObjectOrReply(c, c->argv[3], 1, LONG_MAX, &num_fields, + "Number of fields must be a positive integer") != C_OK) + return; + + /* Verify `numFields` is consistent with number of arguments */ + if (num_fields != c->argc - 4) { + addReplyError(c, "The `numfields` parameter must match the number of arguments"); + return; + } + + /* Hash field expiration is optimized to avoid frequent update global HFE DS + * for each field deletion. Eventually active-expiration will run and update + * or remove the hash from global HFE DS gracefully. Nevertheless, statistic + * "subexpiry" might reflect wrong number of hashes with HFE to the user if + * it is the last field with expiration. The following logic checks if this + * is the last field with expiration and removes it from global HFE DS. */ + if (o) { + hfe = hashTypeIsFieldsWithExpire(o); + oldlen = hashTypeLength(o, 0); + } + + addReplyArrayLen(c, num_fields); + for (int i = 4; i < c->argc; i++) { + const int flags = HFE_LAZY_NO_NOTIFICATION | + HFE_LAZY_NO_SIGNAL | + HFE_LAZY_AVOID_HASH_DEL; + res = addHashFieldToReply(c, o, c->argv[i]->ptr, flags); + expired += (res == GETF_EXPIRED); + /* Try to delete only if it's found and not expired lazily. */ + if (res == GETF_OK) { + deleted++; + serverAssert(hashTypeDelete(o, c->argv[i]->ptr, 1) == 1); + } + } + + /* Return if no modification has been made. */ + if (expired == 0 && deleted == 0) + return; + + signalModifiedKey(c, c->db, c->argv[1]); + + if (expired) + notifyKeyspaceEvent(NOTIFY_HASH, "hexpired", c->argv[1], c->db->id); + if (deleted) { + notifyKeyspaceEvent(NOTIFY_HASH, "hdel", c->argv[1], c->db->id); + server.dirty += deleted; + + /* Propagate as HDEL command. + * Orig: HGETDEL FIELDS field1 field2 ... + * Repl: HDEL field1 field2 ... */ + rewriteClientCommandArgument(c, 0, shared.hdel); + rewriteClientCommandArgument(c, 2, NULL); /* Delete FIELDS arg */ + rewriteClientCommandArgument(c, 2, NULL); /* Delete arg */ + } + + /* Key may have become empty because of deleting fields or lazy expire. */ + newlen = hashTypeLength(o, 0); + if (newlen == 0) { + dbDelete(c->db, c->argv[1]); + notifyKeyspaceEvent(NOTIFY_GENERIC, "del", c->argv[1], c->db->id); + } else if (hfe && (hashTypeIsFieldsWithExpire(o) == 0)) { /*is it last HFE*/ + ebRemove(&c->db->hexpires, &hashExpireBucketsType, o); + } + + if (oldlen != newlen) + updateKeysizesHist(c->db, getKeySlot(c->argv[1]->ptr), OBJ_HASH, + oldlen, newlen); +} + +/* Get and delete the value of one or more fields of a given hash key. + * + * HGETEX + * [EX seconds | PX milliseconds | EXAT unix-time-seconds | PXAT unix-time-milliseconds | PERSIST] + * FIELDS field1 field2 ... + * + * Reply: list of the value associated with each field or nil if the field + * doesn’t exist. + */ +void hgetexCommand(client *c) { + int expired = 0, deleted = 0, updated = 0; + int num_fields_pos = 3, cond = 0; + long num_fields; + unsigned long oldlen = 0, newlen = 0; + long long expire_time = 0; + robj *o; + HashTypeSetEx setex; + + o = lookupKeyWrite(c->db, c->argv[1]); + if (checkType(c, o, OBJ_HASH)) + return; + + /* Read optional arg */ + if (!strcasecmp(c->argv[2]->ptr, "ex")) + cond = HFE_EX; + else if (!strcasecmp(c->argv[2]->ptr, "px")) + cond = HFE_PX; + else if (!strcasecmp(c->argv[2]->ptr, "exat")) + cond = HFE_EXAT; + else if (!strcasecmp(c->argv[2]->ptr, "pxat")) + cond = HFE_PXAT; + else if (!strcasecmp(c->argv[2]->ptr, "persist")) + cond = HFE_PERSIST; + + /* Parse expiration time */ + if (cond & (HFE_EX | HFE_PX | HFE_EXAT | HFE_PXAT)) { + num_fields_pos += 2; + int unit = (cond & (HFE_EX | HFE_EXAT)) ? UNIT_SECONDS : UNIT_MILLISECONDS; + long long basetime = cond & (HFE_EX | HFE_PX) ? commandTimeSnapshot() : 0; + if (parseExpireTime(c, c->argv[3], unit, basetime, &expire_time) != C_OK) + return; + } else if (cond & HFE_PERSIST) { + num_fields_pos += 1; + } + + if (strcasecmp(c->argv[num_fields_pos - 1]->ptr, "FIELDS") != 0) { + addReplyError(c, "Mandatory argument FIELDS is missing or not at the right position"); + return; + } + + /* Read number of fields */ + if (getRangeLongFromObjectOrReply(c, c->argv[num_fields_pos], 1, LONG_MAX, &num_fields, + "Number of fields must be a positive integer") != C_OK) + return; + + /* Check number of fields is consistent with number of arguments */ + if (num_fields != c->argc - num_fields_pos - 1) { + addReplyError(c, "The `numfields` parameter must match the number of arguments"); + return; + } + + /* Non-existing keys and empty hashes are the same thing. Reply null if the + * key does not exist.*/ + if (!o) { + addReplyArrayLen(c, num_fields); + for (int i = 0; i < num_fields; i++) + addReplyNull(c); + return; + } + + oldlen = hashTypeLength(o, 0); + if (cond) + hashTypeSetExInit(c->argv[1], o, c, c->db, 0, &setex); + + addReplyArrayLen(c, num_fields); + for (int i = num_fields_pos + 1; i < c->argc; i++) { + const int flags = HFE_LAZY_NO_NOTIFICATION | + HFE_LAZY_NO_SIGNAL | + HFE_LAZY_AVOID_HASH_DEL; + sds field = c->argv[i]->ptr; + int res = addHashFieldToReply(c, o, field, flags); + expired += (res == GETF_EXPIRED); + + /* Set expiration only if the field exists and not expired lazily. */ + if (res == GETF_OK && cond) { + if (cond & HFE_PERSIST) + expire_time = EB_EXPIRE_TIME_INVALID; + + res = hashTypeSetEx(o, field, expire_time, &setex); + deleted += (res == HSETEX_DELETED); + updated += (res == HSETEX_OK); + } + } + + if (cond) + hashTypeSetExDone(&setex); + + /* Exit early if no modification has been made. */ + if (expired == 0 && deleted == 0 && updated == 0) + return; + + server.dirty += deleted + updated; + signalModifiedKey(c, c->db, c->argv[1]); + + /* Key may become empty due to lazy expiry in addHashFieldToReply() + * or the new expiration time is in the past.*/ + newlen = hashTypeLength(o, 0); + if (newlen == 0) { + dbDelete(c->db, c->argv[1]); + notifyKeyspaceEvent(NOTIFY_GENERIC, "del", c->argv[1], c->db->id); + } + if (oldlen != newlen) + updateKeysizesHist(c->db, getKeySlot(c->argv[1]->ptr), OBJ_HASH, + oldlen, newlen); + + /* This command will never be propagated as it is. It will be propagated as + * HDELs when fields are lazily expired or deleted, if the new timestamp is + * in the past. HDEL's will be emitted as part of addHashFieldToReply() + * or hashTypeSetEx() in this case. + * + * If PERSIST flags is used, it will be propagated as HPERSIST command. + * IF EX/EXAT/PX/PXAT flags are used, it will be replicated as HPEXPRITEAT. + */ + if (expired) + notifyKeyspaceEvent(NOTIFY_HASH, "hexpired", c->argv[1], c->db->id); + if (updated) { + if (cond & HFE_PERSIST) { + notifyKeyspaceEvent(NOTIFY_HASH, "hpersist", c->argv[1], c->db->id); + + /* Propagate as HPERSIST command. + * Orig: HGETEX PERSIST FIELDS field1 field2 ... + * Repl: HPERSIST FIELDS field1 field2 ... */ + rewriteClientCommandArgument(c, 0, shared.hpersist); + rewriteClientCommandArgument(c, 2, NULL); /* Delete PERSIST arg */ + } else { + notifyKeyspaceEvent(NOTIFY_HASH, "hexpire", c->argv[1], c->db->id); + + /* Propagate as HPEXPIREAT command. + * Orig: HGETEX [EX|PX|EXAT|PXAT] ttl FIELDS field1 field2 ... + * Repl: HPEXPIREAT ttl FIELDS field1 field2 ... */ + rewriteClientCommandArgument(c, 0, shared.hpexpireat); + rewriteClientCommandArgument(c, 2, NULL); /* Del [EX|PX|EXAT|PXAT]*/ + + /* Rewrite TTL if it is not unix time milliseconds already. */ + if (!(cond & HFE_PXAT)) { + robj *expire = createStringObjectFromLongLong(expire_time); + rewriteClientCommandArgument(c, 2, expire); + decrRefCount(expire); + } + } + } else if (deleted) { + /* If we are here, fields are deleted because new timestamp was in the + * past. HDELs are already propagated as part of hashTypeSetEx(). */ + notifyKeyspaceEvent(NOTIFY_HASH, "hdel", c->argv[1], c->db->id); + preventCommandPropagation(c); + } +} + void hdelCommand(client *c) { robj *o; int j, deleted = 0, keyremoved = 0; @@ -3174,10 +3698,11 @@ static void httlGenericCommand(client *c, const char *cmd, long long basetime, i * not met, then command will be rejected. Otherwise, EXPIRE command will be * propagated for given key. */ -static void hexpireGenericCommand(client *c, const char *cmd, long long basetime, int unit) { +static void hexpireGenericCommand(client *c, long long basetime, int unit) { long numFields = 0, numFieldsAt = 4; long long expire; /* unix time in msec */ - int fieldAt, fieldsNotSet = 0, expireSetCond = 0; + int fieldAt, fieldsNotSet = 0, expireSetCond = 0, updated = 0, deleted = 0; + unsigned long oldlen, newlen; robj *hashObj, *keyArg = c->argv[1], *expireArg = c->argv[2]; /* Read the hash object */ @@ -3186,29 +3711,9 @@ static void hexpireGenericCommand(client *c, const char *cmd, long long basetime return; /* Read the expiry time from command */ - if (getLongLongFromObjectOrReply(c, expireArg, &expire, NULL) != C_OK) + if (parseExpireTime(c, expireArg, unit, basetime, &expire) != C_OK) return; - if (expire < 0) { - addReplyError(c,"invalid expire time, must be >= 0"); - return; - } - - if (unit == UNIT_SECONDS) { - if (expire > (long long) HFE_MAX_ABS_TIME_MSEC / 1000) { - addReplyErrorExpireTime(c); - return; - } - expire *= 1000; - } - - /* Ensure that the final absolute Unix timestamp does not exceed EB_EXPIRE_TIME_MAX. */ - if (expire > (long long) HFE_MAX_ABS_TIME_MSEC - basetime) { - addReplyErrorExpireTime(c); - return; - } - expire += basetime; - /* Read optional expireSetCond [NX|XX|GT|LT] */ char *optArg = c->argv[3]->ptr; if (!strcasecmp(optArg, "nx")) { @@ -3247,14 +3752,18 @@ static void hexpireGenericCommand(client *c, const char *cmd, long long basetime return; } + oldlen = hashTypeLength(hashObj, 0); + HashTypeSetEx exCtx; - hashTypeSetExInit(keyArg, hashObj, c, c->db, cmd, expireSetCond, &exCtx); + hashTypeSetExInit(keyArg, hashObj, c, c->db, expireSetCond, &exCtx); addReplyArrayLen(c, numFields); fieldAt = numFieldsAt + 1; while (fieldAt < c->argc) { sds field = c->argv[fieldAt]->ptr; SetExRes res = hashTypeSetEx(hashObj, field, expire, &exCtx); + updated += (res == HSETEX_OK); + deleted += (res == HSETEX_DELETED); if (unlikely(res != HSETEX_OK)) { /* If the field was not set, prevent field propagation */ @@ -3269,17 +3778,34 @@ static void hexpireGenericCommand(client *c, const char *cmd, long long basetime hashTypeSetExDone(&exCtx); + if (deleted + updated > 0) { + server.dirty += deleted + updated; + signalModifiedKey(c, c->db, keyArg); + notifyKeyspaceEvent(NOTIFY_HASH, deleted ? "hdel" : "hexpire", + keyArg, c->db->id); + } + + newlen = hashTypeLength(hashObj, 0); + if (newlen == 0) { + dbDelete(c->db, keyArg); + notifyKeyspaceEvent(NOTIFY_GENERIC, "del", keyArg, c->db->id); + } + + if (oldlen != newlen) + updateKeysizesHist(c->db, getKeySlot(c->argv[1]->ptr), OBJ_HASH, + oldlen, newlen); + /* Avoid propagating command if not even one field was updated (Either because * the time is in the past, and corresponding HDELs were sent, or conditions * not met) then it is useless and invalid to propagate command with no fields */ - if (exCtx.fieldUpdated == 0) { + if (updated == 0) { preventCommandPropagation(c); return; } /* If some fields were dropped, rewrite the number of fields */ if (fieldsNotSet) { - robj *numFieldsObj = createStringObjectFromLongLong(exCtx.fieldUpdated); + robj *numFieldsObj = createStringObjectFromLongLong(updated); rewriteClientCommandArgument(c, numFieldsAt, numFieldsObj); decrRefCount(numFieldsObj); } @@ -3297,48 +3823,48 @@ static void hexpireGenericCommand(client *c, const char *cmd, long long basetime } } -/* HPEXPIRE key milliseconds [ NX | XX | GT | LT] numfields */ +/* HPEXPIRE key milliseconds [ NX | XX | GT | LT] FIELDS numfields */ void hpexpireCommand(client *c) { - hexpireGenericCommand(c,"hpexpire", commandTimeSnapshot(),UNIT_MILLISECONDS); + hexpireGenericCommand(c,commandTimeSnapshot(),UNIT_MILLISECONDS); } -/* HEXPIRE key seconds [NX | XX | GT | LT] numfields */ +/* HEXPIRE key seconds [NX | XX | GT | LT] FIELDS numfields */ void hexpireCommand(client *c) { - hexpireGenericCommand(c,"hexpire", commandTimeSnapshot(),UNIT_SECONDS); + hexpireGenericCommand(c,commandTimeSnapshot(),UNIT_SECONDS); } -/* HEXPIREAT key unix-time-seconds [NX | XX | GT | LT] numfields */ +/* HEXPIREAT key unix-time-seconds [NX | XX | GT | LT] FIELDS numfields */ void hexpireatCommand(client *c) { - hexpireGenericCommand(c,"hexpireat", 0,UNIT_SECONDS); + hexpireGenericCommand(c,0,UNIT_SECONDS); } -/* HPEXPIREAT key unix-time-milliseconds [NX | XX | GT | LT] numfields */ +/* HPEXPIREAT key unix-time-milliseconds [NX | XX | GT | LT] FIELDS numfields */ void hpexpireatCommand(client *c) { - hexpireGenericCommand(c,"hpexpireat", 0,UNIT_MILLISECONDS); + hexpireGenericCommand(c,0,UNIT_MILLISECONDS); } /* for each specified field: get the remaining time to live in seconds*/ -/* HTTL key numfields */ +/* HTTL key FIELDS numfields */ void httlCommand(client *c) { httlGenericCommand(c, "httl", commandTimeSnapshot(), UNIT_SECONDS); } -/* HPTTL key numfields */ +/* HPTTL key FIELDS numfields */ void hpttlCommand(client *c) { httlGenericCommand(c, "hpttl", commandTimeSnapshot(), UNIT_MILLISECONDS); } -/* HEXPIRETIME key numFields */ +/* HEXPIRETIME key FIELDS numfields */ void hexpiretimeCommand(client *c) { httlGenericCommand(c, "hexpiretime", 0, UNIT_SECONDS); } -/* HPEXPIRETIME key numFields */ +/* HPEXPIRETIME key FIELDS numfields */ void hpexpiretimeCommand(client *c) { httlGenericCommand(c, "hexpiretime", 0, UNIT_MILLISECONDS); } -/* HPERSIST key */ +/* HPERSIST key FIELDS numfields */ void hpersistCommand(client *c) { robj *hashObj; long numFields = 0, numFieldsAt = 3; diff --git a/tests/unit/info-keysizes.tcl b/tests/unit/info-keysizes.tcl index 98d6d4e6f..a866efcfd 100644 --- a/tests/unit/info-keysizes.tcl +++ b/tests/unit/info-keysizes.tcl @@ -334,6 +334,31 @@ proc test_all_keysizes { {replMode 0} } { run_cmd_verify_hist {$server HSET h2 2 2} {db0_HASH:2=1} run_cmd_verify_hist {$server HDEL h2 1} {db0_HASH:1=1} run_cmd_verify_hist {$server HDEL h2 2} {} + # HGETDEL + run_cmd_verify_hist {$server FLUSHALL} {} + run_cmd_verify_hist {$server HSETEX h2 FIELDS 1 1 1} {db0_HASH:1=1} + run_cmd_verify_hist {$server HSETEX h2 FIELDS 1 2 2} {db0_HASH:2=1} + run_cmd_verify_hist {$server HGETDEL h2 FIELDS 1 1} {db0_HASH:1=1} + run_cmd_verify_hist {$server HGETDEL h2 FIELDS 1 3} {db0_HASH:1=1} + run_cmd_verify_hist {$server HGETDEL h2 FIELDS 1 2} {} + # HGETEX + run_cmd_verify_hist {$server FLUSHALL} {} + run_cmd_verify_hist {$server HSETEX h1 FIELDS 2 f1 1 f2 1} {db0_HASH:2=1} + run_cmd_verify_hist {$server HGETEX h1 PXAT 1 FIELDS 1 f1} {db0_HASH:1=1} + run_cmd_verify_hist {$server HSETEX h1 FIELDS 1 f3 1} {db0_HASH:2=1} + run_cmd_verify_hist {$server HGETEX h1 PX 50 FIELDS 1 f2} {db0_HASH:2=1} + run_cmd_verify_hist {} {db0_HASH:1=1} 1 + run_cmd_verify_hist {$server HGETEX h1 PX 50 FIELDS 1 f3} {db0_HASH:1=1} + run_cmd_verify_hist {} {} 1 + # HSETEX + run_cmd_verify_hist {$server FLUSHALL} {} + run_cmd_verify_hist {$server HSETEX h1 FIELDS 2 f1 1 f2 1} {db0_HASH:2=1} + run_cmd_verify_hist {$server HSETEX h1 PXAT 1 FIELDS 1 f1 v1} {db0_HASH:1=1} + run_cmd_verify_hist {$server HSETEX h1 FIELDS 1 f3 1} {db0_HASH:2=1} + run_cmd_verify_hist {$server HSETEX h1 PX 50 FIELDS 1 f2 v2} {db0_HASH:2=1} + run_cmd_verify_hist {} {db0_HASH:1=1} 1 + run_cmd_verify_hist {$server HSETEX h1 PX 50 FIELDS 1 f3 v3} {db0_HASH:1=1} + run_cmd_verify_hist {} {} 1 # HMSET run_cmd_verify_hist {$server FLUSHALL} {} run_cmd_verify_hist {$server HMSET h1 1 1 2 2 3 3} {db0_HASH:2=1} diff --git a/tests/unit/pubsub.tcl b/tests/unit/pubsub.tcl index 9a4f1196b..def271908 100644 --- a/tests/unit/pubsub.tcl +++ b/tests/unit/pubsub.tcl @@ -414,6 +414,58 @@ start_server {tags {"pubsub network"}} { assert_equal "pmessage * __keyspace@${db}__:myhash del" [$rd1 read] r debug set-active-expire 1 + + # Test HSETEX, HGETEX and HGETDEL notifications + r hsetex myhash FIELDS 3 f4 v4 f5 v5 f6 v6 + assert_equal "pmessage * __keyspace@${db}__:myhash hset" [$rd1 read] + + # hgetex sets ttl in past + r hgetex myhash PX 0 FIELDS 1 f4 + assert_equal "pmessage * __keyspace@${db}__:myhash hdel" [$rd1 read] + + # hgetex sets ttl + r hgetex myhash EXAT [expr {[clock seconds] + 999999}] FIELDS 1 f5 + assert_equal "pmessage * __keyspace@${db}__:myhash hexpire" [$rd1 read] + + # hgetex persists field + r hgetex myhash PERSIST FIELDS 1 f5 + assert_equal "pmessage * __keyspace@${db}__:myhash hpersist" [$rd1 read] + + # hgetdel deletes a field + r hgetdel myhash FIELDS 1 f5 + assert_equal "pmessage * __keyspace@${db}__:myhash hdel" [$rd1 read] + + # hsetex sets field and expiry time + r hsetex myhash EXAT [expr {[clock seconds] + 999999}] FIELDS 1 f6 v6 + assert_equal "pmessage * __keyspace@${db}__:myhash hset" [$rd1 read] + assert_equal "pmessage * __keyspace@${db}__:myhash hexpire" [$rd1 read] + + # hsetex sets field and ttl in the past + r hsetex myhash PX 0 FIELDS 1 f6 v6 + assert_equal "pmessage * __keyspace@${db}__:myhash hset" [$rd1 read] + assert_equal "pmessage * __keyspace@${db}__:myhash hdel" [$rd1 read] + assert_equal "pmessage * __keyspace@${db}__:myhash del" [$rd1 read] + + # Test that we will get `hexpired` notification when a hash field is + # removed by lazy expire using hgetdel command + r debug set-active-expire 0 + r hsetex myhash PX 10 FIELDS 1 f1 v1 + assert_equal "pmessage * __keyspace@${db}__:myhash hset" [$rd1 read] + assert_equal "pmessage * __keyspace@${db}__:myhash hexpire" [$rd1 read] + + # Set another field + r hsetex myhash FIELDS 1 f2 v2 + assert_equal "pmessage * __keyspace@${db}__:myhash hset" [$rd1 read] + # Wait until field expires + after 20 + r hgetdel myhash FIELDS 1 f1 + assert_equal "pmessage * __keyspace@${db}__:myhash hexpired" [$rd1 read] + # Get and delete the only field + r hgetdel myhash FIELDS 1 f2 + assert_equal "pmessage * __keyspace@${db}__:myhash hdel" [$rd1 read] + assert_equal "pmessage * __keyspace@${db}__:myhash del" [$rd1 read] + r debug set-active-expire 1 + $rd1 close } {0} {needs:debug} } ;# foreach diff --git a/tests/unit/type/hash-field-expire.tcl b/tests/unit/type/hash-field-expire.tcl index 0d1840091..02cac6008 100644 --- a/tests/unit/type/hash-field-expire.tcl +++ b/tests/unit/type/hash-field-expire.tcl @@ -855,6 +855,430 @@ start_server {tags {"external:skip needs:debug"}} { assert_equal [r HINCRBYFLOAT h1 f1 2.5] 12.5 assert_range [r HPTTL h1 FIELDS 1 f1] 1 20 } + + test "HGETDEL - delete field with ttl ($type)" { + r debug set-active-expire 0 + r del h1 + + # Test deleting only field in a hash. Due to lazy expiry, + # reply will be null and the field and the key will be deleted. + r hsetex h1 PX 5 FIELDS 1 f1 10 + after 15 + assert_equal [r hgetdel h1 fields 1 f1] "{}" + assert_equal [r exists h1] 0 + + # Test deleting one field among many. f2 will lazily expire + r hsetex h1 FIELDS 3 f1 10 f2 20 f3 value3 + r hpexpire h1 5 FIELDS 1 f2 + after 15 + assert_equal [r hgetdel h1 fields 2 f2 f3] "{} value3" + assert_equal [lsort [r hgetall h1]] [lsort "f1 10"] + + # Try to delete the last field, along with non-existing fields + assert_equal [r hgetdel h1 fields 4 f1 f2 f3 f4] "10 {} {} {}" + r debug set-active-expire 1 + } + + test "HGETEX - input validation ($type)" { + r del h1 + assert_error "*wrong number of arguments*" {r HGETEX} + assert_error "*wrong number of arguments*" {r HGETEX h1} + assert_error "*wrong number of arguments*" {r HGETEX h1 FIELDS} + assert_error "*wrong number of arguments*" {r HGETEX h1 FIELDS 0} + assert_error "*wrong number of arguments*" {r HGETEX h1 FIELDS 1} + assert_error "*argument FIELDS is missing*" {r HGETEX h1 XFIELDX 1 a} + assert_error "*argument FIELDS is missing*" {r HGETEX h1 PXAT 1 1} + assert_error "*argument FIELDS is missing*" {r HGETEX h1 PERSIST 1 FIELDS 1 a} + assert_error "*must match the number of arguments*" {r HGETEX h1 FIELDS 2 a} + assert_error "*Number of fields must be a positive integer*" {r HGETEX h1 FIELDS 0 a} + assert_error "*Number of fields must be a positive integer*" {r HGETEX h1 FIELDS -1 a} + assert_error "*Number of fields must be a positive integer*" {r HGETEX h1 FIELDS 9223372036854775808 a} + } + + test "HGETEX - input validation (expire time) ($type)" { + assert_error "*value is not an integer or out of range*" {r HGETEX h1 EX bla FIELDS 1 a} + assert_error "*value is not an integer or out of range*" {r HGETEX h1 EX 9223372036854775808 FIELDS 1 a} + assert_error "*value is not an integer or out of range*" {r HGETEX h1 EXAT 9223372036854775808 FIELDS 1 a} + assert_error "*invalid expire time, must be >= 0*" {r HGETEX h1 PX -1 FIELDS 1 a} + assert_error "*invalid expire time, must be >= 0*" {r HGETEX h1 PXAT -1 FIELDS 1 a} + assert_error "*invalid expire time*" {r HGETEX h1 EX -1 FIELDS 1 a} + assert_error "*invalid expire time*" {r HGETEX h1 EX [expr (1<<48)] FIELDS 1 a} + assert_error "*invalid expire time*" {r HGETEX h1 EX [expr (1<<46) - [clock seconds] + 100 ] FIELDS 1 a} + assert_error "*invalid expire time*" {r HGETEX h1 EXAT [expr (1<<46) + 100 ] FIELDS 1 a} + assert_error "*invalid expire time*" {r HGETEX h1 PX [expr (1<<46) - [clock milliseconds] + 100 ] FIELDS 1 a} + assert_error "*invalid expire time*" {r HGETEX h1 PXAT [expr (1<<46) + 100 ] FIELDS 1 a} + } + + test "HGETEX - get without setting ttl ($type)" { + r del h1 + r hset h1 a 1 b 2 c strval + assert_equal [r hgetex h1 fields 1 a] "1" + assert_equal [r hgetex h1 fields 2 a b] "1 2" + assert_equal [r hgetex h1 fields 3 a b c] "1 2 strval" + assert_equal [r HTTL h1 FIELDS 3 a b c] "$T_NO_EXPIRY $T_NO_EXPIRY $T_NO_EXPIRY" + } + + test "HGETEX - get and set the ttl ($type)" { + r del h1 + r hset h1 a 1 b 2 c strval + assert_equal [r hgetex h1 EX 10000 fields 1 a] "1" + assert_range [r HTTL h1 FIELDS 1 a] 9000 10000 + assert_equal [r hgetex h1 EX 10000 fields 1 c] "strval" + assert_range [r HTTL h1 FIELDS 1 c] 9000 10000 + } + + test "HGETEX - Test 'EX' flag ($type)" { + r del myhash + r hset myhash field1 value1 field2 value2 field3 value3 + assert_equal [r hgetex myhash EX 1000 FIELDS 1 field1] [list "value1"] + assert_range [r httl myhash FIELDS 1 field1] 1 1000 + } + + test "HGETEX - Test 'EXAT' flag ($type)" { + r del myhash + r hset myhash field1 value1 field2 value2 field3 value3 + assert_equal [r hgetex myhash EXAT 4000000000 FIELDS 1 field2] [list "value2"] + assert_range [expr [r httl myhash FIELDS 1 field2] + [clock seconds]] 3900000000 4000000000 + } + + test "HGETEX - Test 'PX' flag ($type)" { + r del myhash + r hset myhash field1 value1 field2 value2 field3 value3 + assert_equal [r hgetex myhash PX 1000000 FIELDS 1 field3] [list "value3"] + assert_range [r httl myhash FIELDS 1 field3] 900 1000 + } + + test "HGETEX - Test 'PXAT' flag ($type)" { + r del myhash + r hset myhash field1 value1 field2 value2 field3 value3 + assert_equal [r hgetex myhash PXAT 4000000000000 FIELDS 1 field3] [list "value3"] + assert_range [expr [r httl myhash FIELDS 1 field3] + [clock seconds]] 3900000000 4000000000 + } + + test "HGETEX - Test 'PERSIST' flag ($type)" { + r del myhash + r debug set-active-expire 0 + + r hsetex myhash PX 5000 FIELDS 3 f1 v1 f2 v2 f3 v3 + 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" + + # Persist f1 and verify it does not have TTL anymore + assert_equal [r hgetex myhash PERSIST FIELDS 1 f1] "v1" + assert_equal [r httl myhash FIELDS 1 f1] "$T_NO_EXPIRY" + + # Persist rest of the fields + assert_equal [r hgetex myhash PERSIST FIELDS 2 f2 f3] "v2 v3" + assert_equal [r httl myhash FIELDS 2 f2 f3] "$T_NO_EXPIRY $T_NO_EXPIRY" + + # Redo the operation. It should be noop as fields are persisted already. + assert_equal [r hgetex myhash PERSIST FIELDS 2 f2 f3] "v2 v3" + assert_equal [r httl myhash FIELDS 2 f2 f3] "$T_NO_EXPIRY $T_NO_EXPIRY" + + # Final sanity, fields exist and have no attached ttl. + assert_equal [lsort [r hgetall myhash]] [lsort "f1 v1 f2 v2 f3 v3"] + assert_equal [r httl myhash FIELDS 3 f1 f2 f3] "$T_NO_EXPIRY $T_NO_EXPIRY $T_NO_EXPIRY" + r debug set-active-expire 1 + } + + test "HGETEX - Test setting ttl in the past will delete the key ($type)" { + r del myhash + r hset myhash f1 v1 f2 v2 f3 v3 + + # hgetex without setting ttl + assert_equal [lsort [r hgetex 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 an expired ttl and verify the key is deleted + r hgetex myhash PXAT 1 fields 3 f1 f2 f3 + assert_equal [r exists myhash] 0 + } + + test "HGETEX - 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 + assert_equal [lsort [r hgetex myhash PXAT 1 FIELDS 5 f1 f2 f3 f4 f5]] [lsort "v1 v2 v3 v4 v5"] + + r debug set-active-expire 1 + wait_for_condition 50 20 { [r EXISTS myhash] == 0 } else { fail "'myhash' should be expired" } + } + + test "HGETEX - A field with TTL overridden with another value (TTL discarded) ($type)" { + r del myhash + r hset myhash f1 v1 f2 v2 f3 v3 + r hgetex myhash PX 10000 FIELDS 1 f1 + r hgetex myhash EX 100 FIELDS 1 f2 + + # f2 ttl will be discarded + r hset myhash f2 v22 + assert_equal [r hget myhash f2] "v22" + assert_equal [r httl myhash FIELDS 2 f2 f3] "$T_NO_EXPIRY $T_NO_EXPIRY" + + # Other field is not affected (still has TTL) + assert_not_equal [r httl myhash FIELDS 1 f1] "$T_NO_EXPIRY" + } + + test "HGETEX - Test with lazy expiry ($type)" { + r del myhash + r debug set-active-expire 0 + + r hsetex myhash PX 1 FIELDS 2 f1 v1 f2 v2 + after 5 + assert_equal [r hgetex myhash FIELDS 2 f1 f2] "{} {}" + assert_equal [r exists myhash] 0 + + r debug set-active-expire 1 + } + + test "HSETEX - input validation ($type)" { + assert_error {*wrong number of arguments*} {r hsetex myhash} + assert_error {*wrong number of arguments*} {r hsetex myhash fields} + assert_error {*wrong number of arguments*} {r hsetex myhash fields 1} + assert_error {*wrong number of arguments*} {r hsetex myhash fields 2 a b} + assert_error {*wrong number of arguments*} {r hsetex myhash fields 2 a b c} + assert_error {*wrong number of arguments*} {r hsetex myhash fields 2 a b c d e} + assert_error {*wrong number of arguments*} {r hsetex myhash fields 3 a b c d} + assert_error {*wrong number of arguments*} {r hsetex myhash fields 3 a b c d e} + assert_error {*wrong number of arguments*} {r hsetex myhash fields 3 a b c d e f g} + assert_error {*wrong number of arguments*} {r hsetex myhash fields 3 a b} + assert_error {*wrong number of arguments*} {r hsetex myhash fields 1 a b unknown} + assert_error {*unknown argument*} {r hsetex myhash nx fields 1 a b} + assert_error {*unknown argument*} {r hsetex myhash 1 fields 1 a b} + + # Only one of FNX or FXX + assert_error {*Only one of FXX or FNX arguments *} {r hsetex myhash fxx fxx EX 100 fields 1 a b} + assert_error {*Only one of FXX or FNX arguments *} {r hsetex myhash fxx fnx EX 100 fields 1 a b} + assert_error {*Only one of FXX or FNX arguments *} {r hsetex myhash fnx fxx EX 100 fields 1 a b} + assert_error {*Only one of FXX or FNX arguments *} {r hsetex myhash fnx fnx EX 100 fields 1 a b} + assert_error {*Only one of FXX or FNX arguments *} {r hsetex myhash fxx fnx fxx EX 100 fields 1 a b} + assert_error {*Only one of FXX or FNX arguments *} {r hsetex myhash fnx fxx fnx EX 100 fields 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 hsetex myhash EX 100 PX 1000 fields 1 a b} + assert_error {*Only one of EX, PX, EXAT, PXAT or KEEPTTL arguments*} {r hsetex myhash EX 100 EXAT 100 fields 1 a b} + assert_error {*Only one of EX, PX, EXAT, PXAT or KEEPTTL arguments*} {r hsetex myhash EXAT 100 EX 1000 fields 1 a b} + assert_error {*Only one of EX, PX, EXAT, PXAT or KEEPTTL arguments*} {r hsetex myhash EXAT 100 PX 1000 fields 1 a b} + assert_error {*Only one of EX, PX, EXAT, PXAT or KEEPTTL arguments*} {r hsetex myhash PX 100 EXAT 100 fields 1 a b} + assert_error {*Only one of EX, PX, EXAT, PXAT or KEEPTTL arguments*} {r hsetex myhash PX 100 PXAT 100 fields 1 a b} + assert_error {*Only one of EX, PX, EXAT, PXAT or KEEPTTL arguments*} {r hsetex myhash PXAT 100 EX 100 fields 1 a b} + assert_error {*Only one of EX, PX, EXAT, PXAT or KEEPTTL arguments*} {r hsetex myhash PXAT 100 EXAT 100 fields 1 a b} + assert_error {*Only one of EX, PX, EXAT, PXAT or KEEPTTL arguments*} {r hsetex myhash EX 100 KEEPTTL fields 1 a b} + assert_error {*Only one of EX, PX, EXAT, PXAT or KEEPTTL arguments*} {r hsetex myhash KEEPTTL EX 100 fields 1 a b} + assert_error {*Only one of EX, PX, EXAT, PXAT or KEEPTTL arguments*} {r hsetex myhash EX 100 EX 100 fields 1 a b} + assert_error {*Only one of EX, PX, EXAT, PXAT or KEEPTTL arguments*} {r hsetex myhash EXAT 100 EXAT 100 fields 1 a b} + assert_error {*Only one of EX, PX, EXAT, PXAT or KEEPTTL arguments*} {r hsetex myhash PX 10 PX 10 fields 1 a b} + assert_error {*Only one of EX, PX, EXAT, PXAT or KEEPTTL arguments*} {r hsetex myhash PXAT 10 PXAT 10 fields 1 a b} + assert_error {*Only one of EX, PX, EXAT, PXAT or KEEPTTL arguments*} {r hsetex myhash KEEPTTL KEEPTTL fields 1 a b} + + # missing expire time + assert_error {*not an integer or out of range*} {r hsetex myhash ex fields 1 a b} + assert_error {*not an integer or out of range*} {r hsetex myhash px fields 1 a b} + assert_error {*not an integer or out of range*} {r hsetex myhash exat fields 1 a b} + assert_error {*not an integer or out of range*} {r hsetex myhash pxat fields 1 a b} + + # expire time more than 2 ^ 48 + assert_error {*invalid expire time*} {r hsetex myhash EXAT [expr (1<<48)] 1 a b} + assert_error {*invalid expire time*} {r hsetex myhash PXAT [expr (1<<48)] 1 a b} + assert_error {*invalid expire time*} {r hsetex myhash EX [expr (1<<48) - [clock seconds] + 1000 ] 1 a b} + assert_error {*invalid expire time*} {r hsetex myhash PX [expr (1<<48) - [clock milliseconds] + 1000 ] 1 a b} + + # invalid expire time + assert_error {*invalid expire time*} {r hsetex myhash EXAT -1 1 a b} + assert_error {*not an integer or out of range*} {r hsetex myhash EXAT 9223372036854775808 1 a b} + assert_error {*not an integer or out of range*} {r hsetex myhash EXAT x 1 a b} + + # invalid numfields arg + assert_error {*invalid number of fields*} {r hsetex myhash fields x a b} + assert_error {*invalid number of fields*} {r hsetex myhash fields 9223372036854775808 a b} + assert_error {*invalid number of fields*} {r hsetex myhash fields 0 a b} + assert_error {*invalid number of fields*} {r hsetex myhash fields -1 a b} + } + + test "HSETEX - Basic test ($type)" { + r del myhash + + # set field + assert_equal [r hsetex myhash FIELDS 1 f1 v1] 1 + assert_equal [r hget myhash f1] "v1" + + # override + assert_equal [r hsetex myhash FIELDS 1 f1 v11] 1 + assert_equal [r hget myhash f1] "v11" + + # set multiple + assert_equal [r hsetex myhash FIELDS 2 f1 v1 f2 v2] 1 + assert_equal [lsort [r hgetall myhash]] [lsort "f1 v1 f2 v2"] + assert_equal [r hsetex myhash FIELDS 3 f1 v111 f2 v222 f3 v333] 1 + assert_equal [lsort [r hgetall myhash]] [lsort "f1 v111 f2 v222 f3 v333"] + } + + test "HSETEX - Test FXX flag ($type)" { + r del myhash + + # Key is empty, command fails due to FXX + assert_equal [r hsetex myhash FXX FIELDS 2 f1 v1 f2 v2] 0 + # Verify it did not leave the key empty + assert_equal [r exists myhash] 0 + + # Command fails and no change on fields + r hset myhash f1 v1 + assert_equal [r hsetex myhash FXX FIELDS 2 f1 v1 f2 v2] 0 + assert_equal [lsort [r hgetall myhash]] [lsort "f1 v1"] + + # Command executed successfully + assert_equal [r hsetex myhash FXX FIELDS 1 f1 v11] 1 + assert_equal [lsort [r hgetall myhash]] [lsort "f1 v11"] + + # Try with multiple fields + r hset myhash f2 v2 + assert_equal [r hsetex myhash FXX FIELDS 2 f1 v111 f2 v222] 1 + assert_equal [lsort [r hgetall myhash]] [lsort "f1 v111 f2 v222"] + + # Try with expiry + assert_equal [r hsetex myhash FXX EX 100 FIELDS 2 f1 v1 f2 v2] 1 + assert_equal [lsort [r hgetall myhash]] [lsort "f1 v1 f2 v2"] + assert_range [r httl myhash FIELDS 1 f1] 80 100 + assert_range [r httl myhash FIELDS 1 f2] 80 100 + + # Try with expiry, FXX arg comes after TTL + assert_equal [r hsetex myhash PX 5000 FXX FIELDS 2 f1 v1 f2 v2] 1 + assert_equal [lsort [r hgetall myhash]] [lsort "f1 v1 f2 v2"] + assert_range [r hpttl myhash FIELDS 1 f1] 4500 5000 + assert_range [r hpttl myhash FIELDS 1 f2] 4500 5000 + } + + test "HSETEX - Test FXX flag with lazy expire ($type)" { + r del myhash + r debug set-active-expire 0 + + r hsetex myhash PX 10 FIELDS 1 f1 v1 + after 15 + assert_equal [r hsetex myhash FXX FIELDS 1 f1 v11] 0 + assert_equal [r exists myhash] 0 + r debug set-active-expire 1 + } + + test "HSETEX - Test FNX flag ($type)" { + r del myhash + + # Command successful on an empty key + assert_equal [r hsetex myhash FNX FIELDS 1 f1 v1] 1 + + # Command fails and no change on fields + assert_equal [r hsetex myhash FNX FIELDS 2 f1 v1 f2 v2] 0 + assert_equal [lsort [r hgetall myhash]] [lsort "f1 v1"] + + # Command executed successfully + assert_equal [r hsetex myhash FNX FIELDS 2 f2 v2 f3 v3] 1 + assert_equal [lsort [r hgetall myhash]] [lsort "f1 v1 f2 v2 f3 v3"] + assert_equal [r hsetex myhash FXX FIELDS 3 f1 v11 f2 v22 f3 v33] 1 + assert_equal [lsort [r hgetall myhash]] [lsort "f1 v11 f2 v22 f3 v33"] + + # Try with expiry + r del myhash + assert_equal [r hsetex myhash FNX EX 100 FIELDS 2 f1 v1 f2 v2] 1 + assert_equal [lsort [r hgetall myhash]] [lsort "f1 v1 f2 v2"] + assert_range [r httl myhash FIELDS 1 f1] 80 100 + assert_range [r httl myhash FIELDS 1 f2] 80 100 + + # Try with expiry, FNX arg comes after TTL + assert_equal [r hsetex myhash PX 5000 FNX FIELDS 1 f3 v3] 1 + assert_equal [lsort [r hgetall myhash]] [lsort "f1 v1 f2 v2 f3 v3"] + assert_range [r hpttl myhash FIELDS 1 f3] 4500 5000 + } + + test "HSETEX - Test 'EX' flag ($type)" { + r del myhash + r hset myhash f1 v1 f2 v2 + assert_equal [r hsetex myhash EX 1000 FIELDS 1 f3 v3 ] 1 + assert_range [r httl myhash FIELDS 1 f3] 900 1000 + } + + test "HSETEX - Test 'EXAT' flag ($type)" { + r del myhash + r hset myhash f1 v1 f2 v2 + assert_equal [r hsetex myhash EXAT 4000000000 FIELDS 1 f3 v3] 1 + assert_range [expr [r httl myhash FIELDS 1 f3] + [clock seconds]] 3900000000 4000000000 + } + + test "HSETEX - Test 'PX' flag ($type)" { + r del myhash + assert_equal [r hsetex myhash PX 1000000 FIELDS 1 f3 v3] 1 + assert_range [r httl myhash FIELDS 1 f3] 990 1000 + } + + test "HSETEX - Test 'PXAT' flag ($type)" { + r del myhash + r hset myhash f1 v2 f2 v2 f3 v3 + assert_equal [r hsetex myhash PXAT 4000000000000 FIELDS 1 f2 v2] 1 + assert_range [expr [r httl myhash FIELDS 1 f2] + [clock seconds]] 3900000000 4000000000 + } + + test "HSETEX - Test 'KEEPTTL' flag ($type)" { + r del myhash + + r hsetex myhash FIELDS 2 f1 v1 f2 v2 + r hsetex myhash PX 20000 FIELDS 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 preserves the TTL + assert_equal [r hsetex myhash KEEPTTL FIELDS 1 f2 v22] 1 + assert_equal [r hget myhash f2] "v22" + assert_not_equal [r httl myhash FIELDS 1 f2] "$T_NO_EXPIRY" + + # Try with multiple fields. First, set fields and TTL + r hsetex myhash EX 10000 FIELDS 3 f1 v1 f2 v2 f3 v3 + + # Update fields with KEEPTTL flag + r hsetex myhash KEEPTTL FIELDS 3 f1 v111 f2 v222 f3 v333 + + # Verify values are set, ttls are untouched + assert_equal [lsort [r hgetall myhash]] [lsort "f1 v111 f2 v222 f3 v333"] + assert_range [r httl myhash FIELDS 1 f1] 9000 10000 + assert_range [r httl myhash FIELDS 1 f2] 9000 10000 + assert_range [r httl myhash FIELDS 1 f3] 9000 10000 + } + + test "HSETEX - Test no expiry flag discards TTL ($type)" { + r del myhash + + r hsetex myhash FIELDS 1 f1 v1 + r hsetex myhash PX 100000 FIELDS 1 f2 v2 + assert_range [r hpttl myhash FIELDS 1 f2] 1 100000 + + assert_equal [r hsetex myhash FIELDS 2 f1 v1 f2 v2] 1 + assert_equal [r httl myhash FIELDS 2 f1 f2] "$T_NO_EXPIRY $T_NO_EXPIRY" + } + + test "HSETEX - Test with active expiry" { + r del myhash + r debug set-active-expire 0 + + r hsetex myhash PX 10 FIELDS 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 "HSETEX - Set time in the past ($type)" { + r del myhash + + # Try on an empty key + assert_equal [r hsetex myhash EXAT [expr {[clock seconds] - 1}] FIELDS 2 f1 v1 f2 v2] 1 + assert_equal [r hexists myhash field1] 0 + + # Try with existing fields + r hset myhash fields 2 f1 v1 f2 v2 + assert_equal [r hsetex myhash EXAT [expr {[clock seconds] - 1}] FIELDS 2 f1 v1 f2 v2] 1 + assert_equal [r hexists myhash field1] 0 + } } test "Statistics - Hashes with HFEs ($type)" { @@ -879,6 +1303,13 @@ start_server {tags {"external:skip needs:debug"}} { r hdel myhash3 f2 assert_match [get_stat_subexpiry r] 2 + # hash4: 2 fields, 1 with TTL. HGETDEL field with TTL. subexpiry decr -1 + r hset myhash4 f1 v1 f2 v2 + r hpexpire myhash4 100 FIELDS 1 f2 + assert_match [get_stat_subexpiry r] 3 + r hgetdel myhash4 FIELDS 1 f2 + assert_match [get_stat_subexpiry r] 2 + # Expired fields of hash1 and hash2. subexpiry decr -2 wait_for_condition 50 50 { [get_stat_subexpiry r] == 0 @@ -887,6 +1318,21 @@ start_server {tags {"external:skip needs:debug"}} { } } + test "HFE commands against wrong type" { + r set wrongtype somevalue + assert_error "WRONGTYPE Operation against a key*" {r hexpire wrongtype 10 fields 1 f1} + assert_error "WRONGTYPE Operation against a key*" {r hexpireat wrongtype 10 fields 1 f1} + assert_error "WRONGTYPE Operation against a key*" {r hpexpire wrongtype 10 fields 1 f1} + assert_error "WRONGTYPE Operation against a key*" {r hpexpireat wrongtype 10 fields 1 f1} + assert_error "WRONGTYPE Operation against a key*" {r hexpiretime wrongtype fields 1 f1} + assert_error "WRONGTYPE Operation against a key*" {r hpexpiretime wrongtype fields 1 f1} + assert_error "WRONGTYPE Operation against a key*" {r httl wrongtype fields 1 f1} + assert_error "WRONGTYPE Operation against a key*" {r hpttl wrongtype fields 1 f1} + assert_error "WRONGTYPE Operation against a key*" {r hpersist wrongtype fields 1 f1} + assert_error "WRONGTYPE Operation against a key*" {r hgetex wrongtype fields 1 f1} + assert_error "WRONGTYPE Operation against a key*" {r hsetex wrongtype fields 1 f1 v1} + } + r config set hash-max-listpack-entries 512 } @@ -1048,6 +1494,54 @@ start_server {tags {"external:skip needs:debug"}} { fail "Field f2 of hash h2 wasn't deleted" } + # HSETEX + r hsetex h3 FIELDS 1 f1 v1 + r hsetex h3 FXX FIELDS 1 f1 v11 + r hsetex h3 FNX FIELDS 1 f2 v22 + r hsetex h3 KEEPTTL FIELDS 1 f2 v22 + + # Next one will fail due to FNX arg and it won't be replicated + r hsetex h3 FNX FIELDS 2 f1 v1 f2 v2 + + # Commands with EX/PX/PXAT/EXAT will be replicated as PXAT + r hsetex h3 EX 10000 FIELDS 1 f1 v111 + r hsetex h3 PX 10000 FIELDS 1 f1 v111 + r hsetex h3 PXAT [expr [clock milliseconds]+100000] FIELDS 1 f1 v111 + r hsetex h3 EXAT [expr [clock seconds]+100000] FIELDS 1 f1 v111 + + # Following commands will set and then delete the fields because + # of TTL in the past. HDELs will be propagated. + r hsetex h3 PX 0 FIELDS 1 f1 v111 + r hsetex h3 PX 0 FIELDS 3 f1 v2 f2 v2 f3 v3 + + # HGETEX + r hsetex h4 FIELDS 3 f1 v1 f2 v2 f3 v3 + # No change on expiry, it won't be replicated. + r hgetex h4 FIELDS 1 f1 + + # Commands with EX/PX/PXAT/EXAT will be replicated as + # HPEXPIREAT command. + r hgetex h4 EX 10000 FIELDS 1 f1 + r hgetex h4 PX 10000 FIELDS 1 f1 + r hgetex h4 PXAT [expr [clock milliseconds]+100000] FIELDS 1 f1 + r hgetex h4 EXAT [expr [clock seconds]+100000] FIELDS 1 f1 + + # Following commands will delete the fields because of TTL in + # the past. HDELs will be propagated. + r hgetex h4 PX 0 FIELDS 1 f1 + # HDELs will be propagated for f2 and f3 as only those exist. + r hgetex h4 PX 0 FIELDS 3 f1 f2 f3 + + # HGETEX with PERSIST flag will be replicated as HPERSIST + r hsetex h4 EX 1000 FIELDS 1 f4 v4 + r hgetex h4 PERSIST FIELDS 1 f4 + + # Nothing will be replicated as f4 is persisted already. + r hgetex h4 PERSIST FIELDS 1 f4 + + # Replicated as hdel + r hgetdel h4 FIELDS 1 f4 + # Assert that each TTL-related command are persisted with absolute timestamps in AOF assert_aof_content $aof { {select *} @@ -1068,6 +1562,33 @@ start_server {tags {"external:skip needs:debug"}} { {hdel h1 f2} {hdel h2 f1} {hdel h2 f2} + {hsetex h3 FIELDS 1 f1 v1} + {hsetex h3 FXX FIELDS 1 f1 v11} + {hsetex h3 FNX FIELDS 1 f2 v22} + {hsetex h3 KEEPTTL FIELDS 1 f2 v22} + {hsetex h3 PXAT * 1 f1 v111} + {hsetex h3 PXAT * 1 f1 v111} + {hsetex h3 PXAT * 1 f1 v111} + {hsetex h3 PXAT * 1 f1 v111} + {hdel h3 f1} + {multi} + {hdel h3 f1} + {hdel h3 f2} + {hdel h3 f3} + {exec} + {hsetex h4 FIELDS 3 f1 v1 f2 v2 f3 v3} + {hpexpireat h4 * FIELDS 1 f1} + {hpexpireat h4 * FIELDS 1 f1} + {hpexpireat h4 * FIELDS 1 f1} + {hpexpireat h4 * FIELDS 1 f1} + {hdel h4 f1} + {multi} + {hdel h4 f2} + {hdel h4 f3} + {exec} + {hsetex h4 PXAT * FIELDS 1 f4 v4} + {hpersist h4 FIELDS 1 f4} + {hdel h4 f4} } } } {} {needs:debug} @@ -1135,6 +1656,16 @@ start_server {tags {"external:skip needs:debug"}} { r hpexpire h2 1 FIELDS 2 f1 f2 after 200 + r hsetex h3 EX 100000 FIELDS 2 f1 v1 f2 v2 + r hsetex h3 EXAT [expr [clock seconds] + 1000] FIELDS 2 f1 v1 f2 v2 + r hsetex h3 PX 100000 FIELDS 2 f1 v1 f2 v2 + r hsetex h3 PXAT [expr [clock milliseconds]+100000] FIELDS 2 f1 v1 f2 v2 + + r hgetex h3 EX 100000 FIELDS 2 f1 f2 + r hgetex h3 EXAT [expr [clock seconds] + 1000] FIELDS 2 f1 f2 + r hgetex h3 PX 100000 FIELDS 2 f1 f2 + r hgetex h3 PXAT [expr [clock milliseconds]+100000] FIELDS 2 f1 f2 + assert_aof_content $aof { {select *} {hset h1 f1 v1 f2 v2 f3 v3 f4 v4 f5 v5 f6 v6} @@ -1146,6 +1677,14 @@ start_server {tags {"external:skip needs:debug"}} { {hpexpireat h2 * FIELDS 2 f1 f2} {hdel h2 *} {hdel h2 *} + {hsetex h3 PXAT * FIELDS 2 f1 v1 f2 v2} + {hsetex h3 PXAT * FIELDS 2 f1 v1 f2 v2} + {hsetex h3 PXAT * FIELDS 2 f1 v1 f2 v2} + {hsetex h3 PXAT * FIELDS 2 f1 v1 f2 v2} + {hpexpireat h3 * FIELDS 2 f1 f2} + {hpexpireat h3 * FIELDS 2 f1 f2} + {hpexpireat h3 * FIELDS 2 f1 f2} + {hpexpireat h3 * FIELDS 2 f1 f2} } array set keyAndFields1 [dumpAllHashes r] @@ -1265,6 +1804,23 @@ start_server {tags {"external:skip needs:debug"}} { $primary hpexpireat h5 [expr [clock milliseconds]-100000] FIELDS 1 f $primary hset h9 f v + $primary hsetex h10 EX 100000 FIELDS 1 f v + $primary hsetex h11 EXAT [expr [clock seconds] + 1000] FIELDS 1 f v + $primary hsetex h12 PX 100000 FIELDS 1 f v + $primary hsetex h13 PXAT [expr [clock milliseconds]+100000] FIELDS 1 f v + $primary hsetex h14 PXAT 1 FIELDS 1 f v + + $primary hsetex h15 FIELDS 1 f v + $primary hgetex h15 EX 100000 FIELDS 1 f + $primary hsetex h16 FIELDS 1 f v + $primary hgetex h16 EXAT [expr [clock seconds] + 1000] FIELDS 1 f + $primary hsetex h17 FIELDS 1 f v + $primary hgetex h17 PX 100000 FIELDS 1 f + $primary hsetex h18 FIELDS 1 f v + $primary hgetex h18 PXAT [expr [clock milliseconds]+100000] FIELDS 1 f + $primary hsetex h19 FIELDS 1 f v + $primary hgetex h19 PXAT 1 FIELDS 1 f + # Wait for replica to get the keys and TTLs assert {[$primary wait 1 0] == 1} @@ -1273,5 +1829,102 @@ start_server {tags {"external:skip needs:debug"}} { assert_equal [dumpAllHashes $primary] [dumpAllHashes $replica] } } + + test "Test HSETEX command replication" { + r flushall + set repl [attach_to_replication_stream] + + # Create a field and delete it in a single command due to timestamp + # being in the past. It will be propagated as HDEL. + r hsetex h1 PXAT 1 FIELDS 1 f1 v1 + + # Following ones will be propagated with PXAT arg + r hsetex h1 EX 100000 FIELDS 1 f1 v1 + r hsetex h1 EXAT [expr [clock seconds] + 1000] FIELDS 1 f1 v1 + r hsetex h1 PX 100000 FIELDS 1 f1 v1 + r hsetex h1 PXAT [expr [clock milliseconds]+100000] FIELDS 1 f1 v1 + + # Propagate with KEEPTTL flag + r hsetex h1 KEEPTTL FIELDS 1 f1 v1 + + # Following commands will fail and won't be propagated + r hsetex h1 FNX FIELDS 1 f1 v11 + r hsetex h1 FXX FIELDS 1 f2 v2 + + # Propagate with FNX and FXX flags + r hsetex h1 FNX FIELDS 1 f2 v2 + r hsetex h1 FXX FIELDS 1 f2 v22 + + assert_replication_stream $repl { + {select *} + {hdel h1 f1} + {hsetex h1 PXAT * FIELDS 1 f1 v1} + {hsetex h1 PXAT * FIELDS 1 f1 v1} + {hsetex h1 PXAT * FIELDS 1 f1 v1} + {hsetex h1 PXAT * FIELDS 1 f1 v1} + {hsetex h1 KEEPTTL FIELDS 1 f1 v1} + {hsetex h1 FNX FIELDS 1 f2 v2} + {hsetex h1 FXX FIELDS 1 f2 v22} + } + close_replication_stream $repl + } {} {needs:repl} + + test "Test HGETEX command replication" { + r flushall + r debug set-active-expire 0 + set repl [attach_to_replication_stream] + + # If no fields are found, command won't be replicated + r hgetex h1 EX 10000 FIELDS 1 f0 + r hgetex h1 PERSIST FIELDS 1 f0 + + # Get without setting expiry will not be replicated + r hsetex h1 FIELDS 1 f0 v0 + r hgetex h1 FIELDS 1 f0 + + # Lazy expired field will be replicated as HDEL + r hsetex h1 PX 10 FIELDS 1 f1 v1 + after 15 + r hgetex h1 EX 1000 FIELDS 1 f1 + + # If new TTL is in the past, it will be replicated as HDEL + r hsetex h1 EX 10000 FIELDS 1 f2 v2 + r hgetex h1 EXAT 1 FIELDS 1 f2 + + # A field will expire lazily and other field will be deleted due to + # TTL is being in the past. It'll be propagated as two HDEL's. + r hsetex h1 PX 10 FIELDS 1 f3 v3 + after 15 + r hsetex h1 FIELDS 1 f4 v4 + r hgetex h1 EXAT 1 FIELDS 2 f3 f4 + + # TTL update, it will be replicated as HPEXPIREAT + r hsetex h1 FIELDS 1 f5 v5 + r hgetex h1 EX 10000 FIELDS 1 f5 + + # If PERSIST flag is used, it will be replicated as HPERSIST + r hsetex h1 EX 10000 FIELDS 1 f6 v6 + r hgetex h1 PERSIST FIELDS 1 f6 + + assert_replication_stream $repl { + {select *} + {hsetex h1 FIELDS 1 f0 v0} + {hsetex h1 PXAT * FIELDS 1 f1 v1} + {hdel h1 f1} + {hsetex h1 PXAT * FIELDS 1 f2 v2} + {hdel h1 f2} + {hsetex h1 PXAT * FIELDS 1 f3 v3} + {hsetex h1 FIELDS 1 f4 v4} + {multi} + {hdel h1 f3} + {hdel h1 f4} + {exec} + {hsetex h1 FIELDS 1 f5 v5} + {hpexpireat h1 * FIELDS 1 f5} + {hsetex h1 PXAT * FIELDS 1 f6 v6} + {hpersist h1 FIELDS 1 f6} + } + close_replication_stream $repl + } {} {needs:repl} } } diff --git a/tests/unit/type/hash.tcl b/tests/unit/type/hash.tcl index 1cb422455..a3d6867f8 100644 --- a/tests/unit/type/hash.tcl +++ b/tests/unit/type/hash.tcl @@ -371,6 +371,7 @@ start_server {tags {"hash"}} { assert_error "WRONGTYPE Operation against a key*" {r hsetnx wrongtype field1 val1} assert_error "WRONGTYPE Operation against a key*" {r hlen wrongtype} assert_error "WRONGTYPE Operation against a key*" {r hscan wrongtype 0} + assert_error "WRONGTYPE Operation against a key*" {r hgetdel wrongtype fields 1 a} } test {HMGET - small hash} { @@ -710,6 +711,89 @@ start_server {tags {"hash"}} { r config set hash-max-listpack-value $original_max_value } + test {HGETDEL input validation} { + r del key1 + assert_error "*wrong number of arguments*" {r hgetdel} + assert_error "*wrong number of arguments*" {r hgetdel key1} + assert_error "*wrong number of arguments*" {r hgetdel key1 FIELDS} + assert_error "*wrong number of arguments*" {r hgetdel key1 FIELDS 0} + assert_error "*wrong number of arguments*" {r hgetdel key1 FIELDX} + assert_error "*argument FIELDS is missing*" {r hgetdel key1 XFIELDX 1 a} + assert_error "*numfields*parameter*must match*number of arguments*" {r hgetdel key1 FIELDS 2 a} + assert_error "*numfields*parameter*must match*number of arguments*" {r hgetdel key1 FIELDS 2 a b c} + assert_error "*Number of fields must be a positive integer*" {r hgetdel key1 FIELDS 0 a} + assert_error "*Number of fields must be a positive integer*" {r hgetdel key1 FIELDS -1 a} + assert_error "*Number of fields must be a positive integer*" {r hgetdel key1 FIELDS b a} + assert_error "*Number of fields must be a positive integer*" {r hgetdel key1 FIELDS 9223372036854775808 a} + } + + foreach type {listpack ht} { + set orig_config [lindex [r config get hash-max-listpack-entries] 1] + r del key1 + + if {$type == "listpack"} { + r config set hash-max-listpack-entries $orig_config + r hset key1 f1 1 f2 2 f3 3 strfield strval + assert_encoding listpack key1 + } else { + r config set hash-max-listpack-entries 0 + r hset key1 f1 1 f2 2 f3 3 strfield strval + assert_encoding hashtable key1 + } + + test {HGETDEL basic test} { + r del key1 + r hset key1 f1 1 f2 2 f3 3 strfield strval + assert_equal [r hgetdel key1 fields 1 f2] 2 + assert_equal [r hlen key1] 3 + assert_equal [r hget key1 f1] 1 + assert_equal [r hget key1 f2] "" + assert_equal [r hget key1 f3] 3 + assert_equal [r hget key1 strfield] strval + + assert_equal [r hgetdel key1 fields 1 f1] 1 + assert_equal [lsort [r hgetall key1]] [lsort "f3 3 strfield strval"] + assert_equal [r hgetdel key1 fields 1 f3] 3 + assert_equal [r hgetdel key1 fields 1 strfield] strval + assert_equal [r hgetall key1] "" + assert_equal [r exists key1] 0 + } + + test {HGETDEL test with non existing fields} { + r del key1 + r hset key1 f1 1 f2 2 f3 3 + assert_equal [r hgetdel key1 fields 4 x1 x2 x3 x4] "{} {} {} {}" + assert_equal [r hgetdel key1 fields 4 x1 x2 f3 x4] "{} {} 3 {}" + assert_equal [lsort [r hgetall key1]] [lsort "f1 1 f2 2"] + assert_equal [r hgetdel key1 fields 3 f1 f2 f3] "1 2 {}" + assert_equal [r hgetdel key1 fields 3 f1 f2 f3] "{} {} {}" + } + + r config set hash-max-listpack-entries $orig_config + } + + test {HGETDEL propagated as HDEL command to replica} { + set repl [attach_to_replication_stream] + r hset key1 f1 v1 f2 v2 f3 v3 f4 v4 f5 v5 + r hgetdel key1 fields 1 f1 + r hgetdel key1 fields 2 f2 f3 + + # make sure non-existing fields are not replicated + r hgetdel key1 fields 2 f7 f8 + + # delete more + r hgetdel key1 fields 3 f4 f5 f6 + + assert_replication_stream $repl { + {select *} + {hset key1 f1 v1 f2 v2 f3 v3 f4 v4 f5 v5} + {hdel key1 f1} + {hdel key1 f2 f3} + {hdel key1 f4 f5 f6} + } + close_replication_stream $repl + } {} {needs:repl} + test {Hash ziplist regression test for large keys} { r hset hash kkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk a r hset hash kkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk b