mirror of https://mirror.osredm.com/root/redis.git
Add HGETDEL, HGETEX and HSETEX hash commands (#13798)
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 <key> FIELDS <numfields> 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 <key> [EX seconds | PX milliseconds | EXAT unix-time-seconds | PXAT unix-time-milliseconds | PERSIST] FIELDS <numfields> 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 <key> [FNX | FXX] [EX seconds | PX milliseconds | EXAT unix-time-seconds | PXAT unix-time-milliseconds | KEEPTTL] FIELDS <numfields> 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
This commit is contained in:
parent
57807cd338
commit
e2608478b6
129
src/commands.def
129
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)},
|
{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 ********************/
|
/********** HINCRBY ********************/
|
||||||
|
|
||||||
#ifndef SKIP_CMD_HISTORY_TABLE
|
#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},
|
{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 ********************/
|
/********** HSETNX ********************/
|
||||||
|
|
||||||
#ifndef SKIP_CMD_HISTORY_TABLE
|
#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("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("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("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("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("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},
|
{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("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("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("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("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("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},
|
{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},
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
|
@ -2034,6 +2034,7 @@ void createSharedObjects(void) {
|
||||||
shared.set = createStringObject("SET",3);
|
shared.set = createStringObject("SET",3);
|
||||||
shared.eval = createStringObject("EVAL",4);
|
shared.eval = createStringObject("EVAL",4);
|
||||||
shared.hpexpireat = createStringObject("HPEXPIREAT",10);
|
shared.hpexpireat = createStringObject("HPEXPIREAT",10);
|
||||||
|
shared.hpersist = createStringObject("HPERSIST",8);
|
||||||
shared.hdel = createStringObject("HDEL",4);
|
shared.hdel = createStringObject("HDEL",4);
|
||||||
|
|
||||||
/* Shared command argument */
|
/* Shared command argument */
|
||||||
|
|
|
@ -1434,7 +1434,7 @@ struct sharedObjectsStruct {
|
||||||
*rpop, *lpop, *lpush, *rpoplpush, *lmove, *blmove, *zpopmin, *zpopmax,
|
*rpop, *lpop, *lpush, *rpoplpush, *lmove, *blmove, *zpopmin, *zpopmax,
|
||||||
*emptyscan, *multi, *exec, *left, *right, *hset, *srem, *xgroup, *xclaim,
|
*emptyscan, *multi, *exec, *left, *right, *hset, *srem, *xgroup, *xclaim,
|
||||||
*script, *replconf, *eval, *persist, *set, *pexpireat, *pexpire,
|
*script, *replconf, *eval, *persist, *set, *pexpireat, *pexpire,
|
||||||
*hdel, *hpexpireat,
|
*hdel, *hpexpireat, *hpersist,
|
||||||
*time, *pxat, *absttl, *retrycount, *force, *justid, *entriesread,
|
*time, *pxat, *absttl, *retrycount, *force, *justid, *entriesread,
|
||||||
*lastid, *ping, *setid, *keepttl, *load, *createconsumer,
|
*lastid, *ping, *setid, *keepttl, *load, *createconsumer,
|
||||||
*getack, *special_asterick, *special_equals, *default_username, *redacted,
|
*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_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
|
#define HFE_LAZY_NO_NOTIFICATION (1<<2) /* Do not send notification, used when multiple fields
|
||||||
* may expire and only one notification is desired. */
|
* 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 hashTypeConvert(robj *o, int enc, ebuckets *hexpires);
|
||||||
void hashTypeTryConversion(redisDb *db, robj *subject, robj **argv, int start, int end);
|
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 zrankCommand(client *c);
|
||||||
void zrevrankCommand(client *c);
|
void zrevrankCommand(client *c);
|
||||||
void hsetCommand(client *c);
|
void hsetCommand(client *c);
|
||||||
|
void hsetexCommand(client *c);
|
||||||
void hpexpireCommand(client *c);
|
void hpexpireCommand(client *c);
|
||||||
void hexpireCommand(client *c);
|
void hexpireCommand(client *c);
|
||||||
void hpexpireatCommand(client *c);
|
void hpexpireatCommand(client *c);
|
||||||
|
@ -3893,6 +3896,8 @@ void hpersistCommand(client *c);
|
||||||
void hsetnxCommand(client *c);
|
void hsetnxCommand(client *c);
|
||||||
void hgetCommand(client *c);
|
void hgetCommand(client *c);
|
||||||
void hmgetCommand(client *c);
|
void hmgetCommand(client *c);
|
||||||
|
void hgetexCommand(client *c);
|
||||||
|
void hgetdelCommand(client *c);
|
||||||
void hdelCommand(client *c);
|
void hdelCommand(client *c);
|
||||||
void hlenCommand(client *c);
|
void hlenCommand(client *c);
|
||||||
void hstrlenCommand(client *c);
|
void hstrlenCommand(client *c);
|
||||||
|
|
770
src/t_hash.c
770
src/t_hash.c
|
@ -48,7 +48,7 @@ typedef listpackEntry CommonEntry; /* extend usage beyond lp */
|
||||||
static ExpireAction onFieldExpire(eItem item, void *ctx);
|
static ExpireAction onFieldExpire(eItem item, void *ctx);
|
||||||
static ExpireMeta* hfieldGetExpireMeta(const eItem field);
|
static ExpireMeta* hfieldGetExpireMeta(const eItem field);
|
||||||
static ExpireMeta *hashGetExpireMeta(const eItem hash);
|
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 ExpireAction hashTypeActiveExpire(eItem hashObj, void *ctx);
|
||||||
static uint64_t hashTypeExpire(robj *o, ExpireCtx *expireCtx, int updateGlobalHFE);
|
static uint64_t hashTypeExpire(robj *o, ExpireCtx *expireCtx, int updateGlobalHFE);
|
||||||
static void hfieldPersist(robj *hashObj, hfield field);
|
static void hfieldPersist(robj *hashObj, hfield field);
|
||||||
|
@ -214,15 +214,13 @@ typedef struct HashTypeSetEx {
|
||||||
* minimum expiration time. If minimum recorded
|
* minimum expiration time. If minimum recorded
|
||||||
* is above minExpire of the hash, then we don't
|
* is above minExpire of the hash, then we don't
|
||||||
* have to update global HFE DS */
|
* have to update global HFE DS */
|
||||||
int fieldDeleted; /* Number of fields deleted */
|
|
||||||
int fieldUpdated; /* Number of fields updated */
|
|
||||||
|
|
||||||
/* Optionally provide client for notification */
|
/* Optionally provide client for notification */
|
||||||
client *c;
|
client *c;
|
||||||
const char *cmd;
|
const char *cmd;
|
||||||
} HashTypeSetEx;
|
} 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);
|
ExpireSetCond expireSetCond, HashTypeSetEx *ex);
|
||||||
|
|
||||||
SetExRes hashTypeSetEx(robj *o, sds field, uint64_t expireAt, HashTypeSetEx *exInfo);
|
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;
|
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) {
|
if (prevExpire == EB_EXPIRE_TIME_INVALID) {
|
||||||
/* For fields without expiry, LT condition is considered valid */
|
/* For fields without expiry, LT condition is considered valid */
|
||||||
if (ex->expireSetCond & (HFE_XX | HFE_GT))
|
if (ex->expireSetCond & (HFE_XX | HFE_GT))
|
||||||
|
@ -551,13 +558,7 @@ SetExRes hashTypeSetExpiryListpack(HashTypeSetEx *ex, sds field,
|
||||||
if (unlikely(checkAlreadyExpired(expireAt))) {
|
if (unlikely(checkAlreadyExpired(expireAt))) {
|
||||||
propagateHashFieldDeletion(ex->db, ex->key->ptr, field, sdslen(field));
|
propagateHashFieldDeletion(ex->db, ex->key->ptr, field, sdslen(field));
|
||||||
hashTypeDelete(ex->hashObj, field, 1);
|
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++;
|
server.stat_expired_subkeys++;
|
||||||
ex->fieldDeleted++;
|
|
||||||
return HSETEX_DELETED;
|
return HSETEX_DELETED;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -565,7 +566,6 @@ SetExRes hashTypeSetExpiryListpack(HashTypeSetEx *ex, sds field,
|
||||||
ex->minExpireFields = expireAt;
|
ex->minExpireFields = expireAt;
|
||||||
|
|
||||||
listpackExUpdateExpiry(ex->hashObj, field, fptr, vptr, expireAt);
|
listpackExUpdateExpiry(ex->hashObj, field, fptr, vptr, expireAt);
|
||||||
ex->fieldUpdated++;
|
|
||||||
return HSETEX_OK;
|
return HSETEX_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -788,7 +788,8 @@ GetFieldRes hashTypeGetValue(redisDb *db, robj *o, sds field, unsigned char **vs
|
||||||
dbDelete(db,keyObj);
|
dbDelete(db,keyObj);
|
||||||
res = GETF_EXPIRED_HASH;
|
res = GETF_EXPIRED_HASH;
|
||||||
}
|
}
|
||||||
signalModifiedKey(NULL, db, keyObj);
|
if (!(hfeFlags & HFE_LAZY_NO_SIGNAL))
|
||||||
|
signalModifiedKey(NULL, db, keyObj);
|
||||||
decrRefCount(keyObj);
|
decrRefCount(keyObj);
|
||||||
return res;
|
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) {
|
SetExRes hashTypeSetExpiryHT(HashTypeSetEx *exInfo, sds field, uint64_t expireAt) {
|
||||||
dict *ht = exInfo->hashObj->ptr;
|
dict *ht = exInfo->hashObj->ptr;
|
||||||
dictEntry *existingEntry = NULL;
|
dictEntry *existingEntry = NULL;
|
||||||
|
hfield hfNew = NULL;
|
||||||
|
|
||||||
/* New field with expiration metadata */
|
if ((existingEntry = dictFind(ht, field)) == NULL)
|
||||||
hfield hfNew = hfieldNew(field, sdslen(field), 1 /*withExpireMeta*/);
|
|
||||||
|
|
||||||
if ((existingEntry = dictFind(ht, field)) == NULL) {
|
|
||||||
hfieldFree(hfNew);
|
|
||||||
return HSETEX_NO_FIELD;
|
return HSETEX_NO_FIELD;
|
||||||
}
|
|
||||||
|
|
||||||
hfield hfOld = dictGetKey(existingEntry);
|
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 field doesn't have expiry metadata attached */
|
||||||
if (!hfieldIsExpireAttached(hfOld)) {
|
if (!hfieldIsExpireAttached(hfOld)) {
|
||||||
|
|
||||||
/* For fields without expiry, LT condition is considered valid */
|
/* For fields without expiry, LT condition is considered valid */
|
||||||
if (exInfo->expireSetCond & (HFE_XX | HFE_GT)) {
|
if (exInfo->expireSetCond & (HFE_XX | HFE_GT))
|
||||||
hfieldFree(hfNew);
|
|
||||||
return HSETEX_NO_CONDITION_MET;
|
return HSETEX_NO_CONDITION_MET;
|
||||||
}
|
|
||||||
|
|
||||||
/* Delete old field. Below goanna dictSetKey(..,hfNew) */
|
/* Delete old field. Below goanna dictSetKey(..,hfNew) */
|
||||||
hfieldFree(hfOld);
|
hfieldFree(hfOld);
|
||||||
|
/* New field with expiration metadata */
|
||||||
|
hfNew = hfieldNew(field, sdslen(field), 1);
|
||||||
} else { /* field has ExpireMeta struct attached */
|
} else { /* field has ExpireMeta struct attached */
|
||||||
|
|
||||||
/* No need for hfNew (Just modify expire-time of existing field) */
|
|
||||||
hfieldFree(hfNew);
|
|
||||||
|
|
||||||
uint64_t prevExpire = hfieldGetExpireTime(hfOld);
|
uint64_t prevExpire = hfieldGetExpireTime(hfOld);
|
||||||
|
|
||||||
/* If field has valid expiration time, then check GT|LT|NX */
|
/* 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 expired, then delete the field and propagate the deletion.
|
||||||
* If replica, continue like the field is valid */
|
* If replica, continue like the field is valid */
|
||||||
if (unlikely(checkAlreadyExpired(expireAt))) {
|
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 */
|
/* replicas should not initiate deletion of fields */
|
||||||
propagateHashFieldDeletion(exInfo->db, exInfo->key->ptr, field, sdslen(field));
|
propagateHashFieldDeletion(exInfo->db, exInfo->key->ptr, field, sdslen(field));
|
||||||
hashTypeDelete(exInfo->hashObj, field, 1);
|
hashTypeDelete(exInfo->hashObj, field, 1);
|
||||||
server.stat_expired_subkeys++;
|
server.stat_expired_subkeys++;
|
||||||
exInfo->fieldDeleted++;
|
|
||||||
return HSETEX_DELETED;
|
return HSETEX_DELETED;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1088,7 +1085,6 @@ SetExRes hashTypeSetExpiryHT(HashTypeSetEx *exInfo, sds field, uint64_t expireAt
|
||||||
|
|
||||||
dictExpireMetadata *dm = (dictExpireMetadata *) dictMetadata(ht);
|
dictExpireMetadata *dm = (dictExpireMetadata *) dictMetadata(ht);
|
||||||
ebAdd(&dm->hfe, &hashFieldExpireBucketsType, hfNew, expireAt);
|
ebAdd(&dm->hfe, &hashFieldExpireBucketsType, hfNew, expireAt);
|
||||||
exInfo->fieldUpdated++;
|
|
||||||
return HSETEX_OK;
|
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.
|
* Take care to call first hashTypeSetExInit() and then call this function.
|
||||||
* Finally, call hashTypeSetExDone() to notify and update global HFE DS.
|
* 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)
|
SetExRes hashTypeSetEx(robj *o, sds field, uint64_t expireAt, HashTypeSetEx *exInfo) {
|
||||||
{
|
if (o->encoding == OBJ_ENCODING_LISTPACK_EX) {
|
||||||
if (o->encoding == OBJ_ENCODING_LISTPACK_EX)
|
|
||||||
{
|
|
||||||
unsigned char *fptr = NULL, *vptr = NULL, *tptr = NULL;
|
unsigned char *fptr = NULL, *vptr = NULL, *tptr = NULL;
|
||||||
|
|
||||||
listpackEx *lpt = o->ptr;
|
listpackEx *lpt = o->ptr;
|
||||||
long long expireTime = HASH_LP_NO_TTL;
|
|
||||||
|
|
||||||
if ((fptr = lpFirst(lpt->lp)) == NULL)
|
fptr = lpFirst(lpt->lp);
|
||||||
return HSETEX_NO_FIELD;
|
if (fptr)
|
||||||
|
fptr = lpFind(lpt->lp, fptr, (unsigned char*)field, sdslen(field), 2);
|
||||||
fptr = lpFind(lpt->lp, fptr, (unsigned char*)field, sdslen(field), 2);
|
|
||||||
|
|
||||||
if (!fptr)
|
if (!fptr)
|
||||||
return HSETEX_NO_FIELD;
|
return HSETEX_NO_FIELD;
|
||||||
|
@ -1120,7 +1114,7 @@ SetExRes hashTypeSetEx(robj *o, sds field, uint64_t expireAt, HashTypeSetEx *exI
|
||||||
serverAssert(vptr != NULL);
|
serverAssert(vptr != NULL);
|
||||||
|
|
||||||
tptr = lpNext(lpt->lp, vptr);
|
tptr = lpNext(lpt->lp, vptr);
|
||||||
serverAssert(tptr && lpGetIntegerValue(tptr, &expireTime));
|
serverAssert(tptr);
|
||||||
|
|
||||||
/* update TTL */
|
/* update TTL */
|
||||||
return hashTypeSetExpiryListpack(exInfo, field, fptr, vptr, tptr, expireAt);
|
return hashTypeSetExpiryListpack(exInfo, field, fptr, vptr, tptr, expireAt);
|
||||||
|
@ -1144,19 +1138,16 @@ void initDictExpireMetadata(sds key, robj *o) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Init HashTypeSetEx struct before calling hashTypeSetEx() */
|
/* 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)
|
ExpireSetCond expireSetCond, HashTypeSetEx *ex)
|
||||||
{
|
{
|
||||||
dict *ht = o->ptr;
|
dict *ht = o->ptr;
|
||||||
ex->expireSetCond = expireSetCond;
|
ex->expireSetCond = expireSetCond;
|
||||||
ex->minExpire = EB_EXPIRE_TIME_INVALID;
|
ex->minExpire = EB_EXPIRE_TIME_INVALID;
|
||||||
ex->c = c;
|
ex->c = c;
|
||||||
ex->cmd = cmd;
|
|
||||||
ex->db = db;
|
ex->db = db;
|
||||||
ex->key = key;
|
ex->key = key;
|
||||||
ex->hashObj = o;
|
ex->hashObj = o;
|
||||||
ex->fieldDeleted = 0;
|
|
||||||
ex->fieldUpdated = 0;
|
|
||||||
ex->minExpireFields = EB_EXPIRE_TIME_INVALID;
|
ex->minExpireFields = EB_EXPIRE_TIME_INVALID;
|
||||||
|
|
||||||
/* Take care that HASH support expiration */
|
/* 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
|
* 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) {
|
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 (hashTypeLength(ex->hashObj, 0) == 0)
|
||||||
if (ex->fieldDeleted && hashTypeLength(ex->hashObj, 0) == 0) {
|
return;
|
||||||
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 minimum HFE of the hash is smaller than expiration time of the
|
/* 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
|
* specified fields in the command as well as it is smaller or equal
|
||||||
* than expiration time provided in the command, then the minimum
|
* than expiration time provided in the command, then the minimum
|
||||||
* HFE of the hash won't change following this command. */
|
* HFE of the hash won't change following this command. */
|
||||||
if ((ex->minExpire < ex->minExpireFields))
|
if ((ex->minExpire < ex->minExpireFields))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
/* Retrieve new expired time. It might have changed. */
|
/* Retrieve new expired time. It might have changed. */
|
||||||
uint64_t newMinExpire = hashTypeGetMinExpire(ex->hashObj, 1 /*accurate*/);
|
uint64_t newMinExpire = hashTypeGetMinExpire(ex->hashObj, 1 /*accurate*/);
|
||||||
|
|
||||||
/* Calculate the diff between old minExpire and newMinExpire. If it is
|
/* 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
|
* 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.
|
* case fields of hash will be active-expired up to few seconds later.
|
||||||
*
|
*
|
||||||
* In any case, active-expire operation will know to update global
|
* In any case, active-expire operation will know to update global
|
||||||
* HFE DS more efficiently than here for a single item.
|
* HFE DS more efficiently than here for a single item.
|
||||||
*/
|
*/
|
||||||
uint64_t diff = (ex->minExpire > newMinExpire) ?
|
uint64_t diff = (ex->minExpire > newMinExpire) ?
|
||||||
(ex->minExpire - newMinExpire) : (newMinExpire - ex->minExpire);
|
(ex->minExpire - newMinExpire) : (newMinExpire - ex->minExpire);
|
||||||
if (diff < HASH_NEW_EXPIRE_DIFF_THRESHOLD) return;
|
if (diff < HASH_NEW_EXPIRE_DIFF_THRESHOLD) return;
|
||||||
|
|
||||||
if (ex->minExpire != EB_EXPIRE_TIME_INVALID)
|
if (ex->minExpire != EB_EXPIRE_TIME_INVALID)
|
||||||
ebRemove(&ex->db->hexpires, &hashExpireBucketsType, ex->hashObj);
|
ebRemove(&ex->db->hexpires, &hashExpireBucketsType, ex->hashObj);
|
||||||
if (newMinExpire != EB_EXPIRE_TIME_INVALID)
|
if (newMinExpire != EB_EXPIRE_TIME_INVALID)
|
||||||
ebAdd(&ex->db->hexpires, &hashExpireBucketsType, ex->hashObj, newMinExpire);
|
ebAdd(&ex->db->hexpires, &hashExpireBucketsType, ex->hashObj, newMinExpire);
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Delete an element from a hash.
|
/* Delete an element from a hash.
|
||||||
|
@ -2222,6 +2201,303 @@ void hsetCommand(client *c) {
|
||||||
server.dirty += (c->argc - 2)/2;
|
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 <key>
|
||||||
|
* [FNX|FXX]
|
||||||
|
* [EX seconds|PX milliseconds|EXAT unix-time-seconds|PXAT unix-time-milliseconds|KEEPTTL]
|
||||||
|
* FIELDS <numfields> 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 <numfields> 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 <key> 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) {
|
void hincrbyCommand(client *c) {
|
||||||
long long value, incr, oldvalue;
|
long long value, incr, oldvalue;
|
||||||
robj *o;
|
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 <key> FIELDS <numfields> 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 <key> FIELDS <numfields> field1 field2 ...
|
||||||
|
* Repl: HDEL <key> field1 field2 ... */
|
||||||
|
rewriteClientCommandArgument(c, 0, shared.hdel);
|
||||||
|
rewriteClientCommandArgument(c, 2, NULL); /* Delete FIELDS arg */
|
||||||
|
rewriteClientCommandArgument(c, 2, NULL); /* Delete <numfields> 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 <key>
|
||||||
|
* [EX seconds | PX milliseconds | EXAT unix-time-seconds | PXAT unix-time-milliseconds | PERSIST]
|
||||||
|
* FIELDS <numfields> 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 <key> PERSIST FIELDS <numfields> field1 field2 ...
|
||||||
|
* Repl: HPERSIST <key> FIELDS <numfields> 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 <key> [EX|PX|EXAT|PXAT] ttl FIELDS <numfields> field1 field2 ...
|
||||||
|
* Repl: HPEXPIREAT <key> ttl FIELDS <numfields> 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) {
|
void hdelCommand(client *c) {
|
||||||
robj *o;
|
robj *o;
|
||||||
int j, deleted = 0, keyremoved = 0;
|
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
|
* not met, then command will be rejected. Otherwise, EXPIRE command will be
|
||||||
* propagated for given key.
|
* 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 numFields = 0, numFieldsAt = 4;
|
||||||
long long expire; /* unix time in msec */
|
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];
|
robj *hashObj, *keyArg = c->argv[1], *expireArg = c->argv[2];
|
||||||
|
|
||||||
/* Read the hash object */
|
/* Read the hash object */
|
||||||
|
@ -3186,29 +3711,9 @@ static void hexpireGenericCommand(client *c, const char *cmd, long long basetime
|
||||||
return;
|
return;
|
||||||
|
|
||||||
/* Read the expiry time from command */
|
/* Read the expiry time from command */
|
||||||
if (getLongLongFromObjectOrReply(c, expireArg, &expire, NULL) != C_OK)
|
if (parseExpireTime(c, expireArg, unit, basetime, &expire) != C_OK)
|
||||||
return;
|
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] */
|
/* Read optional expireSetCond [NX|XX|GT|LT] */
|
||||||
char *optArg = c->argv[3]->ptr;
|
char *optArg = c->argv[3]->ptr;
|
||||||
if (!strcasecmp(optArg, "nx")) {
|
if (!strcasecmp(optArg, "nx")) {
|
||||||
|
@ -3247,14 +3752,18 @@ static void hexpireGenericCommand(client *c, const char *cmd, long long basetime
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
oldlen = hashTypeLength(hashObj, 0);
|
||||||
|
|
||||||
HashTypeSetEx exCtx;
|
HashTypeSetEx exCtx;
|
||||||
hashTypeSetExInit(keyArg, hashObj, c, c->db, cmd, expireSetCond, &exCtx);
|
hashTypeSetExInit(keyArg, hashObj, c, c->db, expireSetCond, &exCtx);
|
||||||
addReplyArrayLen(c, numFields);
|
addReplyArrayLen(c, numFields);
|
||||||
|
|
||||||
fieldAt = numFieldsAt + 1;
|
fieldAt = numFieldsAt + 1;
|
||||||
while (fieldAt < c->argc) {
|
while (fieldAt < c->argc) {
|
||||||
sds field = c->argv[fieldAt]->ptr;
|
sds field = c->argv[fieldAt]->ptr;
|
||||||
SetExRes res = hashTypeSetEx(hashObj, field, expire, &exCtx);
|
SetExRes res = hashTypeSetEx(hashObj, field, expire, &exCtx);
|
||||||
|
updated += (res == HSETEX_OK);
|
||||||
|
deleted += (res == HSETEX_DELETED);
|
||||||
|
|
||||||
if (unlikely(res != HSETEX_OK)) {
|
if (unlikely(res != HSETEX_OK)) {
|
||||||
/* If the field was not set, prevent field propagation */
|
/* 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);
|
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
|
/* 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
|
* 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 */
|
* not met) then it is useless and invalid to propagate command with no fields */
|
||||||
if (exCtx.fieldUpdated == 0) {
|
if (updated == 0) {
|
||||||
preventCommandPropagation(c);
|
preventCommandPropagation(c);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* If some fields were dropped, rewrite the number of fields */
|
/* If some fields were dropped, rewrite the number of fields */
|
||||||
if (fieldsNotSet) {
|
if (fieldsNotSet) {
|
||||||
robj *numFieldsObj = createStringObjectFromLongLong(exCtx.fieldUpdated);
|
robj *numFieldsObj = createStringObjectFromLongLong(updated);
|
||||||
rewriteClientCommandArgument(c, numFieldsAt, numFieldsObj);
|
rewriteClientCommandArgument(c, numFieldsAt, numFieldsObj);
|
||||||
decrRefCount(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 <field [field ...]> */
|
/* HPEXPIRE key milliseconds [ NX | XX | GT | LT] FIELDS numfields <field [field ...]> */
|
||||||
void hpexpireCommand(client *c) {
|
void hpexpireCommand(client *c) {
|
||||||
hexpireGenericCommand(c,"hpexpire", commandTimeSnapshot(),UNIT_MILLISECONDS);
|
hexpireGenericCommand(c,commandTimeSnapshot(),UNIT_MILLISECONDS);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* HEXPIRE key seconds [NX | XX | GT | LT] numfields <field [field ...]> */
|
/* HEXPIRE key seconds [NX | XX | GT | LT] FIELDS numfields <field [field ...]> */
|
||||||
void hexpireCommand(client *c) {
|
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 <field [field ...]> */
|
/* HEXPIREAT key unix-time-seconds [NX | XX | GT | LT] FIELDS numfields <field [field ...]> */
|
||||||
void hexpireatCommand(client *c) {
|
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 <field [field ...]> */
|
/* HPEXPIREAT key unix-time-milliseconds [NX | XX | GT | LT] FIELDS numfields <field [field ...]> */
|
||||||
void hpexpireatCommand(client *c) {
|
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*/
|
/* for each specified field: get the remaining time to live in seconds*/
|
||||||
/* HTTL key numfields <field [field ...]> */
|
/* HTTL key FIELDS numfields <field [field ...]> */
|
||||||
void httlCommand(client *c) {
|
void httlCommand(client *c) {
|
||||||
httlGenericCommand(c, "httl", commandTimeSnapshot(), UNIT_SECONDS);
|
httlGenericCommand(c, "httl", commandTimeSnapshot(), UNIT_SECONDS);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* HPTTL key numfields <field [field ...]> */
|
/* HPTTL key FIELDS numfields <field [field ...]> */
|
||||||
void hpttlCommand(client *c) {
|
void hpttlCommand(client *c) {
|
||||||
httlGenericCommand(c, "hpttl", commandTimeSnapshot(), UNIT_MILLISECONDS);
|
httlGenericCommand(c, "hpttl", commandTimeSnapshot(), UNIT_MILLISECONDS);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* HEXPIRETIME key numFields <field [field ...]> */
|
/* HEXPIRETIME key FIELDS numfields <field [field ...]> */
|
||||||
void hexpiretimeCommand(client *c) {
|
void hexpiretimeCommand(client *c) {
|
||||||
httlGenericCommand(c, "hexpiretime", 0, UNIT_SECONDS);
|
httlGenericCommand(c, "hexpiretime", 0, UNIT_SECONDS);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* HPEXPIRETIME key numFields <field [field ...]> */
|
/* HPEXPIRETIME key FIELDS numfields <field [field ...]> */
|
||||||
void hpexpiretimeCommand(client *c) {
|
void hpexpiretimeCommand(client *c) {
|
||||||
httlGenericCommand(c, "hexpiretime", 0, UNIT_MILLISECONDS);
|
httlGenericCommand(c, "hexpiretime", 0, UNIT_MILLISECONDS);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* HPERSIST key <FIELDS count field [field ...]> */
|
/* HPERSIST key FIELDS numfields <field [field ...]> */
|
||||||
void hpersistCommand(client *c) {
|
void hpersistCommand(client *c) {
|
||||||
robj *hashObj;
|
robj *hashObj;
|
||||||
long numFields = 0, numFieldsAt = 3;
|
long numFields = 0, numFieldsAt = 3;
|
||||||
|
|
|
@ -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 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 1} {db0_HASH:1=1}
|
||||||
run_cmd_verify_hist {$server HDEL h2 2} {}
|
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
|
# HMSET
|
||||||
run_cmd_verify_hist {$server FLUSHALL} {}
|
run_cmd_verify_hist {$server FLUSHALL} {}
|
||||||
run_cmd_verify_hist {$server HMSET h1 1 1 2 2 3 3} {db0_HASH:2=1}
|
run_cmd_verify_hist {$server HMSET h1 1 1 2 2 3 3} {db0_HASH:2=1}
|
||||||
|
|
|
@ -414,6 +414,58 @@ start_server {tags {"pubsub network"}} {
|
||||||
assert_equal "pmessage * __keyspace@${db}__:myhash del" [$rd1 read]
|
assert_equal "pmessage * __keyspace@${db}__:myhash del" [$rd1 read]
|
||||||
r debug set-active-expire 1
|
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
|
$rd1 close
|
||||||
} {0} {needs:debug}
|
} {0} {needs:debug}
|
||||||
} ;# foreach
|
} ;# foreach
|
||||||
|
|
|
@ -855,6 +855,430 @@ start_server {tags {"external:skip needs:debug"}} {
|
||||||
assert_equal [r HINCRBYFLOAT h1 f1 2.5] 12.5
|
assert_equal [r HINCRBYFLOAT h1 f1 2.5] 12.5
|
||||||
assert_range [r HPTTL h1 FIELDS 1 f1] 1 20
|
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)" {
|
test "Statistics - Hashes with HFEs ($type)" {
|
||||||
|
@ -879,6 +1303,13 @@ start_server {tags {"external:skip needs:debug"}} {
|
||||||
r hdel myhash3 f2
|
r hdel myhash3 f2
|
||||||
assert_match [get_stat_subexpiry r] 2
|
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
|
# Expired fields of hash1 and hash2. subexpiry decr -2
|
||||||
wait_for_condition 50 50 {
|
wait_for_condition 50 50 {
|
||||||
[get_stat_subexpiry r] == 0
|
[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
|
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"
|
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 that each TTL-related command are persisted with absolute timestamps in AOF
|
||||||
assert_aof_content $aof {
|
assert_aof_content $aof {
|
||||||
{select *}
|
{select *}
|
||||||
|
@ -1068,6 +1562,33 @@ start_server {tags {"external:skip needs:debug"}} {
|
||||||
{hdel h1 f2}
|
{hdel h1 f2}
|
||||||
{hdel h2 f1}
|
{hdel h2 f1}
|
||||||
{hdel h2 f2}
|
{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}
|
} {} {needs:debug}
|
||||||
|
@ -1135,6 +1656,16 @@ start_server {tags {"external:skip needs:debug"}} {
|
||||||
r hpexpire h2 1 FIELDS 2 f1 f2
|
r hpexpire h2 1 FIELDS 2 f1 f2
|
||||||
after 200
|
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 {
|
assert_aof_content $aof {
|
||||||
{select *}
|
{select *}
|
||||||
{hset h1 f1 v1 f2 v2 f3 v3 f4 v4 f5 v5 f6 v6}
|
{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}
|
{hpexpireat h2 * FIELDS 2 f1 f2}
|
||||||
{hdel h2 *}
|
{hdel h2 *}
|
||||||
{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]
|
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 hpexpireat h5 [expr [clock milliseconds]-100000] FIELDS 1 f
|
||||||
$primary hset h9 f v
|
$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
|
# Wait for replica to get the keys and TTLs
|
||||||
assert {[$primary wait 1 0] == 1}
|
assert {[$primary wait 1 0] == 1}
|
||||||
|
|
||||||
|
@ -1273,5 +1829,102 @@ start_server {tags {"external:skip needs:debug"}} {
|
||||||
assert_equal [dumpAllHashes $primary] [dumpAllHashes $replica]
|
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}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 hsetnx wrongtype field1 val1}
|
||||||
assert_error "WRONGTYPE Operation against a key*" {r hlen wrongtype}
|
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 hscan wrongtype 0}
|
||||||
|
assert_error "WRONGTYPE Operation against a key*" {r hgetdel wrongtype fields 1 a}
|
||||||
}
|
}
|
||||||
|
|
||||||
test {HMGET - small hash} {
|
test {HMGET - small hash} {
|
||||||
|
@ -710,6 +711,89 @@ start_server {tags {"hash"}} {
|
||||||
r config set hash-max-listpack-value $original_max_value
|
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} {
|
test {Hash ziplist regression test for large keys} {
|
||||||
r hset hash kkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk a
|
r hset hash kkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk a
|
||||||
r hset hash kkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk b
|
r hset hash kkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk b
|
||||||
|
|
Loading…
Reference in New Issue