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:
Ozan Tezcan 2025-02-14 17:13:35 +03:00 committed by GitHub
parent 57807cd338
commit e2608478b6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 1920 additions and 124 deletions

View File

@ -3472,6 +3472,78 @@ struct COMMAND_ARG HGETALL_Args[] = {
{MAKE_ARG("key",ARG_TYPE_KEY,0,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)},
};
/********** HGETDEL ********************/
#ifndef SKIP_CMD_HISTORY_TABLE
/* HGETDEL history */
#define HGETDEL_History NULL
#endif
#ifndef SKIP_CMD_TIPS_TABLE
/* HGETDEL tips */
#define HGETDEL_Tips NULL
#endif
#ifndef SKIP_CMD_KEY_SPECS_TABLE
/* HGETDEL key specs */
keySpec HGETDEL_Keyspecs[1] = {
{NULL,CMD_KEY_RW|CMD_KEY_ACCESS|CMD_KEY_DELETE,KSPEC_BS_INDEX,.bs.index={1},KSPEC_FK_RANGE,.fk.range={0,1,0}}
};
#endif
/* HGETDEL fields argument table */
struct COMMAND_ARG HGETDEL_fields_Subargs[] = {
{MAKE_ARG("numfields",ARG_TYPE_INTEGER,-1,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)},
{MAKE_ARG("field",ARG_TYPE_STRING,-1,NULL,NULL,NULL,CMD_ARG_MULTIPLE,0,NULL)},
};
/* HGETDEL argument table */
struct COMMAND_ARG HGETDEL_Args[] = {
{MAKE_ARG("key",ARG_TYPE_KEY,0,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)},
{MAKE_ARG("fields",ARG_TYPE_BLOCK,-1,"FIELDS",NULL,NULL,CMD_ARG_NONE,2,NULL),.subargs=HGETDEL_fields_Subargs},
};
/********** HGETEX ********************/
#ifndef SKIP_CMD_HISTORY_TABLE
/* HGETEX history */
#define HGETEX_History NULL
#endif
#ifndef SKIP_CMD_TIPS_TABLE
/* HGETEX tips */
#define HGETEX_Tips NULL
#endif
#ifndef SKIP_CMD_KEY_SPECS_TABLE
/* HGETEX key specs */
keySpec HGETEX_Keyspecs[1] = {
{"RW and UPDATE because it changes the TTL",CMD_KEY_RW|CMD_KEY_ACCESS|CMD_KEY_UPDATE,KSPEC_BS_INDEX,.bs.index={1},KSPEC_FK_RANGE,.fk.range={0,1,0}}
};
#endif
/* HGETEX expiration argument table */
struct COMMAND_ARG HGETEX_expiration_Subargs[] = {
{MAKE_ARG("seconds",ARG_TYPE_INTEGER,-1,"EX",NULL,NULL,CMD_ARG_NONE,0,NULL)},
{MAKE_ARG("milliseconds",ARG_TYPE_INTEGER,-1,"PX",NULL,NULL,CMD_ARG_NONE,0,NULL)},
{MAKE_ARG("unix-time-seconds",ARG_TYPE_UNIX_TIME,-1,"EXAT",NULL,NULL,CMD_ARG_NONE,0,NULL)},
{MAKE_ARG("unix-time-milliseconds",ARG_TYPE_UNIX_TIME,-1,"PXAT",NULL,NULL,CMD_ARG_NONE,0,NULL)},
{MAKE_ARG("persist",ARG_TYPE_PURE_TOKEN,-1,"PERSIST",NULL,NULL,CMD_ARG_NONE,0,NULL)},
};
/* HGETEX fields argument table */
struct COMMAND_ARG HGETEX_fields_Subargs[] = {
{MAKE_ARG("numfields",ARG_TYPE_INTEGER,-1,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)},
{MAKE_ARG("field",ARG_TYPE_STRING,-1,NULL,NULL,NULL,CMD_ARG_MULTIPLE,0,NULL)},
};
/* HGETEX argument table */
struct COMMAND_ARG HGETEX_Args[] = {
{MAKE_ARG("key",ARG_TYPE_KEY,0,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)},
{MAKE_ARG("expiration",ARG_TYPE_ONEOF,-1,NULL,NULL,NULL,CMD_ARG_OPTIONAL,5,NULL),.subargs=HGETEX_expiration_Subargs},
{MAKE_ARG("fields",ARG_TYPE_BLOCK,-1,"FIELDS",NULL,NULL,CMD_ARG_NONE,2,NULL),.subargs=HGETEX_fields_Subargs},
};
/********** HINCRBY ********************/
#ifndef SKIP_CMD_HISTORY_TABLE
@ -3903,6 +3975,60 @@ struct COMMAND_ARG HSET_Args[] = {
{MAKE_ARG("data",ARG_TYPE_BLOCK,-1,NULL,NULL,NULL,CMD_ARG_MULTIPLE,2,NULL),.subargs=HSET_data_Subargs},
};
/********** HSETEX ********************/
#ifndef SKIP_CMD_HISTORY_TABLE
/* HSETEX history */
#define HSETEX_History NULL
#endif
#ifndef SKIP_CMD_TIPS_TABLE
/* HSETEX tips */
#define HSETEX_Tips NULL
#endif
#ifndef SKIP_CMD_KEY_SPECS_TABLE
/* HSETEX key specs */
keySpec HSETEX_Keyspecs[1] = {
{NULL,CMD_KEY_RW|CMD_KEY_UPDATE,KSPEC_BS_INDEX,.bs.index={1},KSPEC_FK_RANGE,.fk.range={0,1,0}}
};
#endif
/* HSETEX condition argument table */
struct COMMAND_ARG HSETEX_condition_Subargs[] = {
{MAKE_ARG("fnx",ARG_TYPE_PURE_TOKEN,-1,"FNX",NULL,NULL,CMD_ARG_NONE,0,NULL)},
{MAKE_ARG("fxx",ARG_TYPE_PURE_TOKEN,-1,"FXX",NULL,NULL,CMD_ARG_NONE,0,NULL)},
};
/* HSETEX expiration argument table */
struct COMMAND_ARG HSETEX_expiration_Subargs[] = {
{MAKE_ARG("seconds",ARG_TYPE_INTEGER,-1,"EX",NULL,NULL,CMD_ARG_NONE,0,NULL)},
{MAKE_ARG("milliseconds",ARG_TYPE_INTEGER,-1,"PX",NULL,NULL,CMD_ARG_NONE,0,NULL)},
{MAKE_ARG("unix-time-seconds",ARG_TYPE_UNIX_TIME,-1,"EXAT",NULL,NULL,CMD_ARG_NONE,0,NULL)},
{MAKE_ARG("unix-time-milliseconds",ARG_TYPE_UNIX_TIME,-1,"PXAT",NULL,NULL,CMD_ARG_NONE,0,NULL)},
{MAKE_ARG("keepttl",ARG_TYPE_PURE_TOKEN,-1,"KEEPTTL",NULL,NULL,CMD_ARG_NONE,0,NULL)},
};
/* HSETEX fields data argument table */
struct COMMAND_ARG HSETEX_fields_data_Subargs[] = {
{MAKE_ARG("field",ARG_TYPE_STRING,-1,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)},
{MAKE_ARG("value",ARG_TYPE_STRING,-1,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)},
};
/* HSETEX fields argument table */
struct COMMAND_ARG HSETEX_fields_Subargs[] = {
{MAKE_ARG("numfields",ARG_TYPE_INTEGER,-1,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)},
{MAKE_ARG("data",ARG_TYPE_BLOCK,-1,NULL,NULL,NULL,CMD_ARG_MULTIPLE,2,NULL),.subargs=HSETEX_fields_data_Subargs},
};
/* HSETEX argument table */
struct COMMAND_ARG HSETEX_Args[] = {
{MAKE_ARG("key",ARG_TYPE_KEY,0,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)},
{MAKE_ARG("condition",ARG_TYPE_ONEOF,-1,NULL,NULL,NULL,CMD_ARG_OPTIONAL,2,NULL),.subargs=HSETEX_condition_Subargs},
{MAKE_ARG("expiration",ARG_TYPE_ONEOF,-1,NULL,NULL,NULL,CMD_ARG_OPTIONAL,5,NULL),.subargs=HSETEX_expiration_Subargs},
{MAKE_ARG("fields",ARG_TYPE_BLOCK,-1,"FIELDS",NULL,NULL,CMD_ARG_NONE,2,NULL),.subargs=HSETEX_fields_Subargs},
};
/********** HSETNX ********************/
#ifndef SKIP_CMD_HISTORY_TABLE
@ -11043,6 +11169,8 @@ struct COMMAND_STRUCT redisCommandTable[] = {
{MAKE_CMD("hexpiretime","Returns the expiration time of a hash field as a Unix timestamp, in seconds.","O(N) where N is the number of specified fields","7.4.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HEXPIRETIME_History,0,HEXPIRETIME_Tips,0,hexpiretimeCommand,-5,CMD_READONLY|CMD_FAST,ACL_CATEGORY_HASH,HEXPIRETIME_Keyspecs,1,NULL,2),.args=HEXPIRETIME_Args},
{MAKE_CMD("hget","Returns the value of a field in a hash.","O(1)","2.0.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HGET_History,0,HGET_Tips,0,hgetCommand,3,CMD_READONLY|CMD_FAST,ACL_CATEGORY_HASH,HGET_Keyspecs,1,NULL,2),.args=HGET_Args},
{MAKE_CMD("hgetall","Returns all fields and values in a hash.","O(N) where N is the size of the hash.","2.0.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HGETALL_History,0,HGETALL_Tips,1,hgetallCommand,2,CMD_READONLY,ACL_CATEGORY_HASH,HGETALL_Keyspecs,1,NULL,1),.args=HGETALL_Args},
{MAKE_CMD("hgetdel","Returns the value of a field and deletes it from the hash.","O(N) where N is the number of specified fields","8.0.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HGETDEL_History,0,HGETDEL_Tips,0,hgetdelCommand,-5,CMD_WRITE|CMD_FAST,ACL_CATEGORY_HASH,HGETDEL_Keyspecs,1,NULL,2),.args=HGETDEL_Args},
{MAKE_CMD("hgetex","Get the value of one or more fields of a given hash key, and optionally set their expiration.","O(N) where N is the number of specified fields","8.0.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HGETEX_History,0,HGETEX_Tips,0,hgetexCommand,-5,CMD_WRITE|CMD_FAST,ACL_CATEGORY_HASH,HGETEX_Keyspecs,1,NULL,3),.args=HGETEX_Args},
{MAKE_CMD("hincrby","Increments the integer value of a field in a hash by a number. Uses 0 as initial value if the field doesn't exist.","O(1)","2.0.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HINCRBY_History,0,HINCRBY_Tips,0,hincrbyCommand,4,CMD_WRITE|CMD_DENYOOM|CMD_FAST,ACL_CATEGORY_HASH,HINCRBY_Keyspecs,1,NULL,3),.args=HINCRBY_Args},
{MAKE_CMD("hincrbyfloat","Increments the floating point value of a field by a number. Uses 0 as initial value if the field doesn't exist.","O(1)","2.6.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HINCRBYFLOAT_History,0,HINCRBYFLOAT_Tips,0,hincrbyfloatCommand,4,CMD_WRITE|CMD_DENYOOM|CMD_FAST,ACL_CATEGORY_HASH,HINCRBYFLOAT_Keyspecs,1,NULL,3),.args=HINCRBYFLOAT_Args},
{MAKE_CMD("hkeys","Returns all fields in a hash.","O(N) where N is the size of the hash.","2.0.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HKEYS_History,0,HKEYS_Tips,1,hkeysCommand,2,CMD_READONLY,ACL_CATEGORY_HASH,HKEYS_Keyspecs,1,NULL,1),.args=HKEYS_Args},
@ -11057,6 +11185,7 @@ struct COMMAND_STRUCT redisCommandTable[] = {
{MAKE_CMD("hrandfield","Returns one or more random fields from a hash.","O(N) where N is the number of fields returned","6.2.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HRANDFIELD_History,0,HRANDFIELD_Tips,1,hrandfieldCommand,-2,CMD_READONLY,ACL_CATEGORY_HASH,HRANDFIELD_Keyspecs,1,NULL,2),.args=HRANDFIELD_Args},
{MAKE_CMD("hscan","Iterates over fields and values of a hash.","O(1) for every call. O(N) for a complete iteration, including enough command calls for the cursor to return back to 0. N is the number of elements inside the collection.","2.8.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HSCAN_History,0,HSCAN_Tips,1,hscanCommand,-3,CMD_READONLY,ACL_CATEGORY_HASH,HSCAN_Keyspecs,1,NULL,5),.args=HSCAN_Args},
{MAKE_CMD("hset","Creates or modifies the value of a field in a hash.","O(1) for each field/value pair added, so O(N) to add N field/value pairs when the command is called with multiple field/value pairs.","2.0.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HSET_History,1,HSET_Tips,0,hsetCommand,-4,CMD_WRITE|CMD_DENYOOM|CMD_FAST,ACL_CATEGORY_HASH,HSET_Keyspecs,1,NULL,2),.args=HSET_Args},
{MAKE_CMD("hsetex","Set the value of one or more fields of a given hash key, and optionally set their expiration.","O(N) where N is the number of fields being set.","8.0.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HSETEX_History,0,HSETEX_Tips,0,hsetexCommand,-6,CMD_WRITE|CMD_DENYOOM|CMD_FAST,ACL_CATEGORY_HASH,HSETEX_Keyspecs,1,NULL,4),.args=HSETEX_Args},
{MAKE_CMD("hsetnx","Sets the value of a field in a hash only when the field doesn't exist.","O(1)","2.0.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HSETNX_History,0,HSETNX_Tips,0,hsetnxCommand,4,CMD_WRITE|CMD_DENYOOM|CMD_FAST,ACL_CATEGORY_HASH,HSETNX_Keyspecs,1,NULL,3),.args=HSETNX_Args},
{MAKE_CMD("hstrlen","Returns the length of the value of a field.","O(1)","3.2.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HSTRLEN_History,0,HSTRLEN_Tips,0,hstrlenCommand,3,CMD_READONLY|CMD_FAST,ACL_CATEGORY_HASH,HSTRLEN_Keyspecs,1,NULL,2),.args=HSTRLEN_Args},
{MAKE_CMD("httl","Returns the TTL in seconds of a hash field.","O(N) where N is the number of specified fields","7.4.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HTTL_History,0,HTTL_Tips,1,httlCommand,-5,CMD_READONLY|CMD_FAST,ACL_CATEGORY_HASH,HTTL_Keyspecs,1,NULL,2),.args=HTTL_Args},

78
src/commands/hgetdel.json Normal file
View File

@ -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
}
]
}
]
}
}

111
src/commands/hgetex.json Normal file
View File

@ -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
}
]
}
]
}
}

132
src/commands/hsetex.json Normal file
View File

@ -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"
}
]
}
]
}
]
}
}

View File

@ -2034,6 +2034,7 @@ void createSharedObjects(void) {
shared.set = createStringObject("SET",3);
shared.eval = createStringObject("EVAL",4);
shared.hpexpireat = createStringObject("HPEXPIREAT",10);
shared.hpersist = createStringObject("HPERSIST",8);
shared.hdel = createStringObject("HDEL",4);
/* Shared command argument */

View File

@ -1434,7 +1434,7 @@ struct sharedObjectsStruct {
*rpop, *lpop, *lpush, *rpoplpush, *lmove, *blmove, *zpopmin, *zpopmax,
*emptyscan, *multi, *exec, *left, *right, *hset, *srem, *xgroup, *xclaim,
*script, *replconf, *eval, *persist, *set, *pexpireat, *pexpire,
*hdel, *hpexpireat,
*hdel, *hpexpireat, *hpersist,
*time, *pxat, *absttl, *retrycount, *force, *justid, *entriesread,
*lastid, *ping, *setid, *keepttl, *load, *createconsumer,
*getack, *special_asterick, *special_equals, *default_username, *redacted,
@ -3361,7 +3361,9 @@ typedef struct dictExpireMetadata {
#define HFE_LAZY_AVOID_HASH_DEL (1<<1) /* Avoid deleting hash if the field is the last one */
#define HFE_LAZY_NO_NOTIFICATION (1<<2) /* Do not send notification, used when multiple fields
* may expire and only one notification is desired. */
#define HFE_LAZY_ACCESS_EXPIRED (1<<3) /* Avoid lazy expire and allow access to expired fields */
#define HFE_LAZY_NO_SIGNAL (1<<3) /* Do not send signal, used when multiple fields
* may expire and only one signal is desired. */
#define HFE_LAZY_ACCESS_EXPIRED (1<<4) /* Avoid lazy expire and allow access to expired fields */
void hashTypeConvert(robj *o, int enc, ebuckets *hexpires);
void hashTypeTryConversion(redisDb *db, robj *subject, robj **argv, int start, int end);
@ -3881,6 +3883,7 @@ void strlenCommand(client *c);
void zrankCommand(client *c);
void zrevrankCommand(client *c);
void hsetCommand(client *c);
void hsetexCommand(client *c);
void hpexpireCommand(client *c);
void hexpireCommand(client *c);
void hpexpireatCommand(client *c);
@ -3893,6 +3896,8 @@ void hpersistCommand(client *c);
void hsetnxCommand(client *c);
void hgetCommand(client *c);
void hmgetCommand(client *c);
void hgetexCommand(client *c);
void hgetdelCommand(client *c);
void hdelCommand(client *c);
void hlenCommand(client *c);
void hstrlenCommand(client *c);

View File

@ -48,7 +48,7 @@ typedef listpackEntry CommonEntry; /* extend usage beyond lp */
static ExpireAction onFieldExpire(eItem item, void *ctx);
static ExpireMeta* hfieldGetExpireMeta(const eItem field);
static ExpireMeta *hashGetExpireMeta(const eItem hash);
static void hexpireGenericCommand(client *c, const char *cmd, long long basetime, int unit);
static void hexpireGenericCommand(client *c, long long basetime, int unit);
static ExpireAction hashTypeActiveExpire(eItem hashObj, void *ctx);
static uint64_t hashTypeExpire(robj *o, ExpireCtx *expireCtx, int updateGlobalHFE);
static void hfieldPersist(robj *hashObj, hfield field);
@ -214,15 +214,13 @@ typedef struct HashTypeSetEx {
* minimum expiration time. If minimum recorded
* is above minExpire of the hash, then we don't
* have to update global HFE DS */
int fieldDeleted; /* Number of fields deleted */
int fieldUpdated; /* Number of fields updated */
/* Optionally provide client for notification */
client *c;
const char *cmd;
} HashTypeSetEx;
int hashTypeSetExInit(robj *key, robj *o, client *c, redisDb *db, const char *cmd,
int hashTypeSetExInit(robj *key, robj *o, client *c, redisDb *db,
ExpireSetCond expireSetCond, HashTypeSetEx *ex);
SetExRes hashTypeSetEx(robj *o, sds field, uint64_t expireAt, HashTypeSetEx *exInfo);
@ -531,6 +529,15 @@ SetExRes hashTypeSetExpiryListpack(HashTypeSetEx *ex, sds field,
prevExpire = (uint64_t) expireTime;
}
/* Special value of EXPIRE_TIME_INVALID indicates field should be persisted.*/
if (expireAt == EB_EXPIRE_TIME_INVALID) {
/* Return error if already there is no ttl. */
if (prevExpire == EB_EXPIRE_TIME_INVALID)
return HSETEX_NO_CONDITION_MET;
listpackExUpdateExpiry(ex->hashObj, field, fptr, vptr, HASH_LP_NO_TTL);
return HSETEX_OK;
}
if (prevExpire == EB_EXPIRE_TIME_INVALID) {
/* For fields without expiry, LT condition is considered valid */
if (ex->expireSetCond & (HFE_XX | HFE_GT))
@ -551,13 +558,7 @@ SetExRes hashTypeSetExpiryListpack(HashTypeSetEx *ex, sds field,
if (unlikely(checkAlreadyExpired(expireAt))) {
propagateHashFieldDeletion(ex->db, ex->key->ptr, field, sdslen(field));
hashTypeDelete(ex->hashObj, field, 1);
/* get listpack length */
listpackEx *lpt = ((listpackEx *) ex->hashObj->ptr);
unsigned long length = lpLength(lpt->lp) / 3;
updateKeysizesHist(ex->db, getKeySlot(ex->key->ptr), OBJ_HASH, length+1, length);
server.stat_expired_subkeys++;
ex->fieldDeleted++;
return HSETEX_DELETED;
}
@ -565,7 +566,6 @@ SetExRes hashTypeSetExpiryListpack(HashTypeSetEx *ex, sds field,
ex->minExpireFields = expireAt;
listpackExUpdateExpiry(ex->hashObj, field, fptr, vptr, expireAt);
ex->fieldUpdated++;
return HSETEX_OK;
}
@ -788,7 +788,8 @@ GetFieldRes hashTypeGetValue(redisDb *db, robj *o, sds field, unsigned char **vs
dbDelete(db,keyObj);
res = GETF_EXPIRED_HASH;
}
signalModifiedKey(NULL, db, keyObj);
if (!(hfeFlags & HFE_LAZY_NO_SIGNAL))
signalModifiedKey(NULL, db, keyObj);
decrRefCount(keyObj);
return res;
}
@ -1010,34 +1011,33 @@ int hashTypeSet(redisDb *db, robj *o, sds field, sds value, int flags) {
SetExRes hashTypeSetExpiryHT(HashTypeSetEx *exInfo, sds field, uint64_t expireAt) {
dict *ht = exInfo->hashObj->ptr;
dictEntry *existingEntry = NULL;
hfield hfNew = NULL;
/* New field with expiration metadata */
hfield hfNew = hfieldNew(field, sdslen(field), 1 /*withExpireMeta*/);
if ((existingEntry = dictFind(ht, field)) == NULL) {
hfieldFree(hfNew);
if ((existingEntry = dictFind(ht, field)) == NULL)
return HSETEX_NO_FIELD;
}
hfield hfOld = dictGetKey(existingEntry);
/* Special value of EXPIRE_TIME_INVALID indicates field should be persisted.*/
if (expireAt == EB_EXPIRE_TIME_INVALID) {
/* Return error if already there is no ttl. */
if (hfieldGetExpireTime(hfOld) == EB_EXPIRE_TIME_INVALID)
return HSETEX_NO_CONDITION_MET;
hfieldPersist(exInfo->hashObj, hfOld);
return HSETEX_OK;
}
/* If field doesn't have expiry metadata attached */
if (!hfieldIsExpireAttached(hfOld)) {
/* For fields without expiry, LT condition is considered valid */
if (exInfo->expireSetCond & (HFE_XX | HFE_GT)) {
hfieldFree(hfNew);
if (exInfo->expireSetCond & (HFE_XX | HFE_GT))
return HSETEX_NO_CONDITION_MET;
}
/* Delete old field. Below goanna dictSetKey(..,hfNew) */
hfieldFree(hfOld);
/* New field with expiration metadata */
hfNew = hfieldNew(field, sdslen(field), 1);
} else { /* field has ExpireMeta struct attached */
/* No need for hfNew (Just modify expire-time of existing field) */
hfieldFree(hfNew);
uint64_t prevExpire = hfieldGetExpireTime(hfOld);
/* If field has valid expiration time, then check GT|LT|NX */
@ -1073,13 +1073,10 @@ SetExRes hashTypeSetExpiryHT(HashTypeSetEx *exInfo, sds field, uint64_t expireAt
/* If expired, then delete the field and propagate the deletion.
* If replica, continue like the field is valid */
if (unlikely(checkAlreadyExpired(expireAt))) {
unsigned long length = dictSize(ht);
updateKeysizesHist(exInfo->db, getKeySlot(exInfo->key->ptr), OBJ_HASH, length, length-1);
/* replicas should not initiate deletion of fields */
propagateHashFieldDeletion(exInfo->db, exInfo->key->ptr, field, sdslen(field));
hashTypeDelete(exInfo->hashObj, field, 1);
server.stat_expired_subkeys++;
exInfo->fieldDeleted++;
return HSETEX_DELETED;
}
@ -1088,7 +1085,6 @@ SetExRes hashTypeSetExpiryHT(HashTypeSetEx *exInfo, sds field, uint64_t expireAt
dictExpireMetadata *dm = (dictExpireMetadata *) dictMetadata(ht);
ebAdd(&dm->hfe, &hashFieldExpireBucketsType, hfNew, expireAt);
exInfo->fieldUpdated++;
return HSETEX_OK;
}
@ -1097,20 +1093,18 @@ SetExRes hashTypeSetExpiryHT(HashTypeSetEx *exInfo, sds field, uint64_t expireAt
*
* Take care to call first hashTypeSetExInit() and then call this function.
* Finally, call hashTypeSetExDone() to notify and update global HFE DS.
*
* Special value of EB_EXPIRE_TIME_INVALID for 'expireAt' argument will persist
* the field.
*/
SetExRes hashTypeSetEx(robj *o, sds field, uint64_t expireAt, HashTypeSetEx *exInfo)
{
if (o->encoding == OBJ_ENCODING_LISTPACK_EX)
{
SetExRes hashTypeSetEx(robj *o, sds field, uint64_t expireAt, HashTypeSetEx *exInfo) {
if (o->encoding == OBJ_ENCODING_LISTPACK_EX) {
unsigned char *fptr = NULL, *vptr = NULL, *tptr = NULL;
listpackEx *lpt = o->ptr;
long long expireTime = HASH_LP_NO_TTL;
if ((fptr = lpFirst(lpt->lp)) == NULL)
return HSETEX_NO_FIELD;
fptr = lpFind(lpt->lp, fptr, (unsigned char*)field, sdslen(field), 2);
fptr = lpFirst(lpt->lp);
if (fptr)
fptr = lpFind(lpt->lp, fptr, (unsigned char*)field, sdslen(field), 2);
if (!fptr)
return HSETEX_NO_FIELD;
@ -1120,7 +1114,7 @@ SetExRes hashTypeSetEx(robj *o, sds field, uint64_t expireAt, HashTypeSetEx *exI
serverAssert(vptr != NULL);
tptr = lpNext(lpt->lp, vptr);
serverAssert(tptr && lpGetIntegerValue(tptr, &expireTime));
serverAssert(tptr);
/* update TTL */
return hashTypeSetExpiryListpack(exInfo, field, fptr, vptr, tptr, expireAt);
@ -1144,19 +1138,16 @@ void initDictExpireMetadata(sds key, robj *o) {
}
/* Init HashTypeSetEx struct before calling hashTypeSetEx() */
int hashTypeSetExInit(robj *key, robj *o, client *c, redisDb *db, const char *cmd,
int hashTypeSetExInit(robj *key, robj *o, client *c, redisDb *db,
ExpireSetCond expireSetCond, HashTypeSetEx *ex)
{
dict *ht = o->ptr;
ex->expireSetCond = expireSetCond;
ex->minExpire = EB_EXPIRE_TIME_INVALID;
ex->c = c;
ex->cmd = cmd;
ex->db = db;
ex->key = key;
ex->hashObj = o;
ex->fieldDeleted = 0;
ex->fieldUpdated = 0;
ex->minExpireFields = EB_EXPIRE_TIME_INVALID;
/* Take care that HASH support expiration */
@ -1220,50 +1211,38 @@ int hashTypeSetExInit(robj *key, robj *o, client *c, redisDb *db, const char *cm
/*
* After calling hashTypeSetEx() for setting fields or their expiry, call this
* function to notify and update global HFE DS.
* function to update global HFE DS.
*/
void hashTypeSetExDone(HashTypeSetEx *ex) {
/* Notify keyspace event, update dirty count and update global HFE DS */
if (ex->fieldDeleted + ex->fieldUpdated > 0) {
server.dirty += ex->fieldDeleted + ex->fieldUpdated;
if (ex->fieldDeleted && hashTypeLength(ex->hashObj, 0) == 0) {
dbDelete(ex->db,ex->key);
signalModifiedKey(ex->c, ex->db, ex->key);
notifyKeyspaceEvent(NOTIFY_HASH, "hdel", ex->key, ex->db->id);
notifyKeyspaceEvent(NOTIFY_GENERIC,"del",ex->key, ex->db->id);
} else {
signalModifiedKey(ex->c, ex->db, ex->key);
notifyKeyspaceEvent(NOTIFY_HASH, ex->fieldDeleted ? "hdel" : "hexpire",
ex->key, ex->db->id);
if (hashTypeLength(ex->hashObj, 0) == 0)
return;
/* If minimum HFE of the hash is smaller than expiration time of the
* specified fields in the command as well as it is smaller or equal
* than expiration time provided in the command, then the minimum
* HFE of the hash won't change following this command. */
if ((ex->minExpire < ex->minExpireFields))
return;
/* If minimum HFE of the hash is smaller than expiration time of the
* specified fields in the command as well as it is smaller or equal
* than expiration time provided in the command, then the minimum
* HFE of the hash won't change following this command. */
if ((ex->minExpire < ex->minExpireFields))
return;
/* Retrieve new expired time. It might have changed. */
uint64_t newMinExpire = hashTypeGetMinExpire(ex->hashObj, 1 /*accurate*/);
/* Retrieve new expired time. It might have changed. */
uint64_t newMinExpire = hashTypeGetMinExpire(ex->hashObj, 1 /*accurate*/);
/* Calculate the diff between old minExpire and newMinExpire. If it is
* only few seconds, then don't have to update global HFE DS. At the worst
* case fields of hash will be active-expired up to few seconds later.
*
* In any case, active-expire operation will know to update global
* HFE DS more efficiently than here for a single item.
*/
uint64_t diff = (ex->minExpire > newMinExpire) ?
(ex->minExpire - newMinExpire) : (newMinExpire - ex->minExpire);
if (diff < HASH_NEW_EXPIRE_DIFF_THRESHOLD) return;
/* Calculate the diff between old minExpire and newMinExpire. If it is
* only few seconds, then don't have to update global HFE DS. At the worst
* case fields of hash will be active-expired up to few seconds later.
*
* In any case, active-expire operation will know to update global
* HFE DS more efficiently than here for a single item.
*/
uint64_t diff = (ex->minExpire > newMinExpire) ?
(ex->minExpire - newMinExpire) : (newMinExpire - ex->minExpire);
if (diff < HASH_NEW_EXPIRE_DIFF_THRESHOLD) return;
if (ex->minExpire != EB_EXPIRE_TIME_INVALID)
ebRemove(&ex->db->hexpires, &hashExpireBucketsType, ex->hashObj);
if (newMinExpire != EB_EXPIRE_TIME_INVALID)
ebAdd(&ex->db->hexpires, &hashExpireBucketsType, ex->hashObj, newMinExpire);
}
}
if (ex->minExpire != EB_EXPIRE_TIME_INVALID)
ebRemove(&ex->db->hexpires, &hashExpireBucketsType, ex->hashObj);
if (newMinExpire != EB_EXPIRE_TIME_INVALID)
ebAdd(&ex->db->hexpires, &hashExpireBucketsType, ex->hashObj, newMinExpire);
}
/* Delete an element from a hash.
@ -2222,6 +2201,303 @@ void hsetCommand(client *c) {
server.dirty += (c->argc - 2)/2;
}
/* Parse expire time from argument and do boundary checks. */
static int parseExpireTime(client *c, robj *o, int unit, long long basetime,
long long *expire)
{
long long val;
/* Read the expiry time from command */
if (getLongLongFromObjectOrReply(c, o, &val, NULL) != C_OK)
return C_ERR;
if (val < 0) {
addReplyError(c,"invalid expire time, must be >= 0");
return C_ERR;
}
if (unit == UNIT_SECONDS) {
if (val > (long long) HFE_MAX_ABS_TIME_MSEC / 1000) {
addReplyErrorExpireTime(c);
return C_ERR;
}
val *= 1000;
}
if (val > (long long) HFE_MAX_ABS_TIME_MSEC - basetime) {
addReplyErrorExpireTime(c);
return C_ERR;
}
val += basetime;
*expire = val;
return C_OK;
}
/* Flags that are used as part of HGETEX and HSETEX commands. */
#define HFE_EX (1<<0) /* Expiration time in seconds */
#define HFE_PX (1<<1) /* Expiration time in milliseconds */
#define HFE_EXAT (1<<2) /* Expiration time in unix seconds */
#define HFE_PXAT (1<<3) /* Expiration time in unix milliseconds */
#define HFE_PERSIST (1<<4) /* Persist fields */
#define HFE_KEEPTTL (1<<5) /* Do not discard field ttl on set op */
#define HFE_FXX (1<<6) /* Set fields if all the fields already exist */
#define HFE_FNX (1<<7) /* Set fields if none of the fields exist */
/* Parse hsetex command arguments.
* HSETEX <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) {
long long value, incr, oldvalue;
robj *o;
@ -2393,6 +2669,254 @@ void hmgetCommand(client *c) {
}
}
/* Get and delete the value of one or more fields of a given hash key.
* HGETDEL <key> FIELDS <numfields> field1 field2 ...
* Reply: list of the value associated with each field or nil if the field
* doesnt 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
* doesnt 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) {
robj *o;
int j, deleted = 0, keyremoved = 0;
@ -3174,10 +3698,11 @@ static void httlGenericCommand(client *c, const char *cmd, long long basetime, i
* not met, then command will be rejected. Otherwise, EXPIRE command will be
* propagated for given key.
*/
static void hexpireGenericCommand(client *c, const char *cmd, long long basetime, int unit) {
static void hexpireGenericCommand(client *c, long long basetime, int unit) {
long numFields = 0, numFieldsAt = 4;
long long expire; /* unix time in msec */
int fieldAt, fieldsNotSet = 0, expireSetCond = 0;
int fieldAt, fieldsNotSet = 0, expireSetCond = 0, updated = 0, deleted = 0;
unsigned long oldlen, newlen;
robj *hashObj, *keyArg = c->argv[1], *expireArg = c->argv[2];
/* Read the hash object */
@ -3186,29 +3711,9 @@ static void hexpireGenericCommand(client *c, const char *cmd, long long basetime
return;
/* Read the expiry time from command */
if (getLongLongFromObjectOrReply(c, expireArg, &expire, NULL) != C_OK)
if (parseExpireTime(c, expireArg, unit, basetime, &expire) != C_OK)
return;
if (expire < 0) {
addReplyError(c,"invalid expire time, must be >= 0");
return;
}
if (unit == UNIT_SECONDS) {
if (expire > (long long) HFE_MAX_ABS_TIME_MSEC / 1000) {
addReplyErrorExpireTime(c);
return;
}
expire *= 1000;
}
/* Ensure that the final absolute Unix timestamp does not exceed EB_EXPIRE_TIME_MAX. */
if (expire > (long long) HFE_MAX_ABS_TIME_MSEC - basetime) {
addReplyErrorExpireTime(c);
return;
}
expire += basetime;
/* Read optional expireSetCond [NX|XX|GT|LT] */
char *optArg = c->argv[3]->ptr;
if (!strcasecmp(optArg, "nx")) {
@ -3247,14 +3752,18 @@ static void hexpireGenericCommand(client *c, const char *cmd, long long basetime
return;
}
oldlen = hashTypeLength(hashObj, 0);
HashTypeSetEx exCtx;
hashTypeSetExInit(keyArg, hashObj, c, c->db, cmd, expireSetCond, &exCtx);
hashTypeSetExInit(keyArg, hashObj, c, c->db, expireSetCond, &exCtx);
addReplyArrayLen(c, numFields);
fieldAt = numFieldsAt + 1;
while (fieldAt < c->argc) {
sds field = c->argv[fieldAt]->ptr;
SetExRes res = hashTypeSetEx(hashObj, field, expire, &exCtx);
updated += (res == HSETEX_OK);
deleted += (res == HSETEX_DELETED);
if (unlikely(res != HSETEX_OK)) {
/* If the field was not set, prevent field propagation */
@ -3269,17 +3778,34 @@ static void hexpireGenericCommand(client *c, const char *cmd, long long basetime
hashTypeSetExDone(&exCtx);
if (deleted + updated > 0) {
server.dirty += deleted + updated;
signalModifiedKey(c, c->db, keyArg);
notifyKeyspaceEvent(NOTIFY_HASH, deleted ? "hdel" : "hexpire",
keyArg, c->db->id);
}
newlen = hashTypeLength(hashObj, 0);
if (newlen == 0) {
dbDelete(c->db, keyArg);
notifyKeyspaceEvent(NOTIFY_GENERIC, "del", keyArg, c->db->id);
}
if (oldlen != newlen)
updateKeysizesHist(c->db, getKeySlot(c->argv[1]->ptr), OBJ_HASH,
oldlen, newlen);
/* Avoid propagating command if not even one field was updated (Either because
* the time is in the past, and corresponding HDELs were sent, or conditions
* not met) then it is useless and invalid to propagate command with no fields */
if (exCtx.fieldUpdated == 0) {
if (updated == 0) {
preventCommandPropagation(c);
return;
}
/* If some fields were dropped, rewrite the number of fields */
if (fieldsNotSet) {
robj *numFieldsObj = createStringObjectFromLongLong(exCtx.fieldUpdated);
robj *numFieldsObj = createStringObjectFromLongLong(updated);
rewriteClientCommandArgument(c, numFieldsAt, numFieldsObj);
decrRefCount(numFieldsObj);
}
@ -3297,48 +3823,48 @@ static void hexpireGenericCommand(client *c, const char *cmd, long long basetime
}
}
/* HPEXPIRE key milliseconds [ NX | XX | GT | LT] numfields <field [field ...]> */
/* HPEXPIRE key milliseconds [ NX | XX | GT | LT] FIELDS numfields <field [field ...]> */
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) {
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) {
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) {
hexpireGenericCommand(c,"hpexpireat", 0,UNIT_MILLISECONDS);
hexpireGenericCommand(c,0,UNIT_MILLISECONDS);
}
/* for each specified field: get the remaining time to live in seconds*/
/* HTTL key numfields <field [field ...]> */
/* HTTL key FIELDS numfields <field [field ...]> */
void httlCommand(client *c) {
httlGenericCommand(c, "httl", commandTimeSnapshot(), UNIT_SECONDS);
}
/* HPTTL key numfields <field [field ...]> */
/* HPTTL key FIELDS numfields <field [field ...]> */
void hpttlCommand(client *c) {
httlGenericCommand(c, "hpttl", commandTimeSnapshot(), UNIT_MILLISECONDS);
}
/* HEXPIRETIME key numFields <field [field ...]> */
/* HEXPIRETIME key FIELDS numfields <field [field ...]> */
void hexpiretimeCommand(client *c) {
httlGenericCommand(c, "hexpiretime", 0, UNIT_SECONDS);
}
/* HPEXPIRETIME key numFields <field [field ...]> */
/* HPEXPIRETIME key FIELDS numfields <field [field ...]> */
void hpexpiretimeCommand(client *c) {
httlGenericCommand(c, "hexpiretime", 0, UNIT_MILLISECONDS);
}
/* HPERSIST key <FIELDS count field [field ...]> */
/* HPERSIST key FIELDS numfields <field [field ...]> */
void hpersistCommand(client *c) {
robj *hashObj;
long numFields = 0, numFieldsAt = 3;

View File

@ -334,6 +334,31 @@ proc test_all_keysizes { {replMode 0} } {
run_cmd_verify_hist {$server HSET h2 2 2} {db0_HASH:2=1}
run_cmd_verify_hist {$server HDEL h2 1} {db0_HASH:1=1}
run_cmd_verify_hist {$server HDEL h2 2} {}
# HGETDEL
run_cmd_verify_hist {$server FLUSHALL} {}
run_cmd_verify_hist {$server HSETEX h2 FIELDS 1 1 1} {db0_HASH:1=1}
run_cmd_verify_hist {$server HSETEX h2 FIELDS 1 2 2} {db0_HASH:2=1}
run_cmd_verify_hist {$server HGETDEL h2 FIELDS 1 1} {db0_HASH:1=1}
run_cmd_verify_hist {$server HGETDEL h2 FIELDS 1 3} {db0_HASH:1=1}
run_cmd_verify_hist {$server HGETDEL h2 FIELDS 1 2} {}
# HGETEX
run_cmd_verify_hist {$server FLUSHALL} {}
run_cmd_verify_hist {$server HSETEX h1 FIELDS 2 f1 1 f2 1} {db0_HASH:2=1}
run_cmd_verify_hist {$server HGETEX h1 PXAT 1 FIELDS 1 f1} {db0_HASH:1=1}
run_cmd_verify_hist {$server HSETEX h1 FIELDS 1 f3 1} {db0_HASH:2=1}
run_cmd_verify_hist {$server HGETEX h1 PX 50 FIELDS 1 f2} {db0_HASH:2=1}
run_cmd_verify_hist {} {db0_HASH:1=1} 1
run_cmd_verify_hist {$server HGETEX h1 PX 50 FIELDS 1 f3} {db0_HASH:1=1}
run_cmd_verify_hist {} {} 1
# HSETEX
run_cmd_verify_hist {$server FLUSHALL} {}
run_cmd_verify_hist {$server HSETEX h1 FIELDS 2 f1 1 f2 1} {db0_HASH:2=1}
run_cmd_verify_hist {$server HSETEX h1 PXAT 1 FIELDS 1 f1 v1} {db0_HASH:1=1}
run_cmd_verify_hist {$server HSETEX h1 FIELDS 1 f3 1} {db0_HASH:2=1}
run_cmd_verify_hist {$server HSETEX h1 PX 50 FIELDS 1 f2 v2} {db0_HASH:2=1}
run_cmd_verify_hist {} {db0_HASH:1=1} 1
run_cmd_verify_hist {$server HSETEX h1 PX 50 FIELDS 1 f3 v3} {db0_HASH:1=1}
run_cmd_verify_hist {} {} 1
# HMSET
run_cmd_verify_hist {$server FLUSHALL} {}
run_cmd_verify_hist {$server HMSET h1 1 1 2 2 3 3} {db0_HASH:2=1}

View File

@ -414,6 +414,58 @@ start_server {tags {"pubsub network"}} {
assert_equal "pmessage * __keyspace@${db}__:myhash del" [$rd1 read]
r debug set-active-expire 1
# Test HSETEX, HGETEX and HGETDEL notifications
r hsetex myhash FIELDS 3 f4 v4 f5 v5 f6 v6
assert_equal "pmessage * __keyspace@${db}__:myhash hset" [$rd1 read]
# hgetex sets ttl in past
r hgetex myhash PX 0 FIELDS 1 f4
assert_equal "pmessage * __keyspace@${db}__:myhash hdel" [$rd1 read]
# hgetex sets ttl
r hgetex myhash EXAT [expr {[clock seconds] + 999999}] FIELDS 1 f5
assert_equal "pmessage * __keyspace@${db}__:myhash hexpire" [$rd1 read]
# hgetex persists field
r hgetex myhash PERSIST FIELDS 1 f5
assert_equal "pmessage * __keyspace@${db}__:myhash hpersist" [$rd1 read]
# hgetdel deletes a field
r hgetdel myhash FIELDS 1 f5
assert_equal "pmessage * __keyspace@${db}__:myhash hdel" [$rd1 read]
# hsetex sets field and expiry time
r hsetex myhash EXAT [expr {[clock seconds] + 999999}] FIELDS 1 f6 v6
assert_equal "pmessage * __keyspace@${db}__:myhash hset" [$rd1 read]
assert_equal "pmessage * __keyspace@${db}__:myhash hexpire" [$rd1 read]
# hsetex sets field and ttl in the past
r hsetex myhash PX 0 FIELDS 1 f6 v6
assert_equal "pmessage * __keyspace@${db}__:myhash hset" [$rd1 read]
assert_equal "pmessage * __keyspace@${db}__:myhash hdel" [$rd1 read]
assert_equal "pmessage * __keyspace@${db}__:myhash del" [$rd1 read]
# Test that we will get `hexpired` notification when a hash field is
# removed by lazy expire using hgetdel command
r debug set-active-expire 0
r hsetex myhash PX 10 FIELDS 1 f1 v1
assert_equal "pmessage * __keyspace@${db}__:myhash hset" [$rd1 read]
assert_equal "pmessage * __keyspace@${db}__:myhash hexpire" [$rd1 read]
# Set another field
r hsetex myhash FIELDS 1 f2 v2
assert_equal "pmessage * __keyspace@${db}__:myhash hset" [$rd1 read]
# Wait until field expires
after 20
r hgetdel myhash FIELDS 1 f1
assert_equal "pmessage * __keyspace@${db}__:myhash hexpired" [$rd1 read]
# Get and delete the only field
r hgetdel myhash FIELDS 1 f2
assert_equal "pmessage * __keyspace@${db}__:myhash hdel" [$rd1 read]
assert_equal "pmessage * __keyspace@${db}__:myhash del" [$rd1 read]
r debug set-active-expire 1
$rd1 close
} {0} {needs:debug}
} ;# foreach

View File

@ -855,6 +855,430 @@ start_server {tags {"external:skip needs:debug"}} {
assert_equal [r HINCRBYFLOAT h1 f1 2.5] 12.5
assert_range [r HPTTL h1 FIELDS 1 f1] 1 20
}
test "HGETDEL - delete field with ttl ($type)" {
r debug set-active-expire 0
r del h1
# Test deleting only field in a hash. Due to lazy expiry,
# reply will be null and the field and the key will be deleted.
r hsetex h1 PX 5 FIELDS 1 f1 10
after 15
assert_equal [r hgetdel h1 fields 1 f1] "{}"
assert_equal [r exists h1] 0
# Test deleting one field among many. f2 will lazily expire
r hsetex h1 FIELDS 3 f1 10 f2 20 f3 value3
r hpexpire h1 5 FIELDS 1 f2
after 15
assert_equal [r hgetdel h1 fields 2 f2 f3] "{} value3"
assert_equal [lsort [r hgetall h1]] [lsort "f1 10"]
# Try to delete the last field, along with non-existing fields
assert_equal [r hgetdel h1 fields 4 f1 f2 f3 f4] "10 {} {} {}"
r debug set-active-expire 1
}
test "HGETEX - input validation ($type)" {
r del h1
assert_error "*wrong number of arguments*" {r HGETEX}
assert_error "*wrong number of arguments*" {r HGETEX h1}
assert_error "*wrong number of arguments*" {r HGETEX h1 FIELDS}
assert_error "*wrong number of arguments*" {r HGETEX h1 FIELDS 0}
assert_error "*wrong number of arguments*" {r HGETEX h1 FIELDS 1}
assert_error "*argument FIELDS is missing*" {r HGETEX h1 XFIELDX 1 a}
assert_error "*argument FIELDS is missing*" {r HGETEX h1 PXAT 1 1}
assert_error "*argument FIELDS is missing*" {r HGETEX h1 PERSIST 1 FIELDS 1 a}
assert_error "*must match the number of arguments*" {r HGETEX h1 FIELDS 2 a}
assert_error "*Number of fields must be a positive integer*" {r HGETEX h1 FIELDS 0 a}
assert_error "*Number of fields must be a positive integer*" {r HGETEX h1 FIELDS -1 a}
assert_error "*Number of fields must be a positive integer*" {r HGETEX h1 FIELDS 9223372036854775808 a}
}
test "HGETEX - input validation (expire time) ($type)" {
assert_error "*value is not an integer or out of range*" {r HGETEX h1 EX bla FIELDS 1 a}
assert_error "*value is not an integer or out of range*" {r HGETEX h1 EX 9223372036854775808 FIELDS 1 a}
assert_error "*value is not an integer or out of range*" {r HGETEX h1 EXAT 9223372036854775808 FIELDS 1 a}
assert_error "*invalid expire time, must be >= 0*" {r HGETEX h1 PX -1 FIELDS 1 a}
assert_error "*invalid expire time, must be >= 0*" {r HGETEX h1 PXAT -1 FIELDS 1 a}
assert_error "*invalid expire time*" {r HGETEX h1 EX -1 FIELDS 1 a}
assert_error "*invalid expire time*" {r HGETEX h1 EX [expr (1<<48)] FIELDS 1 a}
assert_error "*invalid expire time*" {r HGETEX h1 EX [expr (1<<46) - [clock seconds] + 100 ] FIELDS 1 a}
assert_error "*invalid expire time*" {r HGETEX h1 EXAT [expr (1<<46) + 100 ] FIELDS 1 a}
assert_error "*invalid expire time*" {r HGETEX h1 PX [expr (1<<46) - [clock milliseconds] + 100 ] FIELDS 1 a}
assert_error "*invalid expire time*" {r HGETEX h1 PXAT [expr (1<<46) + 100 ] FIELDS 1 a}
}
test "HGETEX - get without setting ttl ($type)" {
r del h1
r hset h1 a 1 b 2 c strval
assert_equal [r hgetex h1 fields 1 a] "1"
assert_equal [r hgetex h1 fields 2 a b] "1 2"
assert_equal [r hgetex h1 fields 3 a b c] "1 2 strval"
assert_equal [r HTTL h1 FIELDS 3 a b c] "$T_NO_EXPIRY $T_NO_EXPIRY $T_NO_EXPIRY"
}
test "HGETEX - get and set the ttl ($type)" {
r del h1
r hset h1 a 1 b 2 c strval
assert_equal [r hgetex h1 EX 10000 fields 1 a] "1"
assert_range [r HTTL h1 FIELDS 1 a] 9000 10000
assert_equal [r hgetex h1 EX 10000 fields 1 c] "strval"
assert_range [r HTTL h1 FIELDS 1 c] 9000 10000
}
test "HGETEX - Test 'EX' flag ($type)" {
r del myhash
r hset myhash field1 value1 field2 value2 field3 value3
assert_equal [r hgetex myhash EX 1000 FIELDS 1 field1] [list "value1"]
assert_range [r httl myhash FIELDS 1 field1] 1 1000
}
test "HGETEX - Test 'EXAT' flag ($type)" {
r del myhash
r hset myhash field1 value1 field2 value2 field3 value3
assert_equal [r hgetex myhash EXAT 4000000000 FIELDS 1 field2] [list "value2"]
assert_range [expr [r httl myhash FIELDS 1 field2] + [clock seconds]] 3900000000 4000000000
}
test "HGETEX - Test 'PX' flag ($type)" {
r del myhash
r hset myhash field1 value1 field2 value2 field3 value3
assert_equal [r hgetex myhash PX 1000000 FIELDS 1 field3] [list "value3"]
assert_range [r httl myhash FIELDS 1 field3] 900 1000
}
test "HGETEX - Test 'PXAT' flag ($type)" {
r del myhash
r hset myhash field1 value1 field2 value2 field3 value3
assert_equal [r hgetex myhash PXAT 4000000000000 FIELDS 1 field3] [list "value3"]
assert_range [expr [r httl myhash FIELDS 1 field3] + [clock seconds]] 3900000000 4000000000
}
test "HGETEX - Test 'PERSIST' flag ($type)" {
r del myhash
r debug set-active-expire 0
r hsetex myhash PX 5000 FIELDS 3 f1 v1 f2 v2 f3 v3
assert_not_equal [r httl myhash FIELDS 1 f1] "$T_NO_EXPIRY"
assert_not_equal [r httl myhash FIELDS 1 f2] "$T_NO_EXPIRY"
assert_not_equal [r httl myhash FIELDS 1 f3] "$T_NO_EXPIRY"
# Persist f1 and verify it does not have TTL anymore
assert_equal [r hgetex myhash PERSIST FIELDS 1 f1] "v1"
assert_equal [r httl myhash FIELDS 1 f1] "$T_NO_EXPIRY"
# Persist rest of the fields
assert_equal [r hgetex myhash PERSIST FIELDS 2 f2 f3] "v2 v3"
assert_equal [r httl myhash FIELDS 2 f2 f3] "$T_NO_EXPIRY $T_NO_EXPIRY"
# Redo the operation. It should be noop as fields are persisted already.
assert_equal [r hgetex myhash PERSIST FIELDS 2 f2 f3] "v2 v3"
assert_equal [r httl myhash FIELDS 2 f2 f3] "$T_NO_EXPIRY $T_NO_EXPIRY"
# Final sanity, fields exist and have no attached ttl.
assert_equal [lsort [r hgetall myhash]] [lsort "f1 v1 f2 v2 f3 v3"]
assert_equal [r httl myhash FIELDS 3 f1 f2 f3] "$T_NO_EXPIRY $T_NO_EXPIRY $T_NO_EXPIRY"
r debug set-active-expire 1
}
test "HGETEX - Test setting ttl in the past will delete the key ($type)" {
r del myhash
r hset myhash f1 v1 f2 v2 f3 v3
# hgetex without setting ttl
assert_equal [lsort [r hgetex myhash fields 3 f1 f2 f3]] [lsort "v1 v2 v3"]
assert_equal [r httl myhash FIELDS 3 f1 f2 f3] "$T_NO_EXPIRY $T_NO_EXPIRY $T_NO_EXPIRY"
# set an expired ttl and verify the key is deleted
r hgetex myhash PXAT 1 fields 3 f1 f2 f3
assert_equal [r exists myhash] 0
}
test "HGETEX - Test active expiry ($type)" {
r del myhash
r debug set-active-expire 0
r hset myhash f1 v1 f2 v2 f3 v3 f4 v4 f5 v5
assert_equal [lsort [r hgetex myhash PXAT 1 FIELDS 5 f1 f2 f3 f4 f5]] [lsort "v1 v2 v3 v4 v5"]
r debug set-active-expire 1
wait_for_condition 50 20 { [r EXISTS myhash] == 0 } else { fail "'myhash' should be expired" }
}
test "HGETEX - A field with TTL overridden with another value (TTL discarded) ($type)" {
r del myhash
r hset myhash f1 v1 f2 v2 f3 v3
r hgetex myhash PX 10000 FIELDS 1 f1
r hgetex myhash EX 100 FIELDS 1 f2
# f2 ttl will be discarded
r hset myhash f2 v22
assert_equal [r hget myhash f2] "v22"
assert_equal [r httl myhash FIELDS 2 f2 f3] "$T_NO_EXPIRY $T_NO_EXPIRY"
# Other field is not affected (still has TTL)
assert_not_equal [r httl myhash FIELDS 1 f1] "$T_NO_EXPIRY"
}
test "HGETEX - Test with lazy expiry ($type)" {
r del myhash
r debug set-active-expire 0
r hsetex myhash PX 1 FIELDS 2 f1 v1 f2 v2
after 5
assert_equal [r hgetex myhash FIELDS 2 f1 f2] "{} {}"
assert_equal [r exists myhash] 0
r debug set-active-expire 1
}
test "HSETEX - input validation ($type)" {
assert_error {*wrong number of arguments*} {r hsetex myhash}
assert_error {*wrong number of arguments*} {r hsetex myhash fields}
assert_error {*wrong number of arguments*} {r hsetex myhash fields 1}
assert_error {*wrong number of arguments*} {r hsetex myhash fields 2 a b}
assert_error {*wrong number of arguments*} {r hsetex myhash fields 2 a b c}
assert_error {*wrong number of arguments*} {r hsetex myhash fields 2 a b c d e}
assert_error {*wrong number of arguments*} {r hsetex myhash fields 3 a b c d}
assert_error {*wrong number of arguments*} {r hsetex myhash fields 3 a b c d e}
assert_error {*wrong number of arguments*} {r hsetex myhash fields 3 a b c d e f g}
assert_error {*wrong number of arguments*} {r hsetex myhash fields 3 a b}
assert_error {*wrong number of arguments*} {r hsetex myhash fields 1 a b unknown}
assert_error {*unknown argument*} {r hsetex myhash nx fields 1 a b}
assert_error {*unknown argument*} {r hsetex myhash 1 fields 1 a b}
# Only one of FNX or FXX
assert_error {*Only one of FXX or FNX arguments *} {r hsetex myhash fxx fxx EX 100 fields 1 a b}
assert_error {*Only one of FXX or FNX arguments *} {r hsetex myhash fxx fnx EX 100 fields 1 a b}
assert_error {*Only one of FXX or FNX arguments *} {r hsetex myhash fnx fxx EX 100 fields 1 a b}
assert_error {*Only one of FXX or FNX arguments *} {r hsetex myhash fnx fnx EX 100 fields 1 a b}
assert_error {*Only one of FXX or FNX arguments *} {r hsetex myhash fxx fnx fxx EX 100 fields 1 a b}
assert_error {*Only one of FXX or FNX arguments *} {r hsetex myhash fnx fxx fnx EX 100 fields 1 a b}
# Only one of EX, PX, EXAT, PXAT or KEEPTTL can be specified
assert_error {*Only one of EX, PX, EXAT, PXAT or KEEPTTL arguments*} {r hsetex myhash EX 100 PX 1000 fields 1 a b}
assert_error {*Only one of EX, PX, EXAT, PXAT or KEEPTTL arguments*} {r hsetex myhash EX 100 EXAT 100 fields 1 a b}
assert_error {*Only one of EX, PX, EXAT, PXAT or KEEPTTL arguments*} {r hsetex myhash EXAT 100 EX 1000 fields 1 a b}
assert_error {*Only one of EX, PX, EXAT, PXAT or KEEPTTL arguments*} {r hsetex myhash EXAT 100 PX 1000 fields 1 a b}
assert_error {*Only one of EX, PX, EXAT, PXAT or KEEPTTL arguments*} {r hsetex myhash PX 100 EXAT 100 fields 1 a b}
assert_error {*Only one of EX, PX, EXAT, PXAT or KEEPTTL arguments*} {r hsetex myhash PX 100 PXAT 100 fields 1 a b}
assert_error {*Only one of EX, PX, EXAT, PXAT or KEEPTTL arguments*} {r hsetex myhash PXAT 100 EX 100 fields 1 a b}
assert_error {*Only one of EX, PX, EXAT, PXAT or KEEPTTL arguments*} {r hsetex myhash PXAT 100 EXAT 100 fields 1 a b}
assert_error {*Only one of EX, PX, EXAT, PXAT or KEEPTTL arguments*} {r hsetex myhash EX 100 KEEPTTL fields 1 a b}
assert_error {*Only one of EX, PX, EXAT, PXAT or KEEPTTL arguments*} {r hsetex myhash KEEPTTL EX 100 fields 1 a b}
assert_error {*Only one of EX, PX, EXAT, PXAT or KEEPTTL arguments*} {r hsetex myhash EX 100 EX 100 fields 1 a b}
assert_error {*Only one of EX, PX, EXAT, PXAT or KEEPTTL arguments*} {r hsetex myhash EXAT 100 EXAT 100 fields 1 a b}
assert_error {*Only one of EX, PX, EXAT, PXAT or KEEPTTL arguments*} {r hsetex myhash PX 10 PX 10 fields 1 a b}
assert_error {*Only one of EX, PX, EXAT, PXAT or KEEPTTL arguments*} {r hsetex myhash PXAT 10 PXAT 10 fields 1 a b}
assert_error {*Only one of EX, PX, EXAT, PXAT or KEEPTTL arguments*} {r hsetex myhash KEEPTTL KEEPTTL fields 1 a b}
# missing expire time
assert_error {*not an integer or out of range*} {r hsetex myhash ex fields 1 a b}
assert_error {*not an integer or out of range*} {r hsetex myhash px fields 1 a b}
assert_error {*not an integer or out of range*} {r hsetex myhash exat fields 1 a b}
assert_error {*not an integer or out of range*} {r hsetex myhash pxat fields 1 a b}
# expire time more than 2 ^ 48
assert_error {*invalid expire time*} {r hsetex myhash EXAT [expr (1<<48)] 1 a b}
assert_error {*invalid expire time*} {r hsetex myhash PXAT [expr (1<<48)] 1 a b}
assert_error {*invalid expire time*} {r hsetex myhash EX [expr (1<<48) - [clock seconds] + 1000 ] 1 a b}
assert_error {*invalid expire time*} {r hsetex myhash PX [expr (1<<48) - [clock milliseconds] + 1000 ] 1 a b}
# invalid expire time
assert_error {*invalid expire time*} {r hsetex myhash EXAT -1 1 a b}
assert_error {*not an integer or out of range*} {r hsetex myhash EXAT 9223372036854775808 1 a b}
assert_error {*not an integer or out of range*} {r hsetex myhash EXAT x 1 a b}
# invalid numfields arg
assert_error {*invalid number of fields*} {r hsetex myhash fields x a b}
assert_error {*invalid number of fields*} {r hsetex myhash fields 9223372036854775808 a b}
assert_error {*invalid number of fields*} {r hsetex myhash fields 0 a b}
assert_error {*invalid number of fields*} {r hsetex myhash fields -1 a b}
}
test "HSETEX - Basic test ($type)" {
r del myhash
# set field
assert_equal [r hsetex myhash FIELDS 1 f1 v1] 1
assert_equal [r hget myhash f1] "v1"
# override
assert_equal [r hsetex myhash FIELDS 1 f1 v11] 1
assert_equal [r hget myhash f1] "v11"
# set multiple
assert_equal [r hsetex myhash FIELDS 2 f1 v1 f2 v2] 1
assert_equal [lsort [r hgetall myhash]] [lsort "f1 v1 f2 v2"]
assert_equal [r hsetex myhash FIELDS 3 f1 v111 f2 v222 f3 v333] 1
assert_equal [lsort [r hgetall myhash]] [lsort "f1 v111 f2 v222 f3 v333"]
}
test "HSETEX - Test FXX flag ($type)" {
r del myhash
# Key is empty, command fails due to FXX
assert_equal [r hsetex myhash FXX FIELDS 2 f1 v1 f2 v2] 0
# Verify it did not leave the key empty
assert_equal [r exists myhash] 0
# Command fails and no change on fields
r hset myhash f1 v1
assert_equal [r hsetex myhash FXX FIELDS 2 f1 v1 f2 v2] 0
assert_equal [lsort [r hgetall myhash]] [lsort "f1 v1"]
# Command executed successfully
assert_equal [r hsetex myhash FXX FIELDS 1 f1 v11] 1
assert_equal [lsort [r hgetall myhash]] [lsort "f1 v11"]
# Try with multiple fields
r hset myhash f2 v2
assert_equal [r hsetex myhash FXX FIELDS 2 f1 v111 f2 v222] 1
assert_equal [lsort [r hgetall myhash]] [lsort "f1 v111 f2 v222"]
# Try with expiry
assert_equal [r hsetex myhash FXX EX 100 FIELDS 2 f1 v1 f2 v2] 1
assert_equal [lsort [r hgetall myhash]] [lsort "f1 v1 f2 v2"]
assert_range [r httl myhash FIELDS 1 f1] 80 100
assert_range [r httl myhash FIELDS 1 f2] 80 100
# Try with expiry, FXX arg comes after TTL
assert_equal [r hsetex myhash PX 5000 FXX FIELDS 2 f1 v1 f2 v2] 1
assert_equal [lsort [r hgetall myhash]] [lsort "f1 v1 f2 v2"]
assert_range [r hpttl myhash FIELDS 1 f1] 4500 5000
assert_range [r hpttl myhash FIELDS 1 f2] 4500 5000
}
test "HSETEX - Test FXX flag with lazy expire ($type)" {
r del myhash
r debug set-active-expire 0
r hsetex myhash PX 10 FIELDS 1 f1 v1
after 15
assert_equal [r hsetex myhash FXX FIELDS 1 f1 v11] 0
assert_equal [r exists myhash] 0
r debug set-active-expire 1
}
test "HSETEX - Test FNX flag ($type)" {
r del myhash
# Command successful on an empty key
assert_equal [r hsetex myhash FNX FIELDS 1 f1 v1] 1
# Command fails and no change on fields
assert_equal [r hsetex myhash FNX FIELDS 2 f1 v1 f2 v2] 0
assert_equal [lsort [r hgetall myhash]] [lsort "f1 v1"]
# Command executed successfully
assert_equal [r hsetex myhash FNX FIELDS 2 f2 v2 f3 v3] 1
assert_equal [lsort [r hgetall myhash]] [lsort "f1 v1 f2 v2 f3 v3"]
assert_equal [r hsetex myhash FXX FIELDS 3 f1 v11 f2 v22 f3 v33] 1
assert_equal [lsort [r hgetall myhash]] [lsort "f1 v11 f2 v22 f3 v33"]
# Try with expiry
r del myhash
assert_equal [r hsetex myhash FNX EX 100 FIELDS 2 f1 v1 f2 v2] 1
assert_equal [lsort [r hgetall myhash]] [lsort "f1 v1 f2 v2"]
assert_range [r httl myhash FIELDS 1 f1] 80 100
assert_range [r httl myhash FIELDS 1 f2] 80 100
# Try with expiry, FNX arg comes after TTL
assert_equal [r hsetex myhash PX 5000 FNX FIELDS 1 f3 v3] 1
assert_equal [lsort [r hgetall myhash]] [lsort "f1 v1 f2 v2 f3 v3"]
assert_range [r hpttl myhash FIELDS 1 f3] 4500 5000
}
test "HSETEX - Test 'EX' flag ($type)" {
r del myhash
r hset myhash f1 v1 f2 v2
assert_equal [r hsetex myhash EX 1000 FIELDS 1 f3 v3 ] 1
assert_range [r httl myhash FIELDS 1 f3] 900 1000
}
test "HSETEX - Test 'EXAT' flag ($type)" {
r del myhash
r hset myhash f1 v1 f2 v2
assert_equal [r hsetex myhash EXAT 4000000000 FIELDS 1 f3 v3] 1
assert_range [expr [r httl myhash FIELDS 1 f3] + [clock seconds]] 3900000000 4000000000
}
test "HSETEX - Test 'PX' flag ($type)" {
r del myhash
assert_equal [r hsetex myhash PX 1000000 FIELDS 1 f3 v3] 1
assert_range [r httl myhash FIELDS 1 f3] 990 1000
}
test "HSETEX - Test 'PXAT' flag ($type)" {
r del myhash
r hset myhash f1 v2 f2 v2 f3 v3
assert_equal [r hsetex myhash PXAT 4000000000000 FIELDS 1 f2 v2] 1
assert_range [expr [r httl myhash FIELDS 1 f2] + [clock seconds]] 3900000000 4000000000
}
test "HSETEX - Test 'KEEPTTL' flag ($type)" {
r del myhash
r hsetex myhash FIELDS 2 f1 v1 f2 v2
r hsetex myhash PX 20000 FIELDS 1 f2 v2
# f1 does not have ttl
assert_equal [r httl myhash FIELDS 1 f1] "$T_NO_EXPIRY"
# f2 has ttl
assert_not_equal [r httl myhash FIELDS 1 f2] "$T_NO_EXPIRY"
# Validate KEEPTTL preserves the TTL
assert_equal [r hsetex myhash KEEPTTL FIELDS 1 f2 v22] 1
assert_equal [r hget myhash f2] "v22"
assert_not_equal [r httl myhash FIELDS 1 f2] "$T_NO_EXPIRY"
# Try with multiple fields. First, set fields and TTL
r hsetex myhash EX 10000 FIELDS 3 f1 v1 f2 v2 f3 v3
# Update fields with KEEPTTL flag
r hsetex myhash KEEPTTL FIELDS 3 f1 v111 f2 v222 f3 v333
# Verify values are set, ttls are untouched
assert_equal [lsort [r hgetall myhash]] [lsort "f1 v111 f2 v222 f3 v333"]
assert_range [r httl myhash FIELDS 1 f1] 9000 10000
assert_range [r httl myhash FIELDS 1 f2] 9000 10000
assert_range [r httl myhash FIELDS 1 f3] 9000 10000
}
test "HSETEX - Test no expiry flag discards TTL ($type)" {
r del myhash
r hsetex myhash FIELDS 1 f1 v1
r hsetex myhash PX 100000 FIELDS 1 f2 v2
assert_range [r hpttl myhash FIELDS 1 f2] 1 100000
assert_equal [r hsetex myhash FIELDS 2 f1 v1 f2 v2] 1
assert_equal [r httl myhash FIELDS 2 f1 f2] "$T_NO_EXPIRY $T_NO_EXPIRY"
}
test "HSETEX - Test with active expiry" {
r del myhash
r debug set-active-expire 0
r hsetex myhash PX 10 FIELDS 5 f1 v1 f2 v2 f3 v3 f4 v4 f5 v5
r debug set-active-expire 1
wait_for_condition 50 20 { [r EXISTS myhash] == 0 } else { fail "'myhash' should be expired" }
}
test "HSETEX - Set time in the past ($type)" {
r del myhash
# Try on an empty key
assert_equal [r hsetex myhash EXAT [expr {[clock seconds] - 1}] FIELDS 2 f1 v1 f2 v2] 1
assert_equal [r hexists myhash field1] 0
# Try with existing fields
r hset myhash fields 2 f1 v1 f2 v2
assert_equal [r hsetex myhash EXAT [expr {[clock seconds] - 1}] FIELDS 2 f1 v1 f2 v2] 1
assert_equal [r hexists myhash field1] 0
}
}
test "Statistics - Hashes with HFEs ($type)" {
@ -879,6 +1303,13 @@ start_server {tags {"external:skip needs:debug"}} {
r hdel myhash3 f2
assert_match [get_stat_subexpiry r] 2
# hash4: 2 fields, 1 with TTL. HGETDEL field with TTL. subexpiry decr -1
r hset myhash4 f1 v1 f2 v2
r hpexpire myhash4 100 FIELDS 1 f2
assert_match [get_stat_subexpiry r] 3
r hgetdel myhash4 FIELDS 1 f2
assert_match [get_stat_subexpiry r] 2
# Expired fields of hash1 and hash2. subexpiry decr -2
wait_for_condition 50 50 {
[get_stat_subexpiry r] == 0
@ -887,6 +1318,21 @@ start_server {tags {"external:skip needs:debug"}} {
}
}
test "HFE commands against wrong type" {
r set wrongtype somevalue
assert_error "WRONGTYPE Operation against a key*" {r hexpire wrongtype 10 fields 1 f1}
assert_error "WRONGTYPE Operation against a key*" {r hexpireat wrongtype 10 fields 1 f1}
assert_error "WRONGTYPE Operation against a key*" {r hpexpire wrongtype 10 fields 1 f1}
assert_error "WRONGTYPE Operation against a key*" {r hpexpireat wrongtype 10 fields 1 f1}
assert_error "WRONGTYPE Operation against a key*" {r hexpiretime wrongtype fields 1 f1}
assert_error "WRONGTYPE Operation against a key*" {r hpexpiretime wrongtype fields 1 f1}
assert_error "WRONGTYPE Operation against a key*" {r httl wrongtype fields 1 f1}
assert_error "WRONGTYPE Operation against a key*" {r hpttl wrongtype fields 1 f1}
assert_error "WRONGTYPE Operation against a key*" {r hpersist wrongtype fields 1 f1}
assert_error "WRONGTYPE Operation against a key*" {r hgetex wrongtype fields 1 f1}
assert_error "WRONGTYPE Operation against a key*" {r hsetex wrongtype fields 1 f1 v1}
}
r config set hash-max-listpack-entries 512
}
@ -1048,6 +1494,54 @@ start_server {tags {"external:skip needs:debug"}} {
fail "Field f2 of hash h2 wasn't deleted"
}
# HSETEX
r hsetex h3 FIELDS 1 f1 v1
r hsetex h3 FXX FIELDS 1 f1 v11
r hsetex h3 FNX FIELDS 1 f2 v22
r hsetex h3 KEEPTTL FIELDS 1 f2 v22
# Next one will fail due to FNX arg and it won't be replicated
r hsetex h3 FNX FIELDS 2 f1 v1 f2 v2
# Commands with EX/PX/PXAT/EXAT will be replicated as PXAT
r hsetex h3 EX 10000 FIELDS 1 f1 v111
r hsetex h3 PX 10000 FIELDS 1 f1 v111
r hsetex h3 PXAT [expr [clock milliseconds]+100000] FIELDS 1 f1 v111
r hsetex h3 EXAT [expr [clock seconds]+100000] FIELDS 1 f1 v111
# Following commands will set and then delete the fields because
# of TTL in the past. HDELs will be propagated.
r hsetex h3 PX 0 FIELDS 1 f1 v111
r hsetex h3 PX 0 FIELDS 3 f1 v2 f2 v2 f3 v3
# HGETEX
r hsetex h4 FIELDS 3 f1 v1 f2 v2 f3 v3
# No change on expiry, it won't be replicated.
r hgetex h4 FIELDS 1 f1
# Commands with EX/PX/PXAT/EXAT will be replicated as
# HPEXPIREAT command.
r hgetex h4 EX 10000 FIELDS 1 f1
r hgetex h4 PX 10000 FIELDS 1 f1
r hgetex h4 PXAT [expr [clock milliseconds]+100000] FIELDS 1 f1
r hgetex h4 EXAT [expr [clock seconds]+100000] FIELDS 1 f1
# Following commands will delete the fields because of TTL in
# the past. HDELs will be propagated.
r hgetex h4 PX 0 FIELDS 1 f1
# HDELs will be propagated for f2 and f3 as only those exist.
r hgetex h4 PX 0 FIELDS 3 f1 f2 f3
# HGETEX with PERSIST flag will be replicated as HPERSIST
r hsetex h4 EX 1000 FIELDS 1 f4 v4
r hgetex h4 PERSIST FIELDS 1 f4
# Nothing will be replicated as f4 is persisted already.
r hgetex h4 PERSIST FIELDS 1 f4
# Replicated as hdel
r hgetdel h4 FIELDS 1 f4
# Assert that each TTL-related command are persisted with absolute timestamps in AOF
assert_aof_content $aof {
{select *}
@ -1068,6 +1562,33 @@ start_server {tags {"external:skip needs:debug"}} {
{hdel h1 f2}
{hdel h2 f1}
{hdel h2 f2}
{hsetex h3 FIELDS 1 f1 v1}
{hsetex h3 FXX FIELDS 1 f1 v11}
{hsetex h3 FNX FIELDS 1 f2 v22}
{hsetex h3 KEEPTTL FIELDS 1 f2 v22}
{hsetex h3 PXAT * 1 f1 v111}
{hsetex h3 PXAT * 1 f1 v111}
{hsetex h3 PXAT * 1 f1 v111}
{hsetex h3 PXAT * 1 f1 v111}
{hdel h3 f1}
{multi}
{hdel h3 f1}
{hdel h3 f2}
{hdel h3 f3}
{exec}
{hsetex h4 FIELDS 3 f1 v1 f2 v2 f3 v3}
{hpexpireat h4 * FIELDS 1 f1}
{hpexpireat h4 * FIELDS 1 f1}
{hpexpireat h4 * FIELDS 1 f1}
{hpexpireat h4 * FIELDS 1 f1}
{hdel h4 f1}
{multi}
{hdel h4 f2}
{hdel h4 f3}
{exec}
{hsetex h4 PXAT * FIELDS 1 f4 v4}
{hpersist h4 FIELDS 1 f4}
{hdel h4 f4}
}
}
} {} {needs:debug}
@ -1135,6 +1656,16 @@ start_server {tags {"external:skip needs:debug"}} {
r hpexpire h2 1 FIELDS 2 f1 f2
after 200
r hsetex h3 EX 100000 FIELDS 2 f1 v1 f2 v2
r hsetex h3 EXAT [expr [clock seconds] + 1000] FIELDS 2 f1 v1 f2 v2
r hsetex h3 PX 100000 FIELDS 2 f1 v1 f2 v2
r hsetex h3 PXAT [expr [clock milliseconds]+100000] FIELDS 2 f1 v1 f2 v2
r hgetex h3 EX 100000 FIELDS 2 f1 f2
r hgetex h3 EXAT [expr [clock seconds] + 1000] FIELDS 2 f1 f2
r hgetex h3 PX 100000 FIELDS 2 f1 f2
r hgetex h3 PXAT [expr [clock milliseconds]+100000] FIELDS 2 f1 f2
assert_aof_content $aof {
{select *}
{hset h1 f1 v1 f2 v2 f3 v3 f4 v4 f5 v5 f6 v6}
@ -1146,6 +1677,14 @@ start_server {tags {"external:skip needs:debug"}} {
{hpexpireat h2 * FIELDS 2 f1 f2}
{hdel h2 *}
{hdel h2 *}
{hsetex h3 PXAT * FIELDS 2 f1 v1 f2 v2}
{hsetex h3 PXAT * FIELDS 2 f1 v1 f2 v2}
{hsetex h3 PXAT * FIELDS 2 f1 v1 f2 v2}
{hsetex h3 PXAT * FIELDS 2 f1 v1 f2 v2}
{hpexpireat h3 * FIELDS 2 f1 f2}
{hpexpireat h3 * FIELDS 2 f1 f2}
{hpexpireat h3 * FIELDS 2 f1 f2}
{hpexpireat h3 * FIELDS 2 f1 f2}
}
array set keyAndFields1 [dumpAllHashes r]
@ -1265,6 +1804,23 @@ start_server {tags {"external:skip needs:debug"}} {
$primary hpexpireat h5 [expr [clock milliseconds]-100000] FIELDS 1 f
$primary hset h9 f v
$primary hsetex h10 EX 100000 FIELDS 1 f v
$primary hsetex h11 EXAT [expr [clock seconds] + 1000] FIELDS 1 f v
$primary hsetex h12 PX 100000 FIELDS 1 f v
$primary hsetex h13 PXAT [expr [clock milliseconds]+100000] FIELDS 1 f v
$primary hsetex h14 PXAT 1 FIELDS 1 f v
$primary hsetex h15 FIELDS 1 f v
$primary hgetex h15 EX 100000 FIELDS 1 f
$primary hsetex h16 FIELDS 1 f v
$primary hgetex h16 EXAT [expr [clock seconds] + 1000] FIELDS 1 f
$primary hsetex h17 FIELDS 1 f v
$primary hgetex h17 PX 100000 FIELDS 1 f
$primary hsetex h18 FIELDS 1 f v
$primary hgetex h18 PXAT [expr [clock milliseconds]+100000] FIELDS 1 f
$primary hsetex h19 FIELDS 1 f v
$primary hgetex h19 PXAT 1 FIELDS 1 f
# Wait for replica to get the keys and TTLs
assert {[$primary wait 1 0] == 1}
@ -1273,5 +1829,102 @@ start_server {tags {"external:skip needs:debug"}} {
assert_equal [dumpAllHashes $primary] [dumpAllHashes $replica]
}
}
test "Test HSETEX command replication" {
r flushall
set repl [attach_to_replication_stream]
# Create a field and delete it in a single command due to timestamp
# being in the past. It will be propagated as HDEL.
r hsetex h1 PXAT 1 FIELDS 1 f1 v1
# Following ones will be propagated with PXAT arg
r hsetex h1 EX 100000 FIELDS 1 f1 v1
r hsetex h1 EXAT [expr [clock seconds] + 1000] FIELDS 1 f1 v1
r hsetex h1 PX 100000 FIELDS 1 f1 v1
r hsetex h1 PXAT [expr [clock milliseconds]+100000] FIELDS 1 f1 v1
# Propagate with KEEPTTL flag
r hsetex h1 KEEPTTL FIELDS 1 f1 v1
# Following commands will fail and won't be propagated
r hsetex h1 FNX FIELDS 1 f1 v11
r hsetex h1 FXX FIELDS 1 f2 v2
# Propagate with FNX and FXX flags
r hsetex h1 FNX FIELDS 1 f2 v2
r hsetex h1 FXX FIELDS 1 f2 v22
assert_replication_stream $repl {
{select *}
{hdel h1 f1}
{hsetex h1 PXAT * FIELDS 1 f1 v1}
{hsetex h1 PXAT * FIELDS 1 f1 v1}
{hsetex h1 PXAT * FIELDS 1 f1 v1}
{hsetex h1 PXAT * FIELDS 1 f1 v1}
{hsetex h1 KEEPTTL FIELDS 1 f1 v1}
{hsetex h1 FNX FIELDS 1 f2 v2}
{hsetex h1 FXX FIELDS 1 f2 v22}
}
close_replication_stream $repl
} {} {needs:repl}
test "Test HGETEX command replication" {
r flushall
r debug set-active-expire 0
set repl [attach_to_replication_stream]
# If no fields are found, command won't be replicated
r hgetex h1 EX 10000 FIELDS 1 f0
r hgetex h1 PERSIST FIELDS 1 f0
# Get without setting expiry will not be replicated
r hsetex h1 FIELDS 1 f0 v0
r hgetex h1 FIELDS 1 f0
# Lazy expired field will be replicated as HDEL
r hsetex h1 PX 10 FIELDS 1 f1 v1
after 15
r hgetex h1 EX 1000 FIELDS 1 f1
# If new TTL is in the past, it will be replicated as HDEL
r hsetex h1 EX 10000 FIELDS 1 f2 v2
r hgetex h1 EXAT 1 FIELDS 1 f2
# A field will expire lazily and other field will be deleted due to
# TTL is being in the past. It'll be propagated as two HDEL's.
r hsetex h1 PX 10 FIELDS 1 f3 v3
after 15
r hsetex h1 FIELDS 1 f4 v4
r hgetex h1 EXAT 1 FIELDS 2 f3 f4
# TTL update, it will be replicated as HPEXPIREAT
r hsetex h1 FIELDS 1 f5 v5
r hgetex h1 EX 10000 FIELDS 1 f5
# If PERSIST flag is used, it will be replicated as HPERSIST
r hsetex h1 EX 10000 FIELDS 1 f6 v6
r hgetex h1 PERSIST FIELDS 1 f6
assert_replication_stream $repl {
{select *}
{hsetex h1 FIELDS 1 f0 v0}
{hsetex h1 PXAT * FIELDS 1 f1 v1}
{hdel h1 f1}
{hsetex h1 PXAT * FIELDS 1 f2 v2}
{hdel h1 f2}
{hsetex h1 PXAT * FIELDS 1 f3 v3}
{hsetex h1 FIELDS 1 f4 v4}
{multi}
{hdel h1 f3}
{hdel h1 f4}
{exec}
{hsetex h1 FIELDS 1 f5 v5}
{hpexpireat h1 * FIELDS 1 f5}
{hsetex h1 PXAT * FIELDS 1 f6 v6}
{hpersist h1 FIELDS 1 f6}
}
close_replication_stream $repl
} {} {needs:repl}
}
}

View File

@ -371,6 +371,7 @@ start_server {tags {"hash"}} {
assert_error "WRONGTYPE Operation against a key*" {r hsetnx wrongtype field1 val1}
assert_error "WRONGTYPE Operation against a key*" {r hlen wrongtype}
assert_error "WRONGTYPE Operation against a key*" {r hscan wrongtype 0}
assert_error "WRONGTYPE Operation against a key*" {r hgetdel wrongtype fields 1 a}
}
test {HMGET - small hash} {
@ -710,6 +711,89 @@ start_server {tags {"hash"}} {
r config set hash-max-listpack-value $original_max_value
}
test {HGETDEL input validation} {
r del key1
assert_error "*wrong number of arguments*" {r hgetdel}
assert_error "*wrong number of arguments*" {r hgetdel key1}
assert_error "*wrong number of arguments*" {r hgetdel key1 FIELDS}
assert_error "*wrong number of arguments*" {r hgetdel key1 FIELDS 0}
assert_error "*wrong number of arguments*" {r hgetdel key1 FIELDX}
assert_error "*argument FIELDS is missing*" {r hgetdel key1 XFIELDX 1 a}
assert_error "*numfields*parameter*must match*number of arguments*" {r hgetdel key1 FIELDS 2 a}
assert_error "*numfields*parameter*must match*number of arguments*" {r hgetdel key1 FIELDS 2 a b c}
assert_error "*Number of fields must be a positive integer*" {r hgetdel key1 FIELDS 0 a}
assert_error "*Number of fields must be a positive integer*" {r hgetdel key1 FIELDS -1 a}
assert_error "*Number of fields must be a positive integer*" {r hgetdel key1 FIELDS b a}
assert_error "*Number of fields must be a positive integer*" {r hgetdel key1 FIELDS 9223372036854775808 a}
}
foreach type {listpack ht} {
set orig_config [lindex [r config get hash-max-listpack-entries] 1]
r del key1
if {$type == "listpack"} {
r config set hash-max-listpack-entries $orig_config
r hset key1 f1 1 f2 2 f3 3 strfield strval
assert_encoding listpack key1
} else {
r config set hash-max-listpack-entries 0
r hset key1 f1 1 f2 2 f3 3 strfield strval
assert_encoding hashtable key1
}
test {HGETDEL basic test} {
r del key1
r hset key1 f1 1 f2 2 f3 3 strfield strval
assert_equal [r hgetdel key1 fields 1 f2] 2
assert_equal [r hlen key1] 3
assert_equal [r hget key1 f1] 1
assert_equal [r hget key1 f2] ""
assert_equal [r hget key1 f3] 3
assert_equal [r hget key1 strfield] strval
assert_equal [r hgetdel key1 fields 1 f1] 1
assert_equal [lsort [r hgetall key1]] [lsort "f3 3 strfield strval"]
assert_equal [r hgetdel key1 fields 1 f3] 3
assert_equal [r hgetdel key1 fields 1 strfield] strval
assert_equal [r hgetall key1] ""
assert_equal [r exists key1] 0
}
test {HGETDEL test with non existing fields} {
r del key1
r hset key1 f1 1 f2 2 f3 3
assert_equal [r hgetdel key1 fields 4 x1 x2 x3 x4] "{} {} {} {}"
assert_equal [r hgetdel key1 fields 4 x1 x2 f3 x4] "{} {} 3 {}"
assert_equal [lsort [r hgetall key1]] [lsort "f1 1 f2 2"]
assert_equal [r hgetdel key1 fields 3 f1 f2 f3] "1 2 {}"
assert_equal [r hgetdel key1 fields 3 f1 f2 f3] "{} {} {}"
}
r config set hash-max-listpack-entries $orig_config
}
test {HGETDEL propagated as HDEL command to replica} {
set repl [attach_to_replication_stream]
r hset key1 f1 v1 f2 v2 f3 v3 f4 v4 f5 v5
r hgetdel key1 fields 1 f1
r hgetdel key1 fields 2 f2 f3
# make sure non-existing fields are not replicated
r hgetdel key1 fields 2 f7 f8
# delete more
r hgetdel key1 fields 3 f4 f5 f6
assert_replication_stream $repl {
{select *}
{hset key1 f1 v1 f2 v2 f3 v3 f4 v4 f5 v5}
{hdel key1 f1}
{hdel key1 f2 f3}
{hdel key1 f4 f5 f6}
}
close_replication_stream $repl
} {} {needs:repl}
test {Hash ziplist regression test for large keys} {
r hset hash kkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk a
r hset hash kkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk b