diff --git a/src/Makefile b/src/Makefile index b8f66522c..cc84d09ad 100644 --- a/src/Makefile +++ b/src/Makefile @@ -354,7 +354,7 @@ endif REDIS_SERVER_NAME=redis-server$(PROG_SUFFIX) REDIS_SENTINEL_NAME=redis-sentinel$(PROG_SUFFIX) -REDIS_SERVER_OBJ=threads_mngr.o adlist.o quicklist.o ae.o anet.o dict.o kvstore.o server.o sds.o zmalloc.o lzf_c.o lzf_d.o pqsort.o zipmap.o sha1.o ziplist.o release.o networking.o util.o object.o db.o replication.o rdb.o t_string.o t_list.o t_set.o t_zset.o t_hash.o config.o aof.o pubsub.o multi.o debug.o sort.o intset.o syncio.o cluster.o cluster_legacy.o crc16.o endianconv.o slowlog.o eval.o bio.o rio.o rand.o memtest.o syscheck.o crcspeed.o crc64.o bitops.o sentinel.o notify.o setproctitle.o blocked.o hyperloglog.o latency.o sparkline.o redis-check-rdb.o redis-check-aof.o geo.o lazyfree.o module.o evict.o expire.o geohash.o geohash_helper.o childinfo.o defrag.o siphash.o rax.o t_stream.o listpack.o localtime.o lolwut.o lolwut5.o lolwut6.o acl.o tracking.o socket.o tls.o sha256.o timeout.o setcpuaffinity.o monotonic.o mt19937-64.o resp_parser.o call_reply.o script_lua.o script.o functions.o function_lua.o commands.o strl.o connection.o unix.o logreqres.o +REDIS_SERVER_OBJ=threads_mngr.o adlist.o quicklist.o ae.o anet.o dict.o ebuckets.o mstr.o kvstore.o server.o sds.o zmalloc.o lzf_c.o lzf_d.o pqsort.o zipmap.o sha1.o ziplist.o release.o networking.o util.o object.o db.o replication.o rdb.o t_string.o t_list.o t_set.o t_zset.o t_hash.o config.o aof.o pubsub.o multi.o debug.o sort.o intset.o syncio.o cluster.o cluster_legacy.o crc16.o endianconv.o slowlog.o eval.o bio.o rio.o rand.o memtest.o syscheck.o crcspeed.o crc64.o bitops.o sentinel.o notify.o setproctitle.o blocked.o hyperloglog.o latency.o sparkline.o redis-check-rdb.o redis-check-aof.o geo.o lazyfree.o module.o evict.o expire.o geohash.o geohash_helper.o childinfo.o defrag.o siphash.o rax.o t_stream.o listpack.o localtime.o lolwut.o lolwut5.o lolwut6.o acl.o tracking.o socket.o tls.o sha256.o timeout.o setcpuaffinity.o monotonic.o mt19937-64.o resp_parser.o call_reply.o script_lua.o script.o functions.o function_lua.o commands.o strl.o connection.o unix.o logreqres.o REDIS_CLI_NAME=redis-cli$(PROG_SUFFIX) REDIS_CLI_OBJ=anet.o adlist.o dict.o redis-cli.o zmalloc.o release.o ae.o redisassert.o crcspeed.o crc64.o siphash.o crc16.o monotonic.o cli_common.o mt19937-64.o strl.o cli_commands.o REDIS_BENCHMARK_NAME=redis-benchmark$(PROG_SUFFIX) diff --git a/src/aof.c b/src/aof.c index 3987bb296..17e72febb 100644 --- a/src/aof.c +++ b/src/aof.c @@ -1939,19 +1939,21 @@ int rewriteSortedSetObject(rio *r, robj *key, robj *o) { * * The function returns 0 on error, non-zero on success. */ static int rioWriteHashIteratorCursor(rio *r, hashTypeIterator *hi, int what) { - if (hi->encoding == OBJ_ENCODING_LISTPACK) { + if ((hi->encoding == OBJ_ENCODING_LISTPACK) || (hi->encoding == OBJ_ENCODING_LISTPACK_EX)) { unsigned char *vstr = NULL; unsigned int vlen = UINT_MAX; long long vll = LLONG_MAX; - hashTypeCurrentFromListpack(hi, what, &vstr, &vlen, &vll); + hashTypeCurrentFromListpack(hi, what, &vstr, &vlen, &vll, NULL); if (vstr) return rioWriteBulkString(r, (char*)vstr, vlen); else return rioWriteBulkLongLong(r, vll); } else if (hi->encoding == OBJ_ENCODING_HT) { - sds value = hashTypeCurrentFromHashTable(hi, what); - return rioWriteBulkString(r, value, sdslen(value)); + char *str; + size_t len; + hashTypeCurrentFromHashTable(hi, what, &str, &len, NULL); + return rioWriteBulkString(r, str, len); } serverPanic("Unknown hash encoding"); @@ -1961,37 +1963,60 @@ static int rioWriteHashIteratorCursor(rio *r, hashTypeIterator *hi, int what) { /* Emit the commands needed to rebuild a hash object. * The function returns 0 on error, 1 on success. */ int rewriteHashObject(rio *r, robj *key, robj *o) { + int res = 0; /*fail*/ + hashTypeIterator *hi; - long long count = 0, items = hashTypeLength(o); + long long count = 0, items = hashTypeLength(o, 0); + int isHFE = hashTypeGetMinExpire(o) != EB_EXPIRE_TIME_INVALID; hi = hashTypeInitIterator(o); - while (hashTypeNext(hi) != C_ERR) { - if (count == 0) { - int cmd_items = (items > AOF_REWRITE_ITEMS_PER_CMD) ? - AOF_REWRITE_ITEMS_PER_CMD : items; - if (!rioWriteBulkCount(r,'*',2+cmd_items*2) || - !rioWriteBulkString(r,"HMSET",5) || - !rioWriteBulkObject(r,key)) - { - hashTypeReleaseIterator(hi); - return 0; + if (!isHFE) { + while (hashTypeNext(hi, 0) != C_ERR) { + if (count == 0) { + int cmd_items = (items > AOF_REWRITE_ITEMS_PER_CMD) ? + AOF_REWRITE_ITEMS_PER_CMD : items; + if (!rioWriteBulkCount(r, '*', 2 + cmd_items * 2) || + !rioWriteBulkString(r, "HMSET", 5) || + !rioWriteBulkObject(r, key)) + goto reHashEnd; + } + + if (!rioWriteHashIteratorCursor(r, hi, OBJ_HASH_KEY) || + !rioWriteHashIteratorCursor(r, hi, OBJ_HASH_VALUE)) + goto reHashEnd; + + if (++count == AOF_REWRITE_ITEMS_PER_CMD) count = 0; + items--; + } + } else { + while (hashTypeNext(hi, 0) != C_ERR) { + + char hmsetCmd[] = "*4\r\n$5\r\nHMSET\r\n"; + if ( (!rioWrite(r, hmsetCmd, sizeof(hmsetCmd) - 1)) || + (!rioWriteBulkObject(r, key)) || + (!rioWriteHashIteratorCursor(r, hi, OBJ_HASH_KEY)) || + (!rioWriteHashIteratorCursor(r, hi, OBJ_HASH_VALUE)) ) + goto reHashEnd; + + if (hi->expire_time != EB_EXPIRE_TIME_INVALID) { + char cmd[] = "*6\r\n$10\r\nHPEXPIREAT\r\n"; + if ( (!rioWrite(r, cmd, sizeof(cmd) - 1)) || + (!rioWriteBulkObject(r, key)) || + (!rioWriteBulkLongLong(r, hi->expire_time)) || + (!rioWriteBulkString(r, "FIELDS", 6)) || + (!rioWriteBulkString(r, "1", 1)) || + (!rioWriteHashIteratorCursor(r, hi, OBJ_HASH_KEY)) ) + goto reHashEnd; } } - - if (!rioWriteHashIteratorCursor(r, hi, OBJ_HASH_KEY) || - !rioWriteHashIteratorCursor(r, hi, OBJ_HASH_VALUE)) - { - hashTypeReleaseIterator(hi); - return 0; - } - if (++count == AOF_REWRITE_ITEMS_PER_CMD) count = 0; - items--; } - hashTypeReleaseIterator(hi); + res = 1; /* success */ - return 1; +reHashEnd: + hashTypeReleaseIterator(hi); + return res; } /* Helper for rewriteStreamObject() that generates a bulk string into the diff --git a/src/cluster.c b/src/cluster.c index 6674c3aa0..9472a018d 100644 --- a/src/cluster.c +++ b/src/cluster.c @@ -176,6 +176,7 @@ void dumpCommand(client *c) { /* RESTORE key ttl serialized-value [REPLACE] [ABSTTL] [IDLETIME seconds] [FREQ frequency] */ void restoreCommand(client *c) { + uint64_t minExpiredField = EB_EXPIRE_TIME_INVALID; long long ttl, lfu_freq = -1, lru_idle = -1, lru_clock = -1; rio payload; int j, type, replace = 0, absttl = 0; @@ -239,7 +240,7 @@ void restoreCommand(client *c) { rioInitWithBuffer(&payload,c->argv[3]->ptr); if (((type = rdbLoadObjectType(&payload)) == -1) || - ((obj = rdbLoadObject(type,&payload,key->ptr,c->db->id,NULL)) == NULL)) + ((obj = rdbLoadObject(type,&payload,key->ptr,c->db,NULL, &minExpiredField)) == NULL)) { addReplyError(c,"Bad data format"); return; @@ -265,7 +266,13 @@ void restoreCommand(client *c) { } /* Create the key and set the TTL if any */ - dbAdd(c->db,key,obj); + dictEntry *de = dbAdd(c->db,key,obj); + + /* If minExpiredField was set, then the object is hash with expiration + * on fields and need to register it in global HFE DS */ + if (minExpiredField != EB_EXPIRE_TIME_INVALID) + hashTypeAddToExpires(c->db, dictGetKey(de), obj, minExpiredField); + if (ttl) { setExpire(c,c->db,key,ttl); if (!absttl) { diff --git a/src/commands.def b/src/commands.def index df8085bab..dea507cf6 100644 --- a/src/commands.def +++ b/src/commands.def @@ -3303,6 +3303,107 @@ struct COMMAND_ARG HEXISTS_Args[] = { {MAKE_ARG("field",ARG_TYPE_STRING,-1,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)}, }; +/********** HEXPIRE ********************/ + +#ifndef SKIP_CMD_HISTORY_TABLE +/* HEXPIRE history */ +#define HEXPIRE_History NULL +#endif + +#ifndef SKIP_CMD_TIPS_TABLE +/* HEXPIRE tips */ +#define HEXPIRE_Tips NULL +#endif + +#ifndef SKIP_CMD_KEY_SPECS_TABLE +/* HEXPIRE key specs */ +keySpec HEXPIRE_Keyspecs[1] = { +{NULL,CMD_KEY_RW|CMD_KEY_UPDATE,KSPEC_BS_INDEX,.bs.index={1},KSPEC_FK_RANGE,.fk.range={0,1,0}} +}; +#endif + +/* HEXPIRE condition argument table */ +struct COMMAND_ARG HEXPIRE_condition_Subargs[] = { +{MAKE_ARG("nx",ARG_TYPE_PURE_TOKEN,-1,"NX",NULL,NULL,CMD_ARG_NONE,0,NULL)}, +{MAKE_ARG("xx",ARG_TYPE_PURE_TOKEN,-1,"XX",NULL,NULL,CMD_ARG_NONE,0,NULL)}, +{MAKE_ARG("gt",ARG_TYPE_PURE_TOKEN,-1,"GT",NULL,NULL,CMD_ARG_NONE,0,NULL)}, +{MAKE_ARG("lt",ARG_TYPE_PURE_TOKEN,-1,"LT",NULL,NULL,CMD_ARG_NONE,0,NULL)}, +}; + +/* HEXPIRE argument table */ +struct COMMAND_ARG HEXPIRE_Args[] = { +{MAKE_ARG("key",ARG_TYPE_KEY,0,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)}, +{MAKE_ARG("seconds",ARG_TYPE_INTEGER,-1,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)}, +{MAKE_ARG("condition",ARG_TYPE_ONEOF,-1,NULL,NULL,NULL,CMD_ARG_OPTIONAL,4,NULL),.subargs=HEXPIRE_condition_Subargs}, +{MAKE_ARG("fields",ARG_TYPE_STRING,-1,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)}, +{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)}, +}; + +/********** HEXPIREAT ********************/ + +#ifndef SKIP_CMD_HISTORY_TABLE +/* HEXPIREAT history */ +#define HEXPIREAT_History NULL +#endif + +#ifndef SKIP_CMD_TIPS_TABLE +/* HEXPIREAT tips */ +#define HEXPIREAT_Tips NULL +#endif + +#ifndef SKIP_CMD_KEY_SPECS_TABLE +/* HEXPIREAT key specs */ +keySpec HEXPIREAT_Keyspecs[1] = { +{NULL,CMD_KEY_RW|CMD_KEY_UPDATE,KSPEC_BS_INDEX,.bs.index={1},KSPEC_FK_RANGE,.fk.range={0,1,0}} +}; +#endif + +/* HEXPIREAT condition argument table */ +struct COMMAND_ARG HEXPIREAT_condition_Subargs[] = { +{MAKE_ARG("nx",ARG_TYPE_PURE_TOKEN,-1,"NX",NULL,NULL,CMD_ARG_NONE,0,NULL)}, +{MAKE_ARG("xx",ARG_TYPE_PURE_TOKEN,-1,"XX",NULL,NULL,CMD_ARG_NONE,0,NULL)}, +{MAKE_ARG("gt",ARG_TYPE_PURE_TOKEN,-1,"GT",NULL,NULL,CMD_ARG_NONE,0,NULL)}, +{MAKE_ARG("lt",ARG_TYPE_PURE_TOKEN,-1,"LT",NULL,NULL,CMD_ARG_NONE,0,NULL)}, +}; + +/* HEXPIREAT argument table */ +struct COMMAND_ARG HEXPIREAT_Args[] = { +{MAKE_ARG("key",ARG_TYPE_KEY,0,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)}, +{MAKE_ARG("unix-time-seconds",ARG_TYPE_UNIX_TIME,-1,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)}, +{MAKE_ARG("condition",ARG_TYPE_ONEOF,-1,NULL,NULL,NULL,CMD_ARG_OPTIONAL,4,NULL),.subargs=HEXPIREAT_condition_Subargs}, +{MAKE_ARG("fields",ARG_TYPE_STRING,-1,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)}, +{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)}, +}; + +/********** HEXPIRETIME ********************/ + +#ifndef SKIP_CMD_HISTORY_TABLE +/* HEXPIRETIME history */ +#define HEXPIRETIME_History NULL +#endif + +#ifndef SKIP_CMD_TIPS_TABLE +/* HEXPIRETIME tips */ +#define HEXPIRETIME_Tips NULL +#endif + +#ifndef SKIP_CMD_KEY_SPECS_TABLE +/* HEXPIRETIME key specs */ +keySpec HEXPIRETIME_Keyspecs[1] = { +{NULL,CMD_KEY_RO|CMD_KEY_ACCESS,KSPEC_BS_INDEX,.bs.index={1},KSPEC_FK_RANGE,.fk.range={0,1,0}} +}; +#endif + +/* HEXPIRETIME argument table */ +struct COMMAND_ARG HEXPIRETIME_Args[] = { +{MAKE_ARG("key",ARG_TYPE_KEY,0,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)}, +{MAKE_ARG("fields",ARG_TYPE_STRING,-1,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)}, +{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)}, +}; + /********** HGET ********************/ #ifndef SKIP_CMD_HISTORY_TABLE @@ -3512,6 +3613,161 @@ struct COMMAND_ARG HMSET_Args[] = { {MAKE_ARG("data",ARG_TYPE_BLOCK,-1,NULL,NULL,NULL,CMD_ARG_MULTIPLE,2,NULL),.subargs=HMSET_data_Subargs}, }; +/********** HPERSIST ********************/ + +#ifndef SKIP_CMD_HISTORY_TABLE +/* HPERSIST history */ +#define HPERSIST_History NULL +#endif + +#ifndef SKIP_CMD_TIPS_TABLE +/* HPERSIST tips */ +#define HPERSIST_Tips NULL +#endif + +#ifndef SKIP_CMD_KEY_SPECS_TABLE +/* HPERSIST key specs */ +keySpec HPERSIST_Keyspecs[1] = { +{NULL,CMD_KEY_RW|CMD_KEY_UPDATE,KSPEC_BS_INDEX,.bs.index={1},KSPEC_FK_RANGE,.fk.range={0,1,0}} +}; +#endif + +/* HPERSIST argument table */ +struct COMMAND_ARG HPERSIST_Args[] = { +{MAKE_ARG("key",ARG_TYPE_KEY,0,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)}, +{MAKE_ARG("fields",ARG_TYPE_STRING,-1,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)}, +{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)}, +}; + +/********** HPEXPIRE ********************/ + +#ifndef SKIP_CMD_HISTORY_TABLE +/* HPEXPIRE history */ +#define HPEXPIRE_History NULL +#endif + +#ifndef SKIP_CMD_TIPS_TABLE +/* HPEXPIRE tips */ +#define HPEXPIRE_Tips NULL +#endif + +#ifndef SKIP_CMD_KEY_SPECS_TABLE +/* HPEXPIRE key specs */ +keySpec HPEXPIRE_Keyspecs[1] = { +{NULL,CMD_KEY_RW|CMD_KEY_UPDATE,KSPEC_BS_INDEX,.bs.index={1},KSPEC_FK_RANGE,.fk.range={0,1,0}} +}; +#endif + +/* HPEXPIRE condition argument table */ +struct COMMAND_ARG HPEXPIRE_condition_Subargs[] = { +{MAKE_ARG("nx",ARG_TYPE_PURE_TOKEN,-1,"NX",NULL,NULL,CMD_ARG_NONE,0,NULL)}, +{MAKE_ARG("xx",ARG_TYPE_PURE_TOKEN,-1,"XX",NULL,NULL,CMD_ARG_NONE,0,NULL)}, +{MAKE_ARG("gt",ARG_TYPE_PURE_TOKEN,-1,"GT",NULL,NULL,CMD_ARG_NONE,0,NULL)}, +{MAKE_ARG("lt",ARG_TYPE_PURE_TOKEN,-1,"LT",NULL,NULL,CMD_ARG_NONE,0,NULL)}, +}; + +/* HPEXPIRE argument table */ +struct COMMAND_ARG HPEXPIRE_Args[] = { +{MAKE_ARG("key",ARG_TYPE_KEY,0,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)}, +{MAKE_ARG("milliseconds",ARG_TYPE_INTEGER,-1,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)}, +{MAKE_ARG("condition",ARG_TYPE_ONEOF,-1,NULL,NULL,NULL,CMD_ARG_OPTIONAL,4,NULL),.subargs=HPEXPIRE_condition_Subargs}, +{MAKE_ARG("fields",ARG_TYPE_STRING,-1,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)}, +{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)}, +}; + +/********** HPEXPIREAT ********************/ + +#ifndef SKIP_CMD_HISTORY_TABLE +/* HPEXPIREAT history */ +#define HPEXPIREAT_History NULL +#endif + +#ifndef SKIP_CMD_TIPS_TABLE +/* HPEXPIREAT tips */ +#define HPEXPIREAT_Tips NULL +#endif + +#ifndef SKIP_CMD_KEY_SPECS_TABLE +/* HPEXPIREAT key specs */ +keySpec HPEXPIREAT_Keyspecs[1] = { +{NULL,CMD_KEY_RW|CMD_KEY_UPDATE,KSPEC_BS_INDEX,.bs.index={1},KSPEC_FK_RANGE,.fk.range={0,1,0}} +}; +#endif + +/* HPEXPIREAT condition argument table */ +struct COMMAND_ARG HPEXPIREAT_condition_Subargs[] = { +{MAKE_ARG("nx",ARG_TYPE_PURE_TOKEN,-1,"NX",NULL,NULL,CMD_ARG_NONE,0,NULL)}, +{MAKE_ARG("xx",ARG_TYPE_PURE_TOKEN,-1,"XX",NULL,NULL,CMD_ARG_NONE,0,NULL)}, +{MAKE_ARG("gt",ARG_TYPE_PURE_TOKEN,-1,"GT",NULL,NULL,CMD_ARG_NONE,0,NULL)}, +{MAKE_ARG("lt",ARG_TYPE_PURE_TOKEN,-1,"LT",NULL,NULL,CMD_ARG_NONE,0,NULL)}, +}; + +/* HPEXPIREAT argument table */ +struct COMMAND_ARG HPEXPIREAT_Args[] = { +{MAKE_ARG("key",ARG_TYPE_KEY,0,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)}, +{MAKE_ARG("unix-time-milliseconds",ARG_TYPE_UNIX_TIME,-1,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)}, +{MAKE_ARG("condition",ARG_TYPE_ONEOF,-1,NULL,NULL,NULL,CMD_ARG_OPTIONAL,4,NULL),.subargs=HPEXPIREAT_condition_Subargs}, +{MAKE_ARG("fields",ARG_TYPE_STRING,-1,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)}, +{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)}, +}; + +/********** HPEXPIRETIME ********************/ + +#ifndef SKIP_CMD_HISTORY_TABLE +/* HPEXPIRETIME history */ +#define HPEXPIRETIME_History NULL +#endif + +#ifndef SKIP_CMD_TIPS_TABLE +/* HPEXPIRETIME tips */ +#define HPEXPIRETIME_Tips NULL +#endif + +#ifndef SKIP_CMD_KEY_SPECS_TABLE +/* HPEXPIRETIME key specs */ +keySpec HPEXPIRETIME_Keyspecs[1] = { +{NULL,CMD_KEY_RO|CMD_KEY_ACCESS,KSPEC_BS_INDEX,.bs.index={1},KSPEC_FK_RANGE,.fk.range={0,1,0}} +}; +#endif + +/* HPEXPIRETIME argument table */ +struct COMMAND_ARG HPEXPIRETIME_Args[] = { +{MAKE_ARG("key",ARG_TYPE_KEY,0,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)}, +{MAKE_ARG("fields",ARG_TYPE_STRING,-1,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)}, +{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)}, +}; + +/********** HPTTL ********************/ + +#ifndef SKIP_CMD_HISTORY_TABLE +/* HPTTL history */ +#define HPTTL_History NULL +#endif + +#ifndef SKIP_CMD_TIPS_TABLE +/* HPTTL tips */ +#define HPTTL_Tips NULL +#endif + +#ifndef SKIP_CMD_KEY_SPECS_TABLE +/* HPTTL key specs */ +keySpec HPTTL_Keyspecs[1] = { +{NULL,CMD_KEY_RO|CMD_KEY_ACCESS,KSPEC_BS_INDEX,.bs.index={1},KSPEC_FK_RANGE,.fk.range={0,1,0}} +}; +#endif + +/* HPTTL argument table */ +struct COMMAND_ARG HPTTL_Args[] = { +{MAKE_ARG("key",ARG_TYPE_KEY,0,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)}, +{MAKE_ARG("fields",ARG_TYPE_STRING,-1,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)}, +{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)}, +}; + /********** HRANDFIELD ********************/ #ifndef SKIP_CMD_HISTORY_TABLE @@ -3659,6 +3915,33 @@ struct COMMAND_ARG HSTRLEN_Args[] = { {MAKE_ARG("field",ARG_TYPE_STRING,-1,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)}, }; +/********** HTTL ********************/ + +#ifndef SKIP_CMD_HISTORY_TABLE +/* HTTL history */ +#define HTTL_History NULL +#endif + +#ifndef SKIP_CMD_TIPS_TABLE +/* HTTL tips */ +#define HTTL_Tips NULL +#endif + +#ifndef SKIP_CMD_KEY_SPECS_TABLE +/* HTTL key specs */ +keySpec HTTL_Keyspecs[1] = { +{NULL,CMD_KEY_RO|CMD_KEY_ACCESS,KSPEC_BS_INDEX,.bs.index={1},KSPEC_FK_RANGE,.fk.range={0,1,0}} +}; +#endif + +/* HTTL argument table */ +struct COMMAND_ARG HTTL_Args[] = { +{MAKE_ARG("key",ARG_TYPE_KEY,0,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)}, +{MAKE_ARG("fields",ARG_TYPE_STRING,-1,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)}, +{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)}, +}; + /********** HVALS ********************/ #ifndef SKIP_CMD_HISTORY_TABLE @@ -10710,6 +10993,9 @@ struct COMMAND_STRUCT redisCommandTable[] = { /* hash */ {MAKE_CMD("hdel","Deletes one or more fields and their values from a hash. Deletes the hash if no fields remain.","O(N) where N is the number of fields to be removed.","2.0.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HDEL_History,1,HDEL_Tips,0,hdelCommand,-3,CMD_WRITE|CMD_FAST,ACL_CATEGORY_HASH,HDEL_Keyspecs,1,NULL,2),.args=HDEL_Args}, {MAKE_CMD("hexists","Determines whether a field exists in a hash.","O(1)","2.0.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HEXISTS_History,0,HEXISTS_Tips,0,hexistsCommand,3,CMD_READONLY|CMD_FAST,ACL_CATEGORY_HASH,HEXISTS_Keyspecs,1,NULL,2),.args=HEXISTS_Args}, +{MAKE_CMD("hexpire","Set expiry for hash field using relative time to expire (seconds)","O(N) where N is the number of specified fields","7.4.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HEXPIRE_History,0,HEXPIRE_Tips,0,hexpireCommand,-6,CMD_WRITE|CMD_DENYOOM|CMD_FAST,ACL_CATEGORY_HASH,HEXPIRE_Keyspecs,1,NULL,6),.args=HEXPIRE_Args}, +{MAKE_CMD("hexpireat","Set expiry for hash field using an absolute Unix timestamp (seconds)","O(N) where N is the number of specified fields","7.4.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HEXPIREAT_History,0,HEXPIREAT_Tips,0,hexpireatCommand,-6,CMD_WRITE|CMD_DENYOOM|CMD_FAST,ACL_CATEGORY_HASH,HEXPIREAT_Keyspecs,1,NULL,6),.args=HEXPIREAT_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,4),.args=HEXPIRETIME_Args}, {MAKE_CMD("hget","Returns the value of a field in a hash.","O(1)","2.0.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HGET_History,0,HGET_Tips,0,hgetCommand,3,CMD_READONLY|CMD_FAST,ACL_CATEGORY_HASH,HGET_Keyspecs,1,NULL,2),.args=HGET_Args}, {MAKE_CMD("hgetall","Returns all fields and values in a hash.","O(N) where N is the size of the hash.","2.0.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HGETALL_History,0,HGETALL_Tips,1,hgetallCommand,2,CMD_READONLY,ACL_CATEGORY_HASH,HGETALL_Keyspecs,1,NULL,1),.args=HGETALL_Args}, {MAKE_CMD("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}, @@ -10718,11 +11004,17 @@ struct COMMAND_STRUCT redisCommandTable[] = { {MAKE_CMD("hlen","Returns the number of fields in a hash.","O(1)","2.0.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HLEN_History,0,HLEN_Tips,0,hlenCommand,2,CMD_READONLY|CMD_FAST,ACL_CATEGORY_HASH,HLEN_Keyspecs,1,NULL,1),.args=HLEN_Args}, {MAKE_CMD("hmget","Returns the values of all fields in a hash.","O(N) where N is the number of fields being requested.","2.0.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HMGET_History,0,HMGET_Tips,0,hmgetCommand,-3,CMD_READONLY|CMD_FAST,ACL_CATEGORY_HASH,HMGET_Keyspecs,1,NULL,2),.args=HMGET_Args}, {MAKE_CMD("hmset","Sets the values of multiple fields.","O(N) where N is the number of fields being set.","2.0.0",CMD_DOC_DEPRECATED,"`HSET` with multiple field-value pairs","4.0.0","hash",COMMAND_GROUP_HASH,HMSET_History,0,HMSET_Tips,0,hsetCommand,-4,CMD_WRITE|CMD_DENYOOM|CMD_FAST,ACL_CATEGORY_HASH,HMSET_Keyspecs,1,NULL,2),.args=HMSET_Args}, +{MAKE_CMD("hpersist","Removes the expiration time for each specified field","O(N) where N is the number of specified fields","7.4.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HPERSIST_History,0,HPERSIST_Tips,0,hpersistCommand,-5,CMD_WRITE|CMD_FAST,ACL_CATEGORY_HASH,HPERSIST_Keyspecs,1,NULL,4),.args=HPERSIST_Args}, +{MAKE_CMD("hpexpire","Set expiry for hash field using relative time to expire (milliseconds)","O(N) where N is the number of specified fields","7.4.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HPEXPIRE_History,0,HPEXPIRE_Tips,0,hpexpireCommand,-6,CMD_WRITE|CMD_DENYOOM|CMD_FAST,ACL_CATEGORY_HASH,HPEXPIRE_Keyspecs,1,NULL,6),.args=HPEXPIRE_Args}, +{MAKE_CMD("hpexpireat","Set expiry for hash field using an absolute Unix timestamp (milliseconds)","O(N) where N is the number of specified fields","7.4.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HPEXPIREAT_History,0,HPEXPIREAT_Tips,0,hpexpireatCommand,-6,CMD_WRITE|CMD_DENYOOM|CMD_FAST,ACL_CATEGORY_HASH,HPEXPIREAT_Keyspecs,1,NULL,6),.args=HPEXPIREAT_Args}, +{MAKE_CMD("hpexpiretime","Returns the expiration time of a hash field as a Unix timestamp, in msec.","O(N) where N is the number of specified fields","7.4.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HPEXPIRETIME_History,0,HPEXPIRETIME_Tips,0,hpexpiretimeCommand,-5,CMD_READONLY|CMD_FAST,ACL_CATEGORY_HASH,HPEXPIRETIME_Keyspecs,1,NULL,4),.args=HPEXPIRETIME_Args}, +{MAKE_CMD("hpttl","Returns the TTL in milliseconds 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,HPTTL_History,0,HPTTL_Tips,0,hpttlCommand,-5,CMD_READONLY|CMD_FAST,ACL_CATEGORY_HASH,HPTTL_Keyspecs,1,NULL,4),.args=HPTTL_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("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("hsetnx","Sets the value of a field in a hash only when the field doesn't exist.","O(1)","2.0.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HSETNX_History,0,HSETNX_Tips,0,hsetnxCommand,4,CMD_WRITE|CMD_DENYOOM|CMD_FAST,ACL_CATEGORY_HASH,HSETNX_Keyspecs,1,NULL,3),.args=HSETNX_Args}, {MAKE_CMD("hstrlen","Returns the length of the value of a field.","O(1)","3.2.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HSTRLEN_History,0,HSTRLEN_Tips,0,hstrlenCommand,3,CMD_READONLY|CMD_FAST,ACL_CATEGORY_HASH,HSTRLEN_Keyspecs,1,NULL,2),.args=HSTRLEN_Args}, +{MAKE_CMD("httl","Returns the TTL in seconds of a hash field.","O(N) where N is the number of specified fields","7.4.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HTTL_History,0,HTTL_Tips,0,httlCommand,-5,CMD_READONLY|CMD_FAST,ACL_CATEGORY_HASH,HTTL_Keyspecs,1,NULL,4),.args=HTTL_Args}, {MAKE_CMD("hvals","Returns all 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,HVALS_History,0,HVALS_Tips,1,hvalsCommand,2,CMD_READONLY,ACL_CATEGORY_HASH,HVALS_Keyspecs,1,NULL,1),.args=HVALS_Args}, /* hyperloglog */ {MAKE_CMD("pfadd","Adds elements to a HyperLogLog key. Creates the key if it doesn't exist.","O(1) to add every element.","2.8.9",CMD_DOC_NONE,NULL,NULL,"hyperloglog",COMMAND_GROUP_HYPERLOGLOG,PFADD_History,0,PFADD_Tips,0,pfaddCommand,-2,CMD_WRITE|CMD_DENYOOM|CMD_FAST,ACL_CATEGORY_HYPERLOGLOG,PFADD_Keyspecs,1,NULL,2),.args=PFADD_Args}, diff --git a/src/commands/hexpire.json b/src/commands/hexpire.json new file mode 100644 index 000000000..8a2154785 --- /dev/null +++ b/src/commands/hexpire.json @@ -0,0 +1,116 @@ +{ + "HEXPIRE": { + "summary": "Set expiry for hash field using relative time to expire (seconds)", + "complexity": "O(N) where N is the number of specified fields", + "group": "hash", + "since": "7.4.0", + "arity": -6, + "function": "hexpireCommand", + "history": [], + "command_flags": [ + "WRITE", + "DENYOOM", + "FAST" + ], + "acl_categories": [ + "HASH" + ], + "key_specs": [ + { + "flags": [ + "RW", + "UPDATE" + ], + "begin_search": { + "index": { + "pos": 1 + } + }, + "find_keys": { + "range": { + "lastkey": 0, + "step": 1, + "limit": 0 + } + } + } + ], + "reply_schema": { + "description": "Array of results. Returns empty array if the key does not exist.", + "type": "array", + "minItems": 0, + "maxItems": 4294967295, + "items": { + "oneOf": [ + { + "description": "The field does not exist.", + "const": -2 + }, + { + "description": "Specified NX | XX | GT | LT condition not met", + "const": 0 + }, + { + "description": "Expiration time was set or updated.", + "const": 1 + }, + { + "description": "Field deleted because the specified expiration time is in the past.", + "const": 2 + } + ] + } + }, + "arguments": [ + { + "name": "key", + "type": "key", + "key_spec_index": 0 + }, + { + "name": "seconds", + "type": "integer" + }, + { + "name": "condition", + "type": "oneof", + "optional": true, + "arguments": [ + { + "name": "nx", + "type": "pure-token", + "token": "NX" + }, + { + "name": "xx", + "type": "pure-token", + "token": "XX" + }, + { + "name": "gt", + "type": "pure-token", + "token": "GT" + }, + { + "name": "lt", + "type": "pure-token", + "token": "LT" + } + ] + }, + { + "name": "FIELDS", + "type": "string" + }, + { + "name": "numfields", + "type": "integer" + }, + { + "name": "field", + "type": "string", + "multiple": true + } + ] + } +} diff --git a/src/commands/hexpireat.json b/src/commands/hexpireat.json new file mode 100644 index 000000000..9ad178276 --- /dev/null +++ b/src/commands/hexpireat.json @@ -0,0 +1,116 @@ +{ + "HEXPIREAT": { + "summary": "Set expiry for hash field using an absolute Unix timestamp (seconds)", + "complexity": "O(N) where N is the number of specified fields", + "group": "hash", + "since": "7.4.0", + "arity": -6, + "function": "hexpireatCommand", + "history": [], + "command_flags": [ + "WRITE", + "DENYOOM", + "FAST" + ], + "acl_categories": [ + "HASH" + ], + "key_specs": [ + { + "flags": [ + "RW", + "UPDATE" + ], + "begin_search": { + "index": { + "pos": 1 + } + }, + "find_keys": { + "range": { + "lastkey": 0, + "step": 1, + "limit": 0 + } + } + } + ], + "reply_schema": { + "description": "Array of results. Returns empty array if the key does not exist.", + "type": "array", + "minItems": 0, + "maxItems": 4294967295, + "items": { + "oneOf": [ + { + "description": "The field does not exist.", + "const": -2 + }, + { + "description": "Specified NX | XX | GT | LT condition not met", + "const": 0 + }, + { + "description": "Expiration time was set or updated.", + "const": 1 + }, + { + "description": "Field deleted because the specified expiration time is in the past.", + "const": 2 + } + ] + } + }, + "arguments": [ + { + "name": "key", + "type": "key", + "key_spec_index": 0 + }, + { + "name": "unix-time-seconds", + "type": "unix-time" + }, + { + "name": "condition", + "type": "oneof", + "optional": true, + "arguments": [ + { + "name": "nx", + "type": "pure-token", + "token": "NX" + }, + { + "name": "xx", + "type": "pure-token", + "token": "XX" + }, + { + "name": "gt", + "type": "pure-token", + "token": "GT" + }, + { + "name": "lt", + "type": "pure-token", + "token": "LT" + } + ] + }, + { + "name": "FIELDS", + "type": "string" + }, + { + "name": "numfields", + "type": "integer" + }, + { + "name": "field", + "type": "string", + "multiple": true + } + ] + } +} \ No newline at end of file diff --git a/src/commands/hexpiretime.json b/src/commands/hexpiretime.json new file mode 100644 index 000000000..c35b5d39d --- /dev/null +++ b/src/commands/hexpiretime.json @@ -0,0 +1,81 @@ +{ + "HEXPIRETIME": { + "summary": "Returns the expiration time of a hash field as a Unix timestamp, in seconds.", + "complexity": "O(N) where N is the number of specified fields", + "group": "hash", + "since": "7.4.0", + "arity": -5, + "function": "hexpiretimeCommand", + "history": [], + "command_flags": [ + "READONLY", + "FAST" + ], + "acl_categories": [ + "HASH" + ], + "key_specs": [ + { + "flags": [ + "RO", + "ACCESS" + ], + "begin_search": { + "index": { + "pos": 1 + } + }, + "find_keys": { + "range": { + "lastkey": 0, + "step": 1, + "limit": 0 + } + } + } + ], + "reply_schema": { + "description": "Array of results. Returns empty array if the key does not exist.", + "type": "array", + "minItems": 0, + "maxItems": 4294967295, + "items": { + "oneOf": [ + { + "description": "The field does not exist.", + "const": -2 + }, + { + "description": "The field exists but has no associated expire.", + "const": -1 + }, + { + "description": "Expiration Unix timestamp in seconds.", + "type": "integer", + "minimum": 1 + } + ] + } + }, + "arguments": [ + { + "name": "key", + "type": "key", + "key_spec_index": 0 + }, + { + "name": "FIELDS", + "type": "string" + }, + { + "name": "numfields", + "type": "integer" + }, + { + "name": "field", + "type": "string", + "multiple": true + } + ] + } +} diff --git a/src/commands/hpersist.json b/src/commands/hpersist.json new file mode 100644 index 000000000..ba79044d1 --- /dev/null +++ b/src/commands/hpersist.json @@ -0,0 +1,80 @@ +{ + "HPERSIST": { + "summary": "Removes the expiration time for each specified field", + "complexity": "O(N) where N is the number of specified fields", + "group": "hash", + "since": "7.4.0", + "arity": -5, + "function": "hpersistCommand", + "history": [], + "command_flags": [ + "WRITE", + "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": { + "description": "Array of results. Returns empty array if the key does not exist.", + "type": "array", + "minItems": 0, + "maxItems": 4294967295, + "items": { + "oneOf": [ + { + "description": "The field does not exist.", + "const": -2 + }, + { + "description": "The field exists but has no associated expire.", + "const": -1 + }, + { + "description": "Expiration time was removed", + "const": 1 + } + ] + } + }, + "arguments": [ + { + "name": "key", + "type": "key", + "key_spec_index": 0 + }, + { + "name": "FIELDS", + "type": "string" + }, + { + "name": "numfields", + "type": "integer" + }, + { + "name": "field", + "type": "string", + "multiple": true + } + ] + } +} diff --git a/src/commands/hpexpire.json b/src/commands/hpexpire.json new file mode 100644 index 000000000..6820987c6 --- /dev/null +++ b/src/commands/hpexpire.json @@ -0,0 +1,116 @@ +{ + "HPEXPIRE": { + "summary": "Set expiry for hash field using relative time to expire (milliseconds)", + "complexity": "O(N) where N is the number of specified fields", + "group": "hash", + "since": "7.4.0", + "arity": -6, + "function": "hpexpireCommand", + "history": [], + "command_flags": [ + "WRITE", + "DENYOOM", + "FAST" + ], + "acl_categories": [ + "HASH" + ], + "key_specs": [ + { + "flags": [ + "RW", + "UPDATE" + ], + "begin_search": { + "index": { + "pos": 1 + } + }, + "find_keys": { + "range": { + "lastkey": 0, + "step": 1, + "limit": 0 + } + } + } + ], + "reply_schema": { + "description": "Array of results. Returns empty array if the key does not exist.", + "type": "array", + "minItems": 0, + "maxItems": 4294967295, + "items": { + "oneOf": [ + { + "description": "The field does not exist.", + "const": -2 + }, + { + "description": "Specified NX | XX | GT | LT condition not met", + "const": 0 + }, + { + "description": "Expiration time was set or updated.", + "const": 1 + }, + { + "description": "Field deleted because the specified expiration time is in the past.", + "const": 2 + } + ] + } + }, + "arguments": [ + { + "name": "key", + "type": "key", + "key_spec_index": 0 + }, + { + "name": "milliseconds", + "type": "integer" + }, + { + "name": "condition", + "type": "oneof", + "optional": true, + "arguments": [ + { + "name": "nx", + "type": "pure-token", + "token": "NX" + }, + { + "name": "xx", + "type": "pure-token", + "token": "XX" + }, + { + "name": "gt", + "type": "pure-token", + "token": "GT" + }, + { + "name": "lt", + "type": "pure-token", + "token": "LT" + } + ] + }, + { + "name": "FIELDS", + "type": "string" + }, + { + "name": "numfields", + "type": "integer" + }, + { + "name": "field", + "type": "string", + "multiple": true + } + ] + } +} \ No newline at end of file diff --git a/src/commands/hpexpireat.json b/src/commands/hpexpireat.json new file mode 100644 index 000000000..0d08bb46e --- /dev/null +++ b/src/commands/hpexpireat.json @@ -0,0 +1,116 @@ +{ + "HPEXPIREAT": { + "summary": "Set expiry for hash field using an absolute Unix timestamp (milliseconds)", + "complexity": "O(N) where N is the number of specified fields", + "group": "hash", + "since": "7.4.0", + "arity": -6, + "function": "hpexpireatCommand", + "history": [], + "command_flags": [ + "WRITE", + "DENYOOM", + "FAST" + ], + "acl_categories": [ + "HASH" + ], + "key_specs": [ + { + "flags": [ + "RW", + "UPDATE" + ], + "begin_search": { + "index": { + "pos": 1 + } + }, + "find_keys": { + "range": { + "lastkey": 0, + "step": 1, + "limit": 0 + } + } + } + ], + "reply_schema": { + "description": "Array of results. Returns empty array if the key does not exist.", + "type": "array", + "minItems": 0, + "maxItems": 4294967295, + "items": { + "oneOf": [ + { + "description": "The field does not exist.", + "const": -2 + }, + { + "description": "Specified NX | XX | GT | LT condition not met", + "const": 0 + }, + { + "description": "Expiration time was set or updated.", + "const": 1 + }, + { + "description": "Field deleted because the specified expiration time is in the past.", + "const": 2 + } + ] + } + }, + "arguments": [ + { + "name": "key", + "type": "key", + "key_spec_index": 0 + }, + { + "name": "unix-time-milliseconds", + "type": "unix-time" + }, + { + "name": "condition", + "type": "oneof", + "optional": true, + "arguments": [ + { + "name": "nx", + "type": "pure-token", + "token": "NX" + }, + { + "name": "xx", + "type": "pure-token", + "token": "XX" + }, + { + "name": "gt", + "type": "pure-token", + "token": "GT" + }, + { + "name": "lt", + "type": "pure-token", + "token": "LT" + } + ] + }, + { + "name": "FIELDS", + "type": "string" + }, + { + "name": "numfields", + "type": "integer" + }, + { + "name": "field", + "type": "string", + "multiple": true + } + ] + } +} \ No newline at end of file diff --git a/src/commands/hpexpiretime.json b/src/commands/hpexpiretime.json new file mode 100644 index 000000000..83fd4610d --- /dev/null +++ b/src/commands/hpexpiretime.json @@ -0,0 +1,81 @@ +{ + "HPEXPIRETIME": { + "summary": "Returns the expiration time of a hash field as a Unix timestamp, in msec.", + "complexity": "O(N) where N is the number of specified fields", + "group": "hash", + "since": "7.4.0", + "arity": -5, + "function": "hpexpiretimeCommand", + "history": [], + "command_flags": [ + "READONLY", + "FAST" + ], + "acl_categories": [ + "HASH" + ], + "key_specs": [ + { + "flags": [ + "RO", + "ACCESS" + ], + "begin_search": { + "index": { + "pos": 1 + } + }, + "find_keys": { + "range": { + "lastkey": 0, + "step": 1, + "limit": 0 + } + } + } + ], + "reply_schema": { + "description": "Array of results. Returns empty array if the key does not exist.", + "type": "array", + "minItems": 0, + "maxItems": 4294967295, + "items": { + "oneOf": [ + { + "description": "The field does not exist.", + "const": -2 + }, + { + "description": "The field exists but has no associated expire.", + "const": -1 + }, + { + "description": "Expiration Unix timestamp in milliseconds.", + "type": "integer", + "minimum": 1 + } + ] + } + }, + "arguments": [ + { + "name": "key", + "type": "key", + "key_spec_index": 0 + }, + { + "name": "FIELDS", + "type": "string" + }, + { + "name": "numfields", + "type": "integer" + }, + { + "name": "field", + "type": "string", + "multiple": true + } + ] + } +} diff --git a/src/commands/hpttl.json b/src/commands/hpttl.json new file mode 100644 index 000000000..7aa3eb72d --- /dev/null +++ b/src/commands/hpttl.json @@ -0,0 +1,81 @@ +{ + "HPTTL": { + "summary": "Returns the TTL in milliseconds of a hash field.", + "complexity": "O(N) where N is the number of specified fields", + "group": "hash", + "since": "7.4.0", + "arity": -5, + "function": "hpttlCommand", + "history": [], + "command_flags": [ + "READONLY", + "FAST" + ], + "acl_categories": [ + "HASH" + ], + "key_specs": [ + { + "flags": [ + "RO", + "ACCESS" + ], + "begin_search": { + "index": { + "pos": 1 + } + }, + "find_keys": { + "range": { + "lastkey": 0, + "step": 1, + "limit": 0 + } + } + } + ], + "reply_schema": { + "description": "Array of results. Returns empty array if the key does not exist.", + "type": "array", + "minItems": 0, + "maxItems": 4294967295, + "items": { + "oneOf": [ + { + "description": "The field does not exist.", + "const": -2 + }, + { + "description": "The field exists but has no associated expire.", + "const": -1 + }, + { + "description": "TTL in milliseconds.", + "type": "integer", + "minimum": 1 + } + ] + } + }, + "arguments": [ + { + "name": "key", + "type": "key", + "key_spec_index": 0 + }, + { + "name": "FIELDS", + "type": "string" + }, + { + "name": "numfields", + "type": "integer" + }, + { + "name": "field", + "type": "string", + "multiple": true + } + ] + } +} diff --git a/src/commands/httl.json b/src/commands/httl.json new file mode 100644 index 000000000..f7c7ce7b5 --- /dev/null +++ b/src/commands/httl.json @@ -0,0 +1,81 @@ +{ + "HTTL": { + "summary": "Returns the TTL in seconds of a hash field.", + "complexity": "O(N) where N is the number of specified fields", + "group": "hash", + "since": "7.4.0", + "arity": -5, + "function": "httlCommand", + "history": [], + "command_flags": [ + "READONLY", + "FAST" + ], + "acl_categories": [ + "HASH" + ], + "key_specs": [ + { + "flags": [ + "RO", + "ACCESS" + ], + "begin_search": { + "index": { + "pos": 1 + } + }, + "find_keys": { + "range": { + "lastkey": 0, + "step": 1, + "limit": 0 + } + } + } + ], + "reply_schema": { + "description": "Array of results. Returns empty array if the key does not exist.", + "type": "array", + "minItems": 0, + "maxItems": 4294967295, + "items": { + "oneOf": [ + { + "description": "The field does not exist.", + "const": -2 + }, + { + "description": "The field exists but has no associated expire.", + "const": -1 + }, + { + "description": "TTL in seconds.", + "type": "integer", + "minimum": 1 + } + ] + } + }, + "arguments": [ + { + "name": "key", + "type": "key", + "key_spec_index": 0 + }, + { + "name": "FIELDS", + "type": "string" + }, + { + "name": "numfields", + "type": "integer" + }, + { + "name": "field", + "type": "string", + "multiple": true + } + ] + } +} diff --git a/src/db.c b/src/db.c index f25960c0f..1991a5807 100644 --- a/src/db.c +++ b/src/db.c @@ -177,13 +177,13 @@ robj *lookupKeyWriteOrReply(client *c, robj *key, robj *reply) { * * If the update_if_existing argument is false, the program is aborted * if the key already exists, otherwise, it can fall back to dbOverwrite. */ -static void dbAddInternal(redisDb *db, robj *key, robj *val, int update_if_existing) { +static dictEntry *dbAddInternal(redisDb *db, robj *key, robj *val, int update_if_existing) { dictEntry *existing; int slot = getKeySlot(key->ptr); dictEntry *de = kvstoreDictAddRaw(db->keys, slot, key->ptr, &existing); if (update_if_existing && existing) { dbSetValue(db, key, val, 1, existing); - return; + return existing; } serverAssertWithInfo(NULL, key, de != NULL); kvstoreDictSetKey(db->keys, slot, de, sdsdup(key->ptr)); @@ -191,10 +191,11 @@ static void dbAddInternal(redisDb *db, robj *key, robj *val, int update_if_exist kvstoreDictSetVal(db->keys, slot, de, val); signalKeyAsReady(db, key, val->type); notifyKeyspaceEvent(NOTIFY_NEW,"new",key,db->id); + return de; } -void dbAdd(redisDb *db, robj *key, robj *val) { - dbAddInternal(db, key, val, 0); +dictEntry *dbAdd(redisDb *db, robj *key, robj *val) { + return dbAddInternal(db, key, val, 0); } /* Returns key's hash slot when cluster mode is enabled, or 0 when disabled. @@ -275,6 +276,11 @@ static void dbSetValue(redisDb *db, robj *key, robj *val, int overwrite, dictEnt old = dictGetVal(de); } kvstoreDictSetVal(db->keys, slot, de, val); + + /* if hash with HFEs, take care to remove from global HFE DS */ + if (old->type == OBJ_HASH) + hashTypeRemoveFromExpires(&db->hexpires, old); + if (server.lazyfree_lazy_server_del) { freeObjAsync(key,old,db->id); } else { @@ -370,6 +376,11 @@ int dbGenericDelete(redisDb *db, robj *key, int async, int flags) { dictEntry *de = kvstoreDictTwoPhaseUnlinkFind(db->keys, slot, key->ptr, &plink, &table); if (de) { robj *val = dictGetVal(de); + + /* If hash object with expiry on fields, remove it from HFE DS of DB */ + if (val->type == OBJ_HASH) + hashTypeRemoveFromExpires(&db->hexpires, val); + /* RM_StringDMA may call dbUnshareStringValue which may free val, so we * need to incr to retain val */ incrRefCount(val); @@ -475,6 +486,9 @@ long long emptyDbStructure(redisDb *dbarray, int dbnum, int async, if (async) { emptyDbAsync(&dbarray[j]); } else { + /* Destroy global HFE DS before deleting the hashes since ebuckets + * DS is embedded in the stored objects. */ + ebDestroy(&dbarray[j].hexpires, &hashExpireBucketsType, NULL); kvstoreEmpty(dbarray[j].keys, callback); kvstoreEmpty(dbarray[j].expires, callback); } @@ -554,6 +568,7 @@ redisDb *initTempDb(void) { tempDb[i].id = i; tempDb[i].keys = kvstoreCreate(&dbDictType, slot_count_bits, flags); tempDb[i].expires = kvstoreCreate(&dbExpiresDictType, slot_count_bits, flags); + tempDb[i].hexpires = ebCreate(); } return tempDb; @@ -566,6 +581,9 @@ void discardTempDb(redisDb *tempDb, void(callback)(dict*)) { /* Release temp DBs. */ emptyDbStructure(tempDb, -1, async, callback); for (int i=0; itype == OBJ_HASH) ? hfieldlen : sdslen */ } scanData; /* Helper function to compare key type in scan commands */ @@ -918,7 +937,7 @@ void scanCallback(void *privdata, const dictEntry *de) { list *keys = data->keys; robj *o = data->o; sds val = NULL; - sds key = NULL; + void *key = NULL; /* if OBJ_HASH then key is of type `hfield`. Otherwise, `sds` */ data->sampled++; /* o and typename can not have values at the same time. */ @@ -932,24 +951,29 @@ void scanCallback(void *privdata, const dictEntry *de) { }*/ /* Filter element if it does not match the pattern. */ - sds keysds = dictGetKey(de); + void *keyStr = dictGetKey(de); if (data->pattern) { - if (!stringmatchlen(data->pattern, sdslen(data->pattern), keysds, sdslen(keysds), 0)) { + if (!stringmatchlen(data->pattern, sdslen(data->pattern), keyStr, data->strlen(keyStr), 0)) { return; } } if (o == NULL) { - key = keysds; + key = keyStr; } else if (o->type == OBJ_SET) { - key = keysds; + key = keyStr; } else if (o->type == OBJ_HASH) { - key = keysds; + key = keyStr; val = dictGetVal(de); + + /* If field is expired, then ignore */ + if (hfieldIsExpired(key)) + return; + } else if (o->type == OBJ_ZSET) { char buf[MAX_LONG_DOUBLE_CHARS]; int len = ld2string(buf, sizeof(buf), *(double *)dictGetVal(de), LD_STR_AUTO); - key = sdsdup(keysds); + key = sdsdup(keyStr); val = sdsnewlen(buf, len); } else { serverPanic("Type not handled in SCAN callback."); @@ -1023,6 +1047,7 @@ char *getObjectTypeName(robj *o) { * In the case of a Hash object the function returns both the field and value * of every element on the Hash. */ void scanGenericCommand(client *c, robj *o, unsigned long long cursor) { + int isKeysHfield = 0; int i, j; listNode *node; long count = 10; @@ -1103,6 +1128,7 @@ void scanGenericCommand(client *c, robj *o, unsigned long long cursor) { } else if (o->type == OBJ_SET && o->encoding == OBJ_ENCODING_HT) { ht = o->ptr; } else if (o->type == OBJ_HASH && o->encoding == OBJ_ENCODING_HT) { + isKeysHfield = 1; ht = o->ptr; } else if (o->type == OBJ_ZSET && o->encoding == OBJ_ENCODING_SKIPLIST) { zset *zs = o->ptr; @@ -1141,7 +1167,7 @@ void scanGenericCommand(client *c, robj *o, unsigned long long cursor) { * working on an empty dict, one with a lot of empty buckets, and * for the buckets are not empty, we need to limit the spampled number * to prevent a long hang time caused by filtering too many keys; - * 6. data.no_values: to control whether values will be returned or + * 6. data.no_values: to control whether values will be returned or * only keys are returned. */ scanData data = { .keys = keys, @@ -1150,6 +1176,7 @@ void scanGenericCommand(client *c, robj *o, unsigned long long cursor) { .pattern = use_pattern ? pat : NULL, .sampled = 0, .no_values = no_values, + .strlen = (isKeysHfield) ? hfieldlen : sdslen, }; /* A pattern may restrict all matching keys to one cluster slot. */ @@ -1211,6 +1238,40 @@ void scanGenericCommand(client *c, robj *o, unsigned long long cursor) { p = lpNext(o->ptr, p); } cursor = 0; + } else if (o->type == OBJ_HASH && o->encoding == OBJ_ENCODING_LISTPACK_EX) { + int64_t len; + long long expire_at; + unsigned char *lp = hashTypeListpackGetLp(o); + unsigned char *p = lpFirst(lp); + unsigned char *str, *val; + unsigned char intbuf[LP_INTBUF_SIZE]; + + while (p) { + str = lpGet(p, &len, intbuf); + p = lpNext(lp, p); + val = p; /* Keep pointer to value */ + + p = lpNext(lp, p); + serverAssert(p && lpGetIntegerValue(p, &expire_at)); + + if (hashTypeIsExpired(o, expire_at) || + (use_pattern && !stringmatchlen(pat, sdslen(pat), (char *)str, len, 0))) + { + /* jump to the next key/val pair */ + p = lpNext(lp, p); + continue; + } + + /* add key object */ + listAddNodeTail(keys, sdsnewlen(str, len)); + /* add value object */ + if (!no_values) { + str = lpGet(val, &len, intbuf); + listAddNodeTail(keys, sdsnewlen(str, len)); + } + p = lpNext(lp, p); + } + cursor = 0; } else { serverPanic("Not handled encoding in SCAN."); } @@ -1243,10 +1304,14 @@ void scanGenericCommand(client *c, robj *o, unsigned long long cursor) { addReplyArrayLen(c, 2); addReplyBulkLongLong(c,cursor); + unsigned long long idx = 0; addReplyArrayLen(c, listLength(keys)); while ((node = listFirst(keys)) != NULL) { - sds key = listNodeValue(node); - addReplyBulkCBuffer(c, key, sdslen(key)); + void *key = listNodeValue(node); + /* For HSCAN, list will contain keys value pairs unless no_values arg + * was given. We should call mstrlen for the keys only. */ + int hfieldkey = isKeysHfield && (no_values || (idx++ % 2 == 0)); + addReplyBulkCBuffer(c, key, hfieldkey ? mstrlen(key) : sdslen(key)); listDelNode(keys, node); } @@ -1339,6 +1404,7 @@ void renameGenericCommand(client *c, int nx) { robj *o; long long expire; int samekey = 0; + uint64_t minHashExpireTime = EB_EXPIRE_TIME_INVALID; /* When source and dest key is the same, no operation is performed, * if the key exists, however we still return an error on unexisting key. */ @@ -1364,9 +1430,21 @@ void renameGenericCommand(client *c, int nx) { * with the same name. */ dbDelete(c->db,c->argv[2]); } - dbAdd(c->db,c->argv[2],o); + dictEntry *de = dbAdd(c->db, c->argv[2], o); if (expire != -1) setExpire(c,c->db,c->argv[2],expire); + + /* If hash with expiration on fields then remove it from global HFE DS and + * keep next expiration time. Otherwise, dbDelete() will remove it from the + * global HFE DS and we will lose the expiration time. */ + if (o->type == OBJ_HASH) + minHashExpireTime = hashTypeRemoveFromExpires(&c->db->hexpires, o); + dbDelete(c->db,c->argv[1]); + + /* If hash with HFEs, register in db->hexpires */ + if (minHashExpireTime != EB_EXPIRE_TIME_INVALID) + hashTypeAddToExpires(c->db, dictGetKey(de), o, minHashExpireTime); + signalModifiedKey(c,c->db,c->argv[1]); signalModifiedKey(c,c->db,c->argv[2]); notifyKeyspaceEvent(NOTIFY_GENERIC,"rename_from", @@ -1390,6 +1468,7 @@ void moveCommand(client *c) { redisDb *src, *dst; int srcid, dbid; long long expire; + uint64_t hashExpireTime = EB_EXPIRE_TIME_INVALID; if (server.cluster_enabled) { addReplyError(c,"MOVE is not allowed in cluster mode"); @@ -1430,12 +1509,25 @@ void moveCommand(client *c) { addReply(c,shared.czero); return; } - dbAdd(dst,c->argv[1],o); + dictEntry *dstDictEntry = dbAdd(dst,c->argv[1],o); if (expire != -1) setExpire(c,dst,c->argv[1],expire); + + /* If hash with expiration on fields, remove it from global HFE DS and keep + * aside registered expiration time. Must be before deletion of the object. + * hexpires (ebuckets) embed in stored items its structure. */ + if (o->type == OBJ_HASH) + hashExpireTime = hashTypeRemoveFromExpires(&src->hexpires, o); + incrRefCount(o); /* OK! key moved, free the entry in the source DB */ dbDelete(src,c->argv[1]); + + /* If object of type hash with expiration on fields. Taken care to add the + * hash to hexpires of `dst` only after dbDelete(). */ + if (hashExpireTime != EB_EXPIRE_TIME_INVALID) + hashTypeAddToExpires(dst, dictGetKey(dstDictEntry), o, hashExpireTime); + signalModifiedKey(c,src,c->argv[1]); signalModifiedKey(c,dst,c->argv[1]); notifyKeyspaceEvent(NOTIFY_GENERIC, @@ -1518,12 +1610,13 @@ void copyCommand(client *c) { /* Duplicate object according to object's type. */ robj *newobj; + uint64_t minHashExpire = EB_EXPIRE_TIME_INVALID; /* HFE feature */ switch(o->type) { case OBJ_STRING: newobj = dupStringObject(o); break; case OBJ_LIST: newobj = listTypeDup(o); break; case OBJ_SET: newobj = setTypeDup(o); break; case OBJ_ZSET: newobj = zsetDup(o); break; - case OBJ_HASH: newobj = hashTypeDup(o); break; + case OBJ_HASH: newobj = hashTypeDup(o, newkey->ptr, &minHashExpire); break; case OBJ_STREAM: newobj = streamDup(o); break; case OBJ_MODULE: newobj = moduleTypeDupOrReply(c, key, newkey, dst->id, o); @@ -1538,8 +1631,16 @@ void copyCommand(client *c) { dbDelete(dst,newkey); } - dbAdd(dst,newkey,newobj); - if (expire != -1) setExpire(c, dst, newkey, expire); + dictEntry *deCopy = dbAdd(dst,newkey,newobj); + + /* if key with expiration then set it */ + if (expire != -1) + setExpire(c, dst, newkey, expire); + + /* If minExpiredField was set, then the object is hash with expiration + * on fields and need to register it in global HFE DS */ + if (minHashExpire != EB_EXPIRE_TIME_INVALID) + hashTypeAddToExpires(dst, dictGetKey(deCopy), newobj, minHashExpire); /* OK! key copied */ signalModifiedKey(c,dst,c->argv[2]); @@ -1629,11 +1730,13 @@ int dbSwapDatabases(int id1, int id2) { * remain in the same DB they were. */ db1->keys = db2->keys; db1->expires = db2->expires; + db1->hexpires = db2->hexpires; db1->avg_ttl = db2->avg_ttl; db1->expires_cursor = db2->expires_cursor; db2->keys = aux.keys; db2->expires = aux.expires; + db2->hexpires = aux.hexpires; db2->avg_ttl = aux.avg_ttl; db2->expires_cursor = aux.expires_cursor; @@ -1671,11 +1774,13 @@ void swapMainDbWithTempDb(redisDb *tempDb) { * remain in the same DB they were. */ activedb->keys = newdb->keys; activedb->expires = newdb->expires; + activedb->hexpires = newdb->hexpires; activedb->avg_ttl = newdb->avg_ttl; activedb->expires_cursor = newdb->expires_cursor; newdb->keys = aux.keys; newdb->expires = aux.expires; + newdb->hexpires = aux.hexpires; newdb->avg_ttl = aux.avg_ttl; newdb->expires_cursor = aux.expires_cursor; @@ -1864,7 +1969,7 @@ int keyIsExpired(redisDb *db, robj *key) { * EXPIRE_AVOID_DELETE_EXPIRED flag. * * The return value of the function is KEY_VALID if the key is still valid. - * The function returns KEY_EXPIRED if the key is expired BUT not deleted, + * The function returns KEY_EXPIRED if the key is expired BUT not deleted, * or returns KEY_DELETED if the key is expired and deleted. */ keyStatus expireIfNeeded(redisDb *db, robj *key, int flags) { if (server.lazy_expire_disabled) return KEY_VALID; @@ -1878,7 +1983,7 @@ keyStatus expireIfNeeded(redisDb *db, robj *key, int flags) { * replicas. * * Still we try to return the right information to the caller, - * that is, KEY_VALID if we think the key should still be valid, + * that is, KEY_VALID if we think the key should still be valid, * KEY_EXPIRED if we think the key is expired but don't want to delete it at this time. * * When replicating commands from the master, keys are never considered diff --git a/src/debug.c b/src/debug.c index 5c3f5c7a5..4b5f73061 100644 --- a/src/debug.c +++ b/src/debug.c @@ -200,17 +200,22 @@ void xorObjectDigest(redisDb *db, robj *keyobj, unsigned char *digest, robj *o) } } else if (o->type == OBJ_HASH) { hashTypeIterator *hi = hashTypeInitIterator(o); - while (hashTypeNext(hi) != C_ERR) { + while (hashTypeNext(hi, 0) != C_ERR) { unsigned char eledigest[20]; sds sdsele; + /* field */ memset(eledigest,0,20); sdsele = hashTypeCurrentObjectNewSds(hi,OBJ_HASH_KEY); mixDigest(eledigest,sdsele,sdslen(sdsele)); sdsfree(sdsele); + /* val */ sdsele = hashTypeCurrentObjectNewSds(hi,OBJ_HASH_VALUE); mixDigest(eledigest,sdsele,sdslen(sdsele)); sdsfree(sdsele); + /* hash-field expiration (HFE) */ + if (hi->expire_time != EB_EXPIRE_TIME_INVALID) + xorDigest(eledigest,"!!hexpire!!",11); xorDigest(digest,eledigest,20); } hashTypeReleaseIterator(hi); @@ -445,9 +450,9 @@ void debugCommand(client *c) { "SEGFAULT", " Crash the server with sigsegv.", "SET-ACTIVE-EXPIRE <0|1>", -" Setting it to 0 disables expiring keys in background when they are not", -" accessed (otherwise the Redis behavior). Setting it to 1 reenables back the", -" default.", +" Setting it to 0 disables expiring keys (and hash-fields) in background ", +" when they are not accessed (otherwise the Redis behavior). Setting it", +" to 1 reenables back the default.", "QUICKLIST-PACKED-THRESHOLD ", " Sets the threshold for elements to be inserted as plain vs packed nodes", " Default value is 1GB, allows values up to 4GB. Setting to 0 restores to default.", @@ -664,10 +669,14 @@ NULL if ((o = objectCommandLookupOrReply(c,c->argv[2],shared.nokeyerr)) == NULL) return; - if (o->encoding != OBJ_ENCODING_LISTPACK) { + if (o->encoding != OBJ_ENCODING_LISTPACK && o->encoding != OBJ_ENCODING_LISTPACK_EX) { addReplyError(c,"Not a listpack encoded object."); } else { - lpRepr(o->ptr); + if (o->encoding == OBJ_ENCODING_LISTPACK) + lpRepr(o->ptr); + else if (o->encoding == OBJ_ENCODING_LISTPACK_EX) + lpRepr(((listpackEx*)o->ptr)->lp); + addReplyStatus(c,"Listpack structure printed on stdout"); } } else if (!strcasecmp(c->argv[1]->ptr,"quicklist") && (c->argc == 3 || c->argc == 4)) { @@ -1081,7 +1090,7 @@ void serverLogObjectDebugInfo(const robj *o) { } else if (o->type == OBJ_SET) { serverLog(LL_WARNING,"Set size: %d", (int) setTypeSize(o)); } else if (o->type == OBJ_HASH) { - serverLog(LL_WARNING,"Hash size: %d", (int) hashTypeLength(o)); + serverLog(LL_WARNING,"Hash size: %d", (int) hashTypeLength(o, 0)); } else if (o->type == OBJ_ZSET) { serverLog(LL_WARNING,"Sorted set size: %d", (int) zsetLength(o)); if (o->encoding == OBJ_ENCODING_SKIPLIST) diff --git a/src/defrag.c b/src/defrag.c index 4f202d246..122598c4a 100644 --- a/src/defrag.c +++ b/src/defrag.c @@ -70,6 +70,22 @@ sds activeDefragSds(sds sdsptr) { return NULL; } +/* Defrag helper for hfield strings + * + * returns NULL in case the allocation wasn't moved. + * when it returns a non-null value, the old pointer was already released + * and should NOT be accessed. */ +hfield activeDefragHfield(hfield hf) { + void *ptr = hfieldGetAllocPtr(hf); + void *newptr = activeDefragAlloc(ptr); + if (newptr) { + size_t offset = hf - (char*)ptr; + hf = (char*)newptr + offset; + return hf; + } + return NULL; +} + /* Defrag helper for robj and/or string objects with expected refcount. * * Like activeDefragStringOb, but it requires the caller to pass in the expected @@ -250,6 +266,31 @@ void activeDefragSdsDictCallback(void *privdata, const dictEntry *de) { UNUSED(de); } +void activeDefragHfieldDictCallback(void *privdata, const dictEntry *de) { + dict *d = privdata; + hfield newhf, hf = dictGetKey(de); + + if (hfieldGetExpireTime(hf) == EB_EXPIRE_TIME_INVALID) { + /* If the hfield does not have TTL, we directly defrag it. */ + newhf = activeDefragHfield(hf); + } else { + /* Update its reference in the ebucket while defragging it. */ + ebuckets *eb = hashTypeGetDictMetaHFE(d); + newhf = ebDefragItem(eb, &hashFieldExpireBucketsType, hf, (ebDefragFunction *)activeDefragHfield); + } + if (newhf) { + /* We can't search in dict for that key after we've released + * the pointer it holds, since it won't be able to do the string + * compare, but we can find the entry using key hash and pointer. */ + dictUseStoredKeyApi(d, 1); + uint64_t hash = dictGetHash(d, newhf); + dictUseStoredKeyApi(d, 0); + dictEntry *de = dictFindEntryByPtrAndHash(d, hf, hash); + serverAssert(de); + dictSetKey(d, de, newhf); + } +} + /* Defrag a dict with sds key and optional value (either ptr, sds or robj string) */ void activeDefragSdsDict(dict* d, int val_type) { unsigned long cursor = 0; @@ -268,6 +309,20 @@ void activeDefragSdsDict(dict* d, int val_type) { } while (cursor != 0); } +/* Defrag a dict with hfield key and sds value. */ +void activeDefragHfieldDict(dict *d) { + unsigned long cursor = 0; + dictDefragFunctions defragfns = { + .defragAlloc = activeDefragAlloc, + .defragKey = NULL, /* Will be defragmented in activeDefragHfieldDictCallback. */ + .defragVal = (dictDefragAllocFunction *)activeDefragSds + }; + do { + cursor = dictScanDefrag(d, cursor, activeDefragHfieldDictCallback, + &defragfns, d); + } while (cursor != 0); +} + /* Defrag a list of ptr, sds or robj string values */ void activeDefragList(list *l, int val_type) { listNode *ln, *newln; @@ -422,10 +477,10 @@ void scanLaterHash(robj *ob, unsigned long *cursor) { dict *d = ob->ptr; dictDefragFunctions defragfns = { .defragAlloc = activeDefragAlloc, - .defragKey = (dictDefragAllocFunction *)activeDefragSds, + .defragKey = NULL, /* Will be defragmented in activeDefragHfieldDictCallback. */ .defragVal = (dictDefragAllocFunction *)activeDefragSds }; - *cursor = dictScanDefrag(d, *cursor, scanCallbackCountScanned, &defragfns, NULL); + *cursor = dictScanDefrag(d, *cursor, activeDefragHfieldDictCallback, &defragfns, d); } void defragQuicklist(redisDb *db, dictEntry *kde) { @@ -477,7 +532,7 @@ void defragHash(redisDb *db, dictEntry *kde) { if (dictSize(d) > server.active_defrag_max_scan_fields) defragLater(db, kde); else - activeDefragSdsDict(d, DEFRAG_SDS_DICT_VAL_IS_SDS); + activeDefragHfieldDict(d); /* defrag the dict struct and tables */ if ((newd = dictDefragTables(ob->ptr))) ob->ptr = newd; @@ -672,7 +727,7 @@ void defragModule(redisDb *db, dictEntry *kde) { * all the various pointers it has. */ void defragKey(defragCtx *ctx, dictEntry *de) { sds keysds = dictGetKey(de); - robj *newob, *ob; + robj *newob, *ob = dictGetVal(de); unsigned char *newzl; sds newsds; redisDb *db = ctx->privdata; @@ -689,11 +744,22 @@ void defragKey(defragCtx *ctx, dictEntry *de) { dictEntry *expire_de = kvstoreDictFindEntryByPtrAndHash(db->expires, slot, keysds, hash); if (expire_de) kvstoreDictSetKey(db->expires, slot, expire_de, newsds); } + + /* Update the key's reference in the dict's metadata or the listpackEx. */ + if (unlikely(ob->type == OBJ_HASH)) + hashTypeUpdateKeyRef(ob, newsds); } /* Try to defrag robj and / or string value. */ - ob = dictGetVal(de); - if ((newob = activeDefragStringOb(ob))) { + if (unlikely(ob->type == OBJ_HASH && hashTypeGetMinExpire(ob) != EB_EXPIRE_TIME_INVALID)) { + /* Update its reference in the ebucket while defragging it. */ + newob = ebDefragItem(&db->hexpires, &hashExpireBucketsType, ob, + (ebDefragFunction *)activeDefragStringOb); + } else { + /* If the dict doesn't have metadata, we directly defrag it. */ + newob = activeDefragStringOb(ob); + } + if (newob) { kvstoreDictSetVal(db->keys, slot, de, newob); ob = newob; } @@ -734,6 +800,12 @@ void defragKey(defragCtx *ctx, dictEntry *de) { if (ob->encoding == OBJ_ENCODING_LISTPACK) { if ((newzl = activeDefragAlloc(ob->ptr))) ob->ptr = newzl; + } else if (ob->encoding == OBJ_ENCODING_LISTPACK_EX) { + listpackEx *newlpt, *lpt = (listpackEx*)ob->ptr; + if ((newlpt = activeDefragAlloc(lpt))) + ob->ptr = lpt = newlpt; + if ((newzl = activeDefragAlloc(lpt->lp))) + lpt->lp = newzl; } else if (ob->encoding == OBJ_ENCODING_HT) { defragHash(db, de); } else { diff --git a/src/dict.c b/src/dict.c index 880042c69..2928d8af5 100644 --- a/src/dict.c +++ b/src/dict.c @@ -67,6 +67,25 @@ static int _dictInit(dict *d, dictType *type); static dictEntry *dictGetNext(const dictEntry *de); static dictEntry **dictGetNextRef(dictEntry *de); static void dictSetNext(dictEntry *de, dictEntry *next); +static int dictDefaultCompare(dict *d, const void *key1, const void *key2); + +/* -------------------------- misc inline functions -------------------------------- */ + +typedef int (*keyCmpFunc)(dict *d, const void *key1, const void *key2); +static inline keyCmpFunc dictGetKeyCmpFunc(dict *d) { + if (d->useStoredKeyApi && d->type->storedKeyCompare) + return d->type->storedKeyCompare; + if (d->type->keyCompare) + return d->type->keyCompare; + return dictDefaultCompare; +} + +static inline uint64_t dictHashKey(dict *d, const void *key, int isStoredKey) { + if (isStoredKey && d->type->storedHashFunction) + return d->type->storedHashFunction(key); + else + return d->type->hashFunction(key); +} /* -------------------------- hash functions -------------------------------- */ @@ -173,6 +192,19 @@ dict *dictCreate(dictType *type) return d; } +/* Change dictType of dict to another one with metadata support + * Rest of dictType's values must stay the same */ +void dictTypeAddMeta(dict **d, dictType *typeWithMeta) { + /* Verify new dictType is compatible with the old one */ + dictType toCmp = *typeWithMeta; + toCmp.dictMetadataBytes = NULL; /* Expected old one not to have metadata */ + toCmp.onDictRelease = (*d)->type->onDictRelease; /* Ignore 'onDictRelease' in comparison */ + assert(memcmp((*d)->type, &toCmp, sizeof(dictType)) == 0); /* The rest of the dictType fields must be the same */ + + *d = zrealloc(*d, sizeof(dict) + typeWithMeta->dictMetadataBytes(*d)); + (*d)->type = typeWithMeta; +} + /* Initialize the hash table */ int _dictInit(dict *d, dictType *type) { @@ -182,6 +214,7 @@ int _dictInit(dict *d, dictType *type) d->rehashidx = -1; d->pauserehash = 0; d->pauseAutoResize = 0; + d->useStoredKeyApi = 0; return DICT_OK; } @@ -285,7 +318,7 @@ static void rehashEntriesInBucketAtIndex(dict *d, uint64_t idx) { void *key = dictGetKey(de); /* Get the index in the new hash table */ if (d->ht_size_exp[1] > d->ht_size_exp[0]) { - h = dictHashKey(d, key) & DICTHT_SIZE_MASK(d->ht_size_exp[1]); + h = dictHashKey(d, key, 1) & DICTHT_SIZE_MASK(d->ht_size_exp[1]); } else { /* We're shrinking the table. The tables sizes are powers of * two, so we simply mask the bucket index in the larger table @@ -572,7 +605,7 @@ static dictEntry *dictGenericDelete(dict *d, const void *key, int nofree) { /* dict is empty */ if (dictSize(d) == 0) return NULL; - h = dictHashKey(d, key); + h = dictHashKey(d, key, d->useStoredKeyApi); idx = h & DICTHT_SIZE_MASK(d->ht_size_exp[0]); if (dictIsRehashing(d)) { @@ -587,6 +620,8 @@ static dictEntry *dictGenericDelete(dict *d, const void *key, int nofree) { } } + keyCmpFunc cmpFunc = dictGetKeyCmpFunc(d); + for (table = 0; table <= 1; table++) { if (table == 0 && (long)idx < d->rehashidx) continue; idx = h & DICTHT_SIZE_MASK(d->ht_size_exp[table]); @@ -594,7 +629,7 @@ static dictEntry *dictGenericDelete(dict *d, const void *key, int nofree) { prevHe = NULL; while(he) { void *he_key = dictGetKey(he); - if (key == he_key || dictCompareKeys(d, key, he_key)) { + if (key == he_key || cmpFunc(d, key, he_key)) { /* Unlink the element from the list */ if (prevHe) dictSetNext(prevHe, dictGetNext(he)); @@ -689,6 +724,10 @@ void dictRelease(dict *d) * destroying the dict fake completion. */ if (dictIsRehashing(d) && d->type->rehashingCompleted) d->type->rehashingCompleted(d); + + if (d->type->onDictRelease) + d->type->onDictRelease(d); + _dictClear(d,0,NULL); _dictClear(d,1,NULL); zfree(d); @@ -701,8 +740,9 @@ dictEntry *dictFind(dict *d, const void *key) if (dictSize(d) == 0) return NULL; /* dict is empty */ - h = dictHashKey(d, key); + h = dictHashKey(d, key, d->useStoredKeyApi); idx = h & DICTHT_SIZE_MASK(d->ht_size_exp[0]); + keyCmpFunc cmpFunc = dictGetKeyCmpFunc(d); if (dictIsRehashing(d)) { if ((long)idx >= d->rehashidx && d->ht_table[0][idx]) { @@ -722,7 +762,7 @@ dictEntry *dictFind(dict *d, const void *key) he = d->ht_table[table][idx]; while(he) { void *he_key = dictGetKey(he); - if (key == he_key || dictCompareKeys(d, key, he_key)) + if (key == he_key || cmpFunc(d, key, he_key)) return he; he = dictGetNext(he); } @@ -759,7 +799,9 @@ dictEntry *dictTwoPhaseUnlinkFind(dict *d, const void *key, dictEntry ***plink, if (dictSize(d) == 0) return NULL; /* dict is empty */ if (dictIsRehashing(d)) _dictRehashStep(d); - h = dictHashKey(d, key); + + h = dictHashKey(d, key, d->useStoredKeyApi); + keyCmpFunc cmpFunc = dictGetKeyCmpFunc(d); for (table = 0; table <= 1; table++) { idx = h & DICTHT_SIZE_MASK(d->ht_size_exp[table]); @@ -767,7 +809,7 @@ dictEntry *dictTwoPhaseUnlinkFind(dict *d, const void *key, dictEntry ***plink, dictEntry **ref = &d->ht_table[table][idx]; while (ref && *ref) { void *de_key = dictGetKey(*ref); - if (key == de_key || dictCompareKeys(d, key, de_key)) { + if (key == de_key || cmpFunc(d, key, de_key)) { *table_index = table; *plink = ref; dictPauseRehashing(d); @@ -1530,8 +1572,8 @@ static signed char _dictNextExp(unsigned long size) void *dictFindPositionForInsert(dict *d, const void *key, dictEntry **existing) { unsigned long idx, table; dictEntry *he; + uint64_t hash = dictHashKey(d, key, d->useStoredKeyApi); if (existing) *existing = NULL; - uint64_t hash = dictHashKey(d, key); idx = hash & DICTHT_SIZE_MASK(d->ht_size_exp[0]); if (dictIsRehashing(d)) { @@ -1548,6 +1590,8 @@ void *dictFindPositionForInsert(dict *d, const void *key, dictEntry **existing) /* Expand the hash table if needed */ _dictExpandIfNeeded(d); + keyCmpFunc cmpFunc = dictGetKeyCmpFunc(d); + for (table = 0; table <= 1; table++) { if (table == 0 && (long)idx < d->rehashidx) continue; idx = hash & DICTHT_SIZE_MASK(d->ht_size_exp[table]); @@ -1555,7 +1599,7 @@ void *dictFindPositionForInsert(dict *d, const void *key, dictEntry **existing) he = d->ht_table[table][idx]; while(he) { void *he_key = dictGetKey(he); - if (key == he_key || dictCompareKeys(d, key, he_key)) { + if (key == he_key || cmpFunc(d, key, he_key)) { if (existing) *existing = he; return NULL; } @@ -1587,7 +1631,7 @@ void dictSetResizeEnabled(dictResizeEnable enable) { } uint64_t dictGetHash(dict *d, const void *key) { - return dictHashKey(d, key); + return dictHashKey(d, key, d->useStoredKeyApi); } /* Finds the dictEntry using pointer and pre-calculated hash. @@ -1732,6 +1776,11 @@ void dictGetStats(char *buf, size_t bufsize, dict *d, int full) { orig_buf[orig_bufsize-1] = '\0'; } +static int dictDefaultCompare(dict *d, const void *key1, const void *key2) { + (void)(d); /*unused*/ + return key1 == key2; +} + /* ------------------------------- Benchmark ---------------------------------*/ #ifdef REDIS_TEST diff --git a/src/dict.h b/src/dict.h index 73a8ab052..1c0e6accd 100644 --- a/src/dict.h +++ b/src/dict.h @@ -62,6 +62,32 @@ typedef struct dictType { unsigned int keys_are_odd:1; /* TODO: Add a 'keys_are_even' flag and use a similar optimization if that * flag is set. */ + /* Sometimes we want the ability to store a key in a given way inside the hash + * function, and lookup it in some other way without resorting to any kind of + * conversion. For instance the key may be stored as a structure also + * representing other things, but the lookup happens via just a pointer to a + * null terminated string. Optionally providing additional hash/cmp functions, + * dict supports such usage. In that case we'll have a hashFunction() that will + * expect a null terminated C string, and a storedHashFunction() that will + * instead expect the structure. Similarly, the two comparison functions will + * work differently. The keyCompare() will treat the first argument as a pointer + * to a C string and the other as a structure (this way we can directly lookup + * the structure key using the C string). While the storedKeyCompare() will + * check if two pointers to the key in structure form are the same. + * + * However, functions of dict that gets key as argument (void *key) don't get + * any indication whether it is a lookup or stored key. To indicate that + * you intend to use key of type stored-key, and, consequently, use + * dedicated compare and hash functions of stored-key, is by calling + * dictUseStoredKeyApi(1) before using any of the dict functions that gets + * key as a parameter and then call again dictUseStoredKeyApi(0) once done. + * + * Set to NULL both functions, if you don't want to support this feature. */ + uint64_t (*storedHashFunction)(const void *key); + int (*storedKeyCompare)(dict *d, const void *key1, const void *key2); + + /* Optional callback called when the dict is destroyed. */ + void (*onDictRelease)(dict *d); } dictType; #define DICTHT_SIZE(exp) ((exp) == -1 ? 0 : (unsigned long)1<<(exp)) @@ -76,7 +102,9 @@ struct dict { long rehashidx; /* rehashing not in progress if rehashidx == -1 */ /* Keep small vars at end for optimal (minimal) struct padding */ - int16_t pauserehash; /* If >0 rehashing is paused (<0 indicates coding error) */ + unsigned pauserehash : 15; /* If >0 rehashing is paused */ + + unsigned useStoredKeyApi : 1; /* See comment of storedHashFunction above */ signed char ht_size_exp[2]; /* exponent of size. (size = 1<0 automatic resizing is disallowed (<0 indicates coding error) */ void *metadata[]; @@ -136,7 +164,6 @@ typedef struct { #define dictMetadataSize(d) ((d)->type->dictMetadataBytes \ ? (d)->type->dictMetadataBytes(d) : 0) -#define dictHashKey(d, key) ((d)->type->hashFunction(key)) #define dictBuckets(d) (DICTHT_SIZE((d)->ht_size_exp[0])+DICTHT_SIZE((d)->ht_size_exp[1])) #define dictSize(d) ((d)->ht_used[0]+(d)->ht_used[1]) #define dictIsEmpty(d) ((d)->ht_used[0] == 0 && (d)->ht_used[1] == 0) @@ -146,6 +173,7 @@ typedef struct { #define dictIsRehashingPaused(d) ((d)->pauserehash > 0) #define dictPauseAutoResize(d) ((d)->pauseAutoResize++) #define dictResumeAutoResize(d) ((d)->pauseAutoResize--) +#define dictUseStoredKeyApi(d, flag) ((d)->useStoredKeyApi = (flag)) /* If our unsigned long type can store a 64 bit number, use a 64 bit PRNG. */ #if ULONG_MAX >= 0xffffffffffffffff @@ -162,6 +190,7 @@ typedef enum { /* API */ dict *dictCreate(dictType *type); +void dictTypeAddMeta(dict **d, dictType *typeWithMeta); int dictExpand(dict *d, unsigned long size); int dictTryExpand(dict *d, unsigned long size); int dictShrink(dict *d, unsigned long size); diff --git a/src/ebuckets.c b/src/ebuckets.c new file mode 100644 index 000000000..387aef88c --- /dev/null +++ b/src/ebuckets.c @@ -0,0 +1,2422 @@ +/* + * Copyright Redis Ltd. 2024 - present + * + * Licensed under your choice of the Redis Source Available License 2.0 (RSALv2) + * or the Server Side Public License v1 (SSPLv1). + */ + +#include +#include +#include +#include +#include "zmalloc.h" +#include "redisassert.h" +#include "config.h" +#include "ebuckets.h" + +#define UNUSED(x) (void)(x) + + +/*** DEBUGGING & VALIDATION + * + * To validate DS on add(), remove() and ebExpire() + * #define EB_VALIDATE_DEBUG 1 + */ + +#if (REDIS_TEST || EB_VALIDATE_DEBUG) && !defined(EB_TEST_BENCHMARK) +#define EB_VALIDATE_STRUCTURE(eb, type) ebValidate(eb, type) +#else +#define EB_VALIDATE_STRUCTURE(eb, type) // Do nothing +#endif + +/*** BENCHMARK + * + * To benchmark ebuckets creation and active-expire with 10 million items, apply + * the following command such that `EB_TEST_BENCHMARK` gets desired distribution + * of expiration times: + * + * # 0=1msec, 1=1sec, 2=1min, 3=1hour, 4=1day, 5=1week, 6=1month + * make REDIS_CFLAGS='-DREDIS_TEST -DEB_TEST_BENCHMARK=3' && ./src/redis-server test ebuckets + */ + +/* + * Keep just enough bytes of bucket-key, taking into consideration configured + * EB_BUCKET_KEY_PRECISION, and ignoring LSB bits that has no impact. + * + * The main motivation is that since the bucket-key size determines the maximum + * depth of the rax tree, then we can prune the tree to be more shallow and thus + * reduce the maintenance and traversal of each node in the B-tree. + */ +#if EB_BUCKET_KEY_PRECISION < 8 +#define EB_KEY_SIZE 6 +#elif EB_BUCKET_KEY_PRECISION >= 8 && EB_BUCKET_KEY_PRECISION < 16 +#define EB_KEY_SIZE 5 +#else +#define EB_KEY_SIZE 4 +#endif + +/* + * EB_SEG_MAX_ITEMS - Maximum number of items in rax-segment before trying to + * split. To simplify, it has the same value as EB_LIST_MAX_ITEMS. + */ +#define EB_SEG_MAX_ITEMS 16 +#define EB_LIST_MAX_ITEMS EB_SEG_MAX_ITEMS + +/* From expiration time to bucket-key */ +#define EB_BUCKET_KEY(exptime) ((exptime) >> EB_BUCKET_KEY_PRECISION) + + /* From bucket-key to expiration time */ +#define EB_BUCKET_EXP_TIME(bucketKey) ((uint64_t)(bucketKey) << EB_BUCKET_KEY_PRECISION) + +/*** structs ***/ + +typedef struct CommonSegHdr { + eItem head; +} CommonSegHdr; + + +/* FirstSegHdr - Header of first segment of a bucket. + * + * A bucket in rax tree with a single segment will be as follows: + * + * +-------------+ +------------+ +------------+ + * | FirstSegHdr | | eItem(1) | | eItem(N) | + * [rax] --> | eItem head | --> | void *next | --> ... --> | void *next | --+ + * +-------------+ +------------+ +------------+ | + * ^ | + * | | + * +-------------------------------------------------------+ + * + * Note that the cyclic references assist to update locally the segment(s) without + * the need to "heavy" traversal of the rax tree for each change. + */ +typedef struct FirstSegHdr { + eItem head; /* first item in the list */ + uint32_t totalItems; /* total items in the bucket, across chained segments */ + uint32_t numSegs; /* number of segments in the bucket */ +} FirstSegHdr; + +/* NextSegHdr - Header of next segment in an extended-segment (bucket) + * + * Here is the layout of an extended-segment, after adding another item to a single, + * full (EB_SEG_MAX_ITEMS=16), segment (all items must have same bucket-key value): + * + * +-------------+ +------------+ +------------+ +------------+ +------------+ + * | FirstSegHdr | | eItem(17) | | NextSegHdr | | eItem(1) | | eItem(16) | + * [rax] --> | eItem head | --> | void *next | --> | eItem head | --> | void *next | --> ... --> | void *next | --+ + * +-------------+ +------------+ +------------+ +------------+ +------------+ | + * ^ | ^ | + * | | | | + * +------------- firstSeg / prevSeg -+ +------------------------------------------------------+ + */ +typedef struct NextSegHdr { + eItem head; + CommonSegHdr *prevSeg; /* pointer to previous segment */ + FirstSegHdr *firstSeg; /* pointer to first segment of the bucket */ +} NextSegHdr; + +/* Selective copy of ifndef from server.h instead of including it */ +#ifndef static_assert +#define static_assert(expr, lit) extern char __static_assert_failure[(expr) ? 1:-1] +#endif +/* Verify that "head" field is aligned in FirstSegHdr, NextSegHdr and CommonSegHdr */ +static_assert(offsetof(FirstSegHdr, head) == 0, "FirstSegHdr head is not aligned"); +static_assert(offsetof(NextSegHdr, head) == 0, "FirstSegHdr head is not aligned"); +static_assert(offsetof(CommonSegHdr, head) == 0, "FirstSegHdr head is not aligned"); +/* Verify attached metadata to rax is aligned */ +static_assert(offsetof(rax, metadata) % sizeof(void*) == 0, "metadata field is not aligned in rax"); + +/* EBucketNew - Indicates the caller to create a new bucket following the addition + * of another item to a bucket (either single-segment or extended-segment). */ +typedef struct EBucketNew { + FirstSegHdr segment; + ExpireMeta *mLast; /* last item in the chain */ + uint64_t ebKey; +} EBucketNew; + +static void ebNewBucket(EbucketsType *type, EBucketNew *newBucket, eItem item, uint64_t key); +static int ebBucketPrint(uint64_t bucketKey, EbucketsType *type, FirstSegHdr *firstSeg); +static uint64_t *ebRaxNumItems(rax *rax); + +/*** Static functions ***/ + +/* Extract pointer to list from ebuckets handler */ +static inline rax *ebGetRaxPtr(ebuckets eb) { return (rax *)eb; } + +/* The lsb in ebuckets pointer determines whether the pointer points to rax or list. */ +static inline int ebIsList(ebuckets eb) { + return (((uintptr_t)(void *)eb & 0x1) == 1); +} +/* set lsb in ebuckets pointer to 1 to mark it as list. Unless empty (NULL) */ +static inline ebuckets ebMarkAsList(eItem item) { + if (item == NULL) return item; + + /* either 'itemsAddrAreOdd' or not, we end up with lsb is set to 1 */ + return (void *) ((uintptr_t) item | 1); +} + +/* Extract pointer to the list from ebuckets handler */ +static inline eItem ebGetListPtr(EbucketsType *type, ebuckets eb) { + /* if 'itemsAddrAreOdd' then no need to reset lsb bit */ + if (type->itemsAddrAreOdd) + return eb; + else + return (void*)((uintptr_t)(eb) & ~1); +} + +/* Converts the logical starting time value of a given bucket-key to its equivalent + * "physical" value in the context of an rax tree (rax-key). Although their values + * are the same, their memory layouts differ. The raxKey layout orders bytes in + * memory is from the MSB to the LSB, and the length of the key is EB_KEY_SIZE. */ +static inline void bucketKey2RaxKey(uint64_t bucketKey, unsigned char *raxKey) { + for (int i = EB_KEY_SIZE-1; i >= 0; --i) { + raxKey[i] = (unsigned char) (bucketKey & 0xFF); + bucketKey >>= 8; + } +} + +/* Converts the "physical" value of rax-key to its logical counterpart, representing + * the starting time value of a bucket. The values are equivalent, but their memory + * layouts differ. The raxKey is assumed to be ordered from the MSB to the LSB with + * a length of EB_KEY_SIZE. The resulting bucket-key is the logical representation + * with respect to ebuckets. */ +static inline uint64_t raxKey2BucketKey(unsigned char *raxKey) { + uint64_t bucketKey = 0; + for (int i = 0; i < EB_KEY_SIZE ; ++i) + bucketKey = (bucketKey<<8) + raxKey[i]; + return bucketKey; +} + +/* Add another item to a bucket that consists of extended-segments. In this + * scenario, all items in the bucket share the same bucket-key value and the first + * segment is already full (if not, the function ebSegAddAvail() would have being + * called). This requires the creation of another segment. The layout of the + * segments before and after the addition of the new item is as follows: + * + * Before: [segHdr] -> {item1,..,item16} -> [..] + * After: [segHdr] -> {newItem} -> [nextSegHdr] -> {item1,..,item16} -> [..] + * + * Taken care to persist `segHdr` to be the same instance after the change. + * This is important because the rax tree is pointing to it. */ +static int ebSegAddExtended(EbucketsType *type, FirstSegHdr *firstSegHdr, eItem newItem) { + /* Allocate nextSegHdr and let it take the items of first segment header */ + NextSegHdr *nextSegHdr = zmalloc(sizeof(NextSegHdr)); + nextSegHdr->head = firstSegHdr->head; + /* firstSegHdr will stay the first and new nextSegHdr will follow it */ + nextSegHdr->prevSeg = (CommonSegHdr *) firstSegHdr; + nextSegHdr->firstSeg = firstSegHdr; + + ExpireMeta *mIter = type->getExpireMeta(nextSegHdr->head); + mIter->firstItemBucket = 0; + for (int i = 0 ; i < EB_SEG_MAX_ITEMS-1 ; i++) + mIter = type->getExpireMeta(mIter->next); + + if (mIter->lastItemBucket) { + mIter->next = nextSegHdr; + } else { + /* Update next-next-segment to point back to next-segment */ + NextSegHdr *nextNextSegHdr = mIter->next; + nextNextSegHdr->prevSeg = (CommonSegHdr *) nextSegHdr; + } + + firstSegHdr->numSegs += 1; + firstSegHdr->totalItems += 1; + firstSegHdr->head = newItem; + + ExpireMeta *mNewItem = type->getExpireMeta(newItem); + mNewItem->numItems = 1; + mNewItem->next = nextSegHdr; + mNewItem->firstItemBucket = 1; + mNewItem->lastInSegment = 1; + + return 0; +} + +/* Add another eItem to a segment with available space. Keep items sorted in ascending order */ +static int ebSegAddAvail(EbucketsType *type, FirstSegHdr *seg, eItem item) { + eItem head = seg->head; + ExpireMeta *nextMeta; + ExpireMeta *mHead = type->getExpireMeta(head); + ExpireMeta *mItem = type->getExpireMeta(item); + uint64_t itemExpireTime = ebGetMetaExpTime(mItem); + + seg->totalItems++; + + assert(mHead->numItems < EB_SEG_MAX_ITEMS); + + /* if new item expiry time is smaller than the head then add it before the head */ + if (ebGetMetaExpTime(mHead) > itemExpireTime) { + /* Insert item as the new head */ + mItem->next = head; + mItem->firstItemBucket = mHead->firstItemBucket; + mItem->numItems = mHead->numItems + 1; + mHead->firstItemBucket = 0; + mHead->numItems = 0; + seg->head = item; + return 0; + } + + /* Insert item in the middle of segment */ + ExpireMeta *mIter = mHead; + for (int i = 1 ; i < mHead->numItems ; i++) { + nextMeta = type->getExpireMeta(mIter->next); + /* Insert item in the middle */ + if (ebGetMetaExpTime(nextMeta) > itemExpireTime) { + mHead->numItems = mHead->numItems + 1; + mItem->next = mIter->next; + mIter->next = item; + return 0; + } + mIter = nextMeta; + } + + /* Insert item as the last item of the segment. Inherit flags from previous last item */ + mHead->numItems = mHead->numItems + 1; + mItem->next = mIter->next; + mItem->lastInSegment = mIter->lastInSegment; + mItem->lastItemBucket = mIter->lastItemBucket; + mIter->lastInSegment = 0; + mIter->lastItemBucket = 0; + mIter->next = item; + return 0; +} + +/* Return 1 if split segment to two succeeded. Else, return 0. The only reason + * the split can fail is that All the items in the segment have the same bucket-key */ +static int ebTrySegSplit(EbucketsType *type, FirstSegHdr *seg, EBucketNew *newBucket) { + int minMidDist=(EB_SEG_MAX_ITEMS / 2), bestMiddleIndex = -1; + uint64_t splitKey = -1; + eItem firstItemSecondPart; + ExpireMeta *mLastItemFirstPart, *mFirstItemSecondPart; + + eItem head = seg->head; + ExpireMeta *mHead = type->getExpireMeta(head); + ExpireMeta *mNext, *mIter = mHead; + + /* Search for best middle index to split the segment into two segments. As the + * items are arranged in ascending order, it cannot split between two items that + * have the same expiration time and therefore the split won't necessarily be + * balanced (Or won't be possible to split at all if all have the same exp-time!) + */ + for (int i = 0 ; i < EB_SEG_MAX_ITEMS-1 ; i++) { + //printf ("i=%d\n", i); + mNext = type->getExpireMeta(mIter->next); + if (EB_BUCKET_KEY(ebGetMetaExpTime(mNext)) > EB_BUCKET_KEY( + ebGetMetaExpTime(mIter))) { + /* If found better middle index before reaching halfway, save it */ + if (i < (EB_SEG_MAX_ITEMS/2)) { + splitKey = EB_BUCKET_KEY(ebGetMetaExpTime(mNext)); + bestMiddleIndex = i; + mLastItemFirstPart = mIter; + mFirstItemSecondPart = mNext; + firstItemSecondPart = mIter->next; + minMidDist = (EB_SEG_MAX_ITEMS / 2) - bestMiddleIndex; + } else { + /* after crossing the middle need only to look for the first diff */ + if (minMidDist > (i + 1 - EB_SEG_MAX_ITEMS / 2)) { + splitKey = EB_BUCKET_KEY(ebGetMetaExpTime(mNext)); + bestMiddleIndex = i; + mLastItemFirstPart = mIter; + mFirstItemSecondPart = mNext; + firstItemSecondPart = mIter->next; + minMidDist = i + 1 - EB_SEG_MAX_ITEMS / 2; + } + } + } + mIter = mNext; + } + + /* If cannot find index to split because all with same EB_BUCKET_KEY(), then + * segment should be treated as extended segment */ + if (bestMiddleIndex == -1) + return 0; + + /* New bucket */ + newBucket->segment.head = firstItemSecondPart; + newBucket->segment.numSegs = 1; + newBucket->segment.totalItems = EB_SEG_MAX_ITEMS - bestMiddleIndex - 1; + mFirstItemSecondPart->numItems = EB_SEG_MAX_ITEMS - bestMiddleIndex - 1; + newBucket->mLast = mIter; + newBucket->ebKey = splitKey; + mIter->lastInSegment = 1; + mIter->lastItemBucket = 1; + mIter->next = &newBucket->segment; /* to be updated by caller */ + mFirstItemSecondPart->firstItemBucket = 1; + + /* update existing bucket */ + seg->totalItems = bestMiddleIndex + 1; + mHead->numItems = bestMiddleIndex + 1; + mLastItemFirstPart->lastInSegment = 1; + mLastItemFirstPart->lastItemBucket = 1; + mLastItemFirstPart->next = seg; + return 1; +} + +/* Return 1 if managed to expire the entire segment. Returns 0 otherwise. */ +int ebSingleSegExpire(FirstSegHdr *firstSegHdr, + EbucketsType *type, + ExpireInfo *info, + eItem *updateList) +{ + uint64_t itemExpTime; + eItem iter = firstSegHdr->head; + ExpireMeta *mIter = type->getExpireMeta(iter); + uint32_t i=0, numItemsInSeg = mIter->numItems; + + while (info->itemsExpired < info->maxToExpire) { + itemExpTime = ebGetMetaExpTime(mIter); + + /* Items are arranged in ascending expire-time order in a segment. Stops + * active expiration when an item's expire time is greater than `now`. */ + if (itemExpTime > info->now) + break; + + /* keep aside next before deletion of iter */ + eItem next = mIter->next; + mIter->trash = 1; + ExpireAction act = info->onExpireItem(iter, info->ctx); + + /* if (act == ACT_REMOVE_EXP_ITEM) + * then don't touch the item. Assume it got deleted */ + + /* If indicated to stop then break (cb didn't delete the item) */ + if (act == ACT_STOP_ACTIVE_EXP) { + mIter->trash = 0; + break; + } + + if (act == ACT_UPDATE_EXP_ITEM) { + mIter->next = *updateList; + *updateList = iter; + } + + ++info->itemsExpired; + + /* if deleted all items in segment, delete header and return */ + if (++i == numItemsInSeg) { + zfree(firstSegHdr); + return 1; + } + + /* More items in the segment. Set iter to next item and update mIter */ + iter = next; + mIter = type->getExpireMeta(iter); + } + + /* Update the single-segment with remaining items */ + mIter->numItems = numItemsInSeg - i; + mIter->firstItemBucket = 1; + firstSegHdr->head = iter; + firstSegHdr->totalItems -= i; + + /* Update nextExpireTime */ + info->nextExpireTime = ebGetMetaExpTime(mIter); + + return 0; +} + +/* return 1 if managed to expire the entire segment. Returns 0 otherwise. */ +static int ebSegExpire(FirstSegHdr *firstSegHdr, + EbucketsType *type, + ExpireInfo *info, + eItem *updateList) +{ + eItem iter = firstSegHdr->head; + uint32_t numSegs = firstSegHdr->numSegs; + void *nextSegHdr = firstSegHdr; + + if (numSegs == 1) + return ebSingleSegExpire(firstSegHdr, type, info, updateList); + + /* + * In an extended-segment, there's no need to verify the expiration time of + * each item. This is because all items in an extended-segment share the same + * bucket-key. Therefore, we can remove all items without checking their + * individual expiration times. This is different from a single-segment + * scenario, where items can have different bucket-keys. + */ + for (uint32_t seg=0 ; seg < numSegs ; seg++) { + uint32_t i; + ExpireMeta *mIter = type->getExpireMeta(iter); + uint32_t numItemsInSeg = mIter->numItems; + + for (i = 0; (i < numItemsInSeg) && (info->itemsExpired < info->maxToExpire) ; ++i) { + mIter = type->getExpireMeta(iter); + + /* keep aside `next` before removing `iter` by onExpireItem */ + eItem next = mIter->next; + mIter->trash = 1; + ExpireAction act = info->onExpireItem(iter, info->ctx); + + /* if (act == ACT_REMOVE_EXP_ITEM) + * then don't touch the item. Assume it got deleted */ + + /* If indicated to stop then break (callback didn't delete the item) */ + if (act == ACT_STOP_ACTIVE_EXP) { + mIter->trash = 0; + break; + } + + if (act == ACT_UPDATE_EXP_ITEM) { + mIter->next = *updateList; + *updateList = iter; + } + + /* Item was REMOVED/UPDATED. Advance to `next` item */ + iter = next; + ++info->itemsExpired; + firstSegHdr->totalItems -= 1; + } + + /* if deleted all items in segment */ + if (i == numItemsInSeg) { + /* If not last segment in bucket, then delete segment header */ + if (seg + 1 < numSegs) { + nextSegHdr = iter; + iter = ((NextSegHdr *) nextSegHdr)->head; + zfree(nextSegHdr); + firstSegHdr->numSegs -= 1; + firstSegHdr->head = iter; + mIter = type->getExpireMeta(iter); + mIter->firstItemBucket = 1; + } + } else { + /* We reached here because for-loop above break due to + * ACT_STOP_ACTIVE_EXP or reached maxToExpire */ + firstSegHdr->head = iter; + mIter = type->getExpireMeta(iter); + mIter->numItems = numItemsInSeg - i; + mIter->firstItemBucket = 1; + info->nextExpireTime = ebGetMetaExpTime(mIter); + + /* If deleted one or more segments, update prevSeg of next seg to point firstSegHdr. + * If it is the last segment, then last item need to point firstSegHdr */ + if (seg>0) { + int numItems = mIter->numItems; + for (int i = 0; i < numItems - 1; i++) + mIter = type->getExpireMeta(mIter->next); + + if (mIter->lastItemBucket) { + mIter->next = firstSegHdr; + } else { + /* Update next-segment to point back to firstSegHdr */ + NextSegHdr *nsh = mIter->next; + nsh->prevSeg = (CommonSegHdr *) firstSegHdr; + } + } + + return 0; + } + } + + /* deleted last segment in bucket */ + zfree(firstSegHdr); + return 1; +} + +/*** Static functions of list ***/ + +/* Convert a list to rax. + * + * To create a new rax, the function first converts the list to a segment by + * allocating a segment header and attaching to it the already existing list. + * Then, it adds the new segment to the rax as the first bucket. */ +static rax *ebConvertListToRax(eItem listHead, EbucketsType *type) { + FirstSegHdr *firstSegHdr = zmalloc(sizeof(FirstSegHdr)); + firstSegHdr->head = listHead; + firstSegHdr->totalItems = EB_LIST_MAX_ITEMS ; + firstSegHdr->numSegs = 1; + + /* update last item to point on the segment header */ + ExpireMeta *metaItem = type->getExpireMeta(listHead); + uint64_t bucketKey = EB_BUCKET_KEY(ebGetMetaExpTime(metaItem)); + while (metaItem->lastItemBucket == 0) + metaItem = type->getExpireMeta(metaItem->next); + metaItem->next = firstSegHdr; + + /* Use min expire-time for the first segment in rax */ + unsigned char raxKey[EB_KEY_SIZE]; + bucketKey2RaxKey(bucketKey, raxKey); + rax *rax = raxNewWithMetadata(sizeof(uint64_t)); + *ebRaxNumItems(rax) = EB_LIST_MAX_ITEMS; + raxInsert(rax, raxKey, EB_KEY_SIZE, firstSegHdr, NULL); + return rax; +} + +/** + * Adds another 'item' to the ebucket of type list, keeping the list sorted by + * ascending expiration time. + * + * @param eb - Pointer to the ebuckets handler of type list. Gets updated if the item is + * added as the new head. + * @param type - Pointer to the EbucketsType structure defining the type of ebucket. + * @param item - The eItem to be added to the list. + * + * @return 1 if the maximum list length is reached; otherwise, return 0. + */ +static int ebAddToList(ebuckets *eb, EbucketsType *type, eItem item) { + ExpireMeta *metaItem = type->getExpireMeta(item); + + /* if ebucket-list is empty (NULL), then create a new list by marking 'item' + * as the head and tail of the list */ + if (unlikely(ebIsEmpty(*eb))) { + metaItem->next = NULL; + metaItem->numItems = 1; + metaItem->lastInSegment = 1; + metaItem->firstItemBucket = 1; + metaItem->lastItemBucket = 1; + *eb = ebMarkAsList(item); + return 0; + } + + eItem head = ebGetListPtr(type, *eb); + ExpireMeta *metaHead = type->getExpireMeta(head); + + /* If reached max items in list, then return 1 */ + if (metaHead->numItems == EB_LIST_MAX_ITEMS) + return 1; + + /* if expiry time of 'item' is smaller than the head then add it as the new head */ + if (ebGetMetaExpTime(metaHead) > ebGetMetaExpTime(metaItem)) { + /* Insert item as the new head */ + metaItem->next = head; + metaItem->firstItemBucket = 1; + metaItem->numItems = metaHead->numItems + 1; + metaHead->firstItemBucket = 0; + metaHead->numItems = 0; + *eb = ebMarkAsList(item); + return 0; + } + + + /* Try insert item in the middle of list */ + ExpireMeta *mIter = metaHead; + for (int i = 1 ; i < metaHead->numItems ; i++) { + ExpireMeta *nextMeta = type->getExpireMeta(mIter->next); + /* Insert item in the middle */ + if (ebGetMetaExpTime(nextMeta) > ebGetMetaExpTime(metaItem)) { + metaHead->numItems += 1; + metaItem->next = mIter->next; + mIter->next = item; + return 0; + } + mIter = nextMeta; + } + + /* Insert item as the last item of the list. */ + metaHead->numItems += 1; + metaItem->next = NULL; + metaItem->lastInSegment = 1; + metaItem->lastItemBucket = 1; + /* Update obsolete last item */ + mIter->lastInSegment = 0; + mIter->lastItemBucket = 0; + mIter->next = item; + return 0; +} + +/* return 1 if removed from list. Otherwise, return 0 */ +static int ebRemoveFromList(ebuckets *eb, EbucketsType *type, eItem item) { + if (ebIsEmpty(*eb)) + return 0; /* not removed */ + + ExpireMeta *metaItem = type->getExpireMeta(item); + eItem head = ebGetListPtr(type, *eb); + + /* if item is the head of the list */ + if (head == item) { + eItem newHead = metaItem->next; + if (newHead != NULL) { + ExpireMeta *mNewHead = type->getExpireMeta(newHead); + mNewHead->numItems = metaItem->numItems - 1; + mNewHead->firstItemBucket = 1; + *eb = ebMarkAsList(newHead); + return 1; /* removed */ + } + *eb = NULL; + return 1; /* removed */ + } + + /* item is not the head of the list */ + ExpireMeta *metaHead = type->getExpireMeta(head); + + eItem iter = head; + while (iter != NULL) { + ExpireMeta *metaIter = type->getExpireMeta(iter); + if (metaIter->next == item) { + metaIter->next = metaItem->next; + /* If deleted item is the last in the list, then update new last item */ + if (metaItem->next == NULL) { + metaIter->lastInSegment = 1; + metaIter->lastItemBucket = 1; + } + metaHead->numItems -= 1; + return 1; /* removed */ + } + iter = metaIter->next; + } + return 0; /* not removed */ +} + +/* return 1 if none left. Otherwise return 0 */ +static int ebListExpire(ebuckets *eb, + EbucketsType *type, + ExpireInfo *info, + eItem *updateList) +{ + uint32_t expired = 0; + eItem item = ebGetListPtr(type, *eb); + ExpireMeta *metaItem = type->getExpireMeta(item); + uint32_t numItems = metaItem->numItems; /* first item must exists */ + + while (item != NULL) { + metaItem = type->getExpireMeta(item); + uint64_t itemExpTime = ebGetMetaExpTime(metaItem); + + /* Items are arranged in ascending expire-time order in a list. Stops list + * active expiration when an item's expiration time is greater than `now`. */ + if (itemExpTime > info->now) + break; + + if (info->itemsExpired == info->maxToExpire) + break; + + /* keep aside `next` before removing `iter` by onExpireItem */ + eItem *next = metaItem->next; + metaItem->trash = 1; + ExpireAction act = info->onExpireItem(item, info->ctx); + + /* if (act == ACT_REMOVE_EXP_ITEM) + * then don't touch the item. Assume it got deleted */ + + /* If indicated to stop then break (cb didn't delete the item) */ + if (act == ACT_STOP_ACTIVE_EXP) { + metaItem->trash = 0; + break; + } + + if (act == ACT_UPDATE_EXP_ITEM) { + metaItem->next = *updateList; + *updateList = item; + } + + ++expired; + ++(info->itemsExpired); + item = next; + } + + if (expired == numItems) { + *eb = NULL; + info->nextExpireTime = 0; + return 1; + } + + metaItem->numItems = numItems - expired; + metaItem->firstItemBucket = 1; + info->nextExpireTime = ebGetMetaExpTime(metaItem); + *eb = ebMarkAsList(item); + return 0; +} + +/* Validate the general structure of the list */ +static void ebValidateList(eItem head, EbucketsType *type) { + if (head == NULL) + return; + + ExpireMeta *mHead = type->getExpireMeta(head); + eItem iter = head; + ExpireMeta *mIter = type->getExpireMeta(iter), *mIterPrev = NULL; + + for (int i = 0; i < mHead->numItems ; ++i) { + mIter = type->getExpireMeta(iter); + if (i == 0) { + /* first item */ + assert(mIter->numItems > 0 && mIter->numItems <= EB_LIST_MAX_ITEMS); + assert(mIter->firstItemBucket == 1); + } else { + /* Verify that expire time of previous item is smaller or equal */ + assert(ebGetMetaExpTime(mIterPrev) <= ebGetMetaExpTime(mIter)); + assert(mIter->numItems == 0); + assert(mIter->firstItemBucket == 0); + } + + if (i == (mHead->numItems - 1)) { + /* last item */ + assert(mIter->lastInSegment == 1); + assert(mIter->lastItemBucket == 1); + assert(mIter->next == NULL); + } else { + assert(mIter->lastInSegment == 0); + assert(mIter->lastItemBucket == 0); + assert(mIter->next != NULL); + mIterPrev = mIter; + iter = mIter->next; + } + } +} + +/*** Static functions of ebuckets / rax ***/ + +static uint64_t *ebRaxNumItems(rax *rax) { + return (uint64_t*) rax->metadata; +} + +/* Allocate a single segment with a single item */ +static void ebNewBucket(EbucketsType *type, EBucketNew *newBucket, eItem item, uint64_t key) { + ExpireMeta *mItem = type->getExpireMeta(item); + + newBucket->segment.head = item; + newBucket->segment.totalItems = 1; + newBucket->segment.numSegs = 1; + newBucket->mLast = type->getExpireMeta(item); + newBucket->ebKey = key; + mItem->numItems = 1; + mItem->firstItemBucket = 1; + mItem->lastInSegment = 1; + mItem->lastItemBucket = 1; + mItem->next = &newBucket->segment; +} + +/* + * ebBucketPrint - Prints all the segments in the bucket and time expiration + * of each item in the following fashion: + * + * Bucket(tot=0008,sgs=0001) : [11, 21, 26, 27, 29, 49, 59, 62] + * Bucket(tot=0007,sgs=0001) : [67, 86, 90, 92, 115, 123, 126] + * Bucket(tot=0005,sgs=0001) : [130, 135, 135, 136, 140] + * Bucket(tot=0009,sgs=0002) : [182] + * [162, 163, 167, 168, 172, 177, 183, 186] + * Bucket(tot=0001,sgs=0001) : [193] + */ +static int ebBucketPrint(uint64_t bucketKey, EbucketsType *type, FirstSegHdr *firstSeg) { + eItem iter; + ExpireMeta *mIter, *mHead; + static int PRINT_EXPIRE_META_FLAGS=0; + + iter = firstSeg->head; + mHead = type->getExpireMeta(iter); + + printf("Bucket(key=%06" PRIu64 ",tot=%04d,sgs=%04d) :", bucketKey, firstSeg->totalItems, firstSeg->numSegs); + while (1) { + mIter = type->getExpireMeta(iter); /* not really needed. Just to hash the compiler */ + printf(" ["); + for (int i = 0; i < mHead->numItems ; ++i) { + mIter = type->getExpireMeta(iter); + uint64_t expireTime = ebGetMetaExpTime(mIter); + + if (i == 0 && PRINT_EXPIRE_META_FLAGS) + printf("%" PRIu64 ", ", + expireTime, mIter->numItems, mIter->firstItemBucket, + mIter->lastInSegment, mIter->lastItemBucket); + else if (i == (mHead->numItems - 1) && PRINT_EXPIRE_META_FLAGS) { + printf("%" PRIu64 "", + expireTime, mIter->numItems, mIter->firstItemBucket, + mIter->lastInSegment, mIter->lastItemBucket); + } else + printf("%" PRIu64 "%s", expireTime, (i == mHead->numItems - 1) ? "" : ", "); + + iter = mIter->next; + } + + if (mIter->lastItemBucket) { + printf("]\n"); + break; + } + printf("]\n "); + iter = ((NextSegHdr *) mIter->next)->head; + mHead = type->getExpireMeta(iter); + + } + return 0; +} + +/* Add another eItem to bucket. If needed return 'newBucket' for insertion in rax tree. + * + * 1) If the bucket is based on a single, not full segment, then add the item to the segment. + * 2) If a single, full segment, then try to split it and then add the item. + * 3) If failed to split, then all items in the bucket have the same bucket-key. + * - If the new item has the same bucket-key, then extend the segment to + * be an extended-segment, if not already, and add the item to it. + * - If the new item has a different bucket-key, then allocate a new bucket + * for it. + */ +static int ebAddToBucket(EbucketsType *type, + FirstSegHdr *firstSegBkt, + eItem item, + EBucketNew *newBucket, + uint64_t *updateBucketKey) +{ + newBucket->segment.head = NULL; /* no new bucket as default */ + + if (firstSegBkt->numSegs == 1) { + /* If bucket is a single, not full segment, then add the item to the segment */ + if (firstSegBkt->totalItems < EB_SEG_MAX_ITEMS) + return ebSegAddAvail(type, firstSegBkt, item); + + /* If bucket is a single, full segment, and segment split succeeded */ + if (ebTrySegSplit(type, firstSegBkt, newBucket) == 1) { + /* The split got failed only because all items in the segment have the + * same bucket-key */ + ExpireMeta *mItem = type->getExpireMeta(item); + + /* Check which of the two segments the new item should be added to. Note that + * after the split, bucket-key of `newBucket` is bigger than bucket-key of + * `firstSegBkt`. That is `firstSegBkt` preserves its bucket-key value + * (and its location in rax tree) before the split */ + if (EB_BUCKET_KEY(ebGetMetaExpTime(type->getExpireMeta(item))) < newBucket->ebKey) { + return ebSegAddAvail(type, firstSegBkt, item); + } else { + /* Add the `item` to the new bucket */ + ebSegAddAvail(type, &(newBucket->segment), item); + + /* if new item is now last item in the segment, then update lastItemBucket */ + if (mItem->lastItemBucket) + newBucket->mLast = mItem; + return 0; + } + } + } + + /* If reached here, then either: + * (1) a bucket with multiple segments + * (2) Or, a single, full segment which failed to split. + * + * Either way, all items in the bucket have the same bucket-key value. Thus: + * (A) If 'item' has the same bucket-key as the ones in this bucket, then add it as well + * (B) Else, allocate a new bucket for it. + */ + + ExpireMeta *mHead = type->getExpireMeta(firstSegBkt->head); + ExpireMeta *mItem = type->getExpireMeta(item); + + uint64_t bucketKey = EB_BUCKET_KEY(ebGetMetaExpTime(mHead)); /* same for all items in the segment */ + uint64_t itemKey = EB_BUCKET_KEY(ebGetMetaExpTime(mItem)); + + if (bucketKey == itemKey) { + /* New item has the same bucket-key as the ones in this bucket, Add it as well */ + if (mHead->numItems < EB_SEG_MAX_ITEMS) + return ebSegAddAvail(type, firstSegBkt, item); /* Add item to first segment */ + else { + /* If a regular segment becomes extended-segment, then update the + * bucket-key to be aligned with the expiration-time of the items + * it contains */ + if (firstSegBkt->numSegs == 1) + *updateBucketKey = bucketKey; + + return ebSegAddExtended(type, firstSegBkt, item); /* Add item in a new segment */ + } + } else { + /* If the item cannot be added to the visited (extended-segment) bucket + * because it has a key not equal to bucket-key, then need to allocate a new + * bucket for the item. If the key of the item is below the bucket-key of + * the visited bucket, then the new item will be added to a new segment + * before it and the visited bucket key will be updated to accurately + * reflect the bucket-key of the (extended-segment) bucket */ + if (bucketKey > itemKey) + *updateBucketKey = bucketKey; + + ebNewBucket(type, newBucket, item, EB_BUCKET_KEY(ebGetMetaExpTime(mItem))); + return 0; + } +} + +/* + * Remove item from rax + * + * Return 1 if removed. Otherwise, return 0 + * + * Note: The function is optimized to remove items locally from segments without + * traversing rax tree or stepping long extended-segments. Therefore, it is + * assumed that the item is present in the bucket without verification. + * + * TODO: Written straightforward. Should be optimized to merge small segments. + */ +static int ebRemoveFromRax(ebuckets *eb, EbucketsType *type, eItem item) { + ExpireMeta *mItem = type->getExpireMeta(item); + rax *rax = ebGetRaxPtr(*eb); + + /* if item is the only one left in a single-segment bucket, then delete bucket */ + if (unlikely(mItem->firstItemBucket && mItem->lastItemBucket)) { + raxIterator ri; + raxStart(&ri, rax); + unsigned char raxKey[EB_KEY_SIZE]; + bucketKey2RaxKey(EB_BUCKET_KEY(ebGetMetaExpTime(mItem)), raxKey); + raxSeek(&ri, "<=", raxKey, EB_KEY_SIZE); + + if (raxNext(&ri) == 0) + return 0; /* not removed */ + + FirstSegHdr *segHdr = ri.data; + + if (segHdr->head != item) + return 0; /* not removed */ + + zfree(segHdr); + raxRemove(ri.rt, ri.key, EB_KEY_SIZE, NULL); + raxStop(&ri); + + /* If last bucket in rax, then delete the rax */ + if (rax->numele == 0) { + raxFree(rax); + *eb = NULL; + return 1; /* removed */ + } + } else if (mItem->numItems == 1) { + /* If the `item` is the only one in its segment, there must be additional + * items and segments in this bucket. If there weren't, the item would + * have been removed by the previous condition. */ + + if (mItem->firstItemBucket) { + /* If the first item/segment in extended-segments, then + * - Remove current segment (with single item) and promote next-segment to be first. + * - Update first item of next-segment to be firstItemBucket + * - Update `prevSeg` next-of-next segment to point new header of next-segment + * - Update FirstSegHdr to totalItems-1, numSegs-1 */ + NextSegHdr *nextHdr = mItem->next; + FirstSegHdr *firstHdr = (FirstSegHdr *) nextHdr->prevSeg; + firstHdr->head = nextHdr->head; + firstHdr->totalItems--; + firstHdr->numSegs--; + zfree(nextHdr); + eItem *iter = firstHdr->head; + ExpireMeta *mIter = type->getExpireMeta(iter); + mIter->firstItemBucket = 1; + while (mIter->lastInSegment == 0) { + iter = mIter->next; + mIter = type->getExpireMeta(iter); + } + if (mIter->lastItemBucket) + mIter->next = firstHdr; + else + ((NextSegHdr *) mIter->next)->prevSeg = (CommonSegHdr *) firstHdr; + + } else if (mItem->lastItemBucket) { + /* If last item/segment in bucket, then + * - promote previous segment to be last segment + * - Update FirstSegHdr to totalItems-1, numSegs-1 */ + NextSegHdr *currHdr = mItem->next; + CommonSegHdr *prevHdr = currHdr->prevSeg; + eItem iter = prevHdr->head; + ExpireMeta *mIter = type->getExpireMeta(iter); + while (mIter->lastInSegment == 0) { + iter = mIter->next; + mIter = type->getExpireMeta(iter); + } + currHdr->firstSeg->totalItems--; + currHdr->firstSeg->numSegs--; + mIter->next = prevHdr; + mIter->lastItemBucket = 1; + zfree(currHdr); + + } else { + /* item/segment is not the first or last item/segment. + * - Update previous segment to point next segment. + * - Update `prevSeg` of next segment + * - Update FirstSegHdr to totalItems-1, numSegs-1 */ + NextSegHdr *nextHdr = mItem->next; + NextSegHdr *currHdr = (NextSegHdr *) nextHdr->prevSeg; + CommonSegHdr *prevHdr = currHdr->prevSeg; + + ExpireMeta *mIter = type->getExpireMeta(prevHdr->head); + while (mIter->lastInSegment == 0) + mIter = type->getExpireMeta(mIter->next); + + mIter->next = nextHdr; + nextHdr->prevSeg = prevHdr; + nextHdr->firstSeg->totalItems--; + nextHdr->firstSeg->numSegs--; + zfree(currHdr); + + } + } else { + /* At least 2 items in current segment */ + if (mItem->numItems) { + /* If item is first item in segment (Must be numItems>1), then + * - Find segment header and update to point next item. + * - Let next inherit 'item' flags {firstItemBucket, numItems-1} + * - Update FirstSegHdr to totalItems-1 */ + ExpireMeta *mIter = mItem; + CommonSegHdr *currHdr; + while (mIter->lastInSegment == 0) + mIter = type->getExpireMeta(mIter->next); + if (mIter->lastItemBucket) + currHdr = (CommonSegHdr *) mIter->next; + else + currHdr = (CommonSegHdr *) ((NextSegHdr *) mIter->next)->prevSeg; + + if (mItem->firstItemBucket) + ((FirstSegHdr *) currHdr)->totalItems--; + else + ((NextSegHdr *) currHdr)->firstSeg->totalItems--; + + eItem *newHead = mItem->next; + ExpireMeta *mNewHead = type->getExpireMeta(newHead); + mNewHead->firstItemBucket = mItem->firstItemBucket; + mNewHead->numItems = mItem->numItems - 1; + currHdr->head = newHead; + + } else if (mItem->lastInSegment) { + /* If item is last in segment, then + * - find previous item and let it inherit (next, lastInSegment, lastItemBucket) + * - Find and update segment header to numItems-1 + * - Update FirstSegHdr to totalItems-1 */ + CommonSegHdr *currHdr; + if (mItem->lastItemBucket) + currHdr = (CommonSegHdr *) mItem->next; + else + currHdr = (CommonSegHdr *) ((NextSegHdr *) mItem->next)->prevSeg; + + ExpireMeta *mHead = type->getExpireMeta(currHdr->head); + mHead->numItems--; + ExpireMeta *mIter = mHead; + while (mIter->next != item) + mIter = type->getExpireMeta(mIter->next); + + mIter->next = mItem->next; + mIter->lastInSegment = mItem->lastInSegment; + mIter->lastItemBucket = mItem->lastItemBucket; + + if (mHead->firstItemBucket) + ((FirstSegHdr *) currHdr)->totalItems--; + else + ((NextSegHdr *) currHdr)->firstSeg->totalItems--; + + } else { + /* - Item is in the middle of segment. Find previous item and update to point next. + * - Find and Update segment header to numItems-1 + * - Update FirstSegHdr to totalItems-1 */ + ExpireMeta *mIter = mItem; + CommonSegHdr *currHdr; + while (mIter->lastInSegment == 0) + mIter = type->getExpireMeta(mIter->next); + if (mIter->lastItemBucket) + currHdr = (CommonSegHdr *) mIter->next; + else + currHdr = (CommonSegHdr *) ((NextSegHdr *) mIter->next)->prevSeg; + + ExpireMeta *mHead = type->getExpireMeta(currHdr->head); + mHead->numItems--; + mIter = mHead; + while (mIter->next != item) + mIter = type->getExpireMeta(mIter->next); + + mIter->next = mItem->next; + mIter->lastInSegment = mItem->lastInSegment; + mIter->lastItemBucket = mItem->lastItemBucket; + + if (mHead->firstItemBucket) + ((FirstSegHdr *) currHdr)->totalItems--; + else + ((NextSegHdr *) currHdr)->firstSeg->totalItems--; + } + } + *ebRaxNumItems(rax) -= 1; + return 1; /* removed */ +} + +int ebAddToRax(ebuckets *eb, EbucketsType *type, eItem item, uint64_t bucketKeyItem) { + EBucketNew newBucket; /* ebAddToBucket takes care to update newBucket.segment.head */ + raxIterator iter; + unsigned char raxKey[EB_KEY_SIZE]; + bucketKey2RaxKey(bucketKeyItem, raxKey); + rax *rax = ebGetRaxPtr(*eb); + raxStart(&iter,rax); + raxSeek(&iter, "<=", raxKey, EB_KEY_SIZE); + *ebRaxNumItems(rax) += 1; + /* If expireTime of the item is below the bucket-key of first bucket in rax, + * then need to add it as a new bucket at the beginning of the rax. */ + if(raxNext(&iter) == 0) { + FirstSegHdr *firstSegHdr = zmalloc(sizeof(FirstSegHdr)); + firstSegHdr->head = item; + firstSegHdr->totalItems = 1; + firstSegHdr->numSegs = 1; + + /* update last item to point on the segment header */ + ExpireMeta *metaItem = type->getExpireMeta(item); + metaItem->lastItemBucket = 1; + metaItem->lastInSegment = 1; + metaItem->firstItemBucket = 1; + metaItem->numItems = 1; + metaItem->next = firstSegHdr; + bucketKey2RaxKey(bucketKeyItem, raxKey); + raxInsert(rax, raxKey, EB_KEY_SIZE, firstSegHdr, NULL); + raxStop(&iter); + return 0; + } + + /* Add the new item into the first segment of the bucket that we found */ + uint64_t updateBucketKey = 0; + ebAddToBucket(type, iter.data, item, &newBucket, &updateBucketKey); + + /* If following the addition need to `updateBucketKey` of `foundBucket` in rax */ + if(unlikely(updateBucketKey && updateBucketKey != raxKey2BucketKey(iter.key))) { + raxRemove(iter.rt, iter.key, EB_KEY_SIZE, NULL); + bucketKey2RaxKey(updateBucketKey, raxKey); + raxInsert(iter.rt, raxKey, EB_KEY_SIZE, iter.data, NULL); + } + + /* If ebAddToBucket() returned a new bucket, then add the bucket to rax. + * + * This might happen when trying to add another item to a bucket that is: + * 1. A single, full segment. Will result in a bucket (segment) split. + * 2. Extended segment with a different bucket-key than the new item. + * Will result in a new bucket (of size 1) for the new item. + */ + if (newBucket.segment.head != NULL) { + /* Allocate segment header for the new bucket */ + FirstSegHdr *newSeg = zmalloc(sizeof(FirstSegHdr)); + /* Move the segment from 'newBucket' to allocated segment header */ + *newSeg = newBucket.segment; + /* Update 'next' of last item in segment to point to 'FirstSegHdr` */ + newBucket.mLast->next = newSeg; + /* Insert the new bucket to rax */ + bucketKey2RaxKey(newBucket.ebKey, raxKey); + raxInsert(iter.rt, raxKey, EB_KEY_SIZE, newSeg, NULL); + } + + raxStop(&iter); + return 0; +} + +/* Validate the general structure of the buckets in rax */ +static void ebValidateRax(rax *rax, EbucketsType *type) { + uint64_t numItemsTotal = 0; + raxIterator raxIter; + raxStart(&raxIter, rax); + raxSeek(&raxIter, "^", NULL, 0); + while (raxNext(&raxIter)) { + int expectFirstItemBucket = 1; + FirstSegHdr *firstSegHdr = raxIter.data; + eItem iter; + ExpireMeta *mIter, *mHead; + iter = firstSegHdr->head; + mHead = type->getExpireMeta(iter); + uint64_t numItemsBucket = 0, countSegments = 0; + + int extendedSeg = (firstSegHdr->numSegs > 1) ? 1 : 0; + void *segHdr = firstSegHdr; + + mIter = type->getExpireMeta(iter); + while (1) { + uint64_t curBktKey, prevBktKey; + for (int i = 0; i < mHead->numItems ; ++i) { + assert(iter != NULL); + mIter = type->getExpireMeta(iter); + curBktKey = EB_BUCKET_KEY(ebGetMetaExpTime(mIter)); + + if (i == 0) { + assert(mIter->numItems > 0 && mIter->numItems <= EB_SEG_MAX_ITEMS); + assert(mIter->firstItemBucket == expectFirstItemBucket); + expectFirstItemBucket = 0; + prevBktKey = curBktKey; + } else { + assert( (extendedSeg && prevBktKey == curBktKey) || + (!extendedSeg && prevBktKey <= curBktKey) ); + assert(mIter->numItems == 0); + assert(mIter->firstItemBucket == 0); + prevBktKey = curBktKey; + } + + if (i == mHead->numItems - 1) + assert(mIter->lastInSegment == 1); + else + assert(mIter->lastInSegment == 0); + + iter = mIter->next; + } + + numItemsBucket += mHead->numItems; + countSegments += 1; + + if (mIter->lastItemBucket) + break; + + NextSegHdr *nextSegHdr = mIter->next; + assert(nextSegHdr->firstSeg == firstSegHdr); + assert(nextSegHdr->prevSeg == segHdr); + iter = nextSegHdr->head; + mHead = type->getExpireMeta(iter); + segHdr = nextSegHdr; + } + /* Verify next of last item, `totalItems` and `numSegs` in iterated bucket */ + assert(mIter->next == segHdr); + assert(numItemsBucket == firstSegHdr->totalItems); + assert(countSegments == firstSegHdr->numSegs); + numItemsTotal += numItemsBucket; + } + raxStop(&raxIter); + assert(numItemsTotal == *ebRaxNumItems(rax)); +} + +struct deleteCbCtx { EbucketsType *type; void *userCtx; }; +void ebRaxDeleteCb(void *item, void *context) { + struct deleteCbCtx *ctx = context; + FirstSegHdr *firstSegHdr = item; + eItem itemIter = firstSegHdr->head; + uint32_t numSegs = firstSegHdr->numSegs; + void *nextSegHdr = firstSegHdr; + + for (uint32_t seg=0 ; seg < numSegs ; seg++) { + zfree(nextSegHdr); + + ExpireMeta *mIter = ctx->type->getExpireMeta(itemIter); + uint32_t numItemsInSeg = mIter->numItems; + + for (uint32_t i = 0; i < numItemsInSeg ; ++i) { + mIter = ctx->type->getExpireMeta(itemIter); + eItem toDelete = itemIter; + mIter->trash = 1; + itemIter = mIter->next; + if (ctx->type->onDeleteItem) ctx->type->onDeleteItem(toDelete, &ctx->userCtx); + } + nextSegHdr = itemIter; + + if (seg + 1 < numSegs) + itemIter = ((NextSegHdr *) nextSegHdr)->head; + } + +} + +static void _ebPrint(ebuckets eb, EbucketsType *type, int64_t usedMem, int printItems) { + if (ebIsEmpty(eb)) { + printf("Empty ebuckets\n"); + return; + } + + if (ebIsList(eb)) { + /* mock rax segment */ + eItem head = ebGetListPtr(type, eb); + ExpireMeta *metaHead = type->getExpireMeta(head); + FirstSegHdr mockSeg = { head, metaHead->numItems, 1}; + if (printItems) + ebBucketPrint(0, type, &mockSeg); + return; + } + + uint64_t totalItems = 0; + uint64_t numBuckets = 0; + uint64_t numSegments = 0; + + rax *rax = ebGetRaxPtr(eb); + raxIterator iter; + raxStart(&iter, rax); + raxSeek(&iter, "^", NULL, 0); + while (raxNext(&iter)) { + FirstSegHdr *seg = iter.data; + if (printItems) + ebBucketPrint(raxKey2BucketKey(iter.key), type, seg); + totalItems += seg->totalItems; + numBuckets++; + numSegments += seg->numSegs; + } + + printf("Total number of items : %" PRIu64 "\n", totalItems); + printf("Total number of buckets : %" PRIu64 "\n", numBuckets); + printf("Total number of segments : %" PRIu64 "\n", numSegments); + printf("Average items per bucket : %.2f\n", + (double) totalItems / numBuckets); + printf("Average items per segment : %.2f\n", + (double) totalItems / numSegments); + printf("Average segments per bucket : %.2f\n", + (double) numSegments / numBuckets); + + if (usedMem != -1) + { + printf("\nEbuckets memory usage (including FirstSegHdr/NexSegHdr):\n"); + printf("Total : %.2f KBytes\n", + (double) usedMem / 1024); + printf("Average per bucket : %" PRIu64 " Bytes\n", + usedMem / numBuckets); + printf("Average per item : %" PRIu64 " Bytes\n", + usedMem / totalItems); + printf("EB_BUCKET_KEY_PRECISION : %d\n", + EB_BUCKET_KEY_PRECISION); + printf("EB_SEG_MAX_ITEMS : %d\n", + EB_SEG_MAX_ITEMS); + } + raxStop(&iter); +} + +/*** API functions ***/ + +/** + * Deletes all items from given ebucket, invoking optional item deletion callbacks. + * + * @param eb - The ebucket to be deleted. + * @param type - Pointer to the EbucketsType structure defining the type of ebucket. + * @param ctx - A context pointer that can be used in optional item deletion callbacks. + */ +void ebDestroy(ebuckets *eb, EbucketsType *type, void *ctx) { + if (ebIsEmpty(*eb)) + return; + + if (ebIsList(*eb)) { + eItem head = ebGetListPtr(type, *eb); + eItem *pItemNext = &head; + while ( (*pItemNext) != NULL) { + eItem toDelete = *pItemNext; + ExpireMeta *metaToDelete = type->getExpireMeta(toDelete); + *pItemNext = metaToDelete->next; + metaToDelete->trash = 1; + if (type->onDeleteItem) type->onDeleteItem(toDelete, ctx); + } + } else { + struct deleteCbCtx deleteCtx = {type, ctx}; + raxFreeWithCbAndContext(ebGetRaxPtr(*eb), ebRaxDeleteCb, &deleteCtx); + } + + *eb = NULL; +} + +/** + * Removes the specified item from the given ebucket, updating the ebuckets handler + * accordingly. The function is optimized to remove items locally from segments + * without traversing rax tree or stepping long extended-segments. Therefore, + * it is assumed that the item is present in the bucket without verification. + * + * @param eb - Pointer to the ebuckets handler, which may get updated if the removal + * affects the structure. + * @param type - Pointer to the EbucketsType structure defining the type of ebucket. + * @param item - The eItem to be removed from the ebucket. + * + * @return 1 if the item was successfully removed; otherwise, return 0. + */ +int ebRemove(ebuckets *eb, EbucketsType *type, eItem item) { + + if (ebIsEmpty(*eb)) + return 0; /* not removed */ + + int res; + if (ebIsList(*eb)) + res = ebRemoveFromList(eb, type, item); + else /* rax */ + res = ebRemoveFromRax(eb, type, item); + + /* if removed then mark as trash */ + if (res) + type->getExpireMeta(item)->trash = 1; + + EB_VALIDATE_STRUCTURE(*eb, type); + + return res; +} + +/** + * Adds the specified item to the ebucket structure based on expiration time. + * If the ebucket is a list or empty, it attempts to add the item to the list. + * Otherwise, it adds the item to rax. If the list reaches its maximum size, it + * is converted to rax. The ebuckets handler may be updated accordingly. + * + * @param eb - Pointer to the ebuckets handler, which may get updated + * @param type - Pointer to the EbucketsType structure defining the type of ebucket. + * @param item - The eItem to be added to the ebucket. + * @param expireTime - The expiration time of the item. + * + * @return 0 (C_OK) if the item was successfully added; + * Otherwise, return -1 (C_ERR) on failure. + */ +int ebAdd(ebuckets *eb, EbucketsType *type, eItem item, uint64_t expireTime) { + int res; + + assert(expireTime <= EB_EXPIRE_TIME_MAX); + + /* Set expire-time and reset segment flags */ + ExpireMeta *itemMeta = type->getExpireMeta(item); + ebSetMetaExpTime(itemMeta, expireTime); + itemMeta->lastInSegment = 0; + itemMeta->firstItemBucket = 0; + itemMeta->lastItemBucket = 0; + itemMeta->numItems = 0; + itemMeta->trash = 0; + + if (ebIsList(*eb) || (ebIsEmpty(*eb))) { + /* Try add item to list */ + if ( (res = ebAddToList(eb, type, item)) == 1) { + /* Failed to add since list reached maximum size. Convert to rax */ + *eb = ebConvertListToRax(ebGetListPtr(type, *eb), type); + res = ebAddToRax(eb, type, item, EB_BUCKET_KEY(expireTime)); + } + } else { + /* Add item to rax */ + res = ebAddToRax(eb, type, item, EB_BUCKET_KEY(expireTime)); + } + + EB_VALIDATE_STRUCTURE(*eb, type); + + return res; +} + +/** + * Performs expiration on the given ebucket, removing items that have expired. + * + * If all items in the data structure are expired, 'eb' will be set to NULL. + * + * @param eb - Pointer to the ebuckets handler, which may get updated + * @param type - Pointer to the EbucketsType structure defining the type of ebucket. + * @param info - Providing information about the expiration action. + */ +void ebExpire(ebuckets *eb, EbucketsType *type, ExpireInfo *info) { + /* updateList - maintain a list of expired items that the callback `onExpireItem` + * indicated to update their expiration time rather than removing them. + * At the end of this function, `updateList` will be `ebAdd()` back. */ + eItem updateList = NULL; + + /* reset info outputs */ + info->nextExpireTime = 0; + info->itemsExpired = 0; + + /* if empty ebuckets */ + if (ebIsEmpty(*eb)) return; + + if (ebIsList(*eb)) { + ebListExpire(eb, type, info, &updateList); + goto END_ACTEXP; + } + + /* handle rax expiry */ + + rax *rax = ebGetRaxPtr(*eb); + raxIterator iter; + + raxStart(&iter, rax); + + uint64_t nowKey = EB_BUCKET_KEY(info->now); + uint64_t itemsExpiredBefore = info->itemsExpired; + + while (1) { + raxSeek(&iter,"^",NULL,0); + if (!raxNext(&iter)) break; + + uint64_t bucketKey = raxKey2BucketKey(iter.key); + + FirstSegHdr *firstSegHdr = iter.data; + + /* We need to take into consideration EB_BUCKET_KEY_PRECISION. The value of + * "info->now" will be adjusted to lookup only for all buckets with assigned + * keys that are older than 1<now). */ + if (bucketKey >= nowKey) { + /* Take care to update next expire time based on next segment to expire */ + info->nextExpireTime = ebGetMetaExpTime( + type->getExpireMeta(firstSegHdr->head)); + break; + } + + /* If not managed to remove entire bucket then return */ + if (ebSegExpire(firstSegHdr, type, info, &updateList) == 0) + break; + + raxRemove(iter.rt, iter.key, EB_KEY_SIZE, NULL); + } + + raxStop(&iter); + *ebRaxNumItems(rax) -= info->itemsExpired - itemsExpiredBefore; + + if(raxEOF(&iter) && (updateList == 0)) { + raxFree(rax); + *eb = NULL; + } + +END_ACTEXP: + /* Add back items with updated expiration time */ + while (updateList) { + ExpireMeta *mItem = type->getExpireMeta(updateList); + eItem next = mItem->next; + ebAdd(eb, type, updateList, ebGetMetaExpTime(mItem)); + updateList = next; + } + + EB_VALIDATE_STRUCTURE(*eb, type); + + return; +} + +/* Performs active expiration dry-run to evaluate number of expired items + * + * It is faster than actual active-expire because it iterates only over the + * headers of the buckets until the first non-expired bucket, and no more than + * EB_SEG_MAX_ITEMS items in the last bucket + * + * @param eb - The ebucket to be checked. + * @param type - Pointer to the EbucketsType structure defining the type of ebucket. + * @param now - The current time in milliseconds. + */ +uint64_t ebExpireDryRun(ebuckets eb, EbucketsType *type, uint64_t now) { + if (ebIsEmpty(eb)) return 0; + + uint64_t numExpired = 0; + + /* If list, then iterate and count expired ones */ + if (ebIsList(eb)) { + ExpireMeta *mIter = type->getExpireMeta(ebGetListPtr(type, eb)); + while (1) { + if (ebGetMetaExpTime(mIter) >= now) + return numExpired; + + numExpired++; + + if (mIter->lastInSegment) + return numExpired; + + mIter = type->getExpireMeta(mIter->next); + } + } + + /* Handle rax active-expire */ + rax *rax = ebGetRaxPtr(eb); + raxIterator iter; + raxStart(&iter, rax); + uint64_t nowKey = EB_BUCKET_KEY(now); + raxSeek(&iter,"^",NULL,0); + assert(raxNext(&iter)); /* must be at least one bucket */ + FirstSegHdr *currBucket = iter.data; + + while (1) { + /* if 'currBucket' is last bucket, then break */ + if(!raxNext(&iter)) break; + FirstSegHdr *nextBucket = iter.data; + + /* if 'nextBucket' is not less than now then break */ + if (raxKey2BucketKey(iter.key) >= nowKey) break; + + /* nextBucket less than now. For sure all items in currBucket are expired */ + numExpired += currBucket->totalItems; + currBucket = nextBucket; + } + raxStop(&iter); + + /* If single segment bucket, iterate over items and count expired ones */ + if (currBucket->numSegs == 1) { + ExpireMeta *mIter = type->getExpireMeta(currBucket->head); + while (1) { + if (ebGetMetaExpTime(mIter) >= now) + return numExpired; + + numExpired++; + + if (mIter->lastInSegment) + return numExpired; + + mIter = type->getExpireMeta(mIter->next); + } + } + + /* Bucket key exactly reflect expiration time of all items (currBucket->numSegs > 1) */ + if (EB_BUCKET_KEY_PRECISION == 0) { + if (ebGetMetaExpTime(type->getExpireMeta(currBucket->head)) >= now) + return numExpired; + else + return numExpired + currBucket->totalItems; + } + + /* Iterate extended-segment and count expired ones */ + + /* Unreachable code, provided for completeness. Following operation is not + * bound in time and this is the main reason why we set above + * EB_BUCKET_KEY_PRECISION to 0 and have early return on previous condition */ + + ExpireMeta *mIter = type->getExpireMeta(currBucket->head); + while(1) { + if (ebGetMetaExpTime(mIter) < now) + numExpired++; + + if (mIter->lastItemBucket) + return numExpired; + + if (mIter->lastInSegment) + mIter = type->getExpireMeta(((NextSegHdr *) mIter->next)->head); + else + mIter = type->getExpireMeta(mIter->next); + } +} + +/** + * Retrieves the expiration time of the item with the nearest expiration + * + * @param eb - The ebucket to be checked. + * @param type - Pointer to the EbucketsType structure defining the type of ebucket. + * + * @return The expiration time of the item with the nearest expiration time in + * the ebucket. If empty, return EB_EXPIRE_TIME_INVALID. If ebuckets is + * of type rax and minimal bucket is extended-segment, then it might not + * return accurate result up-to 1<getExpireMeta(ebGetListPtr(type, eb))); + + /* rax */ + uint64_t minExpire; + rax *rax = ebGetRaxPtr(eb); + raxIterator iter; + raxStart(&iter, rax); + raxSeek(&iter, "^", NULL, 0); + raxNext(&iter); /* seek to the last bucket */ + FirstSegHdr *firstSegHdr = iter.data; + if ((firstSegHdr->numSegs == 1) || (EB_BUCKET_KEY_PRECISION == 0)) { + /* Single segment, or extended-segments that all have same expiration time. + * return the first item with the nearest expiration time */ + minExpire = ebGetMetaExpTime(type->getExpireMeta(firstSegHdr->head)); + } else { + + /* If reached here, then it is because it is extended segment and buckets + * are with lower precision than 1msec. In that case it is better not to + * iterate extended-segments, which might be unbounded, and just return + * worst possible expiration time in this bucket. + * + * The reason we return blindly worst case expiration time value in this + * bucket is because the only usage of this function is to figure out + * when is the next time active expiration should be performed, and it + * is better to do it only after 1 or more items were expired and not the + * other way around. + */ + uint64_t expTime = ebGetMetaExpTime(type->getExpireMeta(firstSegHdr->head)); + minExpire = expTime | ( (1<getExpireMeta(item); + while (em->lastInSegment == 0) + em = type->getExpireMeta(em->next); + return ebGetMetaExpTime(em); + } + + /* rax */ + uint64_t maxExpire; + rax *rax = ebGetRaxPtr(eb); + raxIterator iter; + raxStart(&iter, rax); + raxSeek(&iter, "$", NULL, 0); + raxNext(&iter); /* seek to the last bucket */ + FirstSegHdr *firstSegHdr = iter.data; + if (firstSegHdr->numSegs == 1) { + /* Single segment. return the last item with the highest expiration time */ + ExpireMeta *em = type->getExpireMeta(firstSegHdr->head); + while (em->lastInSegment == 0) + em = type->getExpireMeta(em->next); + maxExpire = ebGetMetaExpTime(em); + } else if (EB_BUCKET_KEY_PRECISION == 0) { + /* Extended-segments that all have same expiration time */ + maxExpire = ebGetMetaExpTime(type->getExpireMeta(firstSegHdr->head)); + } else { + if (accurate == 0) { + /* return upper limit of the last bucket */ + int mask = (1<getExpireMeta(firstSegHdr->head)); + maxExpire = (expTime + (mask+1)) & (~mask); + } else { + maxExpire = 0; + ExpireMeta *mIter = type->getExpireMeta(firstSegHdr->head); + while(1) { + while(1) { + if (maxExpire < ebGetMetaExpTime(mIter)) + maxExpire = ebGetMetaExpTime(mIter); + if (mIter->lastInSegment == 1) break; + mIter = type->getExpireMeta(mIter->next); + } + + if (mIter->lastItemBucket) break; + mIter = type->getExpireMeta(((NextSegHdr *) mIter->next)->head); + } + } + } + raxStop(&iter); + return maxExpire; +} + +/** + * Retrieves the total number of items in the ebucket. + */ +uint64_t ebGetTotalItems(ebuckets eb, EbucketsType *type) { + if (ebIsEmpty(eb)) + return 0; + + if (ebIsList(eb)) + return type->getExpireMeta(ebGetListPtr(type, eb))->numItems; + else + return *ebRaxNumItems(ebGetRaxPtr(eb)); +} + +/* print expiration-time of items, ebuckets layout and some statistics */ +void ebPrint(ebuckets eb, EbucketsType *type) { + _ebPrint(eb, type, -1, 1); +} + +/* Validate the general structure of ebuckets. Calls assert(0) on error. */ +void ebValidate(ebuckets eb, EbucketsType *type) { + if (ebIsEmpty(eb)) + return; + + if (ebIsList(eb)) + ebValidateList(ebGetListPtr(type, eb), type); + else + ebValidateRax(ebGetRaxPtr(eb), type); +} + +/* Reallocates the memory used by the item using the provided allocation function. + * This feature was added for the active defrag feature. + * + * The 'defragfn' callbacks are called with a pointer to memory that callback + * can reallocate. The callbacks should return a new memory address or NULL, + * where NULL means that no reallocation happened and the old memory is still valid. + * + * Note: It is the caller's responsibility to ensure that the item has a valid expire time. */ +eItem ebDefragItem(ebuckets *eb, EbucketsType *type, eItem item, ebDefragFunction *defragfn) { + assert(!ebIsEmpty(*eb)); + if (ebIsList(*eb)) { + ExpireMeta *prevem = NULL; + eItem curitem = ebGetListPtr(type, *eb); + while (curitem != NULL) { + if (curitem == item) { + if ((curitem = defragfn(curitem))) { + if (prevem) + prevem->next = curitem; + else + *eb = ebMarkAsList(curitem); + } + return curitem; + } + + /* Move to the next item in the list. */ + prevem = type->getExpireMeta(curitem); + curitem = prevem->next; + } + } else { + CommonSegHdr *currHdr; + ExpireMeta *mIter = type->getExpireMeta(item); + assert(mIter->trash != 1); + while (mIter->lastInSegment == 0) + mIter = type->getExpireMeta(mIter->next); + + if (mIter->lastItemBucket) + currHdr = (CommonSegHdr *) mIter->next; + else + currHdr = (CommonSegHdr *) ((NextSegHdr *) mIter->next)->prevSeg; + /* If the item is the first in the segment, then update the segment header */ + if (currHdr->head == item) { + if ((item = defragfn(item))) { + currHdr->head = item; + } + return item; + } + + /* Iterate over all items in the segment until the next is 'item' */ + ExpireMeta *mHead = type->getExpireMeta(currHdr->head); + mIter = mHead; + while (mIter->next != item) + mIter = type->getExpireMeta(mIter->next); + assert(mIter->next == item); + + if ((item = defragfn(item))) { + mIter->next = item; + } + return item; + } + redis_unreachable(); +} + +/* Retrieves the expiration time associated with the given item. If associated + * ExpireMeta is marked as trash, then return EB_EXPIRE_TIME_INVALID */ +uint64_t ebGetExpireTime(EbucketsType *type, eItem item) { + ExpireMeta *meta = type->getExpireMeta(item); + if (unlikely(meta->trash)) return EB_EXPIRE_TIME_INVALID; + return ebGetMetaExpTime(meta); +} + +/*** Unit tests ***/ + +#ifdef REDIS_TEST +#include +#include +#include +#include +#include "testhelp.h" + +#define TEST(name) printf("[TEST] >>> %s\n", name); +#define TEST_COND(name, cond) printf("[%s] >>> %s\n", (cond) ? "TEST" : "BYPS", name); if (cond) + +typedef struct MyItem { + int index; + ExpireMeta mexpire; +} MyItem; + +typedef struct TimeRange { + uint64_t start; + uint64_t end; +} TimeRange; + +ExpireMeta *getMyItemExpireMeta(const eItem item) { + return &((MyItem *)item)->mexpire; +} + +ExpireAction expireItemCb(void *item, eItem ctx); +void deleteItemCb(eItem item, void *ctx); +EbucketsType myEbucketsType = { + .getExpireMeta = getMyItemExpireMeta, + .onDeleteItem = deleteItemCb, + .itemsAddrAreOdd = 0, +}; + +EbucketsType myEbucketsType2 = { + .getExpireMeta = getMyItemExpireMeta, + .onDeleteItem = NULL, + .itemsAddrAreOdd = 0, +}; + +/* XOR over all items time-expiration. Must be 0 after all addition/removal */ +uint64_t expItemsHashValue = 0; + +ExpireAction expireItemCb(eItem item, void *ctx) { + ExpireMeta *meta = myEbucketsType.getExpireMeta(item); + uint64_t expTime = ebGetMetaExpTime(meta); + expItemsHashValue = expItemsHashValue ^ expTime; + + TimeRange *range = (TimeRange *) ctx; + /* Verify expiration time is within the range */ + if (range != NULL) assert(expTime >= range->start && expTime <= range->end); + +/* If benchmarking then avoid from heavyweight free operation. It is user side logic */ +#ifndef EB_TEST_BENCHMARK + zfree(item); +#endif + return ACT_REMOVE_EXP_ITEM; +} + +ExpireAction expireUpdateThirdItemCb(eItem item, void *ctx) { + uint64_t expTime = (uint64_t) (uintptr_t) ctx; + static int calls = 0; + if ((calls++) == 3) { + ebSetMetaExpTime(&(((MyItem *)item)->mexpire), expTime ); + return ACT_UPDATE_EXP_ITEM; + } + + return ACT_REMOVE_EXP_ITEM; +} + +void deleteItemCb(eItem item, void *ctx) { + UNUSED(ctx); + zfree(item); +} + +void addItems(ebuckets *eb, uint64_t startExpire, int step, uint64_t numItems, MyItem **ar) { + for (uint64_t i = 0 ; i < numItems ; i++) { + uint64_t expireTime = startExpire + (i * step); + expItemsHashValue = expItemsHashValue ^ expireTime; + MyItem *item = zmalloc(sizeof(MyItem)); + if (ar) ar[i] = item; + ebAdd(eb, &myEbucketsType, item, expireTime); + } +} + +/* expireRanges - is given as bucket-key to be agnostic to the different configuration + * of EB_BUCKET_KEY_PRECISION */ +void distributeTest(int lowestTime, + uint64_t *expireRanges, + const int *ItemsPerRange, + int numRanges, + int isExpire, + int printStat) { + struct timeval timeBefore, timeAfter, timeDryRun, timeCreation, timeDestroy; + ebuckets eb = ebCreate(); + + /* create items with random expiry */ + uint64_t startRange = lowestTime; + + expItemsHashValue = 0; + void *listOfItems = NULL; + for (int i = 0; i < numRanges; i++) { + uint64_t endRange = EB_BUCKET_EXP_TIME(expireRanges[i]); + for (int j = 0; j < ItemsPerRange[i]; j++) { + uint64_t randomExpirey = (rand() % (endRange - startRange)) + startRange; + expItemsHashValue = expItemsHashValue ^ (uint32_t) randomExpirey; + MyItem *item = zmalloc(sizeof(MyItem)); + getMyItemExpireMeta(item)->next = listOfItems; + listOfItems = item; + ebSetMetaExpTime(getMyItemExpireMeta(item), randomExpirey); + } + startRange = EB_BUCKET_EXP_TIME(expireRanges[i]); /* next start range */ + } + + /* Take to sample memory after all items allocated and before insertion to ebuckets */ + size_t usedMemBefore = zmalloc_used_memory(); + + gettimeofday(&timeBefore, NULL); + while (listOfItems) { + MyItem *item = (MyItem *)listOfItems; + listOfItems = getMyItemExpireMeta(item)->next; + uint64_t expireTime = ebGetMetaExpTime(&item->mexpire); + ebAdd(&eb, &myEbucketsType, item, expireTime); + } + gettimeofday(&timeAfter, NULL); + timersub(&timeAfter, &timeBefore, &timeCreation); + + gettimeofday(&timeBefore, NULL); + ebExpireDryRun(eb, &myEbucketsType, 0xFFFFFFFFFFFF); /* expire dry-run all */ + gettimeofday(&timeAfter, NULL); + timersub(&timeAfter, &timeBefore, &timeDryRun); + + if (printStat) { + _ebPrint(eb, &myEbucketsType, zmalloc_used_memory() - usedMemBefore, 0); + } + + gettimeofday(&timeBefore, NULL); + if (isExpire) { + startRange = lowestTime; + /* Active expire according to the ranges */ + for (int i = 0 ; i < numRanges ; i++) { + + /* When checking how many items are expired, we need to take into + * consideration EB_BUCKET_KEY_PRECISION. The value of "info->now" + * will be adjusted by ebActiveExpire() to lookup only for all buckets + * with assigned keys that are older than 1<now) and not "<=". + * But if there is a list behind ebuckets, then this limitation is not + * applied and the operator "<=" will be used instead. + * + * The '-1' in case of list brings makes both cases aligned to have + * same result */ + uint64_t now = EB_BUCKET_EXP_TIME(expireRanges[i]) + (ebIsList(eb) ? -1 : 0); + + TimeRange range = {EB_BUCKET_EXP_TIME(startRange), EB_BUCKET_EXP_TIME(expireRanges[i]) }; + ExpireInfo info = { + .maxToExpire = 0xFFFFFFFF, + .onExpireItem = expireItemCb, + .ctx = &range, + .now = now, + .itemsExpired = 0}; + + ebExpire(&eb, &myEbucketsType, &info); + + assert( (eb==NULL && (i + 1 == numRanges)) || (eb!=NULL && (i + 1 < numRanges)) ); + assert( info.itemsExpired == (uint64_t) ItemsPerRange[i]); + startRange = expireRanges[i]; + } + assert(eb == NULL); + assert( (expItemsHashValue & 0xFFFFFFFF) == 0); + } + ebDestroy(&eb, &myEbucketsType, NULL); + gettimeofday(&timeAfter, NULL); + timersub(&timeAfter, &timeBefore, &timeDestroy); + + if (printStat) { + printf("Time elapsed ebuckets creation : %ld.%06ld\n", (long int)timeCreation.tv_sec, (long int)timeCreation.tv_usec); + printf("Time elapsed active-expire dry-run : %ld.%06ld\n", (long int)timeDryRun.tv_sec, (long int)timeDryRun.tv_usec); + if (isExpire) + printf("Time elapsed active-expire : %ld.%06ld\n", (long int)timeDestroy.tv_sec, (long int)timeDestroy.tv_usec); + else + printf("Time elapsed destroy : %ld.%06ld\n", (long int)timeDestroy.tv_sec, (long int)timeDestroy.tv_usec); + } + +} + +#define UNUSED(x) (void)(x) +#define ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0])) + +eItem defragCallback(const eItem item) { + size_t size = zmalloc_usable_size(item); + eItem newitem = zmalloc(size); + memcpy(newitem, item, size); + zfree(item); + return newitem; +} + +int ebucketsTest(int argc, char **argv, int flags) { + UNUSED(argc); + UNUSED(argv); + srand(0); + + int verbose = (flags & REDIS_TEST_VERBOSE) ? 2 : 1; + UNUSED(verbose); + +#ifdef EB_TEST_BENCHMARK + TEST("ebuckets - benchmark 10 million items: alloc + add + activeExpire") { + + struct TestParams { + uint64_t minExpire; + uint64_t maxExpire; + int items; + const char *description; + } testCases[] = { + { 1805092100000, 1805092100000 + (uint64_t) 1, 10000000, "1 msec distribution" }, + { 1805092100000, 1805092100000 + (uint64_t) 1000, 10000000, "1 sec distribution" }, + { 1805092100000, 1805092100000 + (uint64_t) 1000*60, 10000000, "1 min distribution" }, + { 1805092100000, 1805092100000 + (uint64_t) 1000*60*60, 10000000, "1 hour distribution" }, + { 1805092100000, 1805092100000 + (uint64_t) 1000*60*60*24, 10000000, "1 day distribution" }, + { 1805092100000, 1805092100000 + (uint64_t) 1000*60*60*24*7, 10000000, "1 week distribution" }, + { 1805092100000, 1805092100000 + (uint64_t) 1000*60*60*24*30, 10000000, "1 month distribution" } + }; + + /* selected test */ + uint32_t tid = EB_TEST_BENCHMARK; + + printf("\n------ TEST EBUCKETS: %s ------\n", testCases[tid].description); + uint64_t expireRanges[] = { testCases[tid].minExpire, testCases[tid].maxExpire }; + int itemsPerRange[] = { 0, testCases[tid].items }; + + /* expireRanges[] is provided to distributeTest() as bucket-key values */ + for (uint32_t j = 0; j < ARRAY_SIZE(expireRanges); ++j) { + expireRanges[j] = expireRanges[j] >> EB_BUCKET_KEY_PRECISION; + } + + distributeTest(0, expireRanges, itemsPerRange, ARRAY_SIZE(expireRanges), 1, 1); + return 0; + } +#endif + + TEST("list - Create a single item, get TTL, and remove") { + MyItem *singleItem = zmalloc(sizeof(MyItem)); + ebuckets eb = NULL; + ebAdd(&eb, &myEbucketsType, singleItem, 1000); + assert(ebGetExpireTime(&myEbucketsType, singleItem) == 1000 ); + + /* remove the item */ + assert(ebRemove(&eb, &myEbucketsType, singleItem)); + /* now the ebuckets is empty */ + assert(ebRemove(&eb, &myEbucketsType, singleItem) == 0); + + zfree(singleItem); + + ebDestroy(&eb, &myEbucketsType, NULL); + } + + TEST("list - Create few items on different times, get TTL, and then remove") { + MyItem *items[EB_LIST_MAX_ITEMS]; + ebuckets eb = NULL; + for (int i = 0 ; i < EB_LIST_MAX_ITEMS ; i++) { + items[i] = zmalloc(sizeof(MyItem)); + ebAdd(&eb, &myEbucketsType, items[i], i); + } + + for (uint64_t i = 0 ; i < EB_LIST_MAX_ITEMS ; i++) { + assert(ebGetExpireTime(&myEbucketsType, items[i]) == i ); + assert(ebRemove(&eb, &myEbucketsType, items[i])); + } + + for (int i = 0 ; i < EB_LIST_MAX_ITEMS ; i++) { + zfree(items[i]); + } + + ebDestroy(&eb, &myEbucketsType, NULL); + } + + TEST("list - Create few items on different times, get TTL, and then delete") { + MyItem *items[EB_LIST_MAX_ITEMS]; + ebuckets eb = NULL; + for (int i = 0 ; i < EB_LIST_MAX_ITEMS ; i++) { + items[i] = zmalloc(sizeof(MyItem)); + ebAdd(&eb, &myEbucketsType, items[i], i); + } + + for (uint64_t i = 0 ; i < EB_LIST_MAX_ITEMS ; i++) { + assert(ebGetExpireTime(&myEbucketsType, items[i]) == i ); + } + + ebDestroy(&eb, &myEbucketsType, NULL); + } + + TEST_COND("ebuckets - Add items with increased/decreased expiration time and then expire", + EB_BUCKET_KEY_PRECISION > 0) + { + ebuckets eb = NULL; + + for (int isDecr = 0; isDecr < 2; ++isDecr) { + for (uint32_t numItems = 1; numItems < 64; ++numItems) { + uint64_t step = 1 << EB_BUCKET_KEY_PRECISION; + + if (isDecr == 0) + addItems(&eb, 0, step, numItems, NULL); + else + addItems(&eb, (numItems - 1) * step, -step, numItems, NULL); + + for (uint32_t i = 1; i <= numItems; i++) { + TimeRange range = {EB_BUCKET_EXP_TIME(i - 1), EB_BUCKET_EXP_TIME(i)}; + ExpireInfo info = { + .maxToExpire = 1, + .onExpireItem = expireItemCb, + .ctx = &range, + .now = EB_BUCKET_EXP_TIME(i), + .itemsExpired = 0}; + + ebExpire(&eb, &myEbucketsType, &info); + assert(info.itemsExpired == 1); + if (i == numItems) { /* if last item */ + assert(eb == NULL); + assert(info.nextExpireTime == 0); + } else { + assert(info.nextExpireTime == EB_BUCKET_EXP_TIME(i)); + } + } + } + } + } + + TEST_COND("ebuckets - Create items with same expiration time and then expire", + EB_BUCKET_KEY_PRECISION > 0) + { + ebuckets eb = NULL; + uint64_t expirePerIter = 2; + for (uint32_t numIterations = 1; numIterations < 100; ++numIterations) { + uint32_t numItems = numIterations * expirePerIter; + uint64_t expireTime = (1 << EB_BUCKET_KEY_PRECISION) + 1; + addItems(&eb, expireTime, 0, numItems, NULL); + + for (uint32_t i = 1; i <= numIterations; i++) { + ExpireInfo info = { + .maxToExpire = expirePerIter, + .onExpireItem = expireItemCb, + .ctx = NULL, + .now = (2 << EB_BUCKET_KEY_PRECISION), + .itemsExpired = 0}; + ebExpire(&eb, &myEbucketsType, &info); + assert(info.itemsExpired == expirePerIter); + if (i == numIterations) { /* if last item */ + assert(eb == NULL); + assert(info.nextExpireTime == 0); + } else { + assert(info.nextExpireTime == expireTime); + } + } + } + } + + TEST("list - Create few items on random times and then expire/delete ") { + for (int isExpire = 0 ; isExpire <= 1 ; ++isExpire ) { + uint64_t expireRanges[] = {1000}; /* bucket-keys */ + int itemsPerRange[] = {EB_LIST_MAX_ITEMS}; + distributeTest(0, expireRanges, itemsPerRange, + ARRAY_SIZE(expireRanges), isExpire, 0); + } + } + + TEST("list - Create few items (list) on same time and then active expire/delete ") { + for (int isExpire = 0 ; isExpire <= 1 ; ++isExpire ) { + uint64_t expireRanges[] = {1, 2}; /* bucket-keys */ + int itemsPerRange[] = {0, EB_LIST_MAX_ITEMS}; + + distributeTest(0, expireRanges, itemsPerRange, + ARRAY_SIZE(expireRanges), isExpire, 0); + } + } + + TEST("ebuckets - Create many items on same time and then active expire/delete ") { + for (int isExpire = 1 ; isExpire <= 1 ; ++isExpire ) { + uint64_t expireRanges[] = {1, 2}; /* bucket-keys */ + int itemsPerRange[] = {0, 20}; + + distributeTest(0, expireRanges, itemsPerRange, + ARRAY_SIZE(expireRanges), isExpire, 0); + } + } + + TEST("ebuckets - Create items on different times and then expire/delete ") { + for (int isExpire = 0 ; isExpire <= 0 ; ++isExpire ) { + for (int numItems = 1 ; numItems < 100 ; ++numItems ) { + uint64_t expireRanges[] = {1000000}; /* bucket-keys */ + int itemsPerRange[] = {numItems}; + distributeTest(0, expireRanges, itemsPerRange, + ARRAY_SIZE(expireRanges), 1, 0); + } + } + } + + TEST("ebuckets - Create items on different times and then ebRemove() ") { + ebuckets eb = NULL; + + for (int step = -1 ; step <= 1 ; ++step) { + for (int numItems = 1; numItems <= EB_SEG_MAX_ITEMS*3; ++numItems) { + for (int offset = 0; offset < numItems; offset++) { + MyItem *items[numItems]; + uint64_t startValue = 1000 << EB_BUCKET_KEY_PRECISION; + int stepValue = step * (1 << EB_BUCKET_KEY_PRECISION); + addItems(&eb, startValue, stepValue, numItems, items); + for (int i = 0; i < numItems; i++) { + int at = (i + offset) % numItems; + assert(ebRemove(&eb, &myEbucketsType, items[at])); + zfree(items[at]); + } + assert(eb == NULL); + } + } + } + } + + TEST("ebuckets - test min/max expire time") { + ebuckets eb = NULL; + MyItem items[3*EB_SEG_MAX_ITEMS]; + for (int numItems = 1 ; numItems < (int)ARRAY_SIZE(items) ; numItems++) { + uint64_t minExpTime = RAND_MAX, maxExpTime = 0; + for (int i = 0; i < numItems; i++) { + /* generate random expiration time */ + uint64_t expireTime = rand(); + if (expireTime < minExpTime) minExpTime = expireTime; + if (expireTime > maxExpTime) maxExpTime = expireTime; + ebAdd(&eb, &myEbucketsType2, items + i, expireTime); + assert(ebGetNextTimeToExpire(eb, &myEbucketsType2) == minExpTime); + assert(ebGetMaxExpireTime(eb, &myEbucketsType2, 0) == maxExpTime); + } + ebDestroy(&eb, &myEbucketsType2, NULL); + } + } + + TEST_COND("ebuckets - test min/max expire time, with extended-segment", + (1< 2*EB_SEG_MAX_ITEMS) { + ebuckets eb = NULL; + MyItem items[(2*EB_SEG_MAX_ITEMS)-1]; + for (int numItems = EB_SEG_MAX_ITEMS+1 ; numItems < (int)ARRAY_SIZE(items) ; numItems++) { + /* First reach extended-segment (two chained segments in a bucket) */ + for (int i = 0; i <= EB_SEG_MAX_ITEMS; i++) { + uint64_t itemExpireTime = (1<index = i; + ebAdd(&eb, &myEbucketsType, items[i], i); + } + assert((s <= EB_LIST_MAX_ITEMS) ? ebIsList(eb) : !ebIsList(eb)); + /* Defrag all the items. */ + for (int i = 0; i < s; i++) { + MyItem *newitem = ebDefragItem(&eb, &myEbucketsType, items[i], defragCallback); + if (newitem) items[i] = newitem; + } + /* Verify that the data is not corrupted. */ + ebValidate(eb, &myEbucketsType); + for (int i = 0; i < s; i++) + assert(items[i]->index == i); + ebDestroy(&eb, &myEbucketsType, NULL); + } + } + +// TEST("segment - Add smaller item to full segment that all share same ebucket-key") +// TEST("segment - Add item to full segment and make it extended-segment (all share same ebucket-key)") +// TEST("ebuckets - Create rax tree with extended-segment and add item before") + + return 0; +} + +#endif diff --git a/src/ebuckets.h b/src/ebuckets.h new file mode 100644 index 000000000..66954131a --- /dev/null +++ b/src/ebuckets.h @@ -0,0 +1,308 @@ +/* + * Copyright Redis Ltd. 2024 - present + * + * Licensed under your choice of the Redis Source Available License 2.0 (RSALv2) + * or the Server Side Public License v1 (SSPLv1). + * + * + * WHAT IS EBUCKETS? + * ----------------- + * ebuckets is being used to store items that are set with expiration-time. It + * supports the basic API of add, remove and active expiration. The implementation + * of it is based on rax-tree, or plain linked-list when small. The expiration time + * of the items are used as the key to traverse rax-tree. + * + * Instead of holding a distinct item in each leaf of the rax-tree we can aggregate + * items into small segments and hold it in each leaf. This way we can avoid + * frequent modification of the rax-tree, since many of the modifications + * will be done only at the segment level. It will also save memory because + * rax-tree can be costly, around 40 bytes per leaf (with rax-key limited to 6 + * bytes). Whereas each additional item in the segment will cost the size of the + * 'next' pointer in a list (8 bytes) and few more bytes for maintenance of the + * segment. + * + * EBUCKETS STRUCTURE + * ------------------ + * The ebuckets data structure is organized in a hierarchical manner as follows: + * + * 1. ebuckets: This is the top-level data structure. It can be either a rax tree + * or a plain linked list. It contains one or more buckets, each representing + * an interval in time. + * + * 2. bucket: Each bucket represents an interval in time and contains one or more + * segments. The key in the rax-tree for each bucket represents low + * bound expiration-time for the items within this bucket. The key of the + * following bucket represents the upper bound expiration-time. + * + * 3. segment: Each segment within a bucket can hold up to `EB_SEG_MAX_ITEMS` + * items as a linked list. If there are more, the segment will try to + * split the bucket. To avoid wasting memory, it is a singly linked list (only + * next-item pointer). It is a cyclic linked-list to allow efficient removal of + * items from the middle of the segment without traversing the rax tree. + * + * 4. item: Each item that is stored in ebuckets should embed the ExpireMeta + * struct and supply getter function (see EbucketsType.getExpireMeta). This + * struct holds the expire-time of the item and few more fields that are used + * to maintain the segments data-structure. + * + * SPLITTING BUCKET + * ---------------- + * Each segment can hold up-to `EB_SEG_MAX_ITEMS` items. On insertion of new + * item, it will try to split the segment. Here is an example For adding item + * with expiration of 42 to a segment that already reached its maximum capacity + * which will cause to split of the segment and in turn split of the bucket as + * well to a finer grained ranges: + * + * BUCKETS BUCKETS + * [ 00-10 ] -> size(Seg0) = 11 ==> [ 00-10 ] -> size(Seg0) = 11 + * [ 11-76 ] -> size(Seg1) = 16 [ 11-36 ] -> size(Seg1) = 9 + * [ 37-76 ] -> size(Seg2) = 7 + * + * EXTENDING BUCKET + * ---------------- + * In the example above, the reason it wasn't split evenly is that Seg1 must have + * been holding items with same TTL and they must reside together in the same + * bucket after the split. Which brings us to another important point. If there + * is a segment that reached its maximum capacity and all the items have same + * expiration-time key, then we cannot split the bucket but aggregate all the + * items, with same expiration time key, by allocating an extended-segment and + * chain it to the first segment in visited bucket. In that sense, extended + * segments will only hold items with same expiration-time key. + * + * BUCKETS BUCKETS + * [ 00-10 ] -> size(Seg0)=11 ==> [ 00-10 ] -> size(Seg0)=11 + * [ 11-12 ] -> size(Seg1)=16 [ 11-12 ] -> size(Seg1)=1 -> size(Seg2)=16 + * + * LIMITING RAX TREE DEPTH + * ----------------------- + * The rax tree is basically a B-tree and its depth is bounded by the sizeof of + * the key. Holding 6 bytes for expiration-time key is more than enough to represent + * unix-time in msec, and in turn the depth of the tree is limited to 6 levels. + * At a first glance it might look sufficient but we need take into consideration + * the heavyweight maintenance and traversal of each node in the B-tree. + * + * And so, we can further prune the tree such that holding keys with msec precision + * in the tree doesn't bring with it much value. The active-expiration operation can + * live with deletion of expired items, say, older than 1 sec, which means the size + * of time-expiration keys to the rax tree become no more than ~4.5 bytes and we + * also get rid of the "noisy" bits which most probably will cause to yet another + * branching and modification of the rax tree in case of items with time-expiration + * difference of less than 1 second. The lazy expiration will still be precise and + * without compromise on accuracy because the exact expiration-time is kept + * attached as well to each item, in `ExpireMeta`, and each traversal of item with + * expiration will behave as expected down to the msec. Take care to configure + * `EB_BUCKET_KEY_PRECISION` according to your needs. + * + * EBUCKET KEY + * ----------- + * Taking into account configured value of `EB_BUCKET_KEY_PRECISION`, two items + * with expiration-time t1 and t2 will be considered to have the same key in the + * rax-tree/buckets if and only if: + * + * EB_BUCKET_KEY(t1) == EB_BUCKET_KEY(t2) + * + * EBUCKETS CREATION + * ----------------- + * To avoid the cost of allocating rax data-structure for only few elements, + * ebuckets will start as a simple linked-list and only when it reaches some + * threshold, it will be converted to rax. + * + * TODO + * ---- + * - ebRemove() optimize to merge small segments into one segment. + * - ebAdd() Fix pathological case of cascade addition of items into rax such + * that their values are smaller/bigger than visited extended-segment which ends + * up with multiple segments with a single item in each segment. + */ + +#ifndef __EBUCKETS_H +#define __EBUCKETS_H + +#include +#include +#include +#include +#include "rax.h" + +/* + * EB_BUCKET_KEY_PRECISION - Defines the number of bits to ignore from the + * expiration-time when mapping to buckets. The higher the value, the more items + * with similar expiration-time will be aggregated into the same bucket. The lower + * the value, the more "accurate" the active expiration of buckets will be. + * + * Note that the accurate time expiration of each item is preserved anyway and + * enforced by lazy expiration. It only impacts the active expiration that will + * be able to work on buckets older than (1<> EB_BUCKET_KEY_PRECISION) + + +#define EB_EXPIRE_TIME_MAX ((uint64_t)0x0000FFFFFFFFFFFF) /* Maximum expire-time. */ +#define EB_EXPIRE_TIME_INVALID (EB_EXPIRE_TIME_MAX+1) /* assumed bigger than max */ + +/* Handler to ebuckets DS. Pointer to a list, rax or NULL (empty DS). See also ebIsList(). */ +typedef void *ebuckets; + +/* Users of ebuckets will store `eItem` which is just a void pointer to their + * element. In addition, eItem should embed the ExpireMeta struct and supply + * getter function (see EbucketsType.getExpireMeta). + */ +typedef void *eItem; + +/* This struct Should be embedded inside `eItem` and must be aligned in memory. */ +typedef struct ExpireMeta { + /* 48bits of unix-time in msec. This value is sufficient to represent, in + * unix-time, until the date of 02 August, 10889 + */ + uint32_t expireTimeLo; /* Low bits of expireTime. */ + uint16_t expireTimeHi; /* High bits of expireTime. */ + + unsigned int lastInSegment : 1; /* Last item in segment. If set, then 'next' will + point to the NextSegHdr, unless lastItemBucket=1 + then it will point to segment header of the + current segment. */ + unsigned int firstItemBucket : 1; /* First item in bucket. This flag assist + to manipulate segments directly without + the need to traverse from start the + rax tree */ + unsigned int lastItemBucket : 1; /* Last item in bucket. This flag assist + to manipulate segments directly without + the need to traverse from start the + rax tree */ + unsigned int numItems : 5; /* Only first item in segment will maintain + this value. */ + + unsigned int trash : 1; /* This flag indicates whether the ExpireMeta + associated with the item is leftover. + There is always a potential to reuse the + item after removal/deletion. Note that, + the user can still safely O(1) TTL lookup + a given item and verify whether attached + TTL is valid or leftover. See function + ebGetExpireTime(). */ + + unsigned int userData : 3; /* ebuckets can be used to store in same + instance few different types of items, + such as, listpack and hash. This field + is reserved to store such identification + associated with the item and can help + to distinct on delete or expire callback. + It is not used by ebuckets internally and + should be maintained by the user */ + + unsigned int reserved : 4; + + void *next; /* - If not last item in segment then next + points to next eItem (lastInSegment=0). + - If last in segment but not last in + bucket (lastItemBucket=0) then it + points to next segment header. + - If last in bucket then it points to + current segment header (Can be either + of type FirstSegHdr or NextSegHdr). */ +} ExpireMeta; + +/* Each instance of ebuckets need to have corresponding EbucketsType that holds + * the necessary callbacks and configuration to operate correctly on the type + * of items that are stored in it. Conceptually it should have hold reference + * from ebuckets instance to this type, but to save memory we will pass it as + * an argument to each API call. */ +typedef struct EbucketsType { + /* getter to extract the ExpireMeta from the item */ + ExpireMeta* (*getExpireMeta)(const eItem item); + + /* Called during ebDestroy(). Set to NULL if not needed. */ + void (*onDeleteItem)(eItem item, void *ctx); + + /* Is addresses of items are odd in memory. It is taken into consideration + * and used by ebuckets to know how to distinct between ebuckets pointer to + * rax versus a pointer to item which is head of list. */ + unsigned int itemsAddrAreOdd; +} EbucketsType; + +/* Returned value by `onExpireItem` callback to indicate the action to be taken by + * ebExpire(). */ +typedef enum ExpireAction { + ACT_REMOVE_EXP_ITEM=0, /* Remove the item from ebuckets. */ + ACT_UPDATE_EXP_ITEM, /* Re-insert the item with updated expiration-time. + Before returning this value, the cb need to + update expiration time of the item by assisting + function ebSetMetaExpTime(). The item will be + kept aside and will be added again to ebuckets + at the end of ebExpire() */ + ACT_STOP_ACTIVE_EXP /* Stop active-expiration. It will assume that + provided 'item' wasn't deleted by the callback. */ +} ExpireAction; + +/* ExpireInfo is used to pass input and output parameters to ebExpire(). */ +typedef struct ExpireInfo { + /* onExpireItem - Called during active-expiration by ebExpire() */ + ExpireAction (*onExpireItem)(eItem item, void *ctx); + + uint64_t maxToExpire; /* [INPUT ] Limit of number expired items to scan */ + void *ctx; /* [INPUT ] context to pass to onExpireItem */ + uint64_t now; /* [INPUT ] Current time in msec. */ + uint64_t nextExpireTime; /* [OUTPUT] Next expiration time. Return 0, if none left. */ + + /* TODO: Distinct between expired & updated */ + uint64_t itemsExpired; /* [OUTPUT] Returns the number of expired or updated items. */ + +} ExpireInfo; + +/* ebuckets API */ + +static inline ebuckets ebCreate(void) { return NULL; } /* Empty ebuckets */ + +void ebDestroy(ebuckets *eb, EbucketsType *type, void *deletedItemsCbCtx); + +void ebExpire(ebuckets *eb, EbucketsType *type, ExpireInfo *info); + +uint64_t ebExpireDryRun(ebuckets eb, EbucketsType *type, uint64_t now); + +static inline int ebIsEmpty(ebuckets eb) { return eb == NULL; } + +uint64_t ebGetNextTimeToExpire(ebuckets eb, EbucketsType *type); + +uint64_t ebGetMaxExpireTime(ebuckets eb, EbucketsType *type, int accurate); + +uint64_t ebGetTotalItems(ebuckets eb, EbucketsType *type); + +/* Item related API */ + +int ebRemove(ebuckets *eb, EbucketsType *type, eItem item); + +int ebAdd(ebuckets *eb, EbucketsType *type, eItem item, uint64_t expireTime); + +uint64_t ebGetExpireTime(EbucketsType *type, eItem item); + +typedef eItem (ebDefragFunction)(const eItem item); +eItem ebDefragItem(ebuckets *eb, EbucketsType *type, eItem item, ebDefragFunction *fn); + +static inline uint64_t ebGetMetaExpTime(ExpireMeta *expMeta) { + return (((uint64_t)(expMeta)->expireTimeHi << 32) | (expMeta)->expireTimeLo); +} + +static inline void ebSetMetaExpTime(ExpireMeta *expMeta, uint64_t t) { + expMeta->expireTimeLo = (uint32_t)(t&0xFFFFFFFF); + expMeta->expireTimeHi = (uint16_t)((t) >> 32); +} + +/* Debug API */ + +void ebValidate(ebuckets eb, EbucketsType *type); + +void ebPrint(ebuckets eb, EbucketsType *type); + +#ifdef REDIS_TEST +int ebucketsTest(int argc, char *argv[], int flags); +#endif + +#endif /* __EBUCKETS_H */ diff --git a/src/expire.c b/src/expire.c index b73b5245f..646f752a9 100644 --- a/src/expire.c +++ b/src/expire.c @@ -94,6 +94,7 @@ int activeExpireCycleTryExpire(redisDb *db, dictEntry *de, long long now) { #define ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC 25 /* Max % of CPU to use. */ #define ACTIVE_EXPIRE_CYCLE_ACCEPTABLE_STALE 10 /* % of stale keys after which we do extra efforts. */ +#define HFE_ACTIVE_EXPIRE_CYCLE_FIELDS 1000 /* Data used by the expire dict scan callback. */ typedef struct { @@ -134,6 +135,53 @@ static inline int isExpiryDictValidForSamplingCb(dict *d) { return C_OK; } +/* Active expiration Cycle for hash-fields. + * + * Note that releasing fields is expected to be more predictable and rewarding + * than releasing keys because it is stored in `ebuckets` DS which optimized for + * active expiration and in addition the deletion of fields is simple to handle. */ +static inline void activeExpireHashFieldCycle(int type) { + /* Remember current db across calls */ + static unsigned int currentDb = 0; + + /* Tracks the count of fields actively expired for the current database. + * This count continues as long as it fails to actively expire all expired + * fields of currentDb, indicating a possible need to adjust the value of + * maxToExpire. */ + static uint64_t activeExpirySequence = 0; + /* Threshold for adjusting maxToExpire */ + const uint32_t EXPIRED_FIELDS_TH = 1000000; + /* Maximum number of fields to actively expire in a single call */ + uint32_t maxToExpire = HFE_ACTIVE_EXPIRE_CYCLE_FIELDS; + + redisDb *db = server.db + currentDb; + + /* If db is empty, move to next db and return */ + if (ebIsEmpty(db->hexpires)) { + activeExpirySequence = 0; + currentDb = (currentDb + 1) % server.dbnum; + return; + } + + /* If running for a while and didn't manage to active-expire all expired fields of + * currentDb (i.e. activeExpirySequence becomes significant) then adjust maxToExpire */ + if ((activeExpirySequence > EXPIRED_FIELDS_TH) && (type == ACTIVE_EXPIRE_CYCLE_SLOW)) { + /* maxToExpire is multiplied by a factor between 1 and 32, proportional to + * the number of times activeExpirySequence exceeded EXPIRED_FIELDS_TH */ + uint64_t factor = activeExpirySequence / EXPIRED_FIELDS_TH; + maxToExpire *= (factor<32) ? factor : 32; + } + + if (hashTypeDbActiveExpire(db, maxToExpire) == maxToExpire) { + /* active-expire reached maxToExpire limit */ + activeExpirySequence += maxToExpire; + } else { + /* Managed to active-expire all expired fields of currentDb */ + activeExpirySequence = 0; + currentDb = (currentDb + 1) % server.dbnum; + } +} + void activeExpireCycle(int type) { /* Adjust the running parameters according to the configured expire * effort. The default effort is 1, and the maximum configurable effort @@ -232,6 +280,11 @@ void activeExpireCycle(int type) { * distribute the time evenly across DBs. */ current_db++; + /* Interleaving hash-field expiration with key expiration. Better + * call it before handling expired keys because HFE DS is optimized for + * active expiration */ + activeExpireHashFieldCycle(type); + if (kvstoreSize(db->expires)) dbs_performed++; diff --git a/src/lazyfree.c b/src/lazyfree.c index e743cb204..2b98f9a06 100644 --- a/src/lazyfree.c +++ b/src/lazyfree.c @@ -3,6 +3,7 @@ #include "atomicvar.h" #include "functions.h" #include "cluster.h" +#include "ebuckets.h" static redisAtomic size_t lazyfree_objects = 0; static redisAtomic size_t lazyfreed_objects = 0; @@ -22,7 +23,8 @@ void lazyfreeFreeObject(void *args[]) { void lazyfreeFreeDatabase(void *args[]) { kvstore *da1 = args[0]; kvstore *da2 = args[1]; - + ebuckets oldHfe = args[2]; + ebDestroy(&oldHfe, &hashExpireBucketsType, NULL); size_t numkeys = kvstoreSize(da1); kvstoreRelease(da1); kvstoreRelease(da2); @@ -201,10 +203,12 @@ void emptyDbAsync(redisDb *db) { flags |= KVSTORE_FREE_EMPTY_DICTS; } kvstore *oldkeys = db->keys, *oldexpires = db->expires; + ebuckets oldHfe = db->hexpires; db->keys = kvstoreCreate(&dbDictType, slot_count_bits, flags); db->expires = kvstoreCreate(&dbExpiresDictType, slot_count_bits, flags); + db->hexpires = ebCreate(); atomicIncr(lazyfree_objects, kvstoreSize(oldkeys)); - bioCreateLazyFreeJob(lazyfreeFreeDatabase, 2, oldkeys, oldexpires); + bioCreateLazyFreeJob(lazyfreeFreeDatabase, 3, oldkeys, oldexpires, oldHfe); } /* Free the key tracking table. diff --git a/src/listpack.c b/src/listpack.c index 0fcdfa9ab..5d9028e13 100644 --- a/src/listpack.c +++ b/src/listpack.c @@ -245,51 +245,61 @@ unsigned char* lpShrinkToFit(unsigned char *lp) { static inline void lpEncodeIntegerGetType(int64_t v, unsigned char *intenc, uint64_t *enclen) { if (v >= 0 && v <= 127) { /* Single byte 0-127 integer. */ - intenc[0] = v; - *enclen = 1; + if (intenc != NULL) intenc[0] = v; + if (enclen != NULL) *enclen = 1; } else if (v >= -4096 && v <= 4095) { /* 13 bit integer. */ if (v < 0) v = ((int64_t)1<<13)+v; - intenc[0] = (v>>8)|LP_ENCODING_13BIT_INT; - intenc[1] = v&0xff; - *enclen = 2; + if (intenc != NULL) { + intenc[0] = (v>>8)|LP_ENCODING_13BIT_INT; + intenc[1] = v&0xff; + } + if (enclen != NULL) *enclen = 2; } else if (v >= -32768 && v <= 32767) { /* 16 bit integer. */ if (v < 0) v = ((int64_t)1<<16)+v; - intenc[0] = LP_ENCODING_16BIT_INT; - intenc[1] = v&0xff; - intenc[2] = v>>8; - *enclen = 3; + if (intenc != NULL) { + intenc[0] = LP_ENCODING_16BIT_INT; + intenc[1] = v&0xff; + intenc[2] = v>>8; + } + if (enclen != NULL) *enclen = 3; } else if (v >= -8388608 && v <= 8388607) { /* 24 bit integer. */ if (v < 0) v = ((int64_t)1<<24)+v; - intenc[0] = LP_ENCODING_24BIT_INT; - intenc[1] = v&0xff; - intenc[2] = (v>>8)&0xff; - intenc[3] = v>>16; - *enclen = 4; + if (intenc != NULL) { + intenc[0] = LP_ENCODING_24BIT_INT; + intenc[1] = v&0xff; + intenc[2] = (v>>8)&0xff; + intenc[3] = v>>16; + } + if (enclen != NULL) *enclen = 4; } else if (v >= -2147483648 && v <= 2147483647) { /* 32 bit integer. */ if (v < 0) v = ((int64_t)1<<32)+v; - intenc[0] = LP_ENCODING_32BIT_INT; - intenc[1] = v&0xff; - intenc[2] = (v>>8)&0xff; - intenc[3] = (v>>16)&0xff; - intenc[4] = v>>24; - *enclen = 5; + if (intenc != NULL) { + intenc[0] = LP_ENCODING_32BIT_INT; + intenc[1] = v&0xff; + intenc[2] = (v>>8)&0xff; + intenc[3] = (v>>16)&0xff; + intenc[4] = v>>24; + } + if (enclen != NULL) *enclen = 5; } else { /* 64 bit integer. */ uint64_t uv = v; - intenc[0] = LP_ENCODING_64BIT_INT; - intenc[1] = uv&0xff; - intenc[2] = (uv>>8)&0xff; - intenc[3] = (uv>>16)&0xff; - intenc[4] = (uv>>24)&0xff; - intenc[5] = (uv>>32)&0xff; - intenc[6] = (uv>>40)&0xff; - intenc[7] = (uv>>48)&0xff; - intenc[8] = uv>>56; - *enclen = 9; + if (intenc != NULL) { + intenc[0] = LP_ENCODING_64BIT_INT; + intenc[1] = uv&0xff; + intenc[2] = (uv>>8)&0xff; + intenc[3] = (uv>>16)&0xff; + intenc[4] = (uv>>24)&0xff; + intenc[5] = (uv>>32)&0xff; + intenc[6] = (uv>>40)&0xff; + intenc[7] = (uv>>48)&0xff; + intenc[8] = uv>>56; + } + if (enclen != NULL) *enclen = 9; } } @@ -659,50 +669,47 @@ unsigned char *lpGetValue(unsigned char *p, unsigned int *slen, long long *lval) return vstr; } -/* Find pointer to the entry equal to the specified entry. Skip 'skip' entries - * between every comparison. Returns NULL when the field could not be found. */ -unsigned char *lpFind(unsigned char *lp, unsigned char *p, unsigned char *s, - uint32_t slen, unsigned int skip) { +/* This is just a wrapper to lpGet() that is able to get an integer from an entry directly. + * Returns 1 and stores the integer in 'lval' if the entry is an integer. + * Returns 0 if the entry is a string. */ +int lpGetIntegerValue(unsigned char *p, long long *lval) { + int64_t ele_len; + if (!lpGet(p, &ele_len, NULL)) { + *lval = ele_len; + return 1; + } + return 0; +} + +/* Find pointer to the entry with a comparator callback. + * + * 'cmp' is a comparator callback. If it returns zero, current entry pointer + * will be returned. 'user' is passed to this callback. + * Skip 'skip' entries between every comparison. + * Returns NULL when the field could not be found. */ +unsigned char *lpFindCb(unsigned char *lp, unsigned char *p, + void *user, lpCmp cmp, unsigned int skip) +{ int skipcnt = 0; - unsigned char vencoding = 0; unsigned char *value; - int64_t ll, vll; + int64_t ll; uint64_t entry_size = 123456789; /* initialized to avoid warning. */ uint32_t lp_bytes = lpBytes(lp); - assert(p); + if (!p) + p = lpFirst(lp); + while (p) { if (skipcnt == 0) { value = lpGetWithSize(p, &ll, NULL, &entry_size); if (value) { /* check the value doesn't reach outside the listpack before accessing it */ assert(p >= lp + LP_HDR_SIZE && p + entry_size < lp + lp_bytes); - if (slen == ll && memcmp(value, s, slen) == 0) { - return p; - } - } else { - /* Find out if the searched field can be encoded. Note that - * we do it only the first time, once done vencoding is set - * to non-zero and vll is set to the integer value. */ - if (vencoding == 0) { - /* If the entry can be encoded as integer we set it to - * 1, else set it to UCHAR_MAX, so that we don't retry - * again the next time. */ - if (slen >= 32 || slen == 0 || !lpStringToInt64((const char*)s, slen, &vll)) { - vencoding = UCHAR_MAX; - } else { - vencoding = 1; - } - } - - /* Compare current entry with specified entry, do it only - * if vencoding != UCHAR_MAX because if there is no encoding - * possible for the field it can't be a valid integer. */ - if (vencoding != UCHAR_MAX && ll == vll) { - return p; - } } + if (cmp(lp, p, user, value, ll) == 0) + return p; + /* Reset skip count */ skipcnt = skip; p += entry_size; @@ -727,6 +734,62 @@ unsigned char *lpFind(unsigned char *lp, unsigned char *p, unsigned char *s, return NULL; } +struct lpFindArg { + unsigned char *s; /* Item to search */ + uint32_t slen; /* Item len */ + int vencoding; + int64_t vll; +}; + +/* Comparator function to find item */ +static inline int lpFindCmp(const unsigned char *lp, unsigned char *p, + void *user, unsigned char *s, long long slen) { + (void) lp; + (void) p; + struct lpFindArg *arg = user; + + if (s) { + if (slen == arg->slen && memcmp(arg->s, s, slen) == 0) { + return 0; + } + } else { + /* Find out if the searched field can be encoded. Note that + * we do it only the first time, once done vencoding is set + * to non-zero and vll is set to the integer value. */ + if (arg->vencoding == 0) { + /* If the entry can be encoded as integer we set it to + * 1, else set it to UCHAR_MAX, so that we don't retry + * again the next time. */ + if (arg->slen >= 32 || arg->slen == 0 || !lpStringToInt64((const char*)arg->s, arg->slen, &arg->vll)) { + arg->vencoding = UCHAR_MAX; + } else { + arg->vencoding = 1; + } + } + + /* Compare current entry with specified entry, do it only + * if vencoding != UCHAR_MAX because if there is no encoding + * possible for the field it can't be a valid integer. */ + if (arg->vencoding != UCHAR_MAX && slen == arg->vll) { + return 0; + } + } + + return 1; +} + +/* Find pointer to the entry equal to the specified entry. Skip 'skip' entries + * between every comparison. Returns NULL when the field could not be found. */ +unsigned char *lpFind(unsigned char *lp, unsigned char *p, unsigned char *s, + uint32_t slen, unsigned int skip) +{ + struct lpFindArg arg = { + .s = s, + .slen = slen + }; + return lpFindCb(lp, p, &arg, lpFindCmp, skip); +} + /* Insert, delete or replace the specified string element 'elestr' of length * 'size' or integer element 'eleint' at the specified position 'p', with 'p' * being a listpack element pointer obtained with lpFirst(), lpLast(), lpNext(), @@ -904,6 +967,140 @@ unsigned char *lpInsert(unsigned char *lp, unsigned char *elestr, unsigned char return lp; } +/* Insert the specified elements with 'entries' and 'len' at the specified + * position 'p', with 'p' being a listpack element pointer obtained with + * lpFirst(), lpLast(), lpNext(), lpPrev() or lpSeek(). + * + * This is similar to lpInsert() but allows you to insert batch of entries in + * one call. This function is more efficient than inserting entries one by one + * as it does single realloc()/memmove() calls for all the entries. + * + * In each listpackEntry, if 'sval' is not null, it is assumed entry is string + * and 'sval' and 'slen' will be used. Otherwise, 'lval' will be used to append + * the integer entry. + * + * The elements are inserted before or after the element pointed by 'p' + * depending on the 'where' argument, that can be LP_BEFORE or LP_AFTER. + * + * If 'newp' is not NULL, at the end of a successful call '*newp' will be set + * to the address of the element just added, so that it will be possible to + * continue an interaction with lpNext() and lpPrev(). + * + * Returns NULL on out of memory or when the listpack total length would exceed + * the max allowed size of 2^32-1, otherwise the new pointer to the listpack + * holding the new element is returned (and the old pointer passed is no longer + * considered valid). */ +unsigned char *lpBatchInsert(unsigned char *lp, unsigned char *p, int where, + listpackEntry *entries, unsigned int len, + unsigned char **newp) +{ + assert(where == LP_BEFORE || where == LP_AFTER); + assert(entries != NULL && len > 0); + + struct listpackInsertEntry { + int enctype; + uint64_t enclen; + unsigned char intenc[LP_MAX_INT_ENCODING_LEN]; + unsigned char backlen[LP_MAX_BACKLEN_SIZE]; + unsigned long backlen_size; + }; + + uint64_t addedlen = 0; /* The encoded length of the added elements. */ + struct listpackInsertEntry tmp[3]; /* Encoded entries */ + struct listpackInsertEntry *enc = tmp; + + if (len > sizeof(tmp) / sizeof(struct listpackInsertEntry)) { + /* If 'len' is larger than local buffer size, allocate on heap. */ + enc = zmalloc(len * sizeof(struct listpackInsertEntry)); + } + + /* If we need to insert after the current element, we just jump to the + * next element (that could be the EOF one) and handle the case of + * inserting before. So the function will actually deal with just one + * case: LP_BEFORE. */ + if (where == LP_AFTER) { + p = lpSkip(p); + where = LP_BEFORE; + ASSERT_INTEGRITY(lp, p); + } + + for (unsigned int i = 0; i < len; i++) { + listpackEntry *e = &entries[i]; + if (e->sval) { + /* Calling lpEncodeGetType() results into the encoded version of the + * element to be stored into 'intenc' in case it is representable as + * an integer: in that case, the function returns LP_ENCODING_INT. + * Otherwise, if LP_ENCODING_STR is returned, we'll have to call + * lpEncodeString() to actually write the encoded string on place + * later. + * + * Whatever the returned encoding is, 'enclen' is populated with the + * length of the encoded element. */ + enc[i].enctype = lpEncodeGetType(e->sval, e->slen, + enc[i].intenc, &enc[i].enclen); + } else { + enc[i].enctype = LP_ENCODING_INT; + lpEncodeIntegerGetType(e->lval, enc[i].intenc, &enc[i].enclen); + } + addedlen += enc[i].enclen; + + /* We need to also encode the backward-parsable length of the element + * and append it to the end: this allows to traverse the listpack from + * the end to the start. */ + enc[i].backlen_size = lpEncodeBacklen(enc[i].backlen, enc[i].enclen); + addedlen += enc[i].backlen_size; + } + + uint64_t old_listpack_bytes = lpGetTotalBytes(lp); + uint64_t new_listpack_bytes = old_listpack_bytes + addedlen; + if (new_listpack_bytes > UINT32_MAX) return NULL; + + /* Store the offset of the element 'p', so that we can obtain its + * address again after a reallocation. */ + unsigned long poff = p-lp; + unsigned char *dst = lp + poff; /* May be updated after reallocation. */ + + /* Realloc before: we need more room. */ + if (new_listpack_bytes > old_listpack_bytes && + new_listpack_bytes > lp_malloc_size(lp)) { + if ((lp = lp_realloc(lp,new_listpack_bytes)) == NULL) return NULL; + dst = lp + poff; + } + + /* Setup the listpack relocating the elements to make the exact room + * we need to store the new ones. */ + memmove(dst+addedlen,dst,old_listpack_bytes-poff); + + for (unsigned int i = 0; i < len; i++) { + listpackEntry *ent = &entries[i]; + + if (newp) + *newp = dst; + + if (enc[i].enctype == LP_ENCODING_INT) + memcpy(dst, enc[i].intenc, enc[i].enclen); + else + lpEncodeString(dst, ent->sval, ent->slen); + + dst += enc[i].enclen; + memcpy(dst, enc[i].backlen, enc[i].backlen_size); + dst += enc[i].backlen_size; + } + + /* Update header. */ + uint32_t num_elements = lpGetNumElements(lp); + if (num_elements != LP_HDR_NUMELE_UNKNOWN) { + if ((int64_t) len > (int64_t) LP_HDR_NUMELE_UNKNOWN - (int64_t) num_elements) + lpSetNumElements(lp, LP_HDR_NUMELE_UNKNOWN); + else + lpSetNumElements(lp,num_elements + len); + } + lpSetTotalBytes(lp,new_listpack_bytes); + if (enc != tmp) lp_free(enc); + + return lp; +} + /* This is just a wrapper for lpInsert() to directly use a string. */ unsigned char *lpInsertString(unsigned char *lp, unsigned char *s, uint32_t slen, unsigned char *p, int where, unsigned char **newp) @@ -951,6 +1148,20 @@ unsigned char *lpAppendInteger(unsigned char *lp, long long lval) { return lpInsertInteger(lp, lval, eofptr, LP_BEFORE, NULL); } +/* Append batch of entries to the listpack. + * + * This call is more efficient than multiple lpAppend() calls as it only does + * a single realloc() for all the given entries. + * + * In each listpackEntry, if 'sval' is not null, it is assumed entry is string + * and 'sval' and 'slen' will be used. Otherwise, 'lval' will be used to append + * the integer entry. */ +unsigned char *lpBatchAppend(unsigned char *lp, listpackEntry *entries, unsigned long len) { + uint64_t listpack_bytes = lpGetTotalBytes(lp); + unsigned char *eofptr = lp + listpack_bytes - 1; + return lpBatchInsert(lp, eofptr, LP_BEFORE, entries, len, NULL); +} + /* This is just a wrapper for lpInsert() to directly use a string to replace * the current element. The function returns the new listpack as return * value, and also updates the current cursor by updating '*p'. */ @@ -1199,13 +1410,17 @@ size_t lpBytes(unsigned char *lp) { return lpGetTotalBytes(lp); } +/* Returns the size 'lval' will require when encoded, in bytes */ +size_t lpEntrySizeInteger(long long lval) { + uint64_t enclen; + lpEncodeIntegerGetType(lval, NULL, &enclen); + unsigned long backlen = lpEncodeBacklen(NULL, enclen); + return enclen + backlen; +} + /* Returns the size of a listpack consisting of an integer repeated 'rep' times. */ size_t lpEstimateBytesRepeatedInteger(long long lval, unsigned long rep) { - uint64_t enclen; - unsigned char intenc[LP_MAX_INT_ENCODING_LEN]; - lpEncodeIntegerGetType(lval, intenc, &enclen); - unsigned long backlen = lpEncodeBacklen(NULL, enclen); - return LP_HDR_SIZE + (enclen + backlen) * rep + 1; + return LP_HDR_SIZE + lpEntrySizeInteger(lval) * rep + 1; } /* Seek the specified element and returns the pointer to the seeked element. @@ -1408,15 +1623,20 @@ static inline void lpSaveValue(unsigned char *val, unsigned int len, int64_t lva /* Randomly select a pair of key and value. * total_count is a pre-computed length/2 of the listpack (to avoid calls to lpLength) * 'key' and 'val' are used to store the result key value pair. - * 'val' can be NULL if the value is not needed. */ -void lpRandomPair(unsigned char *lp, unsigned long total_count, listpackEntry *key, listpackEntry *val) { + * 'val' can be NULL if the value is not needed. + * 'tuple_len' indicates entry count of a single logical item. It should be 2 + * if listpack was saved as key-value pair or more for key-value-...(n_entries). */ +void lpRandomPair(unsigned char *lp, unsigned long total_count, + listpackEntry *key, listpackEntry *val, int tuple_len) +{ unsigned char *p; + assert(tuple_len >= 2); + /* Avoid div by zero on corrupt listpack */ assert(total_count); - /* Generate even numbers, because listpack saved K-V pair */ - int r = (rand() % total_count) * 2; + int r = (rand() % total_count) * tuple_len; assert((p = lpSeek(lp, r))); key->sval = lpGetValue(p, &(key->slen), &(key->lval)); @@ -1466,26 +1686,31 @@ void lpRandomEntries(unsigned char *lp, unsigned int count, listpackEntry *entri /* Randomly select count of key value pairs and store into 'keys' and * 'vals' args. The order of the picked entries is random, and the selections * are non-unique (repetitions are possible). - * The 'vals' arg can be NULL in which case we skip these. */ -void lpRandomPairs(unsigned char *lp, unsigned int count, listpackEntry *keys, listpackEntry *vals) { + * The 'vals' arg can be NULL in which case we skip these. + * 'tuple_len' indicates entry count of a single logical item. It should be 2 + * if listpack was saved as key-value pair or more for key-value-...(n_entries). */ +void lpRandomPairs(unsigned char *lp, unsigned int count, listpackEntry *keys, listpackEntry *vals, int tuple_len) { unsigned char *p, *key, *value; unsigned int klen = 0, vlen = 0; long long klval = 0, vlval = 0; + assert(tuple_len >= 2); + /* Notice: the index member must be first due to the use in uintCompare */ typedef struct { unsigned int index; unsigned int order; } rand_pick; rand_pick *picks = lp_malloc(sizeof(rand_pick)*count); - unsigned int total_size = lpLength(lp)/2; + unsigned int total_size = lpLength(lp)/tuple_len; /* Avoid div by zero on corrupt listpack */ assert(total_size); /* create a pool of random indexes (some may be duplicate). */ for (unsigned int i = 0; i < count; i++) { - picks[i].index = (rand() % total_size) * 2; /* Generate even indexes */ + /* Generate indexes that key exist at */ + picks[i].index = (rand() % total_size) * tuple_len; /* keep track of the order we picked them */ picks[i].order = i; } @@ -1507,8 +1732,11 @@ void lpRandomPairs(unsigned char *lp, unsigned int count, listpackEntry *keys, l lpSaveValue(value, vlen, vlval, &vals[storeorder]); pickindex++; } - lpindex += 2; - p = lpNext(lp, p); + lpindex += tuple_len; + + for (int i = 0; i < tuple_len - 1; i++) { + p = lpNext(lp, p); + } } lp_free(picks); @@ -1518,13 +1746,20 @@ void lpRandomPairs(unsigned char *lp, unsigned int count, listpackEntry *keys, l * 'vals' args. The selections are unique (no repetitions), and the order of * the picked entries is NOT-random. * The 'vals' arg can be NULL in which case we skip these. + * 'tuple_len' indicates entry count of a single logical item. It should be 2 + * if listpack was saved as key-value pair or more for key-value-...(n_entries). * The return value is the number of items picked which can be lower than the * requested count if the listpack doesn't hold enough pairs. */ -unsigned int lpRandomPairsUnique(unsigned char *lp, unsigned int count, listpackEntry *keys, listpackEntry *vals) { +unsigned int lpRandomPairsUnique(unsigned char *lp, unsigned int count, + listpackEntry *keys, listpackEntry *vals, + int tuple_len) +{ + assert(tuple_len >= 2); + unsigned char *p, *key; unsigned int klen = 0; long long klval = 0; - unsigned int total_size = lpLength(lp)/2; + unsigned int total_size = lpLength(lp)/tuple_len; unsigned int index = 0; if (count > total_size) count = total_size; @@ -1532,7 +1767,7 @@ unsigned int lpRandomPairsUnique(unsigned char *lp, unsigned int count, listpack p = lpFirst(lp); unsigned int picked = 0, remaining = count; while (picked < count && p) { - assert((p = lpNextRandom(lp, p, &index, remaining, 1))); + assert((p = lpNextRandom(lp, p, &index, remaining, tuple_len))); key = lpGetValue(p, &klen, &klval); lpSaveValue(key, klen, klval, &keys[picked]); assert((p = lpNext(lp, p))); @@ -1554,8 +1789,9 @@ unsigned int lpRandomPairsUnique(unsigned char *lp, unsigned int count, listpack * the end of the list. The 'index' needs to be initialized according to the * current zero-based index matching the position of the starting element 'p' * and is updated to match the returned element's zero-based index. If - * 'even_only' is nonzero, an element with an even index is picked, which is - * useful if the listpack represents a key-value pair sequence. + * 'tuple_len' indicates entry count of a single logical item. e.g. This is + * useful if listpack represents key-value pairs. In this case, tuple_len should + * be two and even indexes will be picked. * * Note that this function can return p. In order to skip the previously * returned element, you need to call lpNext() or lpDelete() after each call to @@ -1565,7 +1801,7 @@ unsigned int lpRandomPairsUnique(unsigned char *lp, unsigned int count, listpack * p = lpFirst(lp); * i = 0; * while (remaining > 0) { - * p = lpNextRandom(lp, p, &i, remaining--, 0); + * p = lpNextRandom(lp, p, &i, remaining--, 1); * * // ... Do stuff with p ... * @@ -1574,8 +1810,9 @@ unsigned int lpRandomPairsUnique(unsigned char *lp, unsigned int count, listpack * } */ unsigned char *lpNextRandom(unsigned char *lp, unsigned char *p, unsigned int *index, - unsigned int remaining, int even_only) + unsigned int remaining, int tuple_len) { + assert(tuple_len > 0); /* To only iterate once, every time we try to pick a member, the probability * we pick it is the quotient of the count left we want to pick and the * count still we haven't visited. This way, we could make every member be @@ -1583,15 +1820,14 @@ unsigned char *lpNextRandom(unsigned char *lp, unsigned char *p, unsigned int *i unsigned int i = *index; unsigned int total_size = lpLength(lp); while (i < total_size && p != NULL) { - if (even_only && i % 2 != 0) { + if (i % tuple_len != 0) { p = lpNext(lp, p); i++; continue; } /* Do we pick this element? */ - unsigned int available = total_size - i; - if (even_only) available /= 2; + unsigned int available = (total_size - i) / tuple_len; double randomDouble = ((double)rand()) / RAND_MAX; double threshold = ((double)remaining) / available; if (randomDouble <= threshold) { @@ -1787,6 +2023,24 @@ static int lpValidation(unsigned char *p, unsigned int head_count, void *userdat return ret; } +static int lpFindCbCmp(const unsigned char *lp, unsigned char *p, void *user, unsigned char *s, long long slen) { + assert(lp); + assert(p); + + char *n = user; + + if (!s) { + int64_t sval; + if (lpStringToInt64((const char*)n, strlen(n), &sval)) + return slen == sval ? 0 : 1; + } else { + if (strlen(n) == (size_t) slen && memcmp(n, s, slen) == 0) + return 0; + } + + return 1; +} + int listpackTest(int argc, char *argv[], int flags) { UNUSED(argc); UNUSED(argv); @@ -2031,6 +2285,111 @@ int listpackTest(int argc, char *argv[], int flags) { zfree(lp); } + TEST("Batch append") { + listpackEntry ent[6] = { + {.sval = (unsigned char*)mixlist[0], .slen = strlen(mixlist[0])}, + {.sval = (unsigned char*)mixlist[1], .slen = strlen(mixlist[1])}, + {.sval = (unsigned char*)mixlist[2], .slen = strlen(mixlist[2])}, + {.lval = 4294967296}, + {.sval = (unsigned char*)mixlist[3], .slen = strlen(mixlist[3])}, + {.lval = -100} + }; + + lp = lpNew(0); + lp = lpBatchAppend(lp, ent, 2); + verifyEntry(lpSeek(lp, 0), ent[0].sval, ent[0].slen); + verifyEntry(lpSeek(lp, 1), ent[1].sval, ent[1].slen); + assert(lpLength(lp) == 2); + + lp = lpBatchAppend(lp, &ent[2], 1); + verifyEntry(lpSeek(lp, 0), ent[0].sval, ent[0].slen); + verifyEntry(lpSeek(lp, 1), ent[1].sval, ent[1].slen); + verifyEntry(lpSeek(lp, 2), ent[2].sval, ent[2].slen); + assert(lpLength(lp) == 3); + + lp = lpDeleteRange(lp, 1, 1); + verifyEntry(lpSeek(lp, 0), ent[0].sval, ent[0].slen); + verifyEntry(lpSeek(lp, 1), ent[2].sval, ent[2].slen); + assert(lpLength(lp) == 2); + + lp = lpBatchAppend(lp, &ent[3], 3); + verifyEntry(lpSeek(lp, 0), ent[0].sval, ent[0].slen); + verifyEntry(lpSeek(lp, 1), ent[2].sval, ent[2].slen); + verifyEntry(lpSeek(lp, 2), (unsigned char*) "4294967296", 10); + verifyEntry(lpSeek(lp, 3), ent[4].sval, ent[4].slen); + verifyEntry(lpSeek(lp, 4), (unsigned char*) "-100", 4); + assert(lpLength(lp) == 5); + + lp = lpDeleteRange(lp, 1, 3); + verifyEntry(lpSeek(lp, 0), ent[0].sval, ent[0].slen); + verifyEntry(lpSeek(lp, 1), (unsigned char*) "-100", 4); + assert(lpLength(lp) == 2); + + lpFree(lp); + } + + TEST("Batch insert") { + lp = lpNew(0); + listpackEntry ent[6] = { + {.sval = (unsigned char*)mixlist[0], .slen = strlen(mixlist[0])}, + {.sval = (unsigned char*)mixlist[1], .slen = strlen(mixlist[1])}, + {.sval = (unsigned char*)mixlist[2], .slen = strlen(mixlist[2])}, + {.lval = 4294967296}, + {.sval = (unsigned char*)mixlist[3], .slen = strlen(mixlist[3])}, + {.lval = -100} + }; + + lp = lpBatchAppend(lp, ent, 4); + assert(lpLength(lp) == 4); + verifyEntry(lpSeek(lp, 0), ent[0].sval, ent[0].slen); + verifyEntry(lpSeek(lp, 1), ent[1].sval, ent[1].slen); + verifyEntry(lpSeek(lp, 2), ent[2].sval, ent[2].slen); + verifyEntry(lpSeek(lp, 3), (unsigned char*)"4294967296", 10); + + /* Insert with LP_BEFORE */ + p = lpSeek(lp, 3); + lp = lpBatchInsert(lp, p, LP_BEFORE, &ent[4], 2, &p); + verifyEntry(p, (unsigned char*)"-100", 4); + assert(lpLength(lp) == 6); + verifyEntry(lpSeek(lp, 0), ent[0].sval, ent[0].slen); + verifyEntry(lpSeek(lp, 1), ent[1].sval, ent[1].slen); + verifyEntry(lpSeek(lp, 2), ent[2].sval, ent[2].slen); + verifyEntry(lpSeek(lp, 3), ent[4].sval, ent[4].slen); + verifyEntry(lpSeek(lp, 4), (unsigned char*)"-100", 4); + verifyEntry(lpSeek(lp, 5), (unsigned char*)"4294967296", 10); + + lp = lpDeleteRange(lp, 1, 2); + assert(lpLength(lp) == 4); + verifyEntry(lpSeek(lp, 0), ent[0].sval, ent[0].slen); + verifyEntry(lpSeek(lp, 1), ent[4].sval, ent[4].slen); + verifyEntry(lpSeek(lp, 2), (unsigned char*)"-100", 4); + verifyEntry(lpSeek(lp, 3), (unsigned char*)"4294967296", 10); + + /* Insert with LP_AFTER */ + p = lpSeek(lp, 0); + lp = lpBatchInsert(lp, p, LP_AFTER, &ent[1], 2, &p); + verifyEntry(p, ent[2].sval, ent[2].slen); + assert(lpLength(lp) == 6); + verifyEntry(lpSeek(lp, 0), ent[0].sval, ent[0].slen); + verifyEntry(lpSeek(lp, 1), ent[1].sval, ent[1].slen); + verifyEntry(lpSeek(lp, 2), ent[2].sval, ent[2].slen); + verifyEntry(lpSeek(lp, 3), ent[4].sval, ent[4].slen); + verifyEntry(lpSeek(lp, 4), (unsigned char*)"-100", 4); + verifyEntry(lpSeek(lp, 5), (unsigned char*)"4294967296", 10); + + lp = lpDeleteRange(lp, 2, 4); + assert(lpLength(lp) == 2); + p = lpSeek(lp, 1); + lp = lpBatchInsert(lp, p, LP_AFTER, &ent[2], 1, &p); + verifyEntry(p, ent[2].sval, ent[2].slen); + assert(lpLength(lp) == 3); + verifyEntry(lpSeek(lp, 0), ent[0].sval, ent[0].slen); + verifyEntry(lpSeek(lp, 1), ent[1].sval, ent[1].slen); + verifyEntry(lpSeek(lp, 2), ent[2].sval, ent[2].slen); + + lpFree(lp); + } + TEST("Batch delete") { unsigned char *lp = createList(); /* char *mixlist[] = {"hello", "foo", "quux", "1024"} */ assert(lpLength(lp) == 4); /* Pre-condition */ @@ -2210,7 +2569,7 @@ int listpackTest(int argc, char *argv[], int flags) { unsigned index = 0; while (remaining > 0) { assert(p != NULL); - p = lpNextRandom(lp, p, &index, remaining--, 0); + p = lpNextRandom(lp, p, &index, remaining--, 1); assert(p != NULL); assert(p != prev); prev = p; @@ -2226,7 +2585,7 @@ int listpackTest(int argc, char *argv[], int flags) { unsigned i = 0; /* Pick from empty listpack returns NULL. */ - assert(lpNextRandom(lp, NULL, &i, 2, 0) == NULL); + assert(lpNextRandom(lp, NULL, &i, 2, 1) == NULL); /* Add some elements and find their pointers within the listpack. */ lp = lpAppend(lp, (unsigned char *)"abc", 3); @@ -2239,19 +2598,19 @@ int listpackTest(int argc, char *argv[], int flags) { assert(lpNext(lp, p2) == NULL); /* Pick zero elements returns NULL. */ - i = 0; assert(lpNextRandom(lp, lpFirst(lp), &i, 0, 0) == NULL); + i = 0; assert(lpNextRandom(lp, lpFirst(lp), &i, 0, 1) == NULL); /* Pick all returns all. */ - i = 0; assert(lpNextRandom(lp, p0, &i, 3, 0) == p0 && i == 0); - i = 1; assert(lpNextRandom(lp, p1, &i, 2, 0) == p1 && i == 1); - i = 2; assert(lpNextRandom(lp, p2, &i, 1, 0) == p2 && i == 2); + i = 0; assert(lpNextRandom(lp, p0, &i, 3, 1) == p0 && i == 0); + i = 1; assert(lpNextRandom(lp, p1, &i, 2, 1) == p1 && i == 1); + i = 2; assert(lpNextRandom(lp, p2, &i, 1, 1) == p2 && i == 2); /* Pick more than one when there's only one left returns the last one. */ - i = 2; assert(lpNextRandom(lp, p2, &i, 42, 0) == p2 && i == 2); + i = 2; assert(lpNextRandom(lp, p2, &i, 42, 1) == p2 && i == 2); /* Pick all even elements returns p0 and p2. */ - i = 0; assert(lpNextRandom(lp, p0, &i, 10, 1) == p0 && i == 0); - i = 1; assert(lpNextRandom(lp, p1, &i, 10, 1) == p2 && i == 2); + i = 0; assert(lpNextRandom(lp, p0, &i, 10, 2) == p0 && i == 0); + i = 1; assert(lpNextRandom(lp, p1, &i, 10, 2) == p2 && i == 2); /* Don't crash even for bad index. */ for (int j = 0; j < 100; j++) { @@ -2264,7 +2623,7 @@ int listpackTest(int argc, char *argv[], int flags) { } i = j % 7; unsigned int remaining = j % 5; - p = lpNextRandom(lp, p, &i, remaining, 0); + p = lpNextRandom(lp, p, &i, remaining, 1); assert(p == p0 || p == p1 || p == p2 || p == NULL); } lpFree(lp); @@ -2275,7 +2634,7 @@ int listpackTest(int argc, char *argv[], int flags) { unsigned char *lp = lpNew(0); lp = lpAppend(lp, (unsigned char*)"abc", 3); lp = lpAppend(lp, (unsigned char*)"123", 3); - lpRandomPair(lp, 1, &key, &val); + lpRandomPair(lp, 1, &key, &val, 2); assert(memcmp(key.sval, "abc", key.slen) == 0); assert(val.lval == 123); lpFree(lp); @@ -2288,7 +2647,7 @@ int listpackTest(int argc, char *argv[], int flags) { lp = lpAppend(lp, (unsigned char*)"123", 3); lp = lpAppend(lp, (unsigned char*)"456", 3); lp = lpAppend(lp, (unsigned char*)"def", 3); - lpRandomPair(lp, 2, &key, &val); + lpRandomPair(lp, 2, &key, &val, 2); if (key.sval) { assert(!memcmp(key.sval, "abc", key.slen)); assert(key.slen == 3); @@ -2301,6 +2660,42 @@ int listpackTest(int argc, char *argv[], int flags) { lpFree(lp); } + TEST("Random pair with tuple_len 3") { + listpackEntry key, val; + unsigned char *lp = lpNew(0); + lp = lpAppend(lp, (unsigned char*)"abc", 3); + lp = lpAppend(lp, (unsigned char*)"123", 3); + lp = lpAppend(lp, (unsigned char*)"xxx", 3); + lp = lpAppend(lp, (unsigned char*)"456", 3); + lp = lpAppend(lp, (unsigned char*)"def", 3); + lp = lpAppend(lp, (unsigned char*)"xxx", 3); + lp = lpAppend(lp, (unsigned char*)"281474976710655", 15); + lp = lpAppend(lp, (unsigned char*)"789", 3); + lp = lpAppend(lp, (unsigned char*)"xxx", 3); + + for (int i = 0; i < 5; i++) { + lpRandomPair(lp, 3, &key, &val, 3); + if (key.sval) { + if (!memcmp(key.sval, "abc", key.slen)) { + assert(key.slen == 3); + assert(val.lval == 123); + } else { + assert(0); + }; + } + if (!key.sval) { + if (key.lval == 456) + assert(!memcmp(val.sval, "def", val.slen)); + else if (key.lval == 281474976710655LL) + assert(val.lval == 789); + else + assert(0); + } + } + + lpFree(lp); + } + TEST("Random pairs with one element") { int count = 5; unsigned char *lp = lpNew(0); @@ -2309,7 +2704,7 @@ int listpackTest(int argc, char *argv[], int flags) { lp = lpAppend(lp, (unsigned char*)"abc", 3); lp = lpAppend(lp, (unsigned char*)"123", 3); - lpRandomPairs(lp, count, keys, vals); + lpRandomPairs(lp, count, keys, vals, 2); assert(memcmp(keys[4].sval, "abc", keys[4].slen) == 0); assert(vals[4].lval == 123); zfree(keys); @@ -2327,7 +2722,7 @@ int listpackTest(int argc, char *argv[], int flags) { lp = lpAppend(lp, (unsigned char*)"123", 3); lp = lpAppend(lp, (unsigned char*)"456", 3); lp = lpAppend(lp, (unsigned char*)"def", 3); - lpRandomPairs(lp, count, keys, vals); + lpRandomPairs(lp, count, keys, vals, 2); for (int i = 0; i < count; i++) { if (keys[i].sval) { assert(!memcmp(keys[i].sval, "abc", keys[i].slen)); @@ -2344,6 +2739,47 @@ int listpackTest(int argc, char *argv[], int flags) { lpFree(lp); } + TEST("Random pairs with many elements and tuple_len 3") { + int count = 5; + lp = lpNew(0); + listpackEntry *keys = zcalloc(sizeof(listpackEntry) * count); + listpackEntry *vals = zcalloc(sizeof(listpackEntry) * count); + + lp = lpAppend(lp, (unsigned char*)"abc", 3); + lp = lpAppend(lp, (unsigned char*)"123", 3); + lp = lpAppend(lp, (unsigned char*)"xxx", 3); + lp = lpAppend(lp, (unsigned char*)"456", 3); + lp = lpAppend(lp, (unsigned char*)"def", 3); + lp = lpAppend(lp, (unsigned char*)"xxx", 3); + lp = lpAppend(lp, (unsigned char*)"281474976710655", 15); + lp = lpAppend(lp, (unsigned char*)"789", 3); + lp = lpAppend(lp, (unsigned char*)"xxx", 3); + + lpRandomPairs(lp, count, keys, vals, 3); + for (int i = 0; i < count; i++) { + if (keys[i].sval) { + if (!memcmp(keys[i].sval, "abc", keys[i].slen)) { + assert(keys[i].slen == 3); + assert(vals[i].lval == 123); + } else { + assert(0); + }; + } + if (!keys[i].sval) { + if (keys[i].lval == 456) + assert(!memcmp(vals[i].sval, "def", vals[i].slen)); + else if (keys[i].lval == 281474976710655LL) + assert(vals[i].lval == 789); + else + assert(0); + } + } + + zfree(keys); + zfree(vals); + lpFree(lp); + } + TEST("Random pairs unique with one element") { unsigned picked; int count = 5; @@ -2353,7 +2789,7 @@ int listpackTest(int argc, char *argv[], int flags) { lp = lpAppend(lp, (unsigned char*)"abc", 3); lp = lpAppend(lp, (unsigned char*)"123", 3); - picked = lpRandomPairsUnique(lp, count, keys, vals); + picked = lpRandomPairsUnique(lp, count, keys, vals, 2); assert(picked == 1); assert(memcmp(keys[0].sval, "abc", keys[0].slen) == 0); assert(vals[0].lval == 123); @@ -2373,7 +2809,7 @@ int listpackTest(int argc, char *argv[], int flags) { lp = lpAppend(lp, (unsigned char*)"123", 3); lp = lpAppend(lp, (unsigned char*)"456", 3); lp = lpAppend(lp, (unsigned char*)"def", 3); - picked = lpRandomPairsUnique(lp, count, keys, vals); + picked = lpRandomPairsUnique(lp, count, keys, vals, 2); assert(picked == 2); for (int i = 0; i < 2; i++) { if (keys[i].sval) { @@ -2391,6 +2827,47 @@ int listpackTest(int argc, char *argv[], int flags) { lpFree(lp); } + TEST("Random pairs unique with many elements and tuple_len 3") { + unsigned picked; + int count = 5; + lp = lpNew(0); + listpackEntry *keys = zmalloc(sizeof(listpackEntry) * count); + listpackEntry *vals = zmalloc(sizeof(listpackEntry) * count); + + lp = lpAppend(lp, (unsigned char*)"abc", 3); + lp = lpAppend(lp, (unsigned char*)"123", 3); + lp = lpAppend(lp, (unsigned char*)"xxx", 3); + lp = lpAppend(lp, (unsigned char*)"456", 3); + lp = lpAppend(lp, (unsigned char*)"def", 3); + lp = lpAppend(lp, (unsigned char*)"xxx", 3); + lp = lpAppend(lp, (unsigned char*)"281474976710655", 15); + lp = lpAppend(lp, (unsigned char*)"789", 3); + lp = lpAppend(lp, (unsigned char*)"xxx", 3); + picked = lpRandomPairsUnique(lp, count, keys, vals, 3); + assert(picked == 3); + for (int i = 0; i < 3; i++) { + if (keys[i].sval) { + if (!memcmp(keys[i].sval, "abc", keys[i].slen)) { + assert(keys[i].slen == 3); + assert(vals[i].lval == 123); + } else { + assert(0); + }; + } + if (!keys[i].sval) { + if (keys[i].lval == 456) + assert(!memcmp(vals[i].sval, "def", vals[i].slen)); + else if (keys[i].lval == 281474976710655LL) + assert(vals[i].lval == 789); + else + assert(0); + } + } + zfree(keys); + zfree(vals); + lpFree(lp); + } + TEST("push various encodings") { lp = lpNew(0); @@ -2449,6 +2926,21 @@ int listpackTest(int argc, char *argv[], int flags) { lpFree(lp); } + TEST("Test lpFindCb") { + lp = createList(); /* "hello", "foo", "quux", "1024" */ + assert(lpFindCb(lp, lpFirst(lp), "abc", lpFindCbCmp, 0) == NULL); + verifyEntry(lpFindCb(lp, NULL, "hello", lpFindCbCmp, 0), (unsigned char*)"hello", 5); + verifyEntry(lpFindCb(lp, NULL, "1024", lpFindCbCmp, 0), (unsigned char*)"1024", 4); + verifyEntry(lpFindCb(lp, NULL, "quux", lpFindCbCmp, 0), (unsigned char*)"quux", 4); + verifyEntry(lpFindCb(lp, NULL, "foo", lpFindCbCmp, 0), (unsigned char*)"foo", 3); + lpFree(lp); + + lp = lpNew(0); + assert(lpFindCb(lp, lpFirst(lp), "hello", lpFindCbCmp, 0) == NULL); + assert(lpFindCb(lp, lpFirst(lp), "1024", lpFindCbCmp, 0) == NULL); + lpFree(lp); + } + TEST("Test lpValidateIntegrity") { lp = createList(); long count = 0; @@ -2471,6 +2963,26 @@ int listpackTest(int argc, char *argv[], int flags) { lpFree(lp); } + TEST("Test number of elements exceeds LP_HDR_NUMELE_UNKNOWN with batch insert") { + listpackEntry ent[2] = { + {.sval = (unsigned char*)mixlist[0], .slen = strlen(mixlist[0])}, + {.sval = (unsigned char*)mixlist[1], .slen = strlen(mixlist[1])} + }; + + lp = lpNew(0); + for (int i = 0; i < (LP_HDR_NUMELE_UNKNOWN/2) + 1; i++) + lp = lpBatchAppend(lp, ent, 2); + + assert(lpGetNumElements(lp) == LP_HDR_NUMELE_UNKNOWN); + assert(lpLength(lp) == LP_HDR_NUMELE_UNKNOWN+1); + + lp = lpDeleteRange(lp, -2, 2); + assert(lpGetNumElements(lp) == LP_HDR_NUMELE_UNKNOWN); + assert(lpLength(lp) == LP_HDR_NUMELE_UNKNOWN-1); + assert(lpGetNumElements(lp) == LP_HDR_NUMELE_UNKNOWN-1); /* update length after lpLength */ + lpFree(lp); + } + TEST("Stress with random payloads of different encoding") { unsigned long long start = usec(); int i,j,len,where; diff --git a/src/listpack.h b/src/listpack.h index 84e1a2a9c..c9fbc5624 100644 --- a/src/listpack.h +++ b/src/listpack.h @@ -49,18 +49,25 @@ unsigned char *lpReplaceInteger(unsigned char *lp, unsigned char **p, long long unsigned char *lpDelete(unsigned char *lp, unsigned char *p, unsigned char **newp); unsigned char *lpDeleteRangeWithEntry(unsigned char *lp, unsigned char **p, unsigned long num); unsigned char *lpDeleteRange(unsigned char *lp, long index, unsigned long num); +unsigned char *lpBatchAppend(unsigned char *lp, listpackEntry *entries, unsigned long len); +unsigned char *lpBatchInsert(unsigned char *lp, unsigned char *p, int where, + listpackEntry *entries, unsigned int len, unsigned char **newp); unsigned char *lpBatchDelete(unsigned char *lp, unsigned char **ps, unsigned long count); unsigned char *lpMerge(unsigned char **first, unsigned char **second); unsigned char *lpDup(unsigned char *lp); unsigned long lpLength(unsigned char *lp); unsigned char *lpGet(unsigned char *p, int64_t *count, unsigned char *intbuf); unsigned char *lpGetValue(unsigned char *p, unsigned int *slen, long long *lval); +int lpGetIntegerValue(unsigned char *p, long long *lval); unsigned char *lpFind(unsigned char *lp, unsigned char *p, unsigned char *s, uint32_t slen, unsigned int skip); +typedef int (*lpCmp)(const unsigned char *lp, unsigned char *p, void *user, unsigned char *s, long long slen); +unsigned char *lpFindCb(unsigned char *lp, unsigned char *p, void *user, lpCmp cmp, unsigned int skip); unsigned char *lpFirst(unsigned char *lp); unsigned char *lpLast(unsigned char *lp); unsigned char *lpNext(unsigned char *lp, unsigned char *p); unsigned char *lpPrev(unsigned char *lp, unsigned char *p); size_t lpBytes(unsigned char *lp); +size_t lpEntrySizeInteger(long long lval); size_t lpEstimateBytesRepeatedInteger(long long lval, unsigned long rep); unsigned char *lpSeek(unsigned char *lp, long index); typedef int (*listpackValidateEntryCB)(unsigned char *p, unsigned int head_count, void *userdata); @@ -69,12 +76,15 @@ int lpValidateIntegrity(unsigned char *lp, size_t size, int deep, unsigned char *lpValidateFirst(unsigned char *lp); int lpValidateNext(unsigned char *lp, unsigned char **pp, size_t lpbytes); unsigned int lpCompare(unsigned char *p, unsigned char *s, uint32_t slen); -void lpRandomPair(unsigned char *lp, unsigned long total_count, listpackEntry *key, listpackEntry *val); -void lpRandomPairs(unsigned char *lp, unsigned int count, listpackEntry *keys, listpackEntry *vals); -unsigned int lpRandomPairsUnique(unsigned char *lp, unsigned int count, listpackEntry *keys, listpackEntry *vals); +void lpRandomPair(unsigned char *lp, unsigned long total_count, + listpackEntry *key, listpackEntry *val, int tuple_len); +void lpRandomPairs(unsigned char *lp, unsigned int count, + listpackEntry *keys, listpackEntry *vals, int tuple_len); +unsigned int lpRandomPairsUnique(unsigned char *lp, unsigned int count, + listpackEntry *keys, listpackEntry *vals, int tuple_len); void lpRandomEntries(unsigned char *lp, unsigned int count, listpackEntry *entries); unsigned char *lpNextRandom(unsigned char *lp, unsigned char *p, unsigned int *index, - unsigned int remaining, int even_only); + unsigned int remaining, int tuple_len); int lpSafeToAdd(unsigned char* lp, size_t add); void lpRepr(unsigned char *lp); diff --git a/src/module.c b/src/module.c index 1ddc78044..1d0c628a3 100644 --- a/src/module.c +++ b/src/module.c @@ -745,7 +745,7 @@ int moduleDelKeyIfEmpty(RedisModuleKey *key) { case OBJ_LIST: isempty = listTypeLength(o) == 0; break; case OBJ_SET: isempty = setTypeSize(o) == 0; break; case OBJ_ZSET: isempty = zsetLength(o) == 0; break; - case OBJ_HASH: isempty = hashTypeLength(o) == 0; break; + case OBJ_HASH: isempty = hashTypeLength(o, 0) == 0; break; case OBJ_STREAM: isempty = streamLength(o) == 0; break; default: isempty = 0; } @@ -4168,7 +4168,7 @@ size_t RM_ValueLength(RedisModuleKey *key) { case OBJ_LIST: return listTypeLength(key->value); case OBJ_SET: return setTypeSize(key->value); case OBJ_ZSET: return zsetLength(key->value); - case OBJ_HASH: return hashTypeLength(key->value); + case OBJ_HASH: return hashTypeLength(key->value, 0); /* OPEN: To subtract expired fields? */ case OBJ_STREAM: return streamLength(key->value); default: return 0; } @@ -5271,7 +5271,10 @@ int RM_HashSet(RedisModuleKey *key, int flags, ...) { /* Handle XX and NX */ if (flags & (REDISMODULE_HASH_XX|REDISMODULE_HASH_NX)) { - int exists = hashTypeExists(key->value, field->ptr); + int isHashDeleted; + int exists = hashTypeExists(key->db, key->value, field->ptr, &isHashDeleted); + /* hash-field-expiration is not exposed to modules */ + serverAssert(isHashDeleted == 0); if (((flags & REDISMODULE_HASH_XX) && !exists) || ((flags & REDISMODULE_HASH_NX) && exists)) { @@ -5282,7 +5285,7 @@ int RM_HashSet(RedisModuleKey *key, int flags, ...) { /* Handle deletion if value is REDISMODULE_HASH_DELETE. */ if (value == REDISMODULE_HASH_DELETE) { - count += hashTypeDelete(key->value, field->ptr); + count += hashTypeDelete(key->value, field->ptr, 1); if (flags & REDISMODULE_HASH_CFIELDS) decrRefCount(field); continue; } @@ -5295,8 +5298,8 @@ int RM_HashSet(RedisModuleKey *key, int flags, ...) { low_flags |= HASH_SET_TAKE_FIELD; robj *argv[2] = {field,value}; - hashTypeTryConversion(key->value,argv,0,1); - int updated = hashTypeSet(key->value, field->ptr, value->ptr, low_flags); + hashTypeTryConversion(key->db,key->value,argv,0,1); + int updated = hashTypeSet(key->db, key->value, field->ptr, value->ptr, low_flags); count += (flags & REDISMODULE_HASH_COUNT_ALL) ? 1 : updated; /* If CFIELDS is active, SDS string ownership is now of hashTypeSet(), @@ -5374,14 +5377,22 @@ int RM_HashGet(RedisModuleKey *key, int flags, ...) { /* Query the hash for existence or value object. */ if (flags & REDISMODULE_HASH_EXISTS) { existsptr = va_arg(ap,int*); - if (key->value) - *existsptr = hashTypeExists(key->value,field->ptr); - else + if (key->value) { + int isHashDeleted; + *existsptr = hashTypeExists(key->db, key->value, field->ptr, &isHashDeleted); + /* hash-field-expiration is not exposed to modules */ + serverAssert(isHashDeleted == 0); + } else { *existsptr = 0; + } } else { + int isHashDeleted; valueptr = va_arg(ap,RedisModuleString**); if (key->value) { - *valueptr = hashTypeGetValueObject(key->value,field->ptr); + *valueptr = hashTypeGetValueObject(key->db,key->value,field->ptr, &isHashDeleted); + + /* Currently hash-field-expiration is not exposed to modules */ + serverAssert(isHashDeleted == 0); if (*valueptr) { robj *decoded = getDecodedObject(*valueptr); decrRefCount(*valueptr); @@ -11071,18 +11082,22 @@ static void moduleScanKeyCallback(void *privdata, const dictEntry *de) { ScanKeyCBData *data = privdata; sds key = dictGetKey(de); robj *o = data->key->value; - robj *field = createStringObject(key, sdslen(key)); + robj *field = NULL; robj *value = NULL; if (o->type == OBJ_SET) { value = NULL; } else if (o->type == OBJ_HASH) { sds val = dictGetVal(de); + field = createStringObject(key, hfieldlen(key)); value = createStringObject(val, sdslen(val)); } else if (o->type == OBJ_ZSET) { double *val = (double*)dictGetVal(de); value = createStringObjectFromLongDouble(*val, 0); } + /* if type is OBJ_HASH then key is of type hfield. Otherwise sds. */ + if (!field) field = createStringObject(key, sdslen(key)); + data->fn(data->key, field, value, data->user_data); decrRefCount(field); if (value) decrRefCount(value); diff --git a/src/mstr.c b/src/mstr.c new file mode 100644 index 000000000..39200d731 --- /dev/null +++ b/src/mstr.c @@ -0,0 +1,524 @@ +/* + * Copyright Redis Ltd. 2024 - present + * + * Licensed under your choice of the Redis Source Available License 2.0 (RSALv2) + * or the Server Side Public License v1 (SSPLv1). + */ + +#include +#include +#include "sdsalloc.h" +#include "mstr.h" +#include "stdio.h" + +#define NULL_SIZE 1 + +static inline char mstrReqType(size_t string_size); +static inline int mstrHdrSize(char type); +static inline int mstrSumMetaLen(mstrKind *k, mstrFlags flags); +static inline size_t mstrAllocLen(const mstr s, struct mstrKind *kind); + +/*** mstr API ***/ + +/* Create mstr without any metadata attached, based on string 'initStr'. + * - If initStr equals NULL, then only allocation will be made. + * - string of mstr is always null-terminated. + */ +mstr mstrNew(const char *initStr, size_t lenStr, int trymalloc) { + unsigned char *pInfo; /* pointer to mstr info field */ + void *sh; + mstr s; + char type = mstrReqType(lenStr); + int mstrHdr = mstrHdrSize(type); + + assert(lenStr + mstrHdr + 1 > lenStr); /* Catch size_t overflow */ + + size_t len = mstrHdr + lenStr + NULL_SIZE; + sh = trymalloc? s_trymalloc(len) : s_malloc(len); + + if (sh == NULL) return NULL; + + s = (char*)sh + mstrHdr; + pInfo = ((unsigned char*)s) - 1; + + switch(type) { + case MSTR_TYPE_5: { + *pInfo = CREATE_MSTR_INFO(lenStr, 0 /*ismeta*/, type); + break; + } + case MSTR_TYPE_8: { + MSTR_HDR_VAR(8,s); + *pInfo = CREATE_MSTR_INFO(0 /*unused*/, 0 /*ismeta*/, type); + sh->len = lenStr; + break; + } + case MSTR_TYPE_16: { + MSTR_HDR_VAR(16,s); + *pInfo = CREATE_MSTR_INFO(0 /*unused*/, 0 /*ismeta*/, type); + sh->len = lenStr; + break; + } + case MSTR_TYPE_64: { + MSTR_HDR_VAR(64,s); + *pInfo = CREATE_MSTR_INFO(0 /*unused*/, 0 /*ismeta*/, type); + sh->len = lenStr; + break; + } + } + + if (initStr && lenStr) + memcpy(s, initStr, lenStr); + + s[lenStr] = '\0'; + return s; +} + +/* Creates mstr with given string. Reserve space for metadata. + * + * Note: mstrNew(s,l) and mstrNewWithMeta(s,l,0) are not the same. The first allocates + * just string. The second allocates a string with flags (yet without any metadata + * structures allocated). + */ +mstr mstrNewWithMeta(struct mstrKind *kind, const char *initStr, size_t lenStr, mstrFlags metaFlags, int trymalloc) { + unsigned char *pInfo; /* pointer to mstr info field */ + char *allocMstr; + mstr mstrPtr; + char type = mstrReqType(lenStr); + int mstrHdr = mstrHdrSize(type); + int sumMetaLen = mstrSumMetaLen(kind, metaFlags); + + + /* mstrSumMetaLen() + sizeof(mstrFlags) + sizeof(mstrhdrX) + lenStr */ + + size_t allocLen = sumMetaLen + sizeof(mstrFlags) + mstrHdr + lenStr + NULL_SIZE; + allocMstr = trymalloc? s_trymalloc(allocLen) : s_malloc(allocLen); + + if (allocMstr == NULL) return NULL; + + /* metadata is located at the beginning of the allocation, then meta-flags and lastly the string */ + mstrFlags *pMetaFlags = (mstrFlags *) (allocMstr + sumMetaLen) ; + mstrPtr = ((char*) pMetaFlags) + sizeof(mstrFlags) + mstrHdr; + pInfo = ((unsigned char*)mstrPtr) - 1; + + switch(type) { + case MSTR_TYPE_5: { + *pInfo = CREATE_MSTR_INFO(lenStr, 1 /*ismeta*/, type); + break; + } + case MSTR_TYPE_8: { + MSTR_HDR_VAR(8, mstrPtr); + sh->len = lenStr; + *pInfo = CREATE_MSTR_INFO(0 /*unused*/, 1 /*ismeta*/, type); + break; + } + case MSTR_TYPE_16: { + MSTR_HDR_VAR(16, mstrPtr); + sh->len = lenStr; + *pInfo = CREATE_MSTR_INFO(0 /*unused*/, 1 /*ismeta*/, type); + break; + } + case MSTR_TYPE_64: { + MSTR_HDR_VAR(64, mstrPtr); + sh->len = lenStr; + *pInfo = CREATE_MSTR_INFO(0 /*unused*/, 1 /*ismeta*/, type); + break; + } + } + *pMetaFlags = metaFlags; + if (initStr != NULL) memcpy(mstrPtr, initStr, lenStr); + mstrPtr[lenStr] = '\0'; + + return mstrPtr; +} + +/* Create copy of mstr. Flags can be modified. For each metadata flag, if + * same flag is set on both, then copy its metadata. */ +mstr mstrNewCopy(struct mstrKind *kind, mstr src, mstrFlags newFlags) { + mstr dst; + + /* if no flags are set, then just copy the string */ + if (newFlags == 0) return mstrNew(src, mstrlen(src), 0); + + dst = mstrNewWithMeta(kind, src, mstrlen(src), newFlags, 0); + memcpy(dst, src, mstrlen(src) + 1); + + /* if metadata is attached to src, then selectively copy metadata */ + if (mstrIsMetaAttached(src)) { + mstrFlags *pFlags1 = mstrFlagsRef(src), + *pFlags2 = mstrFlagsRef(dst); + + mstrFlags flags1Shift = *pFlags1, + flags2Shift = *pFlags2; + + unsigned char *at1 = ((unsigned char *) pFlags1), + *at2 = ((unsigned char *) pFlags2); + + /* if the flag is set on both, then copy the metadata */ + for (int i = 0; flags1Shift != 0; ++i) { + int isFlag1Set = flags1Shift & 0x1; + int isFlag2Set = flags2Shift & 0x1; + + if (isFlag1Set) at1 -= kind->metaSize[i]; + if (isFlag2Set) at2 -= kind->metaSize[i]; + + if (isFlag1Set && isFlag2Set) + memcpy(at2, at1, kind->metaSize[i]); + flags1Shift >>= 1; + flags2Shift >>= 1; + } + } + return dst; +} + +/* Free mstring. Note, mstrKind is required to eval sizeof metadata and find start + * of allocation but if mstrIsMetaAttached(s) is false, you can pass NULL as well. + */ +void mstrFree(struct mstrKind *kind, mstr s) { + if (s != NULL) + s_free(mstrGetAllocPtr(kind, s)); +} + +/* return ref to metadata flags. Useful to modify directly flags which doesn't + * include metadata payload */ +mstrFlags *mstrFlagsRef(mstr s) { + switch(s[-1]&MSTR_TYPE_MASK) { + case MSTR_TYPE_5: + return ((mstrFlags *) (s - sizeof(struct mstrhdr5))) - 1; + case MSTR_TYPE_8: + return ((mstrFlags *) (s - sizeof(struct mstrhdr8))) - 1; + case MSTR_TYPE_16: + return ((mstrFlags *) (s - sizeof(struct mstrhdr16))) - 1; + default: /* MSTR_TYPE_64: */ + return ((mstrFlags *) (s - sizeof(struct mstrhdr64))) - 1; + } +} + +/* Return a reference to corresponding metadata of the specified metadata flag + * index (flagIdx). If the metadata doesn't exist, it still returns a reference + * to the starting location where it would have been written among other metadatas. + * To verify if `flagIdx` of some metadata is attached, use `mstrGetFlag(s, flagIdx)`. + */ +void *mstrMetaRef(mstr s, struct mstrKind *kind, int flagIdx) { + int metaOffset = 0; + /* start iterating from flags backward */ + mstrFlags *pFlags = mstrFlagsRef(s); + mstrFlags tmp = *pFlags; + + for (int i = 0 ; i <= flagIdx ; ++i) { + if (tmp & 0x1) metaOffset += kind->metaSize[i]; + tmp >>= 1; + } + return ((char *)pFlags) - metaOffset; +} + +/* mstr layout: [meta-data#N]...[meta-data#0][mstrFlags][mstrhdr][string][null] */ +void *mstrGetAllocPtr(struct mstrKind *kind, mstr str) { + if (!mstrIsMetaAttached(str)) + return (char*)str - mstrHdrSize(str[-1]); + + int totalMetaLen = mstrSumMetaLen(kind, *mstrFlagsRef(str)); + return (char*)str - mstrHdrSize(str[-1]) - sizeof(mstrFlags) - totalMetaLen; +} + +/* Prints in the following fashion: + * [0x7f8bd8816017] my_mstr: foo (strLen=3, mstrLen=11, isMeta=1, metaFlags=0x1) + * [0x7f8bd8816010] >> meta[0]: 0x78 0x56 0x34 0x12 (metaLen=4) + */ +void mstrPrint(mstr s, struct mstrKind *kind, int verbose) { + mstrFlags mflags, tmp; + int isMeta = mstrIsMetaAttached(s); + + tmp = mflags = (isMeta) ? *mstrFlagsRef(s) : 0; + + if (!isMeta) { + printf("[%p] %s: %s (strLen=%zu, mstrLen=%zu, isMeta=0)\n", + (void *)s, kind->name, s, mstrlen(s), mstrAllocLen(s, kind)); + return; + } + + printf("[%p] %s: %s (strLen=%zu, mstrLen=%zu, isMeta=1, metaFlags=0x%x)\n", + (void *)s, kind->name, s, mstrlen(s), mstrAllocLen(s, kind), mflags); + + if (verbose) { + for (unsigned int i = 0 ; i < NUM_MSTR_FLAGS ; ++i) { + if (tmp & 0x1) { + int mSize = kind->metaSize[i]; + void *mRef = mstrMetaRef(s, kind, i); + printf("[%p] >> meta[%d]:", mRef, i); + for (int j = 0 ; j < mSize ; ++j) { + printf(" 0x%02x", ((unsigned char *) mRef)[j]); + } + printf(" (metaLen=%d)\n", mSize); + } + tmp >>= 1; + } + } +} + +/* return length of the string (ignoring metadata attached) */ +size_t mstrlen(const mstr s) { + unsigned char info = s[-1]; + switch(info & MSTR_TYPE_MASK) { + case MSTR_TYPE_5: + return MSTR_TYPE_5_LEN(info); + case MSTR_TYPE_8: + return MSTR_HDR(8,s)->len; + case MSTR_TYPE_16: + return MSTR_HDR(16,s)->len; + default: /* MSTR_TYPE_64: */ + return MSTR_HDR(64,s)->len; + } +} + +/*** mstr internals ***/ + +static inline int mstrSumMetaLen(mstrKind *k, mstrFlags flags) { + int total = 0; + int i = 0 ; + while (flags) { + total += (flags & 0x1) ? k->metaSize[i] : 0; + flags >>= 1; + ++i; + } + return total; +} + +/* mstrSumMetaLen() + sizeof(mstrFlags) + sizeof(mstrhdrX) + strlen + '\0' */ +static inline size_t mstrAllocLen(const mstr s, struct mstrKind *kind) { + int hdrlen; + mstrFlags *pMetaFlags; + size_t strlen = 0; + + int isMeta = mstrIsMetaAttached(s); + unsigned char info = s[-1]; + + switch(info & MSTR_TYPE_MASK) { + case MSTR_TYPE_5: + strlen = MSTR_TYPE_5_LEN(info); + hdrlen = sizeof(struct mstrhdr5); + pMetaFlags = ((mstrFlags *) MSTR_HDR(5, s)) - 1; + break; + case MSTR_TYPE_8: + strlen = MSTR_HDR(8,s)->len; + hdrlen = sizeof(struct mstrhdr8); + pMetaFlags = ((mstrFlags *) MSTR_HDR(8, s)) - 1; + break; + case MSTR_TYPE_16: + strlen = MSTR_HDR(16,s)->len; + hdrlen = sizeof(struct mstrhdr16); + pMetaFlags = ((mstrFlags *) MSTR_HDR(16, s)) - 1; + break; + default: /* MSTR_TYPE_64: */ + strlen = MSTR_HDR(64,s)->len; + hdrlen = sizeof(struct mstrhdr64); + pMetaFlags = ((mstrFlags *) MSTR_HDR(64, s)) - 1; + break; + } + return hdrlen + strlen + NULL_SIZE + ((isMeta) ? (mstrSumMetaLen(kind, *pMetaFlags) + sizeof(mstrFlags)) : 0); +} + +/* returns pointer to the beginning of malloc() of mstr */ +void *mstrGetStartAlloc(mstr s, struct mstrKind *kind) { + int hdrlen; + mstrFlags *pMetaFlags; + + int isMeta = mstrIsMetaAttached(s); + + switch(s[-1]&MSTR_TYPE_MASK) { + case MSTR_TYPE_5: + hdrlen = sizeof(struct mstrhdr5); + pMetaFlags = ((mstrFlags *) MSTR_HDR(5, s)) - 1; + break; + case MSTR_TYPE_8: + hdrlen = sizeof(struct mstrhdr8); + pMetaFlags = ((mstrFlags *) MSTR_HDR(8, s)) - 1; + break; + case MSTR_TYPE_16: + hdrlen = sizeof(struct mstrhdr16); + pMetaFlags = ((mstrFlags *) MSTR_HDR(16, s)) - 1; + break; + default: /* MSTR_TYPE_64: */ + hdrlen = sizeof(struct mstrhdr64); + pMetaFlags = ((mstrFlags *) MSTR_HDR(64, s)) - 1; + break; + } + return (char *) s - hdrlen - ((isMeta) ? (mstrSumMetaLen(kind, *pMetaFlags) + sizeof(mstrFlags)) : 0); +} + +static inline int mstrHdrSize(char type) { + switch(type&MSTR_TYPE_MASK) { + case MSTR_TYPE_5: + return sizeof(struct mstrhdr5); + case MSTR_TYPE_8: + return sizeof(struct mstrhdr8); + case MSTR_TYPE_16: + return sizeof(struct mstrhdr16); + case MSTR_TYPE_64: + return sizeof(struct mstrhdr64); + } + return 0; +} + +static inline char mstrReqType(size_t string_size) { + if (string_size < 1<<5) + return MSTR_TYPE_5; + if (string_size < 1<<8) + return MSTR_TYPE_8; + if (string_size < 1<<16) + return MSTR_TYPE_16; + return MSTR_TYPE_64; +} + +#ifdef REDIS_TEST +#include +#include +#include "testhelp.h" +#include "limits.h" + +#ifndef UNUSED +#define UNUSED(x) (void)(x) +#endif + +/* Challenge mstr with metadata interesting enough that can include the case of hfield and hkey and more */ +#define B(idx) (1<<(idx)) + +#define META_IDX_MYMSTR_TTL4 0 +#define META_IDX_MYMSTR_TTL8 1 +#define META_IDX_MYMSTR_TYPE_ENC_LRU 2 // 4Bbit type, 4bit encoding, 24bits lru +#define META_IDX_MYMSTR_VALUE_PTR 3 +#define META_IDX_MYMSTR_FLAG_NO_META 4 + +#define TEST_CONTEXT(context) printf("\nContext: %s \n", context); + +int mstrTest(int argc, char **argv, int flags) { + UNUSED(argc); + UNUSED(argv); + UNUSED(flags); + + struct mstrKind kind_mymstr = { + .name = "my_mstr", + .metaSize[META_IDX_MYMSTR_TTL4] = 4, + .metaSize[META_IDX_MYMSTR_TTL8] = 8, + .metaSize[META_IDX_MYMSTR_TYPE_ENC_LRU] = 4, + .metaSize[META_IDX_MYMSTR_VALUE_PTR] = 8, + .metaSize[META_IDX_MYMSTR_FLAG_NO_META] = 0, + }; + + TEST_CONTEXT("Create simple short mstr") + { + char *str = "foo"; + mstr s = mstrNew(str, strlen(str), 0); + size_t expStrLen = strlen(str); + + test_cond("Verify str length and alloc length", + mstrAllocLen(s, NULL) == (1 + expStrLen + 1) && /* mstrhdr5 + str + null */ + mstrlen(s) == expStrLen && /* expected strlen(str) */ + memcmp(s, str, expStrLen + 1) == 0); + mstrFree(&kind_mymstr, s); + } + + TEST_CONTEXT("Create simple 40 bytes mstr") + { + char *str = "0123456789012345678901234567890123456789"; // 40 bytes + mstr s = mstrNew(str, strlen(str), 0); + + test_cond("Verify str length and alloc length", + mstrAllocLen(s, NULL) == (3 + 40 + 1) && /* mstrhdr8 + str + null */ + mstrlen(s) == 40 && + memcmp(s,str,40) == 0); + mstrFree(&kind_mymstr, s); + } + + TEST_CONTEXT("Create mstr with random characters") + { + long unsigned int i; + char str[66000]; + for (i = 0 ; i < sizeof(str) ; ++i) str[i] = rand() % 256; + + size_t len[] = { 31, 32, 33, 255, 256, 257, 65535, 65536, 65537, 66000}; + for (i = 0 ; i < sizeof(len) / sizeof(len[0]) ; ++i) { + char title[100]; + mstr s = mstrNew(str, len[i], 0); + size_t mstrhdrSize = (len[i] < 1<<5) ? sizeof(struct mstrhdr5) : + (len[i] < 1<<8) ? sizeof(struct mstrhdr8) : + (len[i] < 1<<16) ? sizeof(struct mstrhdr16) : + sizeof(struct mstrhdr64); + + snprintf(title, sizeof(title), "Verify string of length %zu", len[i]); + test_cond(title, + mstrAllocLen(s, NULL) == (mstrhdrSize + len[i] + 1) && /* mstrhdrX + str + null */ + mstrlen(s) == len[i] && + memcmp(s,str,len[i]) == 0); + mstrFree(&kind_mymstr, s); + } + } + + TEST_CONTEXT("Create short mstr with TTL4") + { + uint32_t *ttl; + mstr s = mstrNewWithMeta(&kind_mymstr, + "foo", + strlen("foo"), + B(META_IDX_MYMSTR_TTL4), /* allocate with TTL4 metadata */ + 0); + + ttl = mstrMetaRef(s, &kind_mymstr, META_IDX_MYMSTR_TTL4); + *ttl = 0x12345678; + + test_cond("Verify memory-allocation and string lengths", + mstrAllocLen(s, &kind_mymstr) == (1 + 3 + 2 + 1 + 4) && /* mstrhdr5 + str + null + mstrFlags + TLL */ + mstrlen(s) == 3); + + unsigned char expMem[] = {0xFF, 0xFF, 0xFF, 0xFF, 0x01, 0x00, 0x1c, 'f', 'o', 'o', '\0' }; + uint32_t value = 0x12345678; + memcpy(expMem, &value, sizeof(uint32_t)); + test_cond("Verify string and TTL4 payload", memcmp( + mstrMetaRef(s, &kind_mymstr, 0) , expMem, sizeof(expMem)) == 0); + + test_cond("Verify mstrIsMetaAttached() function works", mstrIsMetaAttached(s) != 0); + + mstrFree(&kind_mymstr, s); + } + + TEST_CONTEXT("Create short mstr with TTL4 and value ptr ") + { + mstr s = mstrNewWithMeta(&kind_mymstr, "foo", strlen("foo"), + B(META_IDX_MYMSTR_TTL4) | B(META_IDX_MYMSTR_VALUE_PTR), 0); + *((uint32_t *) (mstrMetaRef(s, &kind_mymstr, + META_IDX_MYMSTR_TTL4))) = 0x12345678; + + test_cond("Verify length and alloc length", + mstrAllocLen(s, &kind_mymstr) == (1 + 3 + 1 + 2 + 4 + 8) && /* mstrhdr5 + str + null + mstrFlags + TLL + PTR */ + mstrlen(s) == 3); + mstrFree(&kind_mymstr, s); + } + + TEST_CONTEXT("Copy mstr and add it TTL4") + { + mstr s1 = mstrNew("foo", strlen("foo"), 0); + mstr s2 = mstrNewCopy(&kind_mymstr, s1, B(META_IDX_MYMSTR_TTL4)); + *((uint32_t *) (mstrMetaRef(s2, &kind_mymstr, META_IDX_MYMSTR_TTL4))) = 0x12345678; + + test_cond("Verify new mstr includes TTL4", + mstrAllocLen(s2, &kind_mymstr) == (1 + 3 + 1 + 2 + 4) && /* mstrhdr5 + str + null + mstrFlags + TTL4 */ + mstrlen(s2) == 3 && /* 'foo' = 3bytes */ + memcmp(s2, "foo\0", 4) == 0); + + mstr s3 = mstrNewCopy(&kind_mymstr, s2, B(META_IDX_MYMSTR_TTL4)); + unsigned char expMem[] = { 0xFF, 0xFF, 0xFF, 0xFF, 0x1, 0x0, 0x1c, 'f', 'o', 'o', '\0' }; + uint32_t value = 0x12345678; + memcpy(expMem, &value, sizeof(uint32_t)); + + char *ppp = mstrGetStartAlloc(s3, &kind_mymstr); + test_cond("Verify string and TTL4 payload", + memcmp(ppp, expMem, sizeof(expMem)) == 0); + + mstrPrint(s3, &kind_mymstr, 1); + mstrFree(&kind_mymstr, s1); + mstrFree(&kind_mymstr, s2); + mstrFree(&kind_mymstr, s3); + } + + return 0; +} +#endif diff --git a/src/mstr.h b/src/mstr.h new file mode 100644 index 000000000..1613a637e --- /dev/null +++ b/src/mstr.h @@ -0,0 +1,226 @@ +/* + * Copyright Redis Ltd. 2024 - present + * + * Licensed under your choice of the Redis Source Available License 2.0 (RSALv2) + * or the Server Side Public License v1 (SSPLv1). + * + * + * WHAT IS MSTR (M-STRING)? + * ------------------------ + * mstr stands for immutable string with optional metadata attached. + * + * sds string is widely used across the system and serves as a general purpose + * container to hold data. The need to optimize memory and aggregate strings + * along with metadata and store it into Redis data-structures as single bulk keep + * reoccur. One thought might be, why not to extend sds to support metadata. The + * answer is that sds is mutable string in its nature, with wide API (split, join, + * etc.). Pushing metadata logic into sds will make it very fragile, and complex + * to maintain. + * + * Another idea involved using a simple struct with flags and a dynamic buf[] at the + * end. While this could be viable, it introduces considerable complexity and would + * need maintenance across different contexts. + * + * As an alternative, we introduce a new implementation of immutable strings, + * with limited API, and with the option to attach metadata. The representation + * of the string, without any metadata, in its basic form, resembles SDS but + * without the API to manipulate the string. Only to attach metadata to it. The + * following diagram shows the memory layout of mstring (mstrhdr8) when no + * metadata is attached: + * + * +----------------------------------------------+ + * | mstrhdr8 | c-string | | + * +--------------------------------+-------------+ + * |8b |2b |1b |5b |?bytes |8b| + * | Len | Type |m-bit=0 | Unused | String |\0| + * +----------------------------------------------+ + * ^ + * | + * mstrNew() returns pointer to here --+ + * + * If metadata-flag is set, depicted in diagram above as m-bit in the diagram, + * then the header will be preceded with additional 16 bits of metadata flags such + * that if i'th bit is set, then the i'th metadata structure is attached to the + * mstring. The metadata layout and their sizes are defined by mstrKind structure + * (More below). + * + * The following diagram shows the memory layout of mstr (mstrhdr8) when 3 bits in mFlags + * are set to indicate that 3 fields of metadata are attached to the mstring at the + * beginning. + * + * +-------------------------------------------------------------------------------+ + * | METADATA FIELDS | mflags | mstrhdr8 | c-string | | + * +-----------------------+--------+--------------------------------+-------------+ + * |?bytes |?bytes |?bytes |16b |8b |2b |1b |5b |?bytes |8b| + * | Meta3 | Meta2 | Meta0 | 0x1101 | Len | Type |m-bit=1 | Unused | String |\0| + * +-------------------------------------------------------------------------------+ + * ^ + * | + * mstrNewWithMeta() returns pointer to here --+ + * + * mstr allows to define different kinds (groups) of mstrings, each with its + * own unique metadata layout. For example, in case of hash-fields, all instances of + * it can optionally have TTL metadata attached to it. This is achieved by first + * prototyping a single mstrKind structure that defines the metadata layout and sizes + * of this specific kind. Now each hash-field instance has still the freedom to + * attach or not attach the metadata to it, and metadata flags (mFlags) of the + * instance will reflect this decision. + * + * In the future, the keys of Redis keyspace can be another kind of mstring that + * has TTL, LRU or even dictEntry metadata embedded into. Unlike vptr in c++, this + * struct won't be attached to mstring but will be passed as yet another argument + * to API, to save memory. In addition, each instance of a given mstrkind can hold + * any subset of metadata and the 8 bits of metadata-flags will reflect it. + * + * The following example shows how to define mstrKind for possible future keyspace + * that aggregates several keyspace related metadata into one compact, singly + * allocated, mstring. + * + * typedef enum HkeyMetaFlags { + * HKEY_META_VAL_REF_COUNT = 0, // refcount + * HKEY_META_VAL_REF = 1, // Val referenced + * HKEY_META_EXPIRE = 2, // TTL and more + * HKEY_META_TYPE_ENC_LRU = 3, // TYPE + LRU + ENC + * HKEY_META_DICT_ENT_NEXT = 4, // Next dict entry + * // Following two must be together and in this order + * HKEY_META_VAL_EMBED8 = 5, // Val embedded, max 7 bytes + * HKEY_META_VAL_EMBED16 = 6, // Val embedded, max 15 bytes (23 with EMBED8) + * } HkeyMetaFlags; + * + * mstrKind hkeyKind = { + * .name = "hkey", + * .metaSize[HKEY_META_VAL_REF_COUNT] = 4, + * .metaSize[HKEY_META_VAL_REF] = 8, + * .metaSize[HKEY_META_EXPIRE] = sizeof(ExpireMeta), + * .metaSize[HKEY_META_TYPE_ENC_LRU] = 8, + * .metaSize[HKEY_META_DICT_ENT_NEXT] = 8, + * .metaSize[HKEY_META_VAL_EMBED8] = 8, + * .metaSize[HKEY_META_VAL_EMBED16] = 16, + * }; + * + * MSTR-ALIGNMENT + * -------------- + * There are two types of alignments to take into consideration: + * 1. Alignment of the metadata. + * 2. Alignment of returned mstr pointer + * + * 1) As the metadatas layout are reversed to their enumeration, it is recommended + * to put metadata with "better" alignment first in memory layout (enumerated + * last) and the worst, or those that simply don't require any alignment will be + * last in memory layout (enumerated first). This is similar the to the applied + * consideration when defining new struct in C. Note also that each metadata + * might either be attached to mstr or not which complicates the design phase + * of a new mstrKind a little. + * + * In the example above, HKEY_META_VAL_REF_COUNT, with worst alignment of 4 + * bytes, is enumerated first, and therefore, will be last in memory layout. + * + * 2) Few optimizations in Redis rely on the fact that sds address is always an odd + * pointer. We can achieve the same with a little effort. It was already taken + * care that all headers of type mstrhdrX has odd size. With that in mind, if + * a new kind of mstr is required to be limited to odd addresses, then we must + * make sure that sizes of all related metadatas that are defined in mstrKind + * are even in size. + */ + +#ifndef __MSTR_H +#define __MSTR_H + +#include +#include +#include + +/* Selective copy of ifndef from server.h instead of including it */ +#ifndef static_assert +#define static_assert(expr, lit) extern char __static_assert_failure[(expr) ? 1:-1] +#endif + +#define MSTR_TYPE_5 0 +#define MSTR_TYPE_8 1 +#define MSTR_TYPE_16 2 +#define MSTR_TYPE_64 3 +#define MSTR_TYPE_MASK 3 +#define MSTR_TYPE_BITS 2 + +#define MSTR_META_MASK 4 + +#define MSTR_HDR(T,s) ((struct mstrhdr##T *)((s)-(sizeof(struct mstrhdr##T)))) +#define MSTR_HDR_VAR(T,s) struct mstrhdr##T *sh = (void*)((s)-(sizeof(struct mstrhdr##T))); + +#define MSTR_META_BITS 1 /* is metadata attached? */ +#define MSTR_TYPE_5_LEN(f) ((f) >> (MSTR_TYPE_BITS + MSTR_META_BITS)) +#define CREATE_MSTR_INFO(len, ismeta, type) ( (((len<ptr * for a string object. This includes internal fragmentation. */ size_t getStringObjectSdsUsedMemory(robj *o) { @@ -3749,7 +3757,9 @@ void replaceClientCommandVector(client *c, int argc, robj **argv) { * 1. Make sure there are no "holes" and all the arguments are set. * 2. If the original argument vector was longer than the one we * want to end with, it's up to the caller to set c->argc and - * free the no longer used objects on c->argv. */ + * free the no longer used objects on c->argv. + * 3. To remove argument at i'th index, pass NULL as new value + */ void rewriteClientCommandArgument(client *c, int i, robj *newval) { robj *oldval; retainOriginalCommandVector(c); @@ -3767,9 +3777,18 @@ void rewriteClientCommandArgument(client *c, int i, robj *newval) { } oldval = c->argv[i]; if (oldval) c->argv_len_sum -= getStringObjectLen(oldval); - if (newval) c->argv_len_sum += getStringObjectLen(newval); - c->argv[i] = newval; - incrRefCount(newval); + + if (newval) { + c->argv[i] = newval; + incrRefCount(newval); + c->argv_len_sum += getStringObjectLen(newval); + } else { + /* move the remaining arguments one step left */ + for (int j = i+1; j < c->argc; j++) { + c->argv[j-1] = c->argv[j]; + } + c->argv[--c->argc] = NULL; + } if (oldval) decrRefCount(oldval); /* If this is the command name make sure to fix c->cmd. */ diff --git a/src/notify.c b/src/notify.c index 16b2b04ef..237716699 100644 --- a/src/notify.c +++ b/src/notify.c @@ -80,7 +80,7 @@ sds keyspaceEventsFlagsToString(int flags) { * 'event' is a C string representing the event name. * 'key' is a Redis object representing the key name. * 'dbid' is the database ID where the key lives. */ -void notifyKeyspaceEvent(int type, char *event, robj *key, int dbid) { +void notifyKeyspaceEvent(int type, const char *event, robj *key, int dbid) { sds chan; robj *chanobj, *eventobj; int len = -1; diff --git a/src/object.c b/src/object.c index e76e7033c..2b42e7b3e 100644 --- a/src/object.c +++ b/src/object.c @@ -333,17 +333,7 @@ void freeZsetObject(robj *o) { } void freeHashObject(robj *o) { - switch (o->encoding) { - case OBJ_ENCODING_HT: - dictRelease((dict*) o->ptr); - break; - case OBJ_ENCODING_LISTPACK: - lpFree(o->ptr); - break; - default: - serverPanic("Unknown hash encoding type"); - break; - } + hashTypeFree(o); } void freeModuleObject(robj *o) { @@ -502,6 +492,9 @@ void dismissHashObject(robj *o, size_t size_hint) { dismissMemory(d->ht_table[1], DICTHT_SIZE(d->ht_size_exp[1])*sizeof(dictEntry*)); } else if (o->encoding == OBJ_ENCODING_LISTPACK) { dismissMemory(o->ptr, lpBytes((unsigned char*)o->ptr)); + } else if (o->encoding == OBJ_ENCODING_LISTPACK_EX) { + listpackEx *lpt = o->ptr; + dismissMemory(lpt->lp, lpBytes((unsigned char*)lpt->lp)); } else { serverPanic("Unknown hash encoding type"); } @@ -939,6 +932,7 @@ char *strEncoding(int encoding) { case OBJ_ENCODING_HT: return "hashtable"; case OBJ_ENCODING_QUICKLIST: return "quicklist"; case OBJ_ENCODING_LISTPACK: return "listpack"; + case OBJ_ENCODING_LISTPACK_EX: return "listpackex"; case OBJ_ENCODING_INTSET: return "intset"; case OBJ_ENCODING_SKIPLIST: return "skiplist"; case OBJ_ENCODING_EMBSTR: return "embstr"; @@ -979,7 +973,6 @@ size_t streamRadixTreeMemoryUsage(rax *rax) { * are checked and averaged to estimate the total size. */ #define OBJ_COMPUTE_SIZE_DEF_SAMPLES 5 /* Default sample size. */ size_t objectComputeSize(robj *key, robj *o, size_t sample_size, int dbid) { - sds ele, ele2; dict *d; dictIterator *di; struct dictEntry *de; @@ -1016,7 +1009,7 @@ size_t objectComputeSize(robj *key, robj *o, size_t sample_size, int dbid) { di = dictGetIterator(d); asize = sizeof(*o)+sizeof(dict)+(sizeof(struct dictEntry*)*dictBuckets(d)); while((de = dictNext(di)) != NULL && samples < sample_size) { - ele = dictGetKey(de); + sds ele = dictGetKey(de); elesize += dictEntryMemUsage() + sdsZmallocSize(ele); samples++; } @@ -1052,14 +1045,17 @@ size_t objectComputeSize(robj *key, robj *o, size_t sample_size, int dbid) { } else if (o->type == OBJ_HASH) { if (o->encoding == OBJ_ENCODING_LISTPACK) { asize = sizeof(*o)+zmalloc_size(o->ptr); + } else if (o->encoding == OBJ_ENCODING_LISTPACK_EX) { + listpackEx *lpt = o->ptr; + asize = sizeof(*o) + zmalloc_size(lpt) + zmalloc_size(lpt->lp); } else if (o->encoding == OBJ_ENCODING_HT) { d = o->ptr; di = dictGetIterator(d); asize = sizeof(*o)+sizeof(dict)+(sizeof(struct dictEntry*)*dictBuckets(d)); while((de = dictNext(di)) != NULL && samples < sample_size) { - ele = dictGetKey(de); - ele2 = dictGetVal(de); - elesize += sdsZmallocSize(ele) + sdsZmallocSize(ele2); + hfield ele = dictGetKey(de); + sds ele2 = dictGetVal(de); + elesize += hfieldZmallocSize(ele) + sdsZmallocSize(ele2); elesize += dictEntryMemUsage(); samples++; } diff --git a/src/rax.c b/src/rax.c index afd4dfe58..491e50aa0 100644 --- a/src/rax.c +++ b/src/rax.c @@ -173,11 +173,16 @@ raxNode *raxNewNode(size_t children, int datafield) { /* Allocate a new rax and return its pointer. On out of memory the function * returns NULL. */ rax *raxNew(void) { - rax *rax = rax_malloc(sizeof(*rax)); + return raxNewWithMetadata(0); +} + +/* Allocate a new rax with metadata */ +rax *raxNewWithMetadata(int metaSize) { + rax *rax = rax_malloc(sizeof(*rax) + metaSize); if (rax == NULL) return NULL; rax->numele = 0; rax->numnodes = 1; - rax->head = raxNewNode(0,0); + rax->head = raxNewNode(0, 0); if (rax->head == NULL) { rax_free(rax); return NULL; @@ -1210,6 +1215,25 @@ void raxRecursiveFree(rax *rax, raxNode *n, void (*free_callback)(void*)) { rax->numnodes--; } +/* Same as raxRecursiveFree() with context argument */ +void raxRecursiveFreeWithCtx(rax *rax, raxNode *n, + void (*free_callback)(void *item, void *ctx), void *ctx) { + debugnode("free traversing",n); + int numchildren = n->iscompr ? 1 : n->size; + raxNode **cp = raxNodeLastChildPtr(n); + while(numchildren--) { + raxNode *child; + memcpy(&child,cp,sizeof(child)); + raxRecursiveFreeWithCtx(rax,child,free_callback, ctx); + cp--; + } + debugnode("free depth-first",n); + if (free_callback && n->iskey && !n->isnull) + free_callback(raxGetData(n), ctx); + rax_free(n); + rax->numnodes--; +} + /* Free a whole radix tree, calling the specified callback in order to * free the auxiliary data. */ void raxFreeWithCallback(rax *rax, void (*free_callback)(void*)) { @@ -1218,6 +1242,15 @@ void raxFreeWithCallback(rax *rax, void (*free_callback)(void*)) { rax_free(rax); } +/* Free a whole radix tree, calling the specified callback in order to + * free the auxiliary data. */ +void raxFreeWithCbAndContext(rax *rax, + void (*free_callback)(void *item, void *ctx), void *ctx) { + raxRecursiveFreeWithCtx(rax,rax->head,free_callback,ctx); + assert(rax->numnodes == 0); + rax_free(rax); +} + /* Free a whole radix tree. */ void raxFree(rax *rax) { raxFreeWithCallback(rax,NULL); diff --git a/src/rax.h b/src/rax.h index c3d182a2b..74963acad 100644 --- a/src/rax.h +++ b/src/rax.h @@ -113,6 +113,7 @@ typedef struct rax { raxNode *head; uint64_t numele; uint64_t numnodes; + void *metadata[]; } rax; /* Stack data structure used by raxLowWalk() in order to, optionally, return @@ -166,12 +167,16 @@ typedef struct raxIterator { /* Exported API. */ rax *raxNew(void); +rax *raxNewWithMetadata(int metaSize); int raxInsert(rax *rax, unsigned char *s, size_t len, void *data, void **old); int raxTryInsert(rax *rax, unsigned char *s, size_t len, void *data, void **old); int raxRemove(rax *rax, unsigned char *s, size_t len, void **old); int raxFind(rax *rax, unsigned char *s, size_t len, void **value); void raxFree(rax *rax); void raxFreeWithCallback(rax *rax, void (*free_callback)(void*)); +void raxFreeWithCbAndContext(rax *rax, + void (*free_callback)(void *item, void *ctx), + void *ctx); void raxStart(raxIterator *it, rax *rt); int raxSeek(raxIterator *it, const char *op, unsigned char *ele, size_t len); int raxNext(raxIterator *it); diff --git a/src/rdb.c b/src/rdb.c index 4ed3726b2..4330c5694 100644 --- a/src/rdb.c +++ b/src/rdb.c @@ -268,8 +268,9 @@ int rdbEncodeInteger(long long value, unsigned char *enc) { * The returned value changes according to the flags, see * rdbGenericLoadStringObject() for more info. */ void *rdbLoadIntegerObject(rio *rdb, int enctype, int flags, size_t *lenptr) { - int plain = flags & RDB_LOAD_PLAIN; - int sds = flags & RDB_LOAD_SDS; + int plainFlag = flags & RDB_LOAD_PLAIN; + int sdsFlag = flags & RDB_LOAD_SDS; + int hfldFlag = flags & (RDB_LOAD_HFLD|RDB_LOAD_HFLD_TTL); int encode = flags & RDB_LOAD_ENC; unsigned char enc[4]; long long val; @@ -295,11 +296,17 @@ void *rdbLoadIntegerObject(rio *rdb, int enctype, int flags, size_t *lenptr) { rdbReportCorruptRDB("Unknown RDB integer encoding type %d",enctype); return NULL; /* Never reached. */ } - if (plain || sds) { + if (plainFlag || sdsFlag || hfldFlag) { char buf[LONG_STR_SIZE], *p; int len = ll2string(buf,sizeof(buf),val); if (lenptr) *lenptr = len; - p = plain ? zmalloc(len) : sdsnewlen(SDS_NOINIT,len); + if (plainFlag) { + p = zmalloc(len); + } else if (sdsFlag) { + p = sdsnewlen(SDS_NOINIT,len); + } else { /* hfldFlag */ + p = hfieldNew(NULL, len, (flags&RDB_LOAD_HFLD) ? 0 : 1); + } memcpy(p,buf,len); return p; } else if (encode) { @@ -368,8 +375,11 @@ ssize_t rdbSaveLzfStringObject(rio *rdb, unsigned char *s, size_t len) { * changes according to 'flags'. For more info check the * rdbGenericLoadStringObject() function. */ void *rdbLoadLzfStringObject(rio *rdb, int flags, size_t *lenptr) { - int plain = flags & RDB_LOAD_PLAIN; - int sds = flags & RDB_LOAD_SDS; + int plainFlag = flags & RDB_LOAD_PLAIN; + int sdsFlag = flags & RDB_LOAD_SDS; + int hfldFlag = flags & (RDB_LOAD_HFLD | RDB_LOAD_HFLD_TTL); + int robjFlag = (!(plainFlag || sdsFlag || hfldFlag)); /* not plain/sds/hfld */ + uint64_t len, clen; unsigned char *c = NULL; char *val = NULL; @@ -382,11 +392,14 @@ void *rdbLoadLzfStringObject(rio *rdb, int flags, size_t *lenptr) { } /* Allocate our target according to the uncompressed size. */ - if (plain) { + if (plainFlag) { val = ztrymalloc(len); - } else { + } else if (sdsFlag || robjFlag) { val = sdstrynewlen(SDS_NOINIT,len); + } else { /* hfldFlag */ + val = hfieldTryNew(NULL, len, (flags&RDB_LOAD_HFLD) ? 0 : 1); } + if (!val) { serverLog(isRestoreContext()? LL_VERBOSE: LL_WARNING, "rdbLoadLzfStringObject failed allocating %llu bytes", (unsigned long long)len); goto err; @@ -402,17 +415,17 @@ void *rdbLoadLzfStringObject(rio *rdb, int flags, size_t *lenptr) { } zfree(c); - if (plain || sds) { - return val; - } else { - return createObject(OBJ_STRING,val); - } + return (robjFlag) ? createObject(OBJ_STRING,val) : (void *) val; + err: zfree(c); - if (plain) + if (plainFlag) { zfree(val); - else + } else if (sdsFlag || robjFlag) { sdsfree(val); + } else { /* hfldFlag*/ + hfieldFree(val); + } return NULL; } @@ -491,12 +504,18 @@ ssize_t rdbSaveStringObject(rio *rdb, robj *obj) { * RDB_LOAD_PLAIN: Return a plain string allocated with zmalloc() * instead of a Redis object with an sds in it. * RDB_LOAD_SDS: Return an SDS string instead of a Redis object. + * RDB_LOAD_HFLD: Return a hash field object (mstr) + * RDB_LOAD_HFLD_TTL: Return a hash field with TTL metadata reserved * * On I/O error NULL is returned. */ void *rdbGenericLoadStringObject(rio *rdb, int flags, size_t *lenptr) { - int plain = flags & RDB_LOAD_PLAIN; - int sds = flags & RDB_LOAD_SDS; + void *buf; + int plainFlag = flags & RDB_LOAD_PLAIN; + int sdsFlag = flags & RDB_LOAD_SDS; + int hfldFlag = flags & (RDB_LOAD_HFLD|RDB_LOAD_HFLD_TTL); + int robjFlag = (!(plainFlag || sdsFlag || hfldFlag)); /* not plain/sds/hfld */ + int isencoded; unsigned long long len; @@ -517,22 +536,8 @@ void *rdbGenericLoadStringObject(rio *rdb, int flags, size_t *lenptr) { } } - if (plain || sds) { - void *buf = plain ? ztrymalloc(len) : sdstrynewlen(SDS_NOINIT,len); - if (!buf) { - serverLog(isRestoreContext()? LL_VERBOSE: LL_WARNING, "rdbGenericLoadStringObject failed allocating %llu bytes", len); - return NULL; - } - if (lenptr) *lenptr = len; - if (len && rioRead(rdb,buf,len) == 0) { - if (plain) - zfree(buf); - else - sdsfree(buf); - return NULL; - } - return buf; - } else { + /* return robj */ + if (robjFlag) { robj *o = tryCreateStringObject(SDS_NOINIT,len); if (!o) { serverLog(isRestoreContext()? LL_VERBOSE: LL_WARNING, "rdbGenericLoadStringObject failed allocating %llu bytes", len); @@ -544,6 +549,32 @@ void *rdbGenericLoadStringObject(rio *rdb, int flags, size_t *lenptr) { } return o; } + + /* plain/sds/hfld */ + if (plainFlag) { + buf = ztrymalloc(len); + } else if (sdsFlag) { + buf = sdstrynewlen(SDS_NOINIT,len); + } else { /* hfldFlag */ + buf = hfieldTryNew(NULL, len, (flags&RDB_LOAD_HFLD) ? 0 : 1); + } + if (!buf) { + serverLog(isRestoreContext()? LL_VERBOSE: LL_WARNING, "rdbGenericLoadStringObject failed allocating %llu bytes", len); + return NULL; + } + + if (lenptr) *lenptr = len; + if (len && rioRead(rdb,buf,len) == 0) { + if (plainFlag) + zfree(buf); + else if (sdsFlag) { + sdsfree(buf); + } else { /* hfldFlag */ + hfieldFree(buf); + } + return NULL; + } + return buf; } robj *rdbLoadStringObject(rio *rdb) { @@ -665,9 +696,14 @@ int rdbSaveObjectType(rio *rdb, robj *o) { case OBJ_HASH: if (o->encoding == OBJ_ENCODING_LISTPACK) return rdbSaveType(rdb,RDB_TYPE_HASH_LISTPACK); - else if (o->encoding == OBJ_ENCODING_HT) - return rdbSaveType(rdb,RDB_TYPE_HASH); - else + else if (o->encoding == OBJ_ENCODING_LISTPACK_EX) + return rdbSaveType(rdb,RDB_TYPE_HASH_LISTPACK_EX); + else if (o->encoding == OBJ_ENCODING_HT) { + if (hashTypeGetMinExpire(o) == EB_EXPIRE_TIME_INVALID) + return rdbSaveType(rdb,RDB_TYPE_HASH); + else + return rdbSaveType(rdb,RDB_TYPE_HASH_METADATA); + } else serverPanic("Unknown hash encoding"); case OBJ_STREAM: return rdbSaveType(rdb,RDB_TYPE_STREAM_LISTPACKS_3); @@ -908,32 +944,58 @@ ssize_t rdbSaveObject(rio *rdb, robj *o, robj *key, int dbid) { } } else if (o->type == OBJ_HASH) { /* Save a hash value */ - if (o->encoding == OBJ_ENCODING_LISTPACK) { - size_t l = lpBytes((unsigned char*)o->ptr); + if ((o->encoding == OBJ_ENCODING_LISTPACK) || + (o->encoding == OBJ_ENCODING_LISTPACK_EX)) + { + unsigned char *lp_ptr = hashTypeListpackGetLp(o); + size_t l = lpBytes(lp_ptr); - if ((n = rdbSaveRawString(rdb,o->ptr,l)) == -1) return -1; + if ((n = rdbSaveRawString(rdb,lp_ptr,l)) == -1) return -1; nwritten += n; } else if (o->encoding == OBJ_ENCODING_HT) { dictIterator *di = dictGetIterator(o->ptr); dictEntry *de; + /* Determine the hash layout to use based on the presence of at least + * one field with a valid TTL. If such a field exists, employ the + * RDB_TYPE_HASH_METADATA layout, including tuples of [ttl][field][value]. + * Otherwise, use the standard RDB_TYPE_HASH layout containing only + * the tuples [field][value]. */ + int with_ttl = (hashTypeGetMinExpire(o) != EB_EXPIRE_TIME_INVALID); + /* save number of fields in hash */ if ((n = rdbSaveLen(rdb,dictSize((dict*)o->ptr))) == -1) { dictReleaseIterator(di); return -1; } nwritten += n; + /* save all hash fields */ while((de = dictNext(di)) != NULL) { - sds field = dictGetKey(de); + hfield field = dictGetKey(de); sds value = dictGetVal(de); + /* save the TTL */ + if (with_ttl) { + uint64_t ttl = hfieldGetExpireTime(field); + /* 0 is used to indicate no TTL is set for this field */ + if (ttl == EB_EXPIRE_TIME_INVALID) ttl = 0; + if ((n = rdbSaveLen(rdb, ttl)) == -1) { + dictReleaseIterator(di); + return -1; + } + nwritten += n; + } + + /* save the key */ if ((n = rdbSaveRawString(rdb,(unsigned char*)field, - sdslen(field))) == -1) + hfieldlen(field))) == -1) { dictReleaseIterator(di); return -1; } nwritten += n; + + /* save the value */ if ((n = rdbSaveRawString(rdb,(unsigned char*)value, sdslen(value))) == -1) { @@ -1403,7 +1465,7 @@ werr: return C_ERR; } -/* This helper function is only used for diskless replication. +/* This helper function is only used for diskless replication. * This is just a wrapper to rdbSaveRio() that additionally adds a prefix * and a suffix to the generated RDB dump. The prefix is: * @@ -1753,19 +1815,20 @@ static int _listZiplistEntryConvertAndValidate(unsigned char *p, unsigned int he /* callback for to check the listpack doesn't have duplicate records */ static int _lpEntryValidation(unsigned char *p, unsigned int head_count, void *userdata) { struct { - int pairs; + int tuple_len; long count; dict *fields; + long long last_expireat; } *data = userdata; if (data->fields == NULL) { data->fields = dictCreate(&hashDictType); - dictExpand(data->fields, data->pairs ? head_count/2 : head_count); + dictExpand(data->fields, head_count/data->tuple_len); } /* If we're checking pairs, then even records are field names. Otherwise * we're checking all elements. Add to dict and check that's not a dup */ - if (!data->pairs || ((data->count) & 1) == 0) { + if (data->count % data->tuple_len == 0) { unsigned char *str; int64_t slen; unsigned char buf[LP_INTBUF_SIZE]; @@ -1779,6 +1842,19 @@ static int _lpEntryValidation(unsigned char *p, unsigned int head_count, void *u } } + /* Validate TTL field, only for listpackex. */ + if (data->count % data->tuple_len == 2) { + long long expire_at; + /* Must be an integer. */ + if (!lpGetIntegerValue(p, &expire_at)) return 0; + /* Must be less than EB_EXPIRE_TIME_MAX. */ + if (expire_at < 0 || (unsigned long long)expire_at > EB_EXPIRE_TIME_MAX) return 0; + /* TTL fields are ordered. If the current field has TTL, the previous field must + * also have one, and the current TTL must be greater than the previous one. */ + if (expire_at != 0 && (data->last_expireat == 0 || expire_at < data->last_expireat)) return 0; + data->last_expireat = expire_at; + } + (data->count)++; return 1; } @@ -1786,23 +1862,25 @@ static int _lpEntryValidation(unsigned char *p, unsigned int head_count, void *u /* Validate the integrity of the listpack structure. * when `deep` is 0, only the integrity of the header is validated. * when `deep` is 1, we scan all the entries one by one. - * when `pairs` is 0, all elements need to be unique (it's a set) - * when `pairs` is 1, odd elements need to be unique (it's a key-value map) */ -int lpValidateIntegrityAndDups(unsigned char *lp, size_t size, int deep, int pairs) { + * tuple_len indicates what is a logical entry tuple size. + * Whether tuple is of size 1 (set), 2 (feild-value) or 3 (field-value[-ttl]), + * first element in the tuple must be unique */ +int lpValidateIntegrityAndDups(unsigned char *lp, size_t size, int deep, int tuple_len) { if (!deep) return lpValidateIntegrity(lp, size, 0, NULL, NULL); /* Keep track of the field names to locate duplicate ones */ struct { - int pairs; + int tuple_len; long count; dict *fields; /* Initialisation at the first callback. */ - } data = {pairs, 0, NULL}; + long long last_expireat; /* Last field's expiry time to ensure order in TTL fields. */ + } data = {tuple_len, 0, NULL, -1}; int ret = lpValidateIntegrity(lp, size, 1, _lpEntryValidation, &data); - /* make sure we have an even number of records. */ - if (pairs && data.count & 1) + /* the number of records should be a multiple of the tuple length */ + if (data.count % tuple_len != 0) ret = 0; if (data.fields) dictRelease(data.fields); @@ -1811,9 +1889,18 @@ int lpValidateIntegrityAndDups(unsigned char *lp, size_t size, int deep, int pai /* Load a Redis object of the specified type from the specified file. * On success a newly allocated object is returned, otherwise NULL. - * When the function returns NULL and if 'error' is not NULL, the - * integer pointed by 'error' is set to the type of error that occurred */ -robj *rdbLoadObject(int rdbtype, rio *rdb, sds key, int dbid, int *error) { + * + * error - When the function returns NULL and if 'error' is not NULL, the + * integer pointed by 'error' is set to the type of error that occurred + * minExpiredField - If loading a hash with expiration on fields, then this value + * will be set to the minimum expire time found in the hash fields. If there are + * no fields with expiration or it is not a hash, then it will set be to + * EB_EXPIRE_TIME_INVALID. + */ +robj *rdbLoadObject(int rdbtype, rio *rdb, sds key, redisDb* db, int *error, + uint64_t *minExpiredField) +{ + uint64_t minExpField = EB_EXPIRE_TIME_INVALID; robj *o = NULL, *ele, *dec; uint64_t len; unsigned int i; @@ -1856,7 +1943,7 @@ robj *rdbLoadObject(int rdbtype, rio *rdb, sds key, int dbid, int *error) { decrRefCount(ele); } - listTypeTryConversion(o,LIST_CONV_AUTO,NULL,NULL); + listTypeTryConversion(o, LIST_CONV_AUTO, NULL, NULL); } else if (rdbtype == RDB_TYPE_SET) { /* Read Set value */ if ((len = rdbLoadLen(rdb,NULL)) == RDB_LENERR) return NULL; @@ -1869,7 +1956,7 @@ robj *rdbLoadObject(int rdbtype, rio *rdb, sds key, int dbid, int *error) { o = createSetObject(); /* It's faster to expand the dict to the right size asap in order * to avoid rehashing */ - if (len > DICT_HT_INITIAL_SIZE && dictTryExpand(o->ptr,len) != DICT_OK) { + if (len > DICT_HT_INITIAL_SIZE && dictTryExpand(o->ptr, len) != DICT_OK) { rdbReportCorruptRDB("OOM in dictTryExpand %llu", (unsigned long long)len); decrRefCount(o); return NULL; @@ -1896,7 +1983,7 @@ robj *rdbLoadObject(int rdbtype, rio *rdb, sds key, int dbid, int *error) { /* Fetch integer value from element. */ if (isSdsRepresentableAsLongLong(sdsele,&llval) == C_OK) { uint8_t success; - o->ptr = intsetAdd(o->ptr,llval,&success); + o->ptr = intsetAdd(o->ptr, llval, &success); if (!success) { rdbReportCorruptRDB("Duplicate set members detected"); decrRefCount(o); @@ -1946,7 +2033,7 @@ robj *rdbLoadObject(int rdbtype, rio *rdb, sds key, int dbid, int *error) { /* This will also be called when the set was just converted * to a regular hash table encoded set. */ if (o->encoding == OBJ_ENCODING_HT) { - if (dictAdd((dict*)o->ptr,sdsele,NULL) != DICT_OK) { + if (dictAdd((dict*)o->ptr, sdsele, NULL) != DICT_OK) { rdbReportCorruptRDB("Duplicate set members detected"); decrRefCount(o); sdsfree(sdsele); @@ -2024,12 +2111,13 @@ robj *rdbLoadObject(int rdbtype, rio *rdb, sds key, int dbid, int *error) { maxelelen <= server.zset_max_listpack_value && lpSafeToAdd(NULL, totelelen)) { - zsetConvert(o,OBJ_ENCODING_LISTPACK); + zsetConvert(o, OBJ_ENCODING_LISTPACK); } } else if (rdbtype == RDB_TYPE_HASH) { uint64_t len; int ret; - sds field, value; + sds value; + hfield field; dict *dupSearchDict = NULL; len = rdbLoadLen(rdb, NULL); @@ -2040,7 +2128,7 @@ robj *rdbLoadObject(int rdbtype, rio *rdb, sds key, int dbid, int *error) { /* Too many entries? Use a hash table right from the start. */ if (len > server.hash_max_listpack_entries) - hashTypeConvert(o, OBJ_ENCODING_HT); + hashTypeConvert(o, OBJ_ENCODING_HT, NULL); else if (deep_integrity_validation) { /* In this mode, we need to guarantee that the server won't crash * later when the ziplist is converted to a dict. @@ -2049,48 +2137,50 @@ robj *rdbLoadObject(int rdbtype, rio *rdb, sds key, int dbid, int *error) { dupSearchDict = dictCreate(&hashDictType); } - - /* Load every field and value into the ziplist */ + /* Load every field and value into the listpack */ while (o->encoding == OBJ_ENCODING_LISTPACK && len > 0) { len--; /* Load raw strings */ - if ((field = rdbGenericLoadStringObject(rdb,RDB_LOAD_SDS,NULL)) == NULL) { + if ((field = rdbGenericLoadStringObject(rdb,RDB_LOAD_HFLD,NULL)) == NULL) { decrRefCount(o); if (dupSearchDict) dictRelease(dupSearchDict); return NULL; } if ((value = rdbGenericLoadStringObject(rdb,RDB_LOAD_SDS,NULL)) == NULL) { - sdsfree(field); + hfieldFree(field); decrRefCount(o); if (dupSearchDict) dictRelease(dupSearchDict); return NULL; } if (dupSearchDict) { - sds field_dup = sdsdup(field); + sds field_dup = sdsnewlen(field, hfieldlen(field)); + if (dictAdd(dupSearchDict, field_dup, NULL) != DICT_OK) { rdbReportCorruptRDB("Hash with dup elements"); dictRelease(dupSearchDict); decrRefCount(o); sdsfree(field_dup); - sdsfree(field); + hfieldFree(field); sdsfree(value); return NULL; } } /* Convert to hash table if size threshold is exceeded */ - if (sdslen(field) > server.hash_max_listpack_value || + if (hfieldlen(field) > server.hash_max_listpack_value || sdslen(value) > server.hash_max_listpack_value || - !lpSafeToAdd(o->ptr, sdslen(field)+sdslen(value))) + !lpSafeToAdd(o->ptr, hfieldlen(field) + sdslen(value))) { - hashTypeConvert(o, OBJ_ENCODING_HT); + hashTypeConvert(o, OBJ_ENCODING_HT, NULL); + dictUseStoredKeyApi((dict *)o->ptr, 1); ret = dictAdd((dict*)o->ptr, field, value); + dictUseStoredKeyApi((dict *)o->ptr, 0); if (ret == DICT_ERR) { rdbReportCorruptRDB("Duplicate hash fields detected"); if (dupSearchDict) dictRelease(dupSearchDict); sdsfree(value); - sdsfree(field); + hfieldFree(field); decrRefCount(o); return NULL; } @@ -2098,10 +2188,10 @@ robj *rdbLoadObject(int rdbtype, rio *rdb, sds key, int dbid, int *error) { } /* Add pair to listpack */ - o->ptr = lpAppend(o->ptr, (unsigned char*)field, sdslen(field)); + o->ptr = lpAppend(o->ptr, (unsigned char*)field, hfieldlen(field)); o->ptr = lpAppend(o->ptr, (unsigned char*)value, sdslen(value)); - sdsfree(field); + hfieldFree(field); sdsfree(value); } @@ -2113,7 +2203,7 @@ robj *rdbLoadObject(int rdbtype, rio *rdb, sds key, int dbid, int *error) { } if (o->encoding == OBJ_ENCODING_HT && len > DICT_HT_INITIAL_SIZE) { - if (dictTryExpand(o->ptr,len) != DICT_OK) { + if (dictTryExpand(o->ptr, len) != DICT_OK) { rdbReportCorruptRDB("OOM in dictTryExpand %llu", (unsigned long long)len); decrRefCount(o); return NULL; @@ -2124,22 +2214,25 @@ robj *rdbLoadObject(int rdbtype, rio *rdb, sds key, int dbid, int *error) { while (o->encoding == OBJ_ENCODING_HT && len > 0) { len--; /* Load encoded strings */ - if ((field = rdbGenericLoadStringObject(rdb,RDB_LOAD_SDS,NULL)) == NULL) { + if ((field = rdbGenericLoadStringObject(rdb,RDB_LOAD_HFLD,NULL)) == NULL) { decrRefCount(o); return NULL; } if ((value = rdbGenericLoadStringObject(rdb,RDB_LOAD_SDS,NULL)) == NULL) { - sdsfree(field); + hfieldFree(field); decrRefCount(o); return NULL; } /* Add pair to hash table */ - ret = dictAdd((dict*)o->ptr, field, value); + dict *d = o->ptr; + dictUseStoredKeyApi(d, 1); + ret = dictAdd(d, field, value); + dictUseStoredKeyApi(d, 0); if (ret == DICT_ERR) { rdbReportCorruptRDB("Duplicate hash fields detected"); sdsfree(value); - sdsfree(field); + hfieldFree(field); decrRefCount(o); return NULL; } @@ -2147,6 +2240,149 @@ robj *rdbLoadObject(int rdbtype, rio *rdb, sds key, int dbid, int *error) { /* All pairs should be read by now */ serverAssert(len == 0); + } else if (rdbtype == RDB_TYPE_HASH_METADATA) { + size_t fieldLen; + sds value, field; + uint64_t expireAt; + dict *dupSearchDict = NULL; + + len = rdbLoadLen(rdb, NULL); + if (len == RDB_LENERR) return NULL; + if (len == 0) goto emptykey; + /* TODO: create listpackEx or HT directly*/ + o = createHashObject(); + /* Too many entries? Use a hash table right from the start. */ + if (len > server.hash_max_listpack_entries) { + hashTypeConvert(o, OBJ_ENCODING_HT, NULL); + dictTypeAddMeta((dict**)&o->ptr, &mstrHashDictTypeWithHFE); + initDictExpireMetadata(key, o); + } else { + hashTypeConvert(o, OBJ_ENCODING_LISTPACK_EX, NULL); + if (deep_integrity_validation) { + /* In this mode, we need to guarantee that the server won't crash + * later when the listpack is converted to a dict. + * Create a set (dict with no values) for dup search. + * We can dismiss it as soon as we convert the listpack to a hash. */ + dupSearchDict = dictCreate(&hashDictType); + } + } + + while (len > 0) { + len--; + + /* read the TTL */ + if (rdbLoadLenByRef(rdb, NULL, &expireAt) == -1) { + serverLog(LL_WARNING, "failed reading hash TTL"); + decrRefCount(o); + if (dupSearchDict != NULL) dictRelease(dupSearchDict); + return NULL; + } + if (expireAt > EB_EXPIRE_TIME_MAX) { + rdbReportCorruptRDB("invalid expireAt time: %llu", (unsigned long long)expireAt); + decrRefCount(o); + return NULL; + } + + /* if needed create field with TTL metadata */ + if (expireAt !=0) + field = rdbGenericLoadStringObject(rdb, RDB_LOAD_HFLD_TTL, &fieldLen); + else + field = rdbGenericLoadStringObject(rdb, RDB_LOAD_HFLD, &fieldLen); + + if (field == NULL) { + serverLog(LL_WARNING, "failed reading hash field"); + decrRefCount(o); + if (dupSearchDict != NULL) dictRelease(dupSearchDict); + return NULL; + } + + /* read the value */ + if ((value = rdbGenericLoadStringObject(rdb,RDB_LOAD_SDS,NULL)) == NULL) { + serverLog(LL_WARNING, "failed reading hash value"); + decrRefCount(o); + if (dupSearchDict != NULL) dictRelease(dupSearchDict); + hfieldFree(field); + return NULL; + } + + /* keep the nearest expiration to connect listpack object to db expiry */ + if ((expireAt != 0) && (expireAt < minExpField)) minExpField = expireAt; + + /* store the values read - either to listpack or dict */ + if (o->encoding == OBJ_ENCODING_LISTPACK_EX) { + /* integrity - check for key duplication (if required) */ + if (dupSearchDict) { + sds field_dup = sdsnewlen(field, hfieldlen(field)); + + if (dictAdd(dupSearchDict, field_dup, NULL) != DICT_OK) { + rdbReportCorruptRDB("Hash with dup elements"); + dictRelease(dupSearchDict); + decrRefCount(o); + sdsfree(field_dup); + sdsfree(value); + hfieldFree(field); + return NULL; + } + } + + /* check if the values can be saved to listpack (or should convert to dict encoding) */ + if (hfieldlen(field) > server.hash_max_listpack_value || + sdslen(value) > server.hash_max_listpack_value || + !lpSafeToAdd(((listpackEx*)o->ptr)->lp, hfieldlen(field) + sdslen(value) + lpEntrySizeInteger(expireAt))) + { + /* convert to hash */ + hashTypeConvert(o, OBJ_ENCODING_HT, NULL); + + if (len > DICT_HT_INITIAL_SIZE) { /* TODO: this is NOT the original len, but this is also the case for simple hash, is this a bug? */ + if (dictTryExpand(o->ptr, len) != DICT_OK) { + rdbReportCorruptRDB("OOM in dictTryExpand %llu", (unsigned long long)len); + decrRefCount(o); + if (dupSearchDict != NULL) dictRelease(dupSearchDict); + sdsfree(value); + hfieldFree(field); + return NULL; + } + } + + /* don't add the values to the new hash: the next if will catch and the values will be added there */ + } else { + listpackExAddNew(o, field, hfieldlen(field), + value, sdslen(value), expireAt); + hfieldFree(field); + sdsfree(value); + } + } + + if (o->encoding == OBJ_ENCODING_HT) { + /* Add pair to hash table */ + dict *d = o->ptr; + dictUseStoredKeyApi(d, 1); + int ret = dictAdd(d, field, value); + dictUseStoredKeyApi(d, 0); + + /* Attach expiry to the hash field and register in hash private HFE DS */ + if ((ret != DICT_ERR) && expireAt) { + dictExpireMetadata *m = (dictExpireMetadata *) dictMetadata(d); + ret = ebAdd(&m->hfe, &hashFieldExpireBucketsType, field, expireAt); + } + + if (ret == DICT_ERR) { + rdbReportCorruptRDB("Duplicate hash fields detected"); + sdsfree(value); + hfieldFree(field); + decrRefCount(o); + return NULL; + } + } + } + + if (dupSearchDict != NULL) dictRelease(dupSearchDict); + + /* check for empty key (if all fields were expired) */ + if (hashTypeLength(o, 0) == 0) { + decrRefCount(o); + goto expiredHash; + } } else if (rdbtype == RDB_TYPE_LIST_QUICKLIST || rdbtype == RDB_TYPE_LIST_QUICKLIST_2) { if ((len = rdbLoadLen(rdb,NULL)) == RDB_LENERR) return NULL; if (len == 0) goto emptykey; @@ -2221,7 +2457,7 @@ robj *rdbLoadObject(int rdbtype, rio *rdb, sds key, int dbid, int *error) { goto emptykey; } - listTypeTryConversion(o,LIST_CONV_AUTO,NULL,NULL); + listTypeTryConversion(o, LIST_CONV_AUTO, NULL, NULL); } else if (rdbtype == RDB_TYPE_HASH_ZIPMAP || rdbtype == RDB_TYPE_LIST_ZIPLIST || rdbtype == RDB_TYPE_SET_INTSET || @@ -2229,14 +2465,15 @@ robj *rdbLoadObject(int rdbtype, rio *rdb, sds key, int dbid, int *error) { rdbtype == RDB_TYPE_ZSET_ZIPLIST || rdbtype == RDB_TYPE_ZSET_LISTPACK || rdbtype == RDB_TYPE_HASH_ZIPLIST || - rdbtype == RDB_TYPE_HASH_LISTPACK) + rdbtype == RDB_TYPE_HASH_LISTPACK || + rdbtype == RDB_TYPE_HASH_LISTPACK_EX) { size_t encoded_len; unsigned char *encoded = rdbGenericLoadStringObject(rdb,RDB_LOAD_PLAIN,&encoded_len); if (encoded == NULL) return NULL; - o = createObject(OBJ_STRING,encoded); /* Obj type fixed below. */ + o = createObject(OBJ_STRING, encoded); /* Obj type fixed below. */ /* Fix the object encoding, and make sure to convert the encoded * data type into the base type if accordingly to the current @@ -2292,14 +2529,14 @@ robj *rdbLoadObject(int rdbtype, rio *rdb, sds key, int dbid, int *error) { o->type = OBJ_HASH; o->encoding = OBJ_ENCODING_LISTPACK; - if (hashTypeLength(o) > server.hash_max_listpack_entries || + if (hashTypeLength(o, 0) > server.hash_max_listpack_entries || maxlen > server.hash_max_listpack_value) { - hashTypeConvert(o, OBJ_ENCODING_HT); + hashTypeConvert(o, OBJ_ENCODING_HT, NULL); } } break; - case RDB_TYPE_LIST_ZIPLIST: + case RDB_TYPE_LIST_ZIPLIST: { quicklist *ql = quicklistNew(server.list_max_listpack_size, server.list_compress_depth); @@ -2341,11 +2578,11 @@ robj *rdbLoadObject(int rdbtype, rio *rdb, sds key, int dbid, int *error) { o->type = OBJ_SET; o->encoding = OBJ_ENCODING_INTSET; if (intsetLen(o->ptr) > server.set_max_intset_entries) - setTypeConvert(o,OBJ_ENCODING_HT); + setTypeConvert(o, OBJ_ENCODING_HT); break; case RDB_TYPE_SET_LISTPACK: if (deep_integrity_validation) server.stat_dump_payload_sanitizations++; - if (!lpValidateIntegrityAndDups(encoded, encoded_len, deep_integrity_validation, 0)) { + if (!lpValidateIntegrityAndDups(encoded, encoded_len, deep_integrity_validation, 1)) { rdbReportCorruptRDB("Set listpack integrity check failed."); zfree(encoded); o->ptr = NULL; @@ -2386,14 +2623,14 @@ robj *rdbLoadObject(int rdbtype, rio *rdb, sds key, int dbid, int *error) { } if (zsetLength(o) > server.zset_max_listpack_entries) - zsetConvert(o,OBJ_ENCODING_SKIPLIST); + zsetConvert(o, OBJ_ENCODING_SKIPLIST); else o->ptr = lpShrinkToFit(o->ptr); break; } case RDB_TYPE_ZSET_LISTPACK: if (deep_integrity_validation) server.stat_dump_payload_sanitizations++; - if (!lpValidateIntegrityAndDups(encoded, encoded_len, deep_integrity_validation, 1)) { + if (!lpValidateIntegrityAndDups(encoded, encoded_len, deep_integrity_validation, 2)) { rdbReportCorruptRDB("Zset listpack integrity check failed."); zfree(encoded); o->ptr = NULL; @@ -2408,7 +2645,7 @@ robj *rdbLoadObject(int rdbtype, rio *rdb, sds key, int dbid, int *error) { } if (zsetLength(o) > server.zset_max_listpack_entries) - zsetConvert(o,OBJ_ENCODING_SKIPLIST); + zsetConvert(o, OBJ_ENCODING_SKIPLIST); break; case RDB_TYPE_HASH_ZIPLIST: { @@ -2426,35 +2663,57 @@ robj *rdbLoadObject(int rdbtype, rio *rdb, sds key, int dbid, int *error) { o->ptr = lp; o->type = OBJ_HASH; o->encoding = OBJ_ENCODING_LISTPACK; - if (hashTypeLength(o) == 0) { + if (hashTypeLength(o, 0) == 0) { decrRefCount(o); goto emptykey; } - if (hashTypeLength(o) > server.hash_max_listpack_entries) - hashTypeConvert(o, OBJ_ENCODING_HT); + if (hashTypeLength(o, 0) > server.hash_max_listpack_entries) + hashTypeConvert(o, OBJ_ENCODING_HT, NULL); else o->ptr = lpShrinkToFit(o->ptr); break; } case RDB_TYPE_HASH_LISTPACK: + case RDB_TYPE_HASH_LISTPACK_EX: + /* listpack-encoded hash with TTL requires its own struct + * pointed to by o->ptr */ + o->type = OBJ_HASH; + if (rdbtype == RDB_TYPE_HASH_LISTPACK_EX) { + listpackEx *lpt = listpackExCreate(); + lpt->lp = encoded; + lpt->key = key; + o->ptr = lpt; + o->encoding = OBJ_ENCODING_LISTPACK_EX; + } else + o->encoding = OBJ_ENCODING_LISTPACK; + + /* tuple_len is the number of elements for each key: + * key + value for simple hash, key + value + tll for hash with TTL*/ + int tuple_len = (rdbtype == RDB_TYPE_HASH_LISTPACK ? 2 : 3); + /* validate read data */ if (deep_integrity_validation) server.stat_dump_payload_sanitizations++; - if (!lpValidateIntegrityAndDups(encoded, encoded_len, deep_integrity_validation, 1)) { + if (!lpValidateIntegrityAndDups(encoded, encoded_len, + deep_integrity_validation, tuple_len)) { rdbReportCorruptRDB("Hash listpack integrity check failed."); - zfree(encoded); - o->ptr = NULL; decrRefCount(o); return NULL; } - o->type = OBJ_HASH; - o->encoding = OBJ_ENCODING_LISTPACK; - if (hashTypeLength(o) == 0) { + + /* if listpack is empty, delete it */ + if (hashTypeLength(o, 0) == 0) { decrRefCount(o); goto emptykey; } - if (hashTypeLength(o) > server.hash_max_listpack_entries) - hashTypeConvert(o, OBJ_ENCODING_HT); + /* for TTL listpack, find the minimum expiry */ + minExpField = hashTypeGetNextTimeToExpire(o); + + /* Convert listpack to hash table without registering in global HFE DS, + * if has HFEs, since the listpack is not connected yet to the DB */ + if (hashTypeLength(o, 0) > server.hash_max_listpack_entries) + hashTypeConvert(o, OBJ_ENCODING_HT, NULL /*db->hexpires*/); + break; default: /* totally unreachable */ @@ -2540,7 +2799,7 @@ robj *rdbLoadObject(int rdbtype, rio *rdb, sds key, int dbid, int *error) { /* Load the last entry ID. */ s->last_id.ms = rdbLoadLen(rdb,NULL); s->last_id.seq = rdbLoadLen(rdb,NULL); - + if (rdbtype >= RDB_TYPE_STREAM_LISTPACKS_2) { /* Load the first entry ID. */ s->first_id.ms = rdbLoadLen(rdb,NULL); @@ -2559,9 +2818,9 @@ robj *rdbLoadObject(int rdbtype, rio *rdb, sds key, int dbid, int *error) { s->max_deleted_entry_id.ms = 0; s->max_deleted_entry_id.seq = 0; s->entries_added = s->length; - + /* Since the rax is already loaded, we can find the first entry's - * ID. */ + * ID. */ streamGetEdgeID(s,1,1,&s->first_id); } @@ -2794,7 +3053,13 @@ robj *rdbLoadObject(int rdbtype, rio *rdb, sds key, int dbid, int *error) { RedisModuleIO io; robj keyobj; initStaticStringObject(keyobj,key); - moduleInitIOContext(io,mt,rdb,&keyobj,dbid); + /* shouldn't happen since db is NULL only in RDB check mode, and + * in this mode the module load code returns few lines above after + * checking module name, few lines above. So this check is only + * for safety. + */ + if (db == NULL) return NULL; + moduleInitIOContext(io,mt,rdb,&keyobj,db->id); /* Call the rdb_load method of the module providing the 10 bit * encoding version in the lower 10 bits of the module ID. */ void *ptr = mt->rdb_load(&io,moduleid&1023); @@ -2807,7 +3072,7 @@ robj *rdbLoadObject(int rdbtype, rio *rdb, sds key, int dbid, int *error) { uint64_t eof = rdbLoadLen(rdb,NULL); if (eof == RDB_LENERR) { if (ptr) { - o = createModuleObject(mt,ptr); /* creating just in order to easily destroy */ + o = createModuleObject(mt, ptr); /* creating just in order to easily destroy */ decrRefCount(o); } return NULL; @@ -2816,7 +3081,7 @@ robj *rdbLoadObject(int rdbtype, rio *rdb, sds key, int dbid, int *error) { rdbReportCorruptRDB("The RDB file contains module data for the module '%s' that is not terminated by " "the proper module value EOF marker", moduleTypeModuleName(mt)); if (ptr) { - o = createModuleObject(mt,ptr); /* creating just in order to easily destroy */ + o = createModuleObject(mt, ptr); /* creating just in order to easily destroy */ decrRefCount(o); } return NULL; @@ -2828,17 +3093,23 @@ robj *rdbLoadObject(int rdbtype, rio *rdb, sds key, int dbid, int *error) { moduleTypeModuleName(mt)); return NULL; } - o = createModuleObject(mt,ptr); + o = createModuleObject(mt, ptr); } else { rdbReportReadError("Unknown RDB encoding type %d",rdbtype); return NULL; } + + if (minExpiredField) *minExpiredField = minExpField; + if (error) *error = 0; return o; emptykey: if (error) *error = RDB_LOAD_ERR_EMPTY_KEY; return NULL; +expiredHash: + if (error) *error = RDB_LOAD_ERR_EXPIRED_HASH; + return NULL; } /* Mark that we are loading in the global state and setup the fields @@ -3008,6 +3279,7 @@ int rdbLoadRio(rio *rdb, int rdbflags, rdbSaveInfo *rsi) { * currently it only allow to set db object and functionLibCtx to which the data * will be loaded (in the future it might contains more such objects). */ int rdbLoadRioWithLoadingCtx(rio *rdb, int rdbflags, rdbSaveInfo *rsi, rdbLoadingCtx *rdb_loading_ctx) { + uint64_t minExpiredField = EB_EXPIRE_TIME_INVALID; uint64_t dbid = 0; int type, rdbver; uint64_t db_size = 0, expires_size = 0; @@ -3249,15 +3521,15 @@ int rdbLoadRioWithLoadingCtx(rio *rdb, int rdbflags, rdbSaveInfo *rsi, rdbLoadin if ((key = rdbGenericLoadStringObject(rdb,RDB_LOAD_SDS,NULL)) == NULL) goto eoferr; /* Read value */ - val = rdbLoadObject(type,rdb,key,db->id,&error); + val = rdbLoadObject(type,rdb,key,db,&error, &minExpiredField); /* Check if the key already expired. This function is used when loading * an RDB file from disk, either at startup, or when an RDB was * received from the master. In the latter case, the master is * responsible for key expiry. If we would expire keys here, the * snapshot taken by the master may not be reflected on the slave. - * Similarly, if the base AOF is RDB format, we want to load all - * the keys they are, since the log of operations in the incr AOF + * Similarly, if the base AOF is RDB format, we want to load all + * the keys they are, since the log of operations in the incr AOF * is assumed to work in the exact keyspace state. */ if (val == NULL) { /* Since we used to have bug that could lead to empty keys @@ -3268,6 +3540,9 @@ int rdbLoadRioWithLoadingCtx(rio *rdb, int rdbflags, rdbSaveInfo *rsi, rdbLoadin if(empty_keys_skipped++ < 10) serverLog(LL_NOTICE, "rdbLoadObject skipping empty key: %s", key); sdsfree(key); + } else if (error == RDB_LOAD_ERR_EXPIRED_HASH) { + /* Valid flow. Continue. */ + sdsfree(key); } else { sdsfree(key); goto eoferr; @@ -3312,6 +3587,11 @@ int rdbLoadRioWithLoadingCtx(rio *rdb, int rdbflags, rdbSaveInfo *rsi, rdbLoadin } } + /* If minExpiredField was set, then the object is hash with expiration + * on fields and need to register it in global HFE DS */ + if (minExpiredField != EB_EXPIRE_TIME_INVALID) + hashTypeAddToExpires(db, key, val, minExpiredField); + /* Set the expire time if needed */ if (expiretime != -1) { setExpire(NULL,db,&keyobj,expiretime); diff --git a/src/rdb.h b/src/rdb.h index 8ce2aaaf7..f34e139c1 100644 --- a/src/rdb.h +++ b/src/rdb.h @@ -73,10 +73,12 @@ #define RDB_TYPE_STREAM_LISTPACKS_2 19 #define RDB_TYPE_SET_LISTPACK 20 #define RDB_TYPE_STREAM_LISTPACKS_3 21 +#define RDB_TYPE_HASH_METADATA 22 +#define RDB_TYPE_HASH_LISTPACK_EX 23 /* NOTE: WHEN ADDING NEW RDB TYPE, UPDATE rdbIsObjectType(), and rdb_type_string[] */ /* Test if a type is an object type. */ -#define rdbIsObjectType(t) (((t) >= 0 && (t) <= 7) || ((t) >= 9 && (t) <= 21)) +#define rdbIsObjectType(t) (((t) >= 0 && (t) <= 7) || ((t) >= 9 && (t) <= 23)) /* Special RDB opcodes (saved/loaded with rdbSaveType/rdbLoadType). */ #define RDB_OPCODE_SLOT_INFO 244 /* Individual slot info, such as slot id and size (cluster mode only). */ @@ -101,10 +103,12 @@ #define RDB_MODULE_OPCODE_STRING 5 /* String. */ /* rdbLoad...() functions flags. */ -#define RDB_LOAD_NONE 0 -#define RDB_LOAD_ENC (1<<0) -#define RDB_LOAD_PLAIN (1<<1) -#define RDB_LOAD_SDS (1<<2) +#define RDB_LOAD_NONE 0 +#define RDB_LOAD_ENC (1<<0) +#define RDB_LOAD_PLAIN (1<<1) +#define RDB_LOAD_SDS (1<<2) +#define RDB_LOAD_HFLD (1<<3) +#define RDB_LOAD_HFLD_TTL (1<<4) /* flags on the purpose of rdb save or load */ #define RDBFLAGS_NONE 0 /* No special RDB loading or saving. */ @@ -116,8 +120,9 @@ /* When rdbLoadObject() returns NULL, the err flag is * set to hold the type of error that occurred */ -#define RDB_LOAD_ERR_EMPTY_KEY 1 /* Error of empty key */ -#define RDB_LOAD_ERR_OTHER 2 /* Any other errors */ +#define RDB_LOAD_ERR_EMPTY_KEY 1 /* Error of empty key */ +#define RDB_LOAD_ERR_EXPIRED_HASH 2 /* Expired hash since all its fields are expired */ +#define RDB_LOAD_ERR_OTHER 3 /* Any other errors */ ssize_t rdbWriteRaw(rio *rdb, void *p, size_t len); int rdbSaveType(rio *rdb, unsigned char type); @@ -138,7 +143,7 @@ int rdbSaveToFile(const char *filename); int rdbSave(int req, char *filename, rdbSaveInfo *rsi, int rdbflags); ssize_t rdbSaveObject(rio *rdb, robj *o, robj *key, int dbid); size_t rdbSavedObjectLen(robj *o, robj *key, int dbid); -robj *rdbLoadObject(int rdbtype, rio *rdb, sds key, int dbid, int *error); +robj *rdbLoadObject(int rdbtype, rio *rdb, sds key, redisDb *db, int *error, uint64_t *minExpiredField); void backgroundSaveDoneHandler(int exitcode, int bysignal); int rdbSaveKeyValuePair(rio *rdb, robj *key, robj *val, long long expiretime,int dbid); ssize_t rdbSaveSingleModuleAux(rio *rdb, int when, moduleType *mt); diff --git a/src/redis-check-rdb.c b/src/redis-check-rdb.c index b77bad32d..f364bf7b7 100644 --- a/src/redis-check-rdb.c +++ b/src/redis-check-rdb.c @@ -80,6 +80,8 @@ char *rdb_type_string[] = { "stream-v2", "set-listpack", "stream-v3", + "hash-hashtable-md", + "hash-listpack-md", }; /* Show a few stats collected into 'rdbstate' */ @@ -173,7 +175,6 @@ void rdbCheckSetupSignals(void) { * otherwise the already open file 'fp' is checked. */ int redis_check_rdb(char *rdbfilename, FILE *fp) { uint64_t dbid; - int selected_dbid = -1; int type, rdbver; char buf[1024]; long long expiretime, now = mstime(); @@ -245,7 +246,6 @@ int redis_check_rdb(char *rdbfilename, FILE *fp) { if ((dbid = rdbLoadLen(&rdb,NULL)) == RDB_LENERR) goto eoferr; rdbCheckInfo("Selecting DB ID %llu", (unsigned long long)dbid); - selected_dbid = dbid; continue; /* Read type again. */ } else if (type == RDB_OPCODE_RESIZEDB) { /* RESIZEDB: Hint about the size of the keys in the currently @@ -331,7 +331,8 @@ int redis_check_rdb(char *rdbfilename, FILE *fp) { rdbstate.keys++; /* Read value */ rdbstate.doing = RDB_CHECK_DOING_READ_OBJECT_VALUE; - if ((val = rdbLoadObject(type,&rdb,key->ptr,selected_dbid,NULL)) == NULL) goto eoferr; + if ((val = rdbLoadObject(type,&rdb,key->ptr,NULL,NULL,NULL)) == NULL) + goto eoferr; /* Check if the key already expired. */ if (expiretime != -1 && expiretime < now) rdbstate.already_expired++; diff --git a/src/server.c b/src/server.c index e154346fd..236d9d767 100644 --- a/src/server.c +++ b/src/server.c @@ -19,6 +19,8 @@ #include "syscheck.h" #include "threads_mngr.h" #include "fmtargs.h" +#include "mstr.h" +#include "ebuckets.h" #include #include @@ -281,6 +283,18 @@ int dictSdsKeyCompare(dict *d, const void *key1, return memcmp(key1, key2, l1) == 0; } +int dictSdsMstrKeyCompare(dict *d, const void *sdsLookup, const void *mstrStored) +{ + int l1,l2; + UNUSED(d); + + l1 = sdslen((sds)sdsLookup); + l2 = hfieldlen((hfield)mstrStored); + if (l1 != l2) return 0; + return memcmp(sdsLookup, mstrStored, l1) == 0; +} + + /* A case insensitive version used for the command lookup table and other * places where case insensitive non binary-safe comparison is needed. */ int dictSdsKeyCaseCompare(dict *d, const void *key1, @@ -1945,6 +1959,8 @@ void createSharedObjects(void) { shared.persist = createStringObject("PERSIST",7); shared.set = createStringObject("SET",3); shared.eval = createStringObject("EVAL",4); + shared.hpexpireat = createStringObject("HPEXPIREAT",10); + shared.hdel = createStringObject("HDEL",4); /* Shared command argument */ shared.left = createStringObject("left",4); @@ -2504,6 +2520,7 @@ void resetServerStats(void) { server.stat_numcommands = 0; server.stat_numconnections = 0; server.stat_expiredkeys = 0; + server.stat_expired_hash_fields = 0; server.stat_expired_stale_perc = 0; server.stat_expired_time_cap_reached_count = 0; server.stat_expire_cycle_time_used = 0; @@ -2652,6 +2669,7 @@ void initServer(void) { for (j = 0; j < server.dbnum; j++) { server.db[j].keys = kvstoreCreate(&dbDictType, slot_count_bits, flags); server.db[j].expires = kvstoreCreate(&dbExpiresDictType, slot_count_bits, flags); + server.db[j].hexpires = ebCreate(); server.db[j].expires_cursor = 0; server.db[j].blocking_keys = dictCreate(&keylistDictType); server.db[j].blocking_keys_unblock_on_nokey = dictCreate(&objectKeyPointerValueDictType); @@ -5854,6 +5872,7 @@ sds genRedisInfoString(dict *section_dict, int all_sections, int everything) { "sync_full:%lld\r\n", server.stat_sync_full, "sync_partial_ok:%lld\r\n", server.stat_sync_partial_ok, "sync_partial_err:%lld\r\n", server.stat_sync_partial_err, + "expired_hash_fields:%lld\r\n", server.stat_expired_hash_fields, "expired_keys:%lld\r\n", server.stat_expiredkeys, "expired_stale_perc:%.2f\r\n", server.stat_expired_stale_perc*100, "expired_time_cap_reached_count:%lld\r\n", server.stat_expired_time_cap_reached_count, @@ -6092,14 +6111,16 @@ sds genRedisInfoString(dict *section_dict, int all_sections, int everything) { if (sections++) info = sdscat(info,"\r\n"); info = sdscatprintf(info, "# Keyspace\r\n"); for (j = 0; j < server.dbnum; j++) { - long long keys, vkeys; + long long keys, vkeys, hexpires; keys = kvstoreSize(server.db[j].keys); vkeys = kvstoreSize(server.db[j].expires); + hexpires = ebGetTotalItems(server.db[j].hexpires, &hashExpireBucketsType); + if (keys || vkeys) { info = sdscatprintf(info, - "db%d:keys=%lld,expires=%lld,avg_ttl=%lld\r\n", - j, keys, vkeys, server.db[j].avg_ttl); + "db%d:keys=%lld,expires=%lld,avg_ttl=%lld,hashes_with_expiry_fields=%lld\r\n", + j, keys, vkeys, server.db[j].avg_ttl, hexpires); } } } @@ -6871,9 +6892,11 @@ struct redisTest { {"crc64", crc64Test}, {"zmalloc", zmalloc_test}, {"sds", sdsTest}, + {"mstr", mstrTest}, {"dict", dictTest}, {"listpack", listpackTest}, {"kvstore", kvstoreTest}, + {"ebuckets", ebucketsTest}, }; redisTestProc *getTestProcByName(const char *name) { int numtests = sizeof(redisTests)/sizeof(struct redisTest); @@ -6900,6 +6923,7 @@ int main(int argc, char **argv) { if (!strcasecmp(arg, "--accurate")) flags |= REDIS_TEST_ACCURATE; else if (!strcasecmp(arg, "--large-memory")) flags |= REDIS_TEST_LARGE_MEMORY; else if (!strcasecmp(arg, "--valgrind")) flags |= REDIS_TEST_VALGRIND; + else if (!strcasecmp(arg, "--verbose")) flags |= REDIS_TEST_VERBOSE; } if (!strcasecmp(argv[2], "all")) { diff --git a/src/server.h b/src/server.h index 1a662897f..e5116d4c9 100644 --- a/src/server.h +++ b/src/server.h @@ -45,6 +45,8 @@ typedef long long ustime_t; /* microsecond time type. */ #include "ae.h" /* Event driven programming library */ #include "sds.h" /* Dynamic safe strings */ +#include "mstr.h" /* Immutable strings with optional metadata attached */ +#include "ebuckets.h" /* expiry data structure */ #include "dict.h" /* Hash tables */ #include "kvstore.h" /* Slot-based hash table */ #include "adlist.h" /* Linked lists */ @@ -884,6 +886,7 @@ struct RedisModuleDigest { #define OBJ_ENCODING_QUICKLIST 9 /* Encoded as linked list of listpacks */ #define OBJ_ENCODING_STREAM 10 /* Encoded as a radix tree of listpacks */ #define OBJ_ENCODING_LISTPACK 11 /* Encoded as a listpack */ +#define OBJ_ENCODING_LISTPACK_EX 12 /* Encoded as listpack, extended with metadata */ #define LRU_BITS 24 #define LRU_CLOCK_MAX ((1<lru */ @@ -960,6 +963,7 @@ typedef struct replBufBlock { typedef struct redisDb { kvstore *keys; /* The keyspace for this DB */ kvstore *expires; /* Timeout of keys with a timeout set */ + ebuckets hexpires; /* Hash expiration DS. Single TTL per hash (of next min field to expire) */ dict *blocking_keys; /* Keys with clients waiting for data (BLPOP)*/ dict *blocking_keys_unblock_on_nokey; /* Keys with clients waiting for * data, and should be unblocked if key is deleted (XREADEDGROUP). @@ -1313,7 +1317,8 @@ struct sharedObjectsStruct { *unsubscribebulk, *psubscribebulk, *punsubscribebulk, *del, *unlink, *rpop, *lpop, *lpush, *rpoplpush, *lmove, *blmove, *zpopmin, *zpopmax, *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, *time, *pxat, *absttl, *retrycount, *force, *justid, *entriesread, *lastid, *ping, *setid, *keepttl, *load, *createconsumer, *getack, *special_asterick, *special_equals, *default_username, *redacted, @@ -1646,6 +1651,7 @@ struct redisServer { long long stat_numcommands; /* Number of processed commands */ long long stat_numconnections; /* Number of connections received */ long long stat_expiredkeys; /* Number of expired keys */ + long long stat_expired_hash_fields; /* Number of expired hash-fields */ double stat_expired_stale_perc; /* Percentage of keys probably expired */ long long stat_expired_time_cap_reached_count; /* Early expire cycle stops.*/ long long stat_expire_cycle_time_used; /* Cumulative microseconds used. */ @@ -2433,7 +2439,8 @@ typedef struct { robj *subject; int encoding; - unsigned char *fptr, *vptr; + unsigned char *fptr, *vptr, *tptr; + uint64_t expire_time; /* Only used with OBJ_ENCODING_LISTPACK_EX */ dictIterator *di; dictEntry *de; @@ -2449,6 +2456,10 @@ typedef struct { #define IO_THREADS_OP_WRITE 2 extern int io_threads_op; +/* Hash-field data type (of t_hash.c) */ +typedef mstr hfield; +extern mstrKind mstrFieldKind; + /*----------------------------------------------------------------------------- * Extern declarations *----------------------------------------------------------------------------*/ @@ -2463,6 +2474,8 @@ extern dictType zsetDictType; extern dictType dbDictType; extern double R_Zero, R_PosInf, R_NegInf, R_Nan; extern dictType hashDictType; +extern dictType mstrHashDictType; +extern dictType mstrHashDictTypeWithHFE; extern dictType stringSetDictType; extern dictType externalStringType; extern dictType sdsHashDictType; @@ -2474,6 +2487,9 @@ extern dictType sdsReplyDictType; extern dictType keylistDictType; extern dict *modules; +extern EbucketsType hashExpireBucketsType; /* global expires */ +extern EbucketsType hashFieldExpireBucketsType; /* local per hash */ + /*----------------------------------------------------------------------------- * Functions prototypes *----------------------------------------------------------------------------*/ @@ -2616,6 +2632,7 @@ void copyReplicaOutputBuffer(client *dst, client *src); void addListRangeReply(client *c, robj *o, long start, long end, int reverse); void deferredAfterErrorReply(client *c, list *errors); size_t sdsZmallocSize(sds s); +size_t hfieldZmallocSize(hfield s); size_t getStringObjectSdsUsedMemory(robj *o); void freeClientReplyValue(void *o); void *dupClientReplyValue(void *o); @@ -3140,30 +3157,87 @@ void setTypeConvert(robj *subject, int enc); int setTypeConvertAndExpand(robj *setobj, int enc, unsigned long cap, int panic); robj *setTypeDup(robj *o); +/* Data structure for OBJ_ENCODING_LISTPACK_EX for hash. It contains listpack + * and metadata fields for hash field expiration.*/ +typedef struct listpackEx { + ExpireMeta meta; /* To be used in order to register the hash in the + global ebuckets (i.e. db->hexpires) with next, + minimum, hash-field to expire. */ + sds key; /* reference to the key, same one that stored in + db->dict. Will be used from active-expiration flow + for notification and deletion of the object, if + needed. */ + void *lp; /* listpack that contains 'key-value-ttl' tuples which + are ordered by ttl. */ +} listpackEx; + +/* Each dict of hash object that has fields with time-Expiration will have the + * following metadata attached to dict header */ +typedef struct dictExpireMetadata { + ExpireMeta expireMeta; /* embedded ExpireMeta in dict. + To be used in order to register the hash in the + global ebuckets (i.e db->hexpires) with next, + minimum, hash-field to expire */ + ebuckets hfe; /* DS of Hash Fields Expiration, associated to each hash */ + sds key; /* reference to the key, same one that stored in + db->dict. Will be used from active-expiration flow + for notification and deletion of the object, if + needed. */ +} dictExpireMetadata; + /* Hash data type */ #define HASH_SET_TAKE_FIELD (1<<0) #define HASH_SET_TAKE_VALUE (1<<1) #define HASH_SET_COPY 0 -void hashTypeConvert(robj *o, int enc); -void hashTypeTryConversion(robj *subject, robj **argv, int start, int end); -int hashTypeExists(robj *o, sds key); -int hashTypeDelete(robj *o, sds key); -unsigned long hashTypeLength(const robj *o); +void hashTypeConvert(robj *o, int enc, ebuckets *hexpires); +void hashTypeTryConversion(redisDb *db, robj *subject, robj **argv, int start, int end); +int hashTypeExists(redisDb *db, robj *o, sds key, int *isHashDeleted); +int hashTypeDelete(robj *o, void *key, int isSdsField); +unsigned long hashTypeLength(const robj *o, int subtractExpiredFields); hashTypeIterator *hashTypeInitIterator(robj *subject); void hashTypeReleaseIterator(hashTypeIterator *hi); -int hashTypeNext(hashTypeIterator *hi); +int hashTypeNext(hashTypeIterator *hi, int skipExpiredFields); void hashTypeCurrentFromListpack(hashTypeIterator *hi, int what, unsigned char **vstr, unsigned int *vlen, - long long *vll); -sds hashTypeCurrentFromHashTable(hashTypeIterator *hi, int what); -void hashTypeCurrentObject(hashTypeIterator *hi, int what, unsigned char **vstr, unsigned int *vlen, long long *vll); + long long *vll, + uint64_t *expireTime); +void hashTypeCurrentFromHashTable(hashTypeIterator *hi, int what, char **str, + size_t *len, uint64_t *expireTime); +void hashTypeCurrentObject(hashTypeIterator *hi, int what, unsigned char **vstr, + unsigned int *vlen, long long *vll, uint64_t *expireTime); sds hashTypeCurrentObjectNewSds(hashTypeIterator *hi, int what); -robj *hashTypeLookupWriteOrCreate(client *c, robj *key); -robj *hashTypeGetValueObject(robj *o, sds field); -int hashTypeSet(robj *o, sds field, sds value, int flags); -robj *hashTypeDup(robj *o); +hfield hashTypeCurrentObjectNewHfield(hashTypeIterator *hi); +robj *hashTypeGetValueObject(redisDb *db, robj *o, sds field, int *isHashDeleted); +int hashTypeSet(redisDb *db, robj *o, sds field, sds value, int flags); +robj *hashTypeDup(robj *o, sds newkey, uint64_t *minHashExpire); +uint64_t hashTypeRemoveFromExpires(ebuckets *hexpires, robj *o); +void hashTypeAddToExpires(redisDb *db, sds key, robj *hashObj, uint64_t expireTime); +void hashTypeFree(robj *o); +int hashTypeIsExpired(const robj *o, uint64_t expireAt); +uint64_t hashTypeGetMinExpire(robj *o); +unsigned char *hashTypeListpackGetLp(robj *o); +uint64_t hashTypeGetMinExpire(robj *o); +void hashTypeUpdateKeyRef(robj *o, sds newkey); +ebuckets *hashTypeGetDictMetaHFE(dict *d); +uint64_t hashTypeGetMinExpire(robj *keyObj); +uint64_t hashTypeGetNextTimeToExpire(robj *o); +void initDictExpireMetadata(sds key, robj *o); +struct listpackEx *listpackExCreate(void); +void listpackExAddNew(robj *o, char *field, size_t flen, + char *value, size_t vlen, uint64_t expireAt); + +/* Hash-Field data type (of t_hash.c) */ +hfield hfieldNew(const void *field, size_t fieldlen, int withExpireMeta); +hfield hfieldTryNew(const void *field, size_t fieldlen, int withExpireMeta); +int hfieldIsExpireAttached(hfield field); +int hfieldIsExpired(hfield field); +uint64_t hfieldGetExpireTime(hfield field); +static inline void hfieldFree(hfield field) { mstrFree(&mstrFieldKind, field); } +static inline void *hfieldGetAllocPtr(hfield field) { return mstrGetAllocPtr(&mstrFieldKind, field); } +static inline size_t hfieldlen(hfield field) { return mstrlen(field);} +uint64_t hfieldGetExpireTime(hfield field); /* Pub / Sub */ int pubsubUnsubscribeAllChannels(client *c, int notify); @@ -3182,7 +3256,7 @@ dict *getClientPubSubChannels(client *c); dict *getClientPubSubShardChannels(client *c); /* Keyspace events notification */ -void notifyKeyspaceEvent(int type, char *event, robj *key, int dbid); +void notifyKeyspaceEvent(int type, const char *event, robj *key, int dbid); int keyspaceEventsStringToFlags(char *classes); sds keyspaceEventsFlagsToString(int flags); @@ -3266,6 +3340,7 @@ int keyIsExpired(redisDb *db, robj *key); long long getExpire(redisDb *db, robj *key); void setExpire(client *c, redisDb *db, robj *key, long long when); int checkAlreadyExpired(long long when); +int parseExtendedExpireArgumentsOrReply(client *c, int *flags); robj *lookupKeyRead(redisDb *db, robj *key); robj *lookupKeyWrite(redisDb *db, robj *key); robj *lookupKeyReadOrReply(client *c, robj *key, robj *reply); @@ -3284,7 +3359,7 @@ int objectSetLRUOrLFU(robj *val, long long lfu_freq, long long lru_idle, #define LOOKUP_NOEXPIRE (1<<4) /* Avoid deleting lazy expired keys. */ #define LOOKUP_NOEFFECTS (LOOKUP_NONOTIFY | LOOKUP_NOSTATS | LOOKUP_NOTOUCH | LOOKUP_NOEXPIRE) /* Avoid any effects from fetching the key */ -void dbAdd(redisDb *db, robj *key, robj *val); +dictEntry *dbAdd(redisDb *db, robj *key, robj *val); int dbAddRDBLoad(redisDb *db, sds key, robj *val); void dbReplaceValue(redisDb *db, robj *key, robj *val); @@ -3439,6 +3514,7 @@ void expireSlaveKeys(void); void rememberSlaveKeyWithExpire(redisDb *db, robj *key); void flushSlaveKeysWithExpireList(void); size_t getSlaveKeyWithExpireCount(void); +uint64_t hashTypeDbActiveExpire(redisDb *db, uint32_t maxFieldsToExpire); /* evict.c -- maxmemory handling and LRU eviction. */ void evictionPoolAlloc(void); @@ -3456,6 +3532,7 @@ void startEvictionTimeProc(void); uint64_t dictSdsHash(const void *key); uint64_t dictSdsCaseHash(const void *key); int dictSdsKeyCompare(dict *d, const void *key1, const void *key2); +int dictSdsMstrKeyCompare(dict *d, const void *sdsLookup, const void *mstrStored); int dictSdsKeyCaseCompare(dict *d, const void *key1, const void *key2); void dictSdsDestructor(dict *d, void *val); void dictListDestructor(dict *d, void *val); @@ -3611,6 +3688,15 @@ void strlenCommand(client *c); void zrankCommand(client *c); void zrevrankCommand(client *c); void hsetCommand(client *c); +void hpexpireCommand(client *c); +void hexpireCommand(client *c); +void hpexpireatCommand(client *c); +void hexpireatCommand(client *c); +void httlCommand(client *c); +void hpttlCommand(client *c); +void hexpiretimeCommand(client *c); +void hpexpiretimeCommand(client *c); +void hpersistCommand(client *c); void hsetnxCommand(client *c); void hgetCommand(client *c); void hmgetCommand(client *c); diff --git a/src/sort.c b/src/sort.c index 426ff0c1d..d45c380ac 100644 --- a/src/sort.c +++ b/src/sort.c @@ -94,7 +94,12 @@ robj *lookupKeyByPattern(redisDb *db, robj *pattern, robj *subst) { /* Retrieve value from hash by the field name. The returned object * is a new object with refcount already incremented. */ - o = hashTypeGetValueObject(o, fieldobj->ptr); + int isHashDeleted; + o = hashTypeGetValueObject(db, o, fieldobj->ptr, &isHashDeleted); + + if (isHashDeleted) + goto noobj; + } else { if (o->type != OBJ_STRING) goto noobj; diff --git a/src/t_hash.c b/src/t_hash.c index 8c4c21b0d..a4b182d91 100644 --- a/src/t_hash.c +++ b/src/t_hash.c @@ -7,8 +7,600 @@ */ #include "server.h" +#include "ebuckets.h" #include +/* Threshold for HEXPIRE and HPERSIST to be considered whether it is worth to + * update the expiration time of the hash object in global HFE DS. */ +#define HASH_NEW_EXPIRE_DIFF_THRESHOLD max(4000, 1<hexpires) to register hashes that have one or more fields with time-Expiration. + * The hashes will be registered in with the expiration time of the earliest field + * in the hash. + *----------------------------------------------------------------------------*/ +EbucketsType hashExpireBucketsType = { + .onDeleteItem = NULL, + .getExpireMeta = hashGetExpireMeta, /* get ExpireMeta attached to each hash */ + .itemsAddrAreOdd = 0, /* Addresses of dict are even */ +}; + +/* dictExpireMetadata - ebuckets-type for hash fields with time-Expiration. ebuckets + * instance Will be attached to each hash that has at least one field with expiry + * time. */ +EbucketsType hashFieldExpireBucketsType = { + .onDeleteItem = NULL, + .getExpireMeta = hfieldGetExpireMeta, /* get ExpireMeta attached to each field */ + .itemsAddrAreOdd = 1, /* Addresses of hfield (mstr) are odd!! */ +}; + +/* ActiveExpireCtx passed to hashTypeActiveExpire() */ +typedef struct ActiveExpireCtx { + uint32_t fieldsToExpireQuota; + redisDb *db; +} ActiveExpireCtx; + +/* OnFieldExpireCtx passed to OnFieldExpire() */ +typedef struct OnFieldExpireCtx { + robj *hashObj; + redisDb *db; +} OnFieldExpireCtx; + +/* The implementation of hashes by dict was modified from storing fields as sds + * strings to store "mstr" (Immutable string with metadata) in order to be able to + * attach TTL (ExpireMeta) to the hash-field. This usage of mstr opens up the + * opportunity for future features to attach additional metadata by need to the + * fields. + * + * The following defines new hfield kind of mstr */ +typedef enum HfieldMetaFlags { + HFIELD_META_EXPIRE = 0, +} HfieldMetaFlags; + +mstrKind mstrFieldKind = { + .name = "hField", + + /* Taking care that all metaSize[*] values are even ensures that all + * addresses of hfield instances will be odd. */ + .metaSize[HFIELD_META_EXPIRE] = sizeof(ExpireMeta), +}; +static_assert(sizeof(struct ExpireMeta ) % 2 == 0, "must be even!"); + +/* Used by hpersistCommand() */ +typedef enum SetPersistRes { + HFE_PERSIST_NO_FIELD = -2, /* No such hash-field */ + HFE_PERSIST_NO_TTL = -1, /* No TTL attached to the field */ + HFE_PERSIST_OK = 1 +} SetPersistRes; + +static inline int isDictWithMetaHFE(dict *d) { + return d->type == &mstrHashDictTypeWithHFE; +} + +/*----------------------------------------------------------------------------- + * setex* - Set field OR field's expiration + * + * Whereas setting plain fields is rather straightforward, setting expiration + * time to fields might be time-consuming and complex since each update of + * expiration time, not only updates `ebuckets` of corresponding hash, but also + * might update `ebuckets` of global HFE DS. It is required to opt sequence of + * field updates with expirartion for a given hash, such that only once done, + * the global HFE DS will get updated. + * + * To do so, follow the scheme: + * 1. Call hashTypeSetExInit() to initialize the HashTypeSetEx struct. + * 2. Call hashTypeSetEx() one time or more, for each field/expiration update. + * 3. Call hashTypeSetExDone() for notification and update of global HFE. + * + * If expiration is not required, then avoid this API and use instead hashTypeSet() + *----------------------------------------------------------------------------*/ + +/* Returned value of hashTypeSetEx() */ +typedef enum SetExRes { + /* Common res from hashTypeSetEx() */ + HSETEX_OK = 1, /* Expiration time set/updated as expected */ + + /* If provided HashTypeSetEx struct to hashTypeSetEx() */ + HSETEX_NO_FIELD = -2, /* No such hash-field */ + HSETEX_NO_CONDITION_MET = 0, /* Specified NX | XX | GT | LT condition not met */ + HSETEX_DELETED = 2, /* Field deleted because the specified time is in the past */ + + /* If not provided HashTypeSetEx struct to hashTypeSetEx() (plain HSET) */ + HSET_UPDATE = 4, /* Update of the field without expiration time */ + +} SetExRes; + +/* Used by httlGenericCommand() */ +typedef enum GetExpireTimeRes { + HFE_GET_NO_FIELD = -2, /* No such hash-field */ + HFE_GET_NO_TTL = -1, /* No TTL attached to the field */ +} GetExpireTimeRes; + +/* on fail return HSETEX_NO_CONDITION_MET */ +typedef enum FieldSetCond { + FIELD_CREATE_OR_OVRWRT = 0, + FIELD_DONT_CREATE = 1, + FIELD_DONT_CREATE2 = 2, /* on fail return HSETEX_NO_FIELD */ + FIELD_DONT_OVRWRT = 3 +} FieldSetCond; + +typedef enum FieldGet { /* TBD */ + FIELD_GET_NONE = 0, + FIELD_GET_NEW = 1, + FIELD_GET_OLD = 2 +} FieldGet; + +typedef enum ExpireSetCond { + HFE_NX = 1<<0, + HFE_XX = 1<<1, + HFE_GT = 1<<2, + HFE_LT = 1<<3 +} ExpireSetCond; + +typedef struct HashTypeSet { + sds value; + int flags; +} HashTypeSet; + +/* Used by hashTypeSetEx() for setting fields or their expiry */ +typedef struct HashTypeSetEx { + + /*** config ***/ + FieldSetCond fieldSetCond; /* [DCF | DOF] */ + ExpireSetCond expireSetCond; /* [XX | NX | GT | LT] */ + + /*** metadata ***/ + uint64_t minExpire; /* if uninit EB_EXPIRE_TIME_INVALID */ + redisDb *db; + robj *key, *hashObj; + uint64_t minExpireFields; /* Trace updated fields and their previous/new + * 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; + +static SetExRes hashTypeSetExListpack(redisDb *db, robj *o, sds field, HashTypeSet *setParams, + uint64_t expireAt, HashTypeSetEx *exParams); + +int hashTypeSetExInit(robj *key, robj *o, client *c, redisDb *db, const char *cmd, + FieldSetCond fieldSetCond, ExpireSetCond expireSetCond, HashTypeSetEx *ex); + +SetExRes hashTypeSetEx(redisDb *db, robj *o, sds field, HashTypeSet *setKeyVal, + uint64_t expireAt, HashTypeSetEx *exInfo); + +void hashTypeSetExDone(HashTypeSetEx *e); + +/*----------------------------------------------------------------------------- + * Accessor functions for dictType of hash + *----------------------------------------------------------------------------*/ + +static int dictHfieldKeyCompare(dict *d, const void *key1, const void *key2) +{ + int l1,l2; + UNUSED(d); + + l1 = hfieldlen((hfield)key1); + l2 = hfieldlen((hfield)key2); + if (l1 != l2) return 0; + return memcmp(key1, key2, l1) == 0; +} + +static uint64_t dictMstrHash(const void *key) { + return dictGenHashFunction((unsigned char*)key, mstrlen((char*)key)); +} + +static void dictHfieldDestructor(dict *d, void *field) { + + /* If attached TTL to the field, then remove it from hash's private ebuckets. */ + if (hfieldGetExpireTime(field) != EB_EXPIRE_TIME_INVALID) { + dictExpireMetadata *dictExpireMeta = (dictExpireMetadata *) dictMetadata(d); + ebRemove(&dictExpireMeta->hfe, &hashFieldExpireBucketsType, field); + } + + hfieldFree(field); + + /* Don't have to update global HFE DS. It's unnecessary. Implementing this + * would introduce significant complexity and overhead for an operation that + * isn't critical. In the worst case scenario, the hash will be efficiently + * updated later by an active-expire operation, or it will be removed by the + * hash's dbGenericDelete() function. */ +} + +static size_t hashDictWithExpireMetadataBytes(dict *d) { + UNUSED(d); + /* expireMeta of the hash, ref to ebuckets and pointer to hash's key */ + return sizeof(dictExpireMetadata); +} + +static void hashDictWithExpireOnRelease(dict *d) { + /* for sure allocated with metadata. Otherwise, this func won't be registered */ + dictExpireMetadata *dictExpireMeta = (dictExpireMetadata *) dictMetadata(d); + ebDestroy(&dictExpireMeta->hfe, &hashFieldExpireBucketsType, NULL); +} + +/*----------------------------------------------------------------------------- + * listpackEx functions + *----------------------------------------------------------------------------*/ +/* + * If any of hash field expiration command is called on a listpack hash object + * for the first time, we convert it to OBJ_ENCODING_LISTPACK_EX encoding. + * We allocate "struct listpackEx" which holds listpack pointer and metadata to + * register key to the global DS. In the listpack, we append another TTL entry + * for each field-value pair. From now on, listpack will have triplets in it: + * field-value-ttl. If TTL is not set for a field, we store 'zero' as the TTL + * value. 'zero' is encoded as two bytes in the listpack. Memory overhead of a + * non-existing TTL will be two bytes per field. + * + * Fields in the listpack will be ordered by TTL. Field with the smallest expiry + * time will be the first item. Fields without TTL will be at the end of the + * listpack. This way, it is easier/faster to find expired items. + */ + +#define HASH_LP_NO_TTL 0 + +struct listpackEx *listpackExCreate(void) { + listpackEx *lpt = zcalloc(sizeof(*lpt)); + lpt->meta.trash = 1; + lpt->lp = NULL; + lpt->key = NULL; + return lpt; +} + +static void listpackExFree(listpackEx *lpt) { + lpFree(lpt->lp); + zfree(lpt); +} + +struct lpFingArgs { + uint64_t max_to_search; /* [in] Max number of tuples to search */ + uint64_t expire_time; /* [in] Find the tuple that has a TTL larger than expire_time */ + unsigned char *p; /* [out] First item of the tuple that has a TTL larger than expire_time */ + int expired; /* [out] Number of tuples that have TTLs less than expire_time */ + int index; /* Internally used */ + unsigned char *fptr; /* Internally used, temp ptr */ +}; + +/* Callback for lpFindCb(). Used to find number of expired fields as part of + * active expiry or when trying to find the position for the new field according + * to its expiry time.*/ +static int cbFindInListpack(const unsigned char *lp, unsigned char *p, + void *user, unsigned char *s, long long slen) +{ + (void) lp; + struct lpFingArgs *r = user; + + r->index++; + + if (r->max_to_search == 0) + return 0; /* Break the loop and return */ + + if (r->index % 3 == 1) { + r->fptr = p; /* First item of the tuple. */ + } else if (r->index % 3 == 0) { + serverAssert(!s); + + /* Third item of a tuple is expiry time */ + if (slen == HASH_LP_NO_TTL || (uint64_t) slen >= r->expire_time) { + r->p = r->fptr; + return 0; /* Break the loop and return */ + } + r->expired++; + r->max_to_search--; + } + + return 1; +} + +/* Returns number of expired fields. */ +static uint64_t listpackExExpireDryRun(const robj *o) { + serverAssert(o->encoding == OBJ_ENCODING_LISTPACK_EX); + + listpackEx *lpt = o->ptr; + + struct lpFingArgs r = { + .max_to_search = UINT64_MAX, + .expire_time = commandTimeSnapshot(), + }; + + lpFindCb(lpt->lp, NULL, &r, cbFindInListpack, 0); + return r.expired; +} + +/* Returns the expiration time of the item with the nearest expiration. */ +static uint64_t listpackExGetMinExpire(robj *o) { + serverAssert(o->encoding == OBJ_ENCODING_LISTPACK_EX); + + long long expireAt; + unsigned char *fptr; + listpackEx *lpt = o->ptr; + + /* As fields are ordered by expire time, first field will have the smallest + * expiry time. Third element is the expiry time of the first field */ + fptr = lpSeek(lpt->lp, 2); + if (fptr != NULL) { + serverAssert(lpGetIntegerValue(fptr, &expireAt)); + + /* Check if this is a non-volatile field. */ + if (expireAt != HASH_LP_NO_TTL) + return expireAt; + } + + return EB_EXPIRE_TIME_INVALID; +} + +/* Walk over fields and delete the expired ones. */ +void listpackExExpire(redisDb *db, robj *o, ExpireInfo *info) { + serverAssert(o->encoding == OBJ_ENCODING_LISTPACK_EX); + uint64_t expired = 0, min = EB_EXPIRE_TIME_INVALID; + unsigned char *ptr; + listpackEx *lpt = o->ptr; + + ptr = lpFirst(lpt->lp); + + while (ptr != NULL && (info->itemsExpired < info->maxToExpire)) { + long long val; + int64_t flen; + unsigned char intbuf[LP_INTBUF_SIZE], *fref; + + fref = lpGet(ptr, &flen, intbuf); + + ptr = lpNext(lpt->lp, ptr); + serverAssert(ptr); + ptr = lpNext(lpt->lp, ptr); + serverAssert(ptr && lpGetIntegerValue(ptr, &val)); + + /* Fields are ordered by expiry time. If we reached to a non-expired + * or a non-volatile field, we know rest is not yet expired. */ + if (val == HASH_LP_NO_TTL || (uint64_t) val > info->now) + break; + + propagateHashFieldDeletion(db, ((listpackEx *) o->ptr)->key, (char *)((fref) ? fref : intbuf), flen); + + ptr = lpNext(lpt->lp, ptr); + + info->itemsExpired++; + expired++; + } + + if (expired) + lpt->lp = lpDeleteRange(lpt->lp, 0, expired * 3); + + min = hashTypeGetNextTimeToExpire(o); + info->nextExpireTime = (min != EB_EXPIRE_TIME_INVALID) ? min : 0; +} + +static void listpackExAddInternal(robj *o, listpackEntry ent[3]) { + listpackEx *lpt = o->ptr; + + /* Shortcut, just append at the end if this is a non-volatile field. */ + if (ent[2].lval == HASH_LP_NO_TTL) { + lpt->lp = lpBatchAppend(lpt->lp, ent, 3); + return; + } + + struct lpFingArgs r = { + .max_to_search = UINT64_MAX, + .expire_time = ent[2].lval, + }; + + /* Check if there is a field with a larger TTL. */ + lpFindCb(lpt->lp, NULL, &r, cbFindInListpack, 0); + + /* If list is empty or there is no field with a larger TTL, result will be + * NULL. Otherwise, just insert before the found item.*/ + if (r.p) + lpt->lp = lpBatchInsert(lpt->lp, r.p, LP_BEFORE, ent, 3, NULL); + else + lpt->lp = lpBatchAppend(lpt->lp, ent, 3); +} + +/* Add new field ordered by expire time. */ +void listpackExAddNew(robj *o, char *field, size_t flen, + char *value, size_t vlen, uint64_t expireAt) { + listpackEntry ent[3] = { + {.sval = (unsigned char*) field, .slen = flen}, + {.sval = (unsigned char*) value, .slen = vlen}, + {.lval = expireAt} + }; + + listpackExAddInternal(o, ent); +} + +/* If expiry time is changed, this function will place field into the correct + * position. First, it deletes the field and re-inserts to the listpack ordered + * by expiry time. */ +static void listpackExUpdateExpiry(robj *o, sds field, + unsigned char *fptr, + unsigned char *vptr, + uint64_t expire_at) { + unsigned int slen = 0; + long long val = 0; + unsigned char tmp[512] = {0}; + unsigned char *valstr; + sds tmpval = NULL; + listpackEx *lpt = o->ptr; + + /* Copy value */ + valstr = lpGetValue(vptr, &slen, &val); + if (valstr) { + /* Normally, item length in the listpack is limited by + * 'hash-max-listpack-value' config. It is unlikely, but it might be + * larger than sizeof(tmp). */ + if (slen > sizeof(tmp)) + tmpval = sdsnewlen(valstr, slen); + else + memcpy(tmp, valstr, slen); + } + + /* Delete field name, value and expiry time */ + lpt->lp = lpDeleteRangeWithEntry(lpt->lp, &fptr, 3); + + listpackEntry ent[3] = {{0}}; + + ent[0].sval = (unsigned char*) field; + ent[0].slen = sdslen(field); + + if (valstr) { + ent[1].sval = tmpval ? (unsigned char *) tmpval : tmp; + ent[1].slen = slen; + } else { + ent[1].lval = val; + } + ent[2].lval = expire_at; + + listpackExAddInternal(o, ent); + sdsfree(tmpval); +} + +/* Update field expire time. */ +SetExRes hashTypeSetExpiryListpack(HashTypeSetEx *ex, sds field, + unsigned char *fptr, unsigned char *vptr, + unsigned char *tptr, uint64_t expireAt) +{ + long long expireTime; + uint64_t prevExpire = EB_EXPIRE_TIME_INVALID; + + serverAssert(lpGetIntegerValue(tptr, &expireTime)); + + if (expireTime != HASH_LP_NO_TTL) { + prevExpire = (uint64_t) expireTime; + } + + if (prevExpire == EB_EXPIRE_TIME_INVALID) { + /* For fields without expiry, LT condition is considered valid */ + if (ex->expireSetCond & (HFE_XX | HFE_GT)) + return HSETEX_NO_CONDITION_MET; + } else { + if (((ex->expireSetCond == HFE_GT) && (prevExpire >= expireAt)) || + ((ex->expireSetCond == HFE_LT) && (prevExpire <= expireAt)) || + (ex->expireSetCond == HFE_NX) ) + return HSETEX_NO_CONDITION_MET; + + /* Track of minimum expiration time (only later update global HFE DS) */ + if (ex->minExpireFields > prevExpire) + ex->minExpireFields = prevExpire; + } + + /* if expiration time is in the past */ + if (unlikely(checkAlreadyExpired(expireAt))) { + hashTypeDelete(ex->hashObj, field, 1); + ex->fieldDeleted++; + return HSETEX_DELETED; + } + + if (ex->minExpireFields > expireAt) + ex->minExpireFields = expireAt; + + listpackExUpdateExpiry(ex->hashObj, field, fptr, vptr, expireAt); + ex->fieldUpdated++; + return HSETEX_OK; +} + +/* Returns 1 if expired */ +int hashTypeIsExpired(const robj *o, uint64_t expireAt) { + if (o->encoding == OBJ_ENCODING_LISTPACK_EX) { + if (expireAt == HASH_LP_NO_TTL) + return 0; + } else if (o->encoding == OBJ_ENCODING_HT) { + if (expireAt == EB_EXPIRE_TIME_INVALID) + return 0; + } else { + serverPanic("Unknown encoding: %d", o->encoding); + } + + return (mstime_t) expireAt < commandTimeSnapshot(); +} + +/* Returns listpack pointer of the object. */ +unsigned char *hashTypeListpackGetLp(robj *o) { + if (o->encoding == OBJ_ENCODING_LISTPACK) + return o->ptr; + else if (o->encoding == OBJ_ENCODING_LISTPACK_EX) + return ((listpackEx*)o->ptr)->lp; + + serverPanic("Unknown encoding: %d", o->encoding); +} + /*----------------------------------------------------------------------------- * Hash type API *----------------------------------------------------------------------------*/ @@ -16,18 +608,19 @@ /* Check the length of a number of objects to see if we need to convert a * listpack to a real hash. Note that we only check string encoded objects * as their string length can be queried in constant time. */ -void hashTypeTryConversion(robj *o, robj **argv, int start, int end) { +void hashTypeTryConversion(redisDb *db, robj *o, robj **argv, int start, int end) { int i; size_t sum = 0; - if (o->encoding != OBJ_ENCODING_LISTPACK) return; + if (o->encoding != OBJ_ENCODING_LISTPACK && o->encoding != OBJ_ENCODING_LISTPACK_EX) + return; /* We guess that most of the values in the input are unique, so * if there are enough arguments we create a pre-sized hash, which * might over allocate memory if there are duplicates. */ size_t new_fields = (end - start + 1) / 2; if (new_fields > server.hash_max_listpack_entries) { - hashTypeConvert(o, OBJ_ENCODING_HT); + hashTypeConvert(o, OBJ_ENCODING_HT, &db->hexpires); dictExpand(o->ptr, new_fields); return; } @@ -37,122 +630,204 @@ void hashTypeTryConversion(robj *o, robj **argv, int start, int end) { continue; size_t len = sdslen(argv[i]->ptr); if (len > server.hash_max_listpack_value) { - hashTypeConvert(o, OBJ_ENCODING_HT); + hashTypeConvert(o, OBJ_ENCODING_HT, &db->hexpires); return; } sum += len; } - if (!lpSafeToAdd(o->ptr, sum)) - hashTypeConvert(o, OBJ_ENCODING_HT); + if (!lpSafeToAdd(hashTypeListpackGetLp(o), sum)) + hashTypeConvert(o, OBJ_ENCODING_HT, &db->hexpires); } -/* Get the value from a listpack encoded hash, identified by field. - * Returns -1 when the field cannot be found. */ -int hashTypeGetFromListpack(robj *o, sds field, +/* Get the value from a listpack encoded hash, identified by field. */ +GetFieldRes hashTypeGetFromListpack(robj *o, sds field, unsigned char **vstr, unsigned int *vlen, - long long *vll) + long long *vll, + uint64_t *expiredAt) { + *expiredAt = EB_EXPIRE_TIME_INVALID; unsigned char *zl, *fptr = NULL, *vptr = NULL; - serverAssert(o->encoding == OBJ_ENCODING_LISTPACK); - - zl = o->ptr; - fptr = lpFirst(zl); - if (fptr != NULL) { - fptr = lpFind(zl, fptr, (unsigned char*)field, sdslen(field), 1); + if (o->encoding == OBJ_ENCODING_LISTPACK) { + zl = o->ptr; + fptr = lpFirst(zl); if (fptr != NULL) { - /* Grab pointer to the value (fptr points to the field) */ - vptr = lpNext(zl, fptr); - serverAssert(vptr != NULL); + fptr = lpFind(zl, fptr, (unsigned char*)field, sdslen(field), 1); + if (fptr != NULL) { + /* Grab pointer to the value (fptr points to the field) */ + vptr = lpNext(zl, fptr); + serverAssert(vptr != NULL); + } } + } else if (o->encoding == OBJ_ENCODING_LISTPACK_EX) { + long long expire; + unsigned char *h; + listpackEx *lpt = o->ptr; + + fptr = lpFirst(lpt->lp); + if (fptr != NULL) { + fptr = lpFind(lpt->lp, fptr, (unsigned char*)field, sdslen(field), 2); + if (fptr != NULL) { + vptr = lpNext(lpt->lp, fptr); + serverAssert(vptr != NULL); + + h = lpNext(lpt->lp, vptr); + serverAssert(h && lpGetIntegerValue(h, &expire)); + if (expire != HASH_LP_NO_TTL) + *expiredAt = expire; + } + } + } else { + serverPanic("Unknown hash encoding: %d", o->encoding); } if (vptr != NULL) { *vstr = lpGetValue(vptr, vlen, vll); - return 0; + return GETF_OK; } - return -1; + return GETF_NOT_FOUND; } /* Get the value from a hash table encoded hash, identified by field. * Returns NULL when the field cannot be found, otherwise the SDS value * is returned. */ -sds hashTypeGetFromHashTable(robj *o, sds field) { +GetFieldRes hashTypeGetFromHashTable(robj *o, sds field, sds *value, uint64_t *expiredAt) { dictEntry *de; + *expiredAt = EB_EXPIRE_TIME_INVALID; + serverAssert(o->encoding == OBJ_ENCODING_HT); de = dictFind(o->ptr, field); - if (de == NULL) return NULL; - return dictGetVal(de); + + if (de == NULL) + return GETF_NOT_FOUND; + + *expiredAt = hfieldGetExpireTime(dictGetKey(de)); + *value = (sds) dictGetVal(de); + return GETF_OK; } /* Higher level function of hashTypeGet*() that returns the hash value - * associated with the specified field. If the field is found C_OK - * is returned, otherwise C_ERR. The returned object is returned by - * reference in either *vstr and *vlen if it's returned in string form, - * or stored in *vll if it's returned as a number. + * associated with the specified field. * - * If *vll is populated *vstr is set to NULL, so the caller - * can always check the function return by checking the return value - * for C_OK and checking if vll (or vstr) is NULL. */ -int hashTypeGetValue(robj *o, sds field, unsigned char **vstr, unsigned int *vlen, long long *vll) { - if (o->encoding == OBJ_ENCODING_LISTPACK) { + * Returned: + * - GetFieldRes: OK: Return Field's valid value + * NOT_FOUND: Field was not found. + * EXPIRED: Field is expired and Lazy deleted + * EXPIRED_HASH: Returned only if the field is the last one in the + * hash and the hash is deleted. + * - vstr, vlen : if string, ref in either *vstr and *vlen if it's + * returned in string form, + * - vll : or stored in *vll if it's returned as a number. + * If *vll is populated *vstr is set to NULL, so the caller can + * always check the function return by checking the return value + * for GETF_OK and checking if vll (or vstr) is NULL. + * + */ +GetFieldRes hashTypeGetValue(redisDb *db, robj *o, sds field, unsigned char **vstr, + unsigned int *vlen, long long *vll) { + uint64_t expiredAt; + sds key; + GetFieldRes res; + if (o->encoding == OBJ_ENCODING_LISTPACK || + o->encoding == OBJ_ENCODING_LISTPACK_EX) { *vstr = NULL; - if (hashTypeGetFromListpack(o, field, vstr, vlen, vll) == 0) - return C_OK; + res = hashTypeGetFromListpack(o, field, vstr, vlen, vll, &expiredAt); + + if (res == GETF_NOT_FOUND) + return GETF_NOT_FOUND; + } else if (o->encoding == OBJ_ENCODING_HT) { - sds value; - if ((value = hashTypeGetFromHashTable(o, field)) != NULL) { - *vstr = (unsigned char*) value; - *vlen = sdslen(value); - return C_OK; - } + sds value = NULL; + res = hashTypeGetFromHashTable(o, field, &value, &expiredAt); + + if (res == GETF_NOT_FOUND) + return GETF_NOT_FOUND; + + *vstr = (unsigned char*) value; + *vlen = sdslen(value); } else { serverPanic("Unknown hash encoding"); } - return C_ERR; + + /* Don't expire anything while loading. It will be done later. */ + if ( (server.loading) || + (server.lazy_expire_disabled) || + ((server.masterhost) && (server.current_client && (server.current_client->flags & CLIENT_MASTER))) || + (expiredAt >= (uint64_t) commandTimeSnapshot()) ) + return GETF_OK; + + /* Got expired. Extract attached key from LISTPACK_EX/HT */ + if (o->encoding == OBJ_ENCODING_LISTPACK_EX) + key = ((listpackEx *) o->ptr)->key; + else + key = ((dictExpireMetadata *) dictMetadata((dict*)o->ptr))->key; + + /* delete the field and propagate the deletion */ + serverAssert(hashTypeDelete(o, field, 1) == 1); + propagateHashFieldDeletion(db, key, field, sdslen(field)); + + /* If the field is the last one in the hash, then the hash will be deleted */ + if (hashTypeLength(o, 0) == 0) { + robj *keyObj = createStringObject(key, sdslen(key)); + notifyKeyspaceEvent(NOTIFY_GENERIC, "del", keyObj, db->id); + dbDelete(db,keyObj); + decrRefCount(keyObj); + return GETF_EXPIRED_HASH; + } + + return GETF_EXPIRED; } /* Like hashTypeGetValue() but returns a Redis object, which is useful for * interaction with the hash type outside t_hash.c. * The function returns NULL if the field is not found in the hash. Otherwise - * a newly allocated string object with the value is returned. */ -robj *hashTypeGetValueObject(robj *o, sds field) { + * a newly allocated string object with the value is returned. + * + * isHashDeleted - If attempted to access expired field and it's the last field + * in the hash, then the hash will as well be deleted. In this case, + * isHashDeleted will be set to 1. + */ +robj *hashTypeGetValueObject(redisDb *db, robj *o, sds field, int *isHashDeleted) { unsigned char *vstr; unsigned int vlen; long long vll; - if (hashTypeGetValue(o,field,&vstr,&vlen,&vll) == C_ERR) return NULL; - if (vstr) return createStringObject((char*)vstr,vlen); - else return createStringObjectFromLongLong(vll); + *isHashDeleted = 0; /*default*/ + GetFieldRes res = hashTypeGetValue(db,o,field,&vstr,&vlen,&vll); + + if (res == GETF_OK) { + if (vstr) return createStringObject((char*)vstr,vlen); + else return createStringObjectFromLongLong(vll); + } + + if (res == GETF_EXPIRED_HASH) + *isHashDeleted = 1; + + /* GETF_EXPIRED_HASH, GETF_EXPIRED, GETF_NOT_FOUND */ + return NULL; } -/* Higher level function using hashTypeGet*() to return the length of the - * object associated with the requested field, or 0 if the field does not - * exist. */ -size_t hashTypeGetValueLength(robj *o, sds field) { - size_t len = 0; +/* Test if the specified field exists in the given hash. If the field is + * expired (HFE), then it will be lazy deleted + * + * Returns 1 if the field exists, and 0 when it doesn't. + * + * isHashDeleted - If attempted to access expired field and it is the last field + * in the hash, then the hash will as well be deleted. In this case, + * isHashDeleted will be set to 1. + */ +int hashTypeExists(redisDb *db, robj *o, sds field, int *isHashDeleted) { unsigned char *vstr = NULL; unsigned int vlen = UINT_MAX; long long vll = LLONG_MAX; - if (hashTypeGetValue(o, field, &vstr, &vlen, &vll) == C_OK) - len = vstr ? vlen : sdigits10(vll); - - return len; -} - -/* Test if the specified field exists in the given hash. Returns 1 if the field - * exists, and 0 when it doesn't. */ -int hashTypeExists(robj *o, sds field) { - unsigned char *vstr = NULL; - unsigned int vlen = UINT_MAX; - long long vll = LLONG_MAX; - - return hashTypeGetValue(o, field, &vstr, &vlen, &vll) == C_OK; + GetFieldRes res = hashTypeGetValue(db, o, field, &vstr, &vlen, &vll); + *isHashDeleted = (res == GETF_EXPIRED_HASH) ? 1 : 0; + return (res == GETF_OK) ? 1 : 0; } /* Add a new field, overwrite the old with the new value if it already exists. @@ -162,8 +837,9 @@ int hashTypeExists(robj *o, sds field) { * caller retains ownership of the strings passed. However this behavior * can be effected by passing appropriate flags (possibly bitwise OR-ed): * - * HASH_SET_TAKE_FIELD -- The SDS field ownership passes to the function. - * HASH_SET_TAKE_VALUE -- The SDS value ownership passes to the function. + * HASH_SET_TAKE_FIELD -- The SDS field ownership passes to the function. + * HASH_SET_TAKE_VALUE -- The SDS value ownership passes to the function. + * HASH_SET_KEEP_FIELD -- keep original field along with TTL if already exists * * When the flags are used the caller does not need to release the passed * SDS string(s). It's up to the function to use the string to create a new @@ -173,24 +849,332 @@ int hashTypeExists(robj *o, sds field) { * semantics of copying the values if needed. * */ -#define HASH_SET_TAKE_FIELD (1<<0) -#define HASH_SET_TAKE_VALUE (1<<1) +#define HASH_SET_TAKE_FIELD (1<<0) +#define HASH_SET_TAKE_VALUE (1<<1) +#define HASH_SET_KEEP_FIELD (1<<2) #define HASH_SET_COPY 0 -int hashTypeSet(robj *o, sds field, sds value, int flags) { - int update = 0; +int hashTypeSet(redisDb *db, robj *o, sds field, sds value, int flags) { + HashTypeSet set = {value, flags}; + return (hashTypeSetEx(db, o, field, &set, 0, NULL) == HSET_UPDATE) ? 1 : 0; +} + +SetExRes hashTypeSetExpiry(HashTypeSetEx *ex, sds field, uint64_t expireAt, dictEntry **de) { + dict *ht = ex->hashObj->ptr; + dictEntry *newEntry = NULL, *existingEntry = NULL; + + /* New field with expiration metadata */ + hfield hfNew = hfieldNew(field, sdslen(field), 1 /*withExpireMeta*/); + + if ((ex->fieldSetCond == FIELD_DONT_CREATE) || (ex->fieldSetCond == FIELD_DONT_CREATE2)) { + if ((existingEntry = dictFind(ht, field)) == NULL) { + hfieldFree(hfNew); + return (ex->fieldSetCond == FIELD_DONT_CREATE) ? + HSETEX_NO_CONDITION_MET : HSETEX_NO_FIELD; + } + } else { + dictUseStoredKeyApi(ht, 1); + newEntry = dictAddRaw(ht, hfNew, &existingEntry); + dictUseStoredKeyApi(ht, 0); + } + + if (newEntry) { + *de = newEntry; + + if (ex->expireSetCond & (HFE_XX | HFE_LT | HFE_GT)) { + dictDelete(ht, field); + return HSETEX_NO_CONDITION_MET; + } + } else { /* field exist */ + *de = existingEntry; + + if (ex->fieldSetCond == FIELD_DONT_OVRWRT) { + hfieldFree(hfNew); + return HSETEX_NO_CONDITION_MET; + } + + hfield hfOld = dictGetKey(existingEntry); + + /* If field doesn't have expiry metadata attached */ + if (!hfieldIsExpireAttached(hfOld)) { + + /* For fields without expiry, LT condition is considered valid */ + if (ex->expireSetCond & (HFE_XX | HFE_GT)) { + hfieldFree(hfNew); + return HSETEX_NO_CONDITION_MET; + } + + /* Delete old field. Below goanna dictSetKey(..,hfNew) */ + hfieldFree(hfOld); + + } 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 */ + if (prevExpire != EB_EXPIRE_TIME_INVALID) { + if (((ex->expireSetCond == HFE_GT) && (prevExpire >= expireAt)) || + ((ex->expireSetCond == HFE_LT) && (prevExpire <= expireAt)) || + (ex->expireSetCond == HFE_NX) ) + return HSETEX_NO_CONDITION_MET; + + /* remove old expiry time from hash's private ebuckets */ + dictExpireMetadata *dm = (dictExpireMetadata *) dictMetadata(ht); + ebRemove(&dm->hfe, &hashFieldExpireBucketsType, hfOld); + + /* Track of minimum expiration time (only later update global HFE DS) */ + if (ex->minExpireFields > prevExpire) + ex->minExpireFields = prevExpire; + + } else { + /* field has invalid expiry. No need to ebRemove() */ + + /* Check XX|LT|GT */ + if (ex->expireSetCond & (HFE_XX | HFE_GT)) + return HSETEX_NO_CONDITION_MET; + } + + /* Reuse hfOld as hfNew and rewrite its expiry with ebAdd() */ + hfNew = hfOld; + } + + dictSetKey(ht, existingEntry, hfNew); + } + + /* if expiration time is in the past */ + if (unlikely(checkAlreadyExpired(expireAt))) { + hashTypeDelete(ex->hashObj, field, 1); + ex->fieldDeleted++; + return HSETEX_DELETED; + } + + if (ex->minExpireFields > expireAt) + ex->minExpireFields = expireAt; + + dictExpireMetadata *dm = (dictExpireMetadata *) dictMetadata(ht); + ebAdd(&dm->hfe, &hashFieldExpireBucketsType, hfNew, expireAt); + ex->fieldUpdated++; + return HSETEX_OK; +} + +/* + * Set fields OR field's expiration (See also `setex*` comment above) + * + * Take care to call first hashTypeSetExInit() and then call this function. + * Finally, call hashTypeSetExDone() to notify and update global HFE DS. + * + * NOTE: this functions is also called during RDB load to set dict-encoded + * fields with and without expiration. + */ +SetExRes hashTypeSetEx(redisDb *db, robj *o, sds field, HashTypeSet *setKeyVal, + uint64_t expireAt, HashTypeSetEx *exInfo) +{ + SetExRes res = HSETEX_OK; + int isSetKeyValue = (setKeyVal) ? 1 : 0; + int isSetExpire = (exInfo) ? 1 : 0; + int flags = (setKeyVal) ? setKeyVal->flags : 0; /* Check if the field is too long for listpack, and convert before adding the item. * This is needed for HINCRBY* case since in other commands this is handled early by * hashTypeTryConversion, so this check will be a NOP. */ - if (o->encoding == OBJ_ENCODING_LISTPACK) { - if (sdslen(field) > server.hash_max_listpack_value || sdslen(value) > server.hash_max_listpack_value) - hashTypeConvert(o, OBJ_ENCODING_HT); + if (o->encoding == OBJ_ENCODING_LISTPACK || + o->encoding == OBJ_ENCODING_LISTPACK_EX) + { + if ( (isSetKeyValue) && + (sdslen(field) > server.hash_max_listpack_value || + sdslen(setKeyVal->value) > server.hash_max_listpack_value) ) + { + hashTypeConvert(o, OBJ_ENCODING_HT, &db->hexpires); + } else { + res = hashTypeSetExListpack(db, o, field, setKeyVal, expireAt, exInfo); + goto SetExDone; /*done*/ + } } - - if (o->encoding == OBJ_ENCODING_LISTPACK) { - unsigned char *zl, *fptr, *vptr; - zl = o->ptr; + if (o->encoding != OBJ_ENCODING_HT) + serverPanic("Unknown hash encoding"); + + /*** now deal with HT ***/ + hfield newField; + dict *ht = o->ptr; + dictEntry *de; + + /* If needed to set the field along with expiry */ + if (isSetExpire) { + res = hashTypeSetExpiry(exInfo, field, expireAt, &de); + if (res != HSETEX_OK) goto SetExDone; + } else { + dictEntry *existing; + /* Cannot leverage HASH_SET_TAKE_FIELD since hfield is not of type sds */ + newField = hfieldNew(field, sdslen(field), 0); + + /* stored key is different than lookup key */ + dictUseStoredKeyApi(ht, 1); + de = dictAddRaw(ht, newField, &existing); + dictUseStoredKeyApi(ht, 0); + + /* If field already exists, then update "field". "Value" will be set afterward */ + if (de == NULL) { + if (flags & HASH_SET_KEEP_FIELD) { + /* Not keep old field along with TTL */ + hfieldFree(newField); + } else { + /* If attached TTL to the old field, then remove it from hash's private ebuckets */ + hfield oldField = dictGetKey(existing); + hfieldPersist(o, oldField); + hfieldFree(oldField); + dictSetKey(ht, existing, newField); + } + sdsfree(dictGetVal(existing)); + res = HSET_UPDATE; + de = existing; + } + } + + /* If need to set value */ + if (isSetKeyValue) { + if (flags & HASH_SET_TAKE_VALUE) { + dictSetVal(ht, de, setKeyVal->value); + flags &= ~HASH_SET_TAKE_VALUE; + } else { + dictSetVal(ht, de, sdsdup(setKeyVal->value)); + } + } + +SetExDone: + /* Free SDS strings we did not referenced elsewhere if the flags + * want this function to be responsible. */ + if (flags & HASH_SET_TAKE_FIELD && field) sdsfree(field); + if (flags & HASH_SET_TAKE_VALUE && setKeyVal->value) sdsfree(setKeyVal->value); + return res; +} + +void initDictExpireMetadata(sds key, robj *o) { + dict *ht = o->ptr; + + dictExpireMetadata *m = (dictExpireMetadata *) dictMetadata(ht); + m->key = key; + m->hfe = ebCreate(); /* Allocate HFE DS */ + m->expireMeta.trash = 1; /* mark as trash (as long it wasn't ebAdd()) */ +} + +/* + * Init HashTypeSetEx struct before calling hashTypeSetEx() + * + * Don't have to provide client and "cmd". If provided, then notification once + * done by function hashTypeSetExDone(). + */ +int hashTypeSetExInit(robj *key, robj *o, client *c, redisDb *db, const char *cmd, FieldSetCond fieldSetCond, + ExpireSetCond expireSetCond, HashTypeSetEx *ex) +{ + dict *ht = o->ptr; + + ex->fieldSetCond = fieldSetCond; + 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 */ + if (ex->hashObj->encoding == OBJ_ENCODING_LISTPACK) { + hashTypeConvert(ex->hashObj, OBJ_ENCODING_LISTPACK_EX, &c->db->hexpires); + + listpackEx *lpt = ex->hashObj->ptr; + dictEntry *de = dbFind(c->db, key->ptr); + serverAssert(de != NULL); + lpt->key = dictGetKey(de); + } else if (ex->hashObj->encoding == OBJ_ENCODING_HT) { + /* Take care dict has HFE metadata */ + if (!isDictWithMetaHFE(ht)) { + /* Realloc (only header of dict) with metadata for hash-field expiration */ + dictTypeAddMeta(&ht, &mstrHashDictTypeWithHFE); + dictExpireMetadata *m = (dictExpireMetadata *) dictMetadata(ht); + ex->hashObj->ptr = ht; + + /* Find the key in the keyspace. Need to keep reference to the key for + * notifications or even removal of the hash */ + dictEntry *de = dbFind(db, key->ptr); + serverAssert(de != NULL); + + /* Fillup dict HFE metadata */ + m->key = dictGetKey(de); /* reference key in keyspace */ + m->hfe = ebCreate(); /* Allocate HFE DS */ + m->expireMeta.trash = 1; /* mark as trash (as long it wasn't ebAdd()) */ + } + } + + ex->minExpire = hashTypeGetMinExpire(ex->hashObj); + return C_OK; +} + +/* + * After calling hashTypeSetEx() for setting fields or their expiry, call this + * function to notify and 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) { + + if (ex->c) { + server.dirty += ex->fieldDeleted + ex->fieldUpdated; + signalModifiedKey(ex->c, ex->db, ex->key); + notifyKeyspaceEvent(NOTIFY_HASH, "hexpire", ex->key, ex->db->id); + } + if (ex->fieldDeleted && hashTypeLength(ex->hashObj, 0) == 0) { + dbDelete(ex->db,ex->key); + if (ex->c) notifyKeyspaceEvent(NOTIFY_GENERIC,"del",ex->key, ex->db->id); + } else { + /* If minimum HFE of the hash is smaller than expiration time of the + * specified fields in the command as well as it is smaller or equal + * than expiration time provided in the command, then the minimum + * HFE of the hash won't change following this command. */ + if ((ex->minExpire < ex->minExpireFields)) + return; + + /* retrieve new expired time. It might have changed. */ + uint64_t newMinExpire = hashTypeGetNextTimeToExpire(ex->hashObj); + + /* Calculate the diff between old minExpire and newMinExpire. If it is + * only few seconds, then don't have to update global HFE DS. At the worst + * case fields of hash will be active-expired up to few seconds later. + * + * In any case, active-expire operation will know to update global + * HFE DS more efficiently than here for a single item. + */ + uint64_t diff = (ex->minExpire > newMinExpire) ? + (ex->minExpire - newMinExpire) : (newMinExpire - ex->minExpire); + if (diff < HASH_NEW_EXPIRE_DIFF_THRESHOLD) return; + + if (ex->minExpire != EB_EXPIRE_TIME_INVALID) + ebRemove(&ex->db->hexpires, &hashExpireBucketsType, ex->hashObj); + if (newMinExpire != EB_EXPIRE_TIME_INVALID) + ebAdd(&ex->db->hexpires, &hashExpireBucketsType, ex->hashObj, newMinExpire); + } + } +} + +/* Check if the field is too long for listpack, and convert before adding the item. + * This is needed for HINCRBY* case since in other commands this is handled early by + * hashTypeTryConversion, so this check will be a NOP. */ +static SetExRes hashTypeSetExListpack(redisDb *db, robj *o, sds field, HashTypeSet *setParams, + uint64_t expireAt, HashTypeSetEx *exParams) +{ + int res = HSETEX_OK; + unsigned char *fptr = NULL, *vptr = NULL, *tptr = NULL; + + if (o->encoding == OBJ_ENCODING_LISTPACK) { + /* If reached here, then no need to set expiration. Otherwise, as precond + * listpack is converted to listpackex by hashTypeSetExInit() */ + + unsigned char *zl = o->ptr; fptr = lpFirst(zl); if (fptr != NULL) { fptr = lpFind(zl, fptr, (unsigned char*)field, sdslen(field), 1); @@ -198,61 +1182,85 @@ int hashTypeSet(robj *o, sds field, sds value, int flags) { /* Grab pointer to the value (fptr points to the field) */ vptr = lpNext(zl, fptr); serverAssert(vptr != NULL); - update = 1; + res = HSET_UPDATE; /* Replace value */ - zl = lpReplace(zl, &vptr, (unsigned char*)value, sdslen(value)); + zl = lpReplace(zl, &vptr, (unsigned char *) setParams->value, sdslen(setParams->value)); } } - if (!update) { + if (res != HSET_UPDATE) { /* Push new field/value pair onto the tail of the listpack */ zl = lpAppend(zl, (unsigned char*)field, sdslen(field)); - zl = lpAppend(zl, (unsigned char*)value, sdslen(value)); + zl = lpAppend(zl, (unsigned char*)setParams->value, sdslen(setParams->value)); } o->ptr = zl; + goto out; + } else if (o->encoding == OBJ_ENCODING_LISTPACK_EX) { + listpackEx *lpt = o->ptr; + long long expireTime = HASH_LP_NO_TTL; - /* Check if the listpack needs to be converted to a hash table */ - if (hashTypeLength(o) > server.hash_max_listpack_entries) - hashTypeConvert(o, OBJ_ENCODING_HT); - } else if (o->encoding == OBJ_ENCODING_HT) { - dict *ht = o->ptr; - dictEntry *de, *existing; - sds v; - if (flags & HASH_SET_TAKE_VALUE) { - v = value; - value = NULL; - } else { - v = sdsdup(value); - } - de = dictAddRaw(ht, field, &existing); - if (de) { - dictSetVal(ht, de, v); - if (flags & HASH_SET_TAKE_FIELD) { - field = NULL; - } else { - dictSetKey(ht, de, sdsdup(field)); + fptr = lpFirst(lpt->lp); + if (fptr != NULL) { + fptr = lpFind(lpt->lp, fptr, (unsigned char*)field, sdslen(field), 2); + if (fptr != NULL) { + /* Grab pointer to the value (fptr points to the field) */ + vptr = lpNext(lpt->lp, fptr); + serverAssert(vptr != NULL); + + if (setParams) { + /* Replace value */ + lpt->lp = lpReplace(lpt->lp, &vptr, + (unsigned char *) setParams->value, + sdslen(setParams->value)); + + fptr = lpPrev(lpt->lp, vptr); + serverAssert(fptr != NULL); + res = HSET_UPDATE; + } + tptr = lpNext(lpt->lp, vptr); + serverAssert(tptr && lpGetIntegerValue(tptr, &expireTime)); + + /* Keep, update or clear TTL */ + if (setParams && setParams->flags & HASH_SET_KEEP_FIELD) { + /* keep old field along with TTL */ + } else if (exParams) { + res = hashTypeSetExpiryListpack(exParams, field, fptr, vptr, tptr, + expireAt); + if (res != HSETEX_OK) + goto out; + } else if (res == HSET_UPDATE && expireTime != HASH_LP_NO_TTL) { + /* Clear TTL */ + listpackExUpdateExpiry(o, field, fptr, vptr, HASH_LP_NO_TTL); + } } - } else { - sdsfree(dictGetVal(existing)); - dictSetVal(ht, existing, v); - update = 1; } - } else { - serverPanic("Unknown hash encoding"); - } - /* Free SDS strings we did not referenced elsewhere if the flags - * want this function to be responsible. */ - if (flags & HASH_SET_TAKE_FIELD && field) sdsfree(field); - if (flags & HASH_SET_TAKE_VALUE && value) sdsfree(value); - return update; + if (!fptr) { + if (setParams) { + listpackExAddNew(o, field, sdslen(field), + setParams->value, sdslen(setParams->value), + exParams ? expireAt : HASH_LP_NO_TTL); + } else { + res = HSETEX_NO_FIELD; + } + } + } +out: + /* Check if the listpack needs to be converted to a hash table */ + if (hashTypeLength(o, 0) > server.hash_max_listpack_entries) + hashTypeConvert(o, OBJ_ENCODING_HT, &db->hexpires); + + return res; } /* Delete an element from a hash. - * Return 1 on deleted and 0 on not found. */ -int hashTypeDelete(robj *o, sds field) { + * + * Return 1 on deleted and 0 on not found. + * isSdsField - 1 if the field is sds, 0 if it is hfield */ +int hashTypeDelete(robj *o, void *field, int isSdsField) { int deleted = 0; + int fieldLen = (isSdsField) ? sdslen((sds)field) : hfieldlen((hfield)field); if (o->encoding == OBJ_ENCODING_LISTPACK) { unsigned char *zl, *fptr; @@ -260,7 +1268,7 @@ int hashTypeDelete(robj *o, sds field) { zl = o->ptr; fptr = lpFirst(zl); if (fptr != NULL) { - fptr = lpFind(zl, fptr, (unsigned char*)field, sdslen(field), 1); + fptr = lpFind(zl, fptr, (unsigned char*)field, fieldLen, 1); if (fptr != NULL) { /* Delete both of the key and the value. */ zl = lpDeleteRangeWithEntry(zl,&fptr,2); @@ -268,10 +1276,26 @@ int hashTypeDelete(robj *o, sds field) { deleted = 1; } } + } else if (o->encoding == OBJ_ENCODING_LISTPACK_EX) { + unsigned char *fptr; + listpackEx *lpt = o->ptr; + + fptr = lpFirst(lpt->lp); + if (fptr != NULL) { + fptr = lpFind(lpt->lp, fptr, (unsigned char*)field, fieldLen, 2); + if (fptr != NULL) { + /* Delete field, value and ttl */ + lpt->lp = lpDeleteRangeWithEntry(lpt->lp, &fptr, 3); + deleted = 1; + } + } } else if (o->encoding == OBJ_ENCODING_HT) { + /* dictDelete() will call dictHfieldDestructor() */ + dictUseStoredKeyApi((dict*)o->ptr, isSdsField ? 0 : 1); if (dictDelete((dict*)o->ptr, field) == C_OK) { deleted = 1; } + dictUseStoredKeyApi((dict*)o->ptr, 0); } else { serverPanic("Unknown hash encoding"); @@ -279,14 +1303,33 @@ int hashTypeDelete(robj *o, sds field) { return deleted; } -/* Return the number of elements in a hash. */ -unsigned long hashTypeLength(const robj *o) { +/* Return the number of elements in a hash. + * + * Note, subtractExpiredFields=1 might be pricy in case there are many HFEs + */ +unsigned long hashTypeLength(const robj *o, int subtractExpiredFields) { unsigned long length = ULONG_MAX; if (o->encoding == OBJ_ENCODING_LISTPACK) { length = lpLength(o->ptr) / 2; + } else if (o->encoding == OBJ_ENCODING_LISTPACK_EX) { + listpackEx *lpt = o->ptr; + length = lpLength(lpt->lp) / 3; + + if (subtractExpiredFields && lpt->meta.trash == 0) + length -= listpackExExpireDryRun(o); } else if (o->encoding == OBJ_ENCODING_HT) { - length = dictSize((const dict*)o->ptr); + uint64_t expiredItems = 0; + dict *d = (dict*)o->ptr; + if (subtractExpiredFields && isDictWithMetaHFE(d)) { + dictExpireMetadata *meta = (dictExpireMetadata *) dictMetadata(d); + /* If dict registered in global HFE DS */ + if (meta->expireMeta.trash == 0) + expiredItems = ebExpireDryRun(meta->hfe, + &hashFieldExpireBucketsType, + commandTimeSnapshot()); + } + length = dictSize(d) - expiredItems; } else { serverPanic("Unknown hash encoding"); } @@ -298,9 +1341,13 @@ hashTypeIterator *hashTypeInitIterator(robj *subject) { hi->subject = subject; hi->encoding = subject->encoding; - if (hi->encoding == OBJ_ENCODING_LISTPACK) { + if (hi->encoding == OBJ_ENCODING_LISTPACK || + hi->encoding == OBJ_ENCODING_LISTPACK_EX) + { hi->fptr = NULL; hi->vptr = NULL; + hi->tptr = NULL; + hi->expire_time = EB_EXPIRE_TIME_INVALID; } else if (hi->encoding == OBJ_ENCODING_HT) { hi->di = dictGetIterator(subject->ptr); } else { @@ -317,7 +1364,8 @@ void hashTypeReleaseIterator(hashTypeIterator *hi) { /* Move to the next entry in the hash. Return C_OK when the next entry * could be found and C_ERR when the iterator reaches the end. */ -int hashTypeNext(hashTypeIterator *hi) { +int hashTypeNext(hashTypeIterator *hi, int skipExpiredFields) { + hi->expire_time = EB_EXPIRE_TIME_INVALID; if (hi->encoding == OBJ_ENCODING_LISTPACK) { unsigned char *zl; unsigned char *fptr, *vptr; @@ -344,8 +1392,56 @@ int hashTypeNext(hashTypeIterator *hi) { /* fptr, vptr now point to the first or next pair */ hi->fptr = fptr; hi->vptr = vptr; + } else if (hi->encoding == OBJ_ENCODING_LISTPACK_EX) { + long long expire_time; + unsigned char *zl = hashTypeListpackGetLp(hi->subject); + unsigned char *fptr, *vptr, *tptr; + + fptr = hi->fptr; + vptr = hi->vptr; + tptr = hi->tptr; + + if (fptr == NULL) { + /* Initialize cursor */ + serverAssert(vptr == NULL); + fptr = lpFirst(zl); + } else { + /* Advance cursor */ + serverAssert(tptr != NULL); + fptr = lpNext(zl, tptr); + } + if (fptr == NULL) return C_ERR; + + while (fptr != NULL) { + /* Grab pointer to the value (fptr points to the field) */ + vptr = lpNext(zl, fptr); + serverAssert(vptr != NULL); + + tptr = lpNext(zl, vptr); + serverAssert(tptr && lpGetIntegerValue(tptr, &expire_time)); + + if (!skipExpiredFields || !hashTypeIsExpired(hi->subject, expire_time)) + break; + + fptr = lpNext(zl, tptr); + } + if (fptr == NULL) return C_ERR; + + /* fptr, vptr now point to the first or next pair */ + hi->fptr = fptr; + hi->vptr = vptr; + hi->tptr = tptr; + hi->expire_time = (expire_time != HASH_LP_NO_TTL) ? (uint64_t) expire_time : EB_EXPIRE_TIME_INVALID; } else if (hi->encoding == OBJ_ENCODING_HT) { - if ((hi->de = dictNext(hi->di)) == NULL) return C_ERR; + + while ((hi->de = dictNext(hi->di)) != NULL) { + hi->expire_time = hfieldGetExpireTime(dictGetKey(hi->de)); + /* this condition still valid if expire_time equals EB_EXPIRE_TIME_INVALID */ + if (skipExpiredFields && ((mstime_t)hi->expire_time < commandTimeSnapshot())) + continue; + return C_OK; + } + return C_ERR; } else { serverPanic("Unknown hash encoding"); } @@ -357,28 +1453,45 @@ int hashTypeNext(hashTypeIterator *hi) { void hashTypeCurrentFromListpack(hashTypeIterator *hi, int what, unsigned char **vstr, unsigned int *vlen, - long long *vll) + long long *vll, + uint64_t *expireTime) { - serverAssert(hi->encoding == OBJ_ENCODING_LISTPACK); + serverAssert(hi->encoding == OBJ_ENCODING_LISTPACK || + hi->encoding == OBJ_ENCODING_LISTPACK_EX); if (what & OBJ_HASH_KEY) { *vstr = lpGetValue(hi->fptr, vlen, vll); } else { *vstr = lpGetValue(hi->vptr, vlen, vll); } + + if (expireTime) + *expireTime = hi->expire_time; } /* Get the field or value at iterator cursor, for an iterator on a hash value * encoded as a hash table. Prototype is similar to - * `hashTypeGetFromHashTable`. */ -sds hashTypeCurrentFromHashTable(hashTypeIterator *hi, int what) { + * `hashTypeGetFromHashTable`. + * + * expireTime - If parameter is not null, then the function will return the expire + * time of the field. If expiry not set, return EB_EXPIRE_TIME_INVALID + */ +void hashTypeCurrentFromHashTable(hashTypeIterator *hi, int what, char **str, size_t *len, uint64_t *expireTime) { serverAssert(hi->encoding == OBJ_ENCODING_HT); + hfield key = NULL; if (what & OBJ_HASH_KEY) { - return dictGetKey(hi->de); + key = dictGetKey(hi->de); + *str = key; + *len = hfieldlen(key); } else { - return dictGetVal(hi->de); + sds val = dictGetVal(hi->de); + *str = val; + *len = sdslen(val); } + + if (expireTime) + *expireTime = hi->expire_time; } /* Higher level function of hashTypeCurrent*() that returns the hash value @@ -391,14 +1504,24 @@ sds hashTypeCurrentFromHashTable(hashTypeIterator *hi, int what) { * If *vll is populated *vstr is set to NULL, so the caller * can always check the function return by checking the return value * type checking if vstr == NULL. */ -void hashTypeCurrentObject(hashTypeIterator *hi, int what, unsigned char **vstr, unsigned int *vlen, long long *vll) { - if (hi->encoding == OBJ_ENCODING_LISTPACK) { +void hashTypeCurrentObject(hashTypeIterator *hi, + int what, + unsigned char **vstr, + unsigned int *vlen, + long long *vll, + uint64_t *expireTime) +{ + if (hi->encoding == OBJ_ENCODING_LISTPACK || + hi->encoding == OBJ_ENCODING_LISTPACK_EX) + { *vstr = NULL; - hashTypeCurrentFromListpack(hi, what, vstr, vlen, vll); + hashTypeCurrentFromListpack(hi, what, vstr, vlen, vll, expireTime); } else if (hi->encoding == OBJ_ENCODING_HT) { - sds ele = hashTypeCurrentFromHashTable(hi, what); + char *ele; + size_t eleLen; + hashTypeCurrentFromHashTable(hi, what, &ele, &eleLen, expireTime); *vstr = (unsigned char*) ele; - *vlen = sdslen(ele); + *vlen = eleLen; } else { serverPanic("Unknown hash encoding"); } @@ -411,12 +1534,32 @@ sds hashTypeCurrentObjectNewSds(hashTypeIterator *hi, int what) { unsigned int vlen; long long vll; - hashTypeCurrentObject(hi,what,&vstr,&vlen,&vll); + hashTypeCurrentObject(hi,what,&vstr,&vlen,&vll, NULL); if (vstr) return sdsnewlen(vstr,vlen); return sdsfromlonglong(vll); } -robj *hashTypeLookupWriteOrCreate(client *c, robj *key) { +/* Return the key at the current iterator position as a new hfield string. */ +hfield hashTypeCurrentObjectNewHfield(hashTypeIterator *hi) { + char buf[LONG_STR_SIZE]; + unsigned char *vstr; + unsigned int vlen; + long long vll; + uint64_t expireTime; + hfield hf; + + hashTypeCurrentObject(hi,OBJ_HASH_KEY,&vstr,&vlen,&vll, &expireTime); + + if (!vstr) { + vlen = ll2string(buf, sizeof(buf), vll); + vstr = (unsigned char *) buf; + } + + hf = hfieldNew(vstr,vlen, expireTime != EB_EXPIRE_TIME_INVALID); + return hf; +} + +static robj *hashTypeLookupWriteOrCreate(client *c, robj *key) { robj *o = lookupKeyWrite(c->db,key); if (checkType(c,o,OBJ_HASH)) return NULL; @@ -434,25 +1577,43 @@ void hashTypeConvertListpack(robj *o, int enc) { if (enc == OBJ_ENCODING_LISTPACK) { /* Nothing to do... */ + } else if (enc == OBJ_ENCODING_LISTPACK_EX) { + unsigned char *p; + + /* Append HASH_LP_NO_TTL to each field name - value pair. */ + p = lpFirst(o->ptr); + while (p != NULL) { + p = lpNext(o->ptr, p); + serverAssert(p); + + o->ptr = lpInsertInteger(o->ptr, HASH_LP_NO_TTL, p, LP_AFTER, &p); + p = lpNext(o->ptr, p); + } + + listpackEx *lpt = listpackExCreate(); + lpt->lp = o->ptr; + o->encoding = OBJ_ENCODING_LISTPACK_EX; + o->ptr = lpt; } else if (enc == OBJ_ENCODING_HT) { hashTypeIterator *hi; dict *dict; int ret; hi = hashTypeInitIterator(o); - dict = dictCreate(&hashDictType); + dict = dictCreate(&mstrHashDictType); /* Presize the dict to avoid rehashing */ - dictExpand(dict,hashTypeLength(o)); + dictExpand(dict,hashTypeLength(o, 0)); - while (hashTypeNext(hi) != C_ERR) { - sds key, value; + while (hashTypeNext(hi, 0) != C_ERR) { - key = hashTypeCurrentObjectNewSds(hi,OBJ_HASH_KEY); - value = hashTypeCurrentObjectNewSds(hi,OBJ_HASH_VALUE); + hfield key = hashTypeCurrentObjectNewHfield(hi); + sds value = hashTypeCurrentObjectNewSds(hi,OBJ_HASH_VALUE); + dictUseStoredKeyApi(dict, 1); ret = dictAdd(dict, key, value); + dictUseStoredKeyApi(dict, 0); if (ret != DICT_OK) { - sdsfree(key); sdsfree(value); /* Needed for gcc ASAN */ + hfieldFree(key); sdsfree(value); /* Needed for gcc ASAN */ hashTypeReleaseIterator(hi); /* Needed for gcc ASAN */ serverLogHexDump(LL_WARNING,"listpack with dup elements dump", o->ptr,lpBytes(o->ptr)); @@ -468,9 +1629,69 @@ void hashTypeConvertListpack(robj *o, int enc) { } } -void hashTypeConvert(robj *o, int enc) { +void hashTypeConvertListpackEx(robj *o, int enc, ebuckets *hexpires) { + serverAssert(o->encoding == OBJ_ENCODING_LISTPACK_EX); + + if (enc == OBJ_ENCODING_LISTPACK_EX) { + return; + } else if (enc == OBJ_ENCODING_HT) { + int ret; + hashTypeIterator *hi; + dict *dict; + dictExpireMetadata *dictExpireMeta; + listpackEx *lpt = o->ptr; + uint64_t minExpire = hashTypeGetMinExpire(o); + + if (hexpires && lpt->meta.trash != 1) + ebRemove(hexpires, &hashExpireBucketsType, o); + + dict = dictCreate(&mstrHashDictTypeWithHFE); + dictExpand(dict,hashTypeLength(o, 0)); + dictExpireMeta = (dictExpireMetadata *) dictMetadata(dict); + + /* Fillup dict HFE metadata */ + dictExpireMeta->key = lpt->key; /* reference key in keyspace */ + dictExpireMeta->hfe = ebCreate(); /* Allocate HFE DS */ + dictExpireMeta->expireMeta.trash = 1; /* mark as trash (as long it wasn't ebAdd()) */ + + hi = hashTypeInitIterator(o); + + while (hashTypeNext(hi, 0) != C_ERR) { + hfield key = hashTypeCurrentObjectNewHfield(hi); + sds value = hashTypeCurrentObjectNewSds(hi,OBJ_HASH_VALUE); + dictUseStoredKeyApi(dict, 1); + ret = dictAdd(dict, key, value); + dictUseStoredKeyApi(dict, 0); + if (ret != DICT_OK) { + hfieldFree(key); sdsfree(value); /* Needed for gcc ASAN */ + hashTypeReleaseIterator(hi); /* Needed for gcc ASAN */ + serverLogHexDump(LL_WARNING,"listpack with dup elements dump", + lpt->lp,lpBytes(lpt->lp)); + serverPanic("Listpack corruption detected"); + } + + if (hi->expire_time != EB_EXPIRE_TIME_INVALID) + ebAdd(&dictExpireMeta->hfe, &hashFieldExpireBucketsType, key, hi->expire_time); + } + hashTypeReleaseIterator(hi); + listpackExFree(lpt); + + o->encoding = OBJ_ENCODING_HT; + o->ptr = dict; + + if (hexpires && minExpire != EB_EXPIRE_TIME_INVALID) + ebAdd(hexpires, &hashExpireBucketsType, o, minExpire); + } else { + serverPanic("Unknown hash encoding: %d", enc); + } +} + +/* NOTE: hexpires can be NULL (Won't register in global HFE DS) */ +void hashTypeConvert(robj *o, int enc, ebuckets *hexpires) { if (o->encoding == OBJ_ENCODING_LISTPACK) { hashTypeConvertListpack(o, enc); + } else if (o->encoding == OBJ_ENCODING_LISTPACK_EX) { + hashTypeConvertListpackEx(o, enc, hexpires); } else if (o->encoding == OBJ_ENCODING_HT) { serverPanic("Not implemented"); } else { @@ -483,7 +1704,7 @@ void hashTypeConvert(robj *o, int enc) { * has the same encoding as the original one. * * The resulting object always has refcount set to 1 */ -robj *hashTypeDup(robj *o) { +robj *hashTypeDup(robj *o, sds newkey, uint64_t *minHashExpire) { robj *hobj; hashTypeIterator *hi; @@ -496,22 +1717,66 @@ robj *hashTypeDup(robj *o) { memcpy(new_zl, zl, sz); hobj = createObject(OBJ_HASH, new_zl); hobj->encoding = OBJ_ENCODING_LISTPACK; - } else if(o->encoding == OBJ_ENCODING_HT){ - dict *d = dictCreate(&hashDictType); + } else if(o->encoding == OBJ_ENCODING_LISTPACK_EX) { + listpackEx *lpt = o->ptr; + + if (lpt->meta.trash == 0) + *minHashExpire = ebGetMetaExpTime(&lpt->meta); + + listpackEx *dup = listpackExCreate(); + dup->key = newkey; + + size_t sz = lpBytes(lpt->lp); + dup->lp = lpNew(sz); + memcpy(dup->lp, lpt->lp, sz); + + hobj = createObject(OBJ_HASH, dup); + hobj->encoding = OBJ_ENCODING_LISTPACK_EX; + } else if(o->encoding == OBJ_ENCODING_HT) { + dictExpireMetadata *dictExpireMetaSrc, *dictExpireMetaDst = NULL; + dict *d; + + /* If dict doesn't have HFE metadata, then create a new dict without it */ + if (!isDictWithMetaHFE(o->ptr)) { + d = dictCreate(&mstrHashDictType); + } else { + /* Create a new dict with HFE metadata */ + d = dictCreate(&mstrHashDictTypeWithHFE); + dictExpireMetaSrc = (dictExpireMetadata *) dictMetadata((dict *) o->ptr); + dictExpireMetaDst = (dictExpireMetadata *) dictMetadata(d); + dictExpireMetaDst->key = newkey; /* reference key in keyspace */ + dictExpireMetaDst->hfe = ebCreate(); /* Allocate HFE DS */ + dictExpireMetaDst->expireMeta.trash = 1; /* mark as trash (as long it wasn't ebAdd()) */ + + /* Extract the minimum expire time of the source hash (Will be used by caller + * to register the new hash in the global ebuckets, i.e db->hexpires) */ + if (dictExpireMetaSrc->expireMeta.trash == 0) + *minHashExpire = ebGetMetaExpTime(&dictExpireMetaSrc->expireMeta); + } dictExpand(d, dictSize((const dict*)o->ptr)); hi = hashTypeInitIterator(o); - while (hashTypeNext(hi) != C_ERR) { - sds field, value; + while (hashTypeNext(hi, 0) != C_ERR) { + uint64_t expireTime; sds newfield, newvalue; /* Extract a field-value pair from an original hash object.*/ - field = hashTypeCurrentFromHashTable(hi, OBJ_HASH_KEY); - value = hashTypeCurrentFromHashTable(hi, OBJ_HASH_VALUE); - newfield = sdsdup(field); - newvalue = sdsdup(value); + char *field, *value; + size_t fieldLen, valueLen; + hashTypeCurrentFromHashTable(hi, OBJ_HASH_KEY, &field, &fieldLen, &expireTime); + if (expireTime == EB_EXPIRE_TIME_INVALID) { + newfield = hfieldNew(field, fieldLen, 0); + } else { + newfield = hfieldNew(field, fieldLen, 1); + ebAdd(&dictExpireMetaDst->hfe, &hashFieldExpireBucketsType, newfield, expireTime); + } + + hashTypeCurrentFromHashTable(hi, OBJ_HASH_VALUE, &value, &valueLen, NULL); + newvalue = sdsnewlen(value, valueLen); /* Add a field-value pair to a new hash object. */ + dictUseStoredKeyApi(d, 1); dictAdd(d,newfield,newvalue); + dictUseStoredKeyApi(d, 0); } hashTypeReleaseIterator(hi); @@ -543,40 +1808,295 @@ void hashReplyFromListpackEntry(client *c, listpackEntry *e) { void hashTypeRandomElement(robj *hashobj, unsigned long hashsize, listpackEntry *key, listpackEntry *val) { if (hashobj->encoding == OBJ_ENCODING_HT) { dictEntry *de = dictGetFairRandomKey(hashobj->ptr); - sds s = dictGetKey(de); - key->sval = (unsigned char*)s; - key->slen = sdslen(s); + hfield field = dictGetKey(de); + key->sval = (unsigned char*)field; + key->slen = hfieldlen(field); if (val) { sds s = dictGetVal(de); val->sval = (unsigned char*)s; val->slen = sdslen(s); } } else if (hashobj->encoding == OBJ_ENCODING_LISTPACK) { - lpRandomPair(hashobj->ptr, hashsize, key, val); + lpRandomPair(hashobj->ptr, hashsize, key, val, 2); + } else if (hashobj->encoding == OBJ_ENCODING_LISTPACK_EX) { + lpRandomPair(hashTypeListpackGetLp(hashobj), hashsize, key, val, 3); } else { serverPanic("Unknown hash encoding"); } } +/* + * Active expiration of fields in hash + * + * Called by hashTypeDbActiveExpire() for each hash registered in the HFE DB + * (db->hexpires) with an expiration-time less than or equal current time. + * + * This callback performs the following actions for each hash: + * - Delete expired fields as by calling ebExpire(hash) + * - If afterward there are future fields to expire, it will update the hash in + * HFE DB with the next hash-field minimum expiration time by returning + * ACT_UPDATE_EXP_ITEM. + * - If the hash has no more fields to expire, it is removed from the HFE DB + * by returning ACT_REMOVE_EXP_ITEM. + * - If hash has no more fields afterward, it will remove the hash from keyspace. + */ +static ExpireAction hashTypeActiveExpire(eItem _hashObj, void *ctx) { + robj *hashObj = (robj *) _hashObj; + ActiveExpireCtx *activeExpireCtx = (ActiveExpireCtx *) ctx; + sds keystr = NULL; + ExpireInfo info = {0}; + + /* If no more quota left for this callback, stop */ + if (activeExpireCtx->fieldsToExpireQuota == 0) + return ACT_STOP_ACTIVE_EXP; + + if (hashObj->encoding == OBJ_ENCODING_LISTPACK_EX) { + info = (ExpireInfo){ + .maxToExpire = activeExpireCtx->fieldsToExpireQuota, + .now = commandTimeSnapshot(), + .itemsExpired = 0}; + + listpackExExpire(activeExpireCtx->db, hashObj, &info); + server.stat_expired_hash_fields += info.itemsExpired; + keystr = ((listpackEx*)hashObj->ptr)->key; + } else { + serverAssert(hashObj->encoding == OBJ_ENCODING_HT); + + dict *d = hashObj->ptr; + dictExpireMetadata *dictExpireMeta = (dictExpireMetadata *) dictMetadata(d); + + OnFieldExpireCtx onFieldExpireCtx = { + .hashObj = hashObj, + .db = activeExpireCtx->db + }; + + info = (ExpireInfo){ + .maxToExpire = activeExpireCtx->fieldsToExpireQuota, + .onExpireItem = onFieldExpire, + .ctx = &onFieldExpireCtx, + .now = commandTimeSnapshot() + }; + + ebExpire(&dictExpireMeta->hfe, &hashFieldExpireBucketsType, &info); + keystr = dictExpireMeta->key; + } + + /* Update quota left */ + activeExpireCtx->fieldsToExpireQuota -= info.itemsExpired; + + /* If hash has no more fields to expire, remove it from HFE DB */ + if (info.nextExpireTime == 0) { + if (hashTypeLength(hashObj, 0) == 0) { + robj *key = createStringObject(keystr, sdslen(keystr)); + dbDelete(activeExpireCtx->db, key); + notifyKeyspaceEvent(NOTIFY_GENERIC,"del",key, activeExpireCtx->db->id); + server.dirty++; + signalModifiedKey(NULL, &server.db[0], key); + decrRefCount(key); + } + return ACT_REMOVE_EXP_ITEM; + } else { + /* Hash has more fields to expire. Keep hash to pending items that will + * be added back to global HFE DS at the end of ebExpire() */ + ExpireMeta *expireMeta = hashGetExpireMeta(hashObj); + ebSetMetaExpTime(expireMeta, info.nextExpireTime); + return ACT_UPDATE_EXP_ITEM; + } +} + +/* Return the next/minimum expiry time of the hash-field. This is useful if a + * field with the minimum expiry is deleted, and you want to get the next + * minimum expiry. Otherwise, consider using hashTypeGetMinExpire() which will + * be faster. If there is no field with expiry, returns EB_EXPIRE_TIME_INVALID */ +uint64_t hashTypeGetNextTimeToExpire(robj *o) { + if (o->encoding == OBJ_ENCODING_LISTPACK) { + return EB_EXPIRE_TIME_INVALID; + } else if (o->encoding == OBJ_ENCODING_LISTPACK_EX) { + return listpackExGetMinExpire(o); + } else { + serverAssert(o->encoding == OBJ_ENCODING_HT); + + dict *d = o->ptr; + if (!isDictWithMetaHFE(d)) + return EB_EXPIRE_TIME_INVALID; + + dictExpireMetadata *expireMeta = (dictExpireMetadata *) dictMetadata(d); + return ebGetNextTimeToExpire(expireMeta->hfe, &hashFieldExpireBucketsType); + } +} + +/* Return the next/minimum expiry time of the hash-field. + * If not found, return EB_EXPIRE_TIME_INVALID */ +uint64_t hashTypeGetMinExpire(robj *o) { + ExpireMeta *expireMeta = NULL; + + if (o->encoding == OBJ_ENCODING_LISTPACK) { + return EB_EXPIRE_TIME_INVALID; + } else if (o->encoding == OBJ_ENCODING_LISTPACK_EX) { + listpackEx *lpt = o->ptr; + expireMeta = &lpt->meta; + } else { + serverAssert(o->encoding == OBJ_ENCODING_HT); + + dict *d = o->ptr; + if (!isDictWithMetaHFE(d)) + return EB_EXPIRE_TIME_INVALID; + + expireMeta = &((dictExpireMetadata *) dictMetadata(d))->expireMeta; + } + + /* Keep aside next hash-field expiry before updating HFE DS. Verify it is not trash */ + if (expireMeta->trash == 1) + return EB_EXPIRE_TIME_INVALID; + + return ebGetMetaExpTime(expireMeta); +} + +uint64_t hashTypeRemoveFromExpires(ebuckets *hexpires, robj *o) { + if (o->encoding == OBJ_ENCODING_LISTPACK) { + return EB_EXPIRE_TIME_INVALID; + } else if (o->encoding == OBJ_ENCODING_HT) { + /* If dict doesn't holds HFE metadata */ + if (!isDictWithMetaHFE(o->ptr)) + return EB_EXPIRE_TIME_INVALID; + } + + uint64_t expireTime = ebGetExpireTime(&hashExpireBucketsType, o); + + /* If registered in global HFE DS then remove it (not trash) */ + if (expireTime != EB_EXPIRE_TIME_INVALID) + ebRemove(hexpires, &hashExpireBucketsType, o); + + return expireTime; +} + +/* Add hash to global HFE DS and update key for notifications. + * + * key - must be the same key instance that is persisted in db->dict + * expireTime - expiration in msec. + * If eq. 0 then the hash will be added to the global HFE DS with + * the minimum expiration time that is already written in advance + * to attached metadata (which considered as trash as long as it is + * not attached to global HFE DS). + * + * Precondition: It is a hash of type listpackex or HT with HFE metadata. + */ +void hashTypeAddToExpires(redisDb *db, sds key, robj *hashObj, uint64_t expireTime) { + if (expireTime > EB_EXPIRE_TIME_MAX) + return; + + if (hashObj->encoding == OBJ_ENCODING_LISTPACK_EX) { + listpackEx *lpt = hashObj->ptr; + lpt->key = key; + expireTime = (expireTime) ? expireTime : ebGetMetaExpTime(&lpt->meta); + ebAdd(&db->hexpires, &hashExpireBucketsType, hashObj, expireTime); + } else if (hashObj->encoding == OBJ_ENCODING_HT) { + dict *d = hashObj->ptr; + if (isDictWithMetaHFE(d)) { + dictExpireMetadata *meta = (dictExpireMetadata *) dictMetadata(d); + expireTime = (expireTime) ? expireTime : ebGetMetaExpTime(&meta->expireMeta); + meta->key = key; + ebAdd(&db->hexpires, &hashExpireBucketsType, hashObj, expireTime); + } + } +} + +/* DB active expire and update hashes with time-expiration on fields. + * + * The callback function hashTypeActiveExpire() is invoked for each hash registered + * in the HFE DB (db->expires) with an expiration-time less than or equal to the + * current time. This callback performs the following actions for each hash: + * - If the hash has one or more fields to expire, it will delete those fields. + * - If there are more fields to expire, it will update the hash with the next + * expiration time in HFE DB. + * - If the hash has no more fields to expire, it is removed from the HFE DB. + * - If the hash has no more fields, it is removed from the main DB. + * + * Returns number of fields active-expired. + */ +uint64_t hashTypeDbActiveExpire(redisDb *db, uint32_t maxFieldsToExpire) { + ActiveExpireCtx ctx = { .db = db, .fieldsToExpireQuota = maxFieldsToExpire }; + ExpireInfo info = { + .maxToExpire = UINT64_MAX, /* Only maxFieldsToExpire play a role */ + .onExpireItem = hashTypeActiveExpire, + .ctx = &ctx, + .now = commandTimeSnapshot(), + .itemsExpired = 0}; + + ebExpire(&db->hexpires, &hashExpireBucketsType, &info); + + /* Return number of fields active-expired */ + return maxFieldsToExpire - ctx.fieldsToExpireQuota; +} + +void hashTypeFree(robj *o) { + switch (o->encoding) { + case OBJ_ENCODING_HT: + /* Verify hash is not registered in global HFE ds */ + if (isDictWithMetaHFE((dict*)o->ptr)) { + dictExpireMetadata *m = (dictExpireMetadata *)dictMetadata((dict*)o->ptr); + serverAssert(m->expireMeta.trash == 1); + } + dictRelease((dict*) o->ptr); + break; + case OBJ_ENCODING_LISTPACK: + lpFree(o->ptr); + break; + case OBJ_ENCODING_LISTPACK_EX: + /* Verify hash is not registered in global HFE ds */ + serverAssert(((listpackEx *) o->ptr)->meta.trash == 1); + listpackExFree(o->ptr); + break; + default: + serverPanic("Unknown hash encoding type"); + break; + } +} + +/* Attempts to update the reference to the new key. Now it's only used in defrag. */ +void hashTypeUpdateKeyRef(robj *o, sds newkey) { + if (o->encoding == OBJ_ENCODING_LISTPACK_EX) { + listpackEx *lpt = o->ptr; + lpt->key = newkey; + } else if (o->encoding == OBJ_ENCODING_HT && isDictWithMetaHFE(o->ptr)) { + dictExpireMetadata *dictExpireMeta = (dictExpireMetadata *)dictMetadata((dict*)o->ptr); + dictExpireMeta->key = newkey; + } else { + /* Nothing to do. */ + } +} + +ebuckets *hashTypeGetDictMetaHFE(dict *d) { + dictExpireMetadata *dictExpireMeta = (dictExpireMetadata *) dictMetadata(d); + return &dictExpireMeta->hfe; +} /*----------------------------------------------------------------------------- * Hash type commands *----------------------------------------------------------------------------*/ void hsetnxCommand(client *c) { + int isHashDeleted; robj *o; if ((o = hashTypeLookupWriteOrCreate(c,c->argv[1])) == NULL) return; - if (hashTypeExists(o, c->argv[2]->ptr)) { + if (hashTypeExists(c->db, o, c->argv[2]->ptr, &isHashDeleted)) { addReply(c, shared.czero); - } else { - hashTypeTryConversion(o,c->argv,2,3); - hashTypeSet(o,c->argv[2]->ptr,c->argv[3]->ptr,HASH_SET_COPY); - addReply(c, shared.cone); - signalModifiedKey(c,c->db,c->argv[1]); - notifyKeyspaceEvent(NOTIFY_HASH,"hset",c->argv[1],c->db->id); - server.dirty++; + return; } + + /* Field expired and in turn hash deleted. Create new one! */ + if (isHashDeleted) { + o = createHashObject(); + dbAdd(c->db,c->argv[1],o); + } + + hashTypeTryConversion(c->db, o,c->argv,2,3); + hashTypeSet(c->db, o,c->argv[2]->ptr,c->argv[3]->ptr,HASH_SET_COPY); + addReply(c, shared.cone); + signalModifiedKey(c,c->db,c->argv[1]); + notifyKeyspaceEvent(NOTIFY_HASH,"hset",c->argv[1],c->db->id); + server.dirty++; } void hsetCommand(client *c) { @@ -589,10 +2109,10 @@ void hsetCommand(client *c) { } if ((o = hashTypeLookupWriteOrCreate(c,c->argv[1])) == NULL) return; - hashTypeTryConversion(o,c->argv,2,c->argc-1); + hashTypeTryConversion(c->db,o,c->argv,2,c->argc-1); for (i = 2; i < c->argc; i += 2) - created += !hashTypeSet(o,c->argv[i]->ptr,c->argv[i+1]->ptr,HASH_SET_COPY); + created += !hashTypeSet(c->db, o,c->argv[i]->ptr,c->argv[i+1]->ptr,HASH_SET_COPY); /* HMSET (deprecated) and HSET return value is different. */ char *cmdname = c->argv[0]->ptr; @@ -617,14 +2137,21 @@ void hincrbyCommand(client *c) { if (getLongLongFromObjectOrReply(c,c->argv[3],&incr,NULL) != C_OK) return; if ((o = hashTypeLookupWriteOrCreate(c,c->argv[1])) == NULL) return; - if (hashTypeGetValue(o,c->argv[2]->ptr,&vstr,&vlen,&value) == C_OK) { + + GetFieldRes res = hashTypeGetValue(c->db,o,c->argv[2]->ptr,&vstr,&vlen,&value); + if (res == GETF_OK) { if (vstr) { if (string2ll((char*)vstr,vlen,&value) == 0) { addReplyError(c,"hash value is not an integer"); return; } } /* Else hashTypeGetValue() already stored it into &value */ + } else if ((res == GETF_NOT_FOUND) || (res == GETF_EXPIRED)) { + value = 0; } else { + /* Field expired and in turn hash deleted. Create new one! */ + o = createHashObject(); + dbAdd(c->db,c->argv[1],o); value = 0; } @@ -636,7 +2163,7 @@ void hincrbyCommand(client *c) { } value += incr; new = sdsfromlonglong(value); - hashTypeSet(o,c->argv[2]->ptr,new,HASH_SET_TAKE_VALUE); + hashTypeSet(c->db, o,c->argv[2]->ptr,new,HASH_SET_TAKE_VALUE | HASH_SET_KEEP_FIELD); addReplyLongLong(c,value); signalModifiedKey(c,c->db,c->argv[1]); notifyKeyspaceEvent(NOTIFY_HASH,"hincrby",c->argv[1],c->db->id); @@ -657,7 +2184,8 @@ void hincrbyfloatCommand(client *c) { return; } if ((o = hashTypeLookupWriteOrCreate(c,c->argv[1])) == NULL) return; - if (hashTypeGetValue(o,c->argv[2]->ptr,&vstr,&vlen,&ll) == C_OK) { + GetFieldRes res = hashTypeGetValue(c->db, o,c->argv[2]->ptr,&vstr,&vlen,&ll); + if (res == GETF_OK) { if (vstr) { if (string2ld((char*)vstr,vlen,&value) == 0) { addReplyError(c,"hash value is not a float"); @@ -666,7 +2194,12 @@ void hincrbyfloatCommand(client *c) { } else { value = (long double)ll; } + } else if ((res == GETF_NOT_FOUND) || (res == GETF_EXPIRED)) { + value = 0; } else { + /* Field expired and in turn hash deleted. Create new one! */ + o = createHashObject(); + dbAdd(c->db,c->argv[1],o); value = 0; } @@ -679,7 +2212,7 @@ void hincrbyfloatCommand(client *c) { char buf[MAX_LONG_DOUBLE_CHARS]; int len = ld2string(buf,sizeof(buf),value,LD_STR_HUMAN); new = sdsnewlen(buf,len); - hashTypeSet(o,c->argv[2]->ptr,new,HASH_SET_TAKE_VALUE); + hashTypeSet(c->db, o,c->argv[2]->ptr,new,HASH_SET_TAKE_VALUE | HASH_SET_KEEP_FIELD); addReplyBulkCBuffer(c,buf,len); signalModifiedKey(c,c->db,c->argv[1]); notifyKeyspaceEvent(NOTIFY_HASH,"hincrbyfloat",c->argv[1],c->db->id); @@ -695,17 +2228,18 @@ void hincrbyfloatCommand(client *c) { decrRefCount(newobj); } -static void addHashFieldToReply(client *c, robj *o, sds field) { +static GetFieldRes addHashFieldToReply(client *c, robj *o, sds field) { if (o == NULL) { addReplyNull(c); - return; + return GETF_NOT_FOUND; } unsigned char *vstr = NULL; unsigned int vlen = UINT_MAX; long long vll = LLONG_MAX; - if (hashTypeGetValue(o, field, &vstr, &vlen, &vll) == C_OK) { + GetFieldRes res = hashTypeGetValue(c->db, o, field, &vstr, &vlen, &vll); + if (res == GETF_OK) { if (vstr) { addReplyBulkCBuffer(c, vstr, vlen); } else { @@ -714,6 +2248,7 @@ static void addHashFieldToReply(client *c, robj *o, sds field) { } else { addReplyNull(c); } + return res; } void hgetCommand(client *c) { @@ -726,6 +2261,7 @@ void hgetCommand(client *c) { } void hmgetCommand(client *c) { + GetFieldRes res = GETF_OK; robj *o; int i; @@ -735,8 +2271,17 @@ void hmgetCommand(client *c) { if (checkType(c,o,OBJ_HASH)) return; addReplyArrayLen(c, c->argc-2); - for (i = 2; i < c->argc; i++) { - addHashFieldToReply(c, o, c->argv[i]->ptr); + for (i = 2; i < c->argc ; i++) { + + res = addHashFieldToReply(c, o, c->argv[i]->ptr); + + /* If hash got lazy expired since all fields are expired (o is invalid), + * then fill the rest with trivial nulls and return */ + if (res == GETF_EXPIRED_HASH) { + while (++i < c->argc) + addReplyNull(c); + return; + } } } @@ -748,9 +2293,9 @@ void hdelCommand(client *c) { checkType(c,o,OBJ_HASH)) return; for (j = 2; j < c->argc; j++) { - if (hashTypeDelete(o,c->argv[j]->ptr)) { + if (hashTypeDelete(o,c->argv[j]->ptr,1)) { deleted++; - if (hashTypeLength(o) == 0) { + if (hashTypeLength(o, 0) == 0) { dbDelete(c->db,c->argv[1]); keyremoved = 1; break; @@ -774,31 +2319,47 @@ void hlenCommand(client *c) { if ((o = lookupKeyReadOrReply(c,c->argv[1],shared.czero)) == NULL || checkType(c,o,OBJ_HASH)) return; - addReplyLongLong(c,hashTypeLength(o)); + addReplyLongLong(c,hashTypeLength(o, 0)); } void hstrlenCommand(client *c) { robj *o; + unsigned char *vstr = NULL; + unsigned int vlen = UINT_MAX; + long long vll = LLONG_MAX; if ((o = lookupKeyReadOrReply(c,c->argv[1],shared.czero)) == NULL || checkType(c,o,OBJ_HASH)) return; - addReplyLongLong(c,hashTypeGetValueLength(o,c->argv[2]->ptr)); + + GetFieldRes res = hashTypeGetValue(c->db, o, c->argv[2]->ptr, &vstr, &vlen, &vll); + + if (res == GETF_NOT_FOUND || res == GETF_EXPIRED || res == GETF_EXPIRED_HASH) { + addReply(c, shared.czero); + return; + } + + size_t len = vstr ? vlen : sdigits10(vll); + addReplyLongLong(c,len); } static void addHashIteratorCursorToReply(client *c, hashTypeIterator *hi, int what) { - if (hi->encoding == OBJ_ENCODING_LISTPACK) { + if (hi->encoding == OBJ_ENCODING_LISTPACK || + hi->encoding == OBJ_ENCODING_LISTPACK_EX) + { unsigned char *vstr = NULL; unsigned int vlen = UINT_MAX; long long vll = LLONG_MAX; - hashTypeCurrentFromListpack(hi, what, &vstr, &vlen, &vll); + hashTypeCurrentFromListpack(hi, what, &vstr, &vlen, &vll, NULL); if (vstr) addReplyBulkCBuffer(c, vstr, vlen); else addReplyBulkLongLong(c, vll); } else if (hi->encoding == OBJ_ENCODING_HT) { - sds value = hashTypeCurrentFromHashTable(hi, what); - addReplyBulkCBuffer(c, value, sdslen(value)); + char *value; + size_t len; + hashTypeCurrentFromHashTable(hi, what, &value, &len, NULL); + addReplyBulkCBuffer(c, value, len); } else { serverPanic("Unknown hash encoding"); } @@ -816,7 +2377,7 @@ void genericHgetallCommand(client *c, int flags) { /* We return a map if the user requested keys and values, like in the * HGETALL case. Otherwise to use a flat array makes more sense. */ - length = hashTypeLength(o); + length = hashTypeLength(o, 1 /*subtractExpiredFields*/); if (flags & OBJ_HASH_KEY && flags & OBJ_HASH_VALUE) { addReplyMapLen(c, length); } else { @@ -824,7 +2385,12 @@ void genericHgetallCommand(client *c, int flags) { } hi = hashTypeInitIterator(o); - while (hashTypeNext(hi) != C_ERR) { + + /* Skip expired fields if the hash has an expire time set at global HFE DS. We could + * set it to constant 1, but then it will make another lookup for each field expiration */ + int skipExpiredFields = (EB_EXPIRE_TIME_INVALID == hashTypeGetMinExpire(o)) ? 0 : 1; + + while (hashTypeNext(hi, skipExpiredFields) != C_ERR) { if (flags & OBJ_HASH_KEY) { addHashIteratorCursorToReply(c, hi, OBJ_HASH_KEY); count++; @@ -856,10 +2422,11 @@ void hgetallCommand(client *c) { void hexistsCommand(client *c) { robj *o; + int isHashDeleted; if ((o = lookupKeyReadOrReply(c,c->argv[1],shared.czero)) == NULL || checkType(c,o,OBJ_HASH)) return; - addReply(c, hashTypeExists(o,c->argv[2]->ptr) ? shared.cone : shared.czero); + addReply(c,hashTypeExists(c->db,o,c->argv[2]->ptr,&isHashDeleted) ? shared.cone : shared.czero); } void hscanCommand(client *c) { @@ -869,6 +2436,7 @@ void hscanCommand(client *c) { if (parseScanCursorOrReply(c,c->argv[2],&cursor) == C_ERR) return; if ((o = lookupKeyReadOrReply(c,c->argv[1],shared.emptyscan)) == NULL || checkType(c,o,OBJ_HASH)) return; + scanGenericCommand(c,o,cursor); } @@ -906,7 +2474,8 @@ void hrandfieldWithCountCommand(client *c, long l, int withvalues) { if ((hash = lookupKeyReadOrReply(c,c->argv[1],shared.emptyarray)) == NULL || checkType(c,hash,OBJ_HASH)) return; - size = hashTypeLength(hash); + /* TODO: Active-expire */ + size = hashTypeLength(hash, 0); if(l >= 0) { count = (unsigned long) l; @@ -932,22 +2501,25 @@ void hrandfieldWithCountCommand(client *c, long l, int withvalues) { else addReplyArrayLen(c, count); if (hash->encoding == OBJ_ENCODING_HT) { - sds key, value; while (count--) { dictEntry *de = dictGetFairRandomKey(hash->ptr); - key = dictGetKey(de); - value = dictGetVal(de); + hfield field = dictGetKey(de); + sds value = dictGetVal(de); if (withvalues && c->resp > 2) addReplyArrayLen(c,2); - addReplyBulkCBuffer(c, key, sdslen(key)); + addReplyBulkCBuffer(c, field, hfieldlen(field)); if (withvalues) addReplyBulkCBuffer(c, value, sdslen(value)); if (c->flags & CLIENT_CLOSE_ASAP) break; } - } else if (hash->encoding == OBJ_ENCODING_LISTPACK) { + } else if (hash->encoding == OBJ_ENCODING_LISTPACK || + hash->encoding == OBJ_ENCODING_LISTPACK_EX) + { listpackEntry *keys, *vals = NULL; unsigned long limit, sample_count; + unsigned char *lp = hashTypeListpackGetLp(hash); + int tuple_len = hash->encoding == OBJ_ENCODING_LISTPACK ? 2 : 3; limit = count > HRANDFIELD_RANDOM_SAMPLE_LIMIT ? HRANDFIELD_RANDOM_SAMPLE_LIMIT : count; keys = zmalloc(sizeof(listpackEntry)*limit); @@ -956,7 +2528,7 @@ void hrandfieldWithCountCommand(client *c, long l, int withvalues) { while (count) { sample_count = count > limit ? limit : count; count -= sample_count; - lpRandomPairs(hash->ptr, sample_count, keys, vals); + lpRandomPairs(lp, sample_count, keys, vals, tuple_len); hrandfieldReplyWithListpack(c, sample_count, keys, vals); if (c->flags & CLIENT_CLOSE_ASAP) break; @@ -979,7 +2551,7 @@ void hrandfieldWithCountCommand(client *c, long l, int withvalues) { * elements inside the hash: simply return the whole hash. */ if(count >= size) { hashTypeIterator *hi = hashTypeInitIterator(hash); - while (hashTypeNext(hi) != C_ERR) { + while (hashTypeNext(hi, 0) != C_ERR) { if (withvalues && c->resp > 2) addReplyArrayLen(c,2); addHashIteratorCursorToReply(c, hi, OBJ_HASH_KEY); @@ -998,12 +2570,16 @@ void hrandfieldWithCountCommand(client *c, long l, int withvalues) { * * And it is inefficient to repeatedly pick one random element from a * listpack in CASE 4. So we use this instead. */ - if (hash->encoding == OBJ_ENCODING_LISTPACK) { + if (hash->encoding == OBJ_ENCODING_LISTPACK || + hash->encoding == OBJ_ENCODING_LISTPACK_EX) + { + unsigned char *lp = hashTypeListpackGetLp(hash); + int tuple_len = hash->encoding == OBJ_ENCODING_LISTPACK ? 2 : 3; listpackEntry *keys, *vals = NULL; keys = zmalloc(sizeof(listpackEntry)*count); if (withvalues) vals = zmalloc(sizeof(listpackEntry)*count); - serverAssert(lpRandomPairsUnique(hash->ptr, count, keys, vals) == count); + serverAssert(lpRandomPairsUnique(lp, count, keys, vals, tuple_len) == count); hrandfieldReplyWithListpack(c, count, keys, vals); zfree(keys); zfree(vals); @@ -1021,12 +2597,12 @@ void hrandfieldWithCountCommand(client *c, long l, int withvalues) { * used into CASE 4 is highly inefficient. */ if (count*HRANDFIELD_SUB_STRATEGY_MUL > size) { /* Hashtable encoding (generic implementation) */ - dict *d = dictCreate(&sdsReplyDictType); + dict *d = dictCreate(&sdsReplyDictType); /* without metadata! */ dictExpand(d, size); hashTypeIterator *hi = hashTypeInitIterator(hash); /* Add all the elements into the temporary dictionary. */ - while ((hashTypeNext(hi)) != C_ERR) { + while ((hashTypeNext(hi, 0)) != C_ERR) { int ret = DICT_ERR; sds key, value = NULL; @@ -1044,7 +2620,9 @@ void hrandfieldWithCountCommand(client *c, long l, int withvalues) { while (size > count) { dictEntry *de; de = dictGetFairRandomKey(d); + dictUseStoredKeyApi(d, 1); dictUnlink(d,dictGetKey(de)); + dictUseStoredKeyApi(d, 0); sdsfree(dictGetKey(de)); sdsfree(dictGetVal(de)); dictFreeUnlinkedEntry(d,de); @@ -1134,6 +2712,516 @@ void hrandfieldCommand(client *c) { return; } - hashTypeRandomElement(hash,hashTypeLength(hash),&ele,NULL); + hashTypeRandomElement(hash,hashTypeLength(hash, 0),&ele,NULL); hashReplyFromListpackEntry(c, &ele); } + +/*----------------------------------------------------------------------------- + * Hash Field with optional expiry (based on mstr) + *----------------------------------------------------------------------------*/ +static hfield _hfieldNew(const void *field, size_t fieldlen, int withExpireMeta, + int trymalloc) +{ + if (!withExpireMeta) + return mstrNew(field, fieldlen, trymalloc); + + hfield hf = mstrNewWithMeta(&mstrFieldKind, field, fieldlen, + (mstrFlags) 1 << HFIELD_META_EXPIRE, trymalloc); + + if (!hf) return NULL; + + ExpireMeta *expireMeta = mstrMetaRef(hf, &mstrFieldKind, HFIELD_META_EXPIRE); + + /* as long as it is not inside ebuckets, it is considered trash */ + expireMeta->trash = 1; + return hf; +} + +/* if expireAt is 0, then expireAt is ignored and no metadata is attached */ +hfield hfieldNew(const void *field, size_t fieldlen, int withExpireMeta) { + return _hfieldNew(field, fieldlen, withExpireMeta, 0); +} + +hfield hfieldTryNew(const void *field, size_t fieldlen, int withExpireMeta) { + return _hfieldNew(field, fieldlen, withExpireMeta, 1); +} + +int hfieldIsExpireAttached(hfield field) { + return mstrIsMetaAttached(field) && mstrGetFlag(field, (int) HFIELD_META_EXPIRE); +} + +static ExpireMeta* hfieldGetExpireMeta(const eItem field) { + /* extract the expireMeta from the field of type mstr */ + return mstrMetaRef(field, &mstrFieldKind, (int) HFIELD_META_EXPIRE); +} + +/* returned value is unix time in milliseconds */ +uint64_t hfieldGetExpireTime(hfield field) { + if (!hfieldIsExpireAttached(field)) + return EB_EXPIRE_TIME_INVALID; + + ExpireMeta *expireMeta = mstrMetaRef(field, &mstrFieldKind, (int) HFIELD_META_EXPIRE); + if (expireMeta->trash) + return EB_EXPIRE_TIME_INVALID; + + return ebGetMetaExpTime(expireMeta); +} + +/* Remove TTL from the field. Assumed ExpireMeta is attached and has valid value */ +static void hfieldPersist(robj *hashObj, hfield field) { + uint64_t fieldExpireTime = hfieldGetExpireTime(field); + if (fieldExpireTime == EB_EXPIRE_TIME_INVALID) + return; + + /* if field is set with expire, then dict must has HFE metadata attached */ + dict *d = hashObj->ptr; + dictExpireMetadata *dictExpireMeta = (dictExpireMetadata *)dictMetadata(d); + + /* If field has valid expiry then dict must have valid metadata as well */ + serverAssert(dictExpireMeta->expireMeta.trash == 0); + + /* Remove field from private HFE DS */ + ebRemove(&dictExpireMeta->hfe, &hashFieldExpireBucketsType, field); + + /* Don't have to update global HFE DS. It's unnecessary. Implementing this + * would introduce significant complexity and overhead for an operation that + * isn't critical. In the worst case scenario, the hash will be efficiently + * updated later by an active-expire operation, or it will be removed by the + * hash's dbGenericDelete() function. */ +} + +int hfieldIsExpired(hfield field) { + /* Condition remains valid even if hfieldGetExpireTime() returns EB_EXPIRE_TIME_INVALID, + * as the constant is equivalent to (EB_EXPIRE_TIME_MAX + 1). */ + return ( (mstime_t)hfieldGetExpireTime(field) < commandTimeSnapshot()); +} + +/*----------------------------------------------------------------------------- + * Hash Field Expiration (HFE) + *----------------------------------------------------------------------------*/ +/* Can be called either by active-expire cron job or query from the client */ +static void propagateHashFieldDeletion(redisDb *db, sds key, char *field, size_t fieldLen) { + robj *argv[] = { + shared.hdel, + createStringObject((char*) key, sdslen(key)), + createStringObject(field, fieldLen) + }; + + enterExecutionUnit(1, 0); + int prev_replication_allowed = server.replication_allowed; + server.replication_allowed = 1; + alsoPropagate(db->id,argv, 3, PROPAGATE_AOF|PROPAGATE_REPL); + server.replication_allowed = prev_replication_allowed; + exitExecutionUnit(); + + /* Propagate the HDEL command */ + postExecutionUnitOperations(); + + decrRefCount(argv[1]); + decrRefCount(argv[2]); +} + +/* Called during active expiration of hash-fields. Propagate to replica & Delete. */ +static ExpireAction onFieldExpire(eItem item, void *ctx) { + OnFieldExpireCtx *expCtx = ctx; + hfield hf = item; + dict *d = expCtx->hashObj->ptr; + dictExpireMetadata *dictExpireMeta = (dictExpireMetadata *) dictMetadata(d); + propagateHashFieldDeletion(expCtx->db, dictExpireMeta->key, hf, hfieldlen(hf)); + serverAssert(hashTypeDelete(expCtx->hashObj, hf, 0) == 1); + server.stat_expired_hash_fields++; + return ACT_REMOVE_EXP_ITEM; +} + +/* Retrieve the ExpireMeta associated with the hash. + * The caller is responsible for ensuring that it is indeed attached. */ +static ExpireMeta *hashGetExpireMeta(const eItem hash) { + robj *hashObj = (robj *)hash; + if (hashObj->encoding == OBJ_ENCODING_LISTPACK_EX) { + listpackEx *lpt = hashObj->ptr; + return &lpt->meta; + } else if (hashObj->encoding == OBJ_ENCODING_HT) { + dict *d = hashObj->ptr; + dictExpireMetadata *dictExpireMeta = (dictExpireMetadata *) dictMetadata(d); + return &dictExpireMeta->expireMeta; + } else { + serverPanic("Unknown encoding: %d", hashObj->encoding); + } +} +/* HTTL key */ +static void httlGenericCommand(client *c, const char *cmd, long long basetime, int unit) { + UNUSED(cmd); + robj *hashObj; + long numFields = 0, numFieldsAt = 3; + + /* Read the hash object */ + if ((hashObj = lookupKeyReadOrReply(c, c->argv[1], shared.emptyarray)) == NULL || + checkType(c, hashObj, OBJ_HASH)) return; + + if (strcasecmp(c->argv[numFieldsAt-1]->ptr, "FIELDS")) { + addReplyError(c, "Mandatory argument FIELDS is missing or not at the right position"); + return; + } + + /* Read number of fields */ + if (getRangeLongFromObjectOrReply(c, c->argv[numFieldsAt], 1, LONG_MAX, + &numFields, "Number of fields must be a positive integer") != C_OK) + return; + + /* Verify `numFields` is consistent with number of arguments */ + if (numFields > (c->argc - numFieldsAt - 1)) { + addReplyError(c, "Parameter `numFileds` is more than number of arguments"); + return; + } + + if (hashObj->encoding == OBJ_ENCODING_LISTPACK) { + void *lp = hashObj->ptr; + + addReplyArrayLen(c, numFields); + for (int i = 0 ; i < numFields ; i++) { + sds field = c->argv[numFieldsAt+1+i]->ptr; + void *fptr = lpFirst(lp); + if (fptr != NULL) + fptr = lpFind(lp, fptr, (unsigned char *) field, sdslen(field), 1); + + if (!fptr) + addReplyLongLong(c, HFE_GET_NO_FIELD); + else + addReplyLongLong(c, HFE_GET_NO_TTL); + } + return; + } else if (hashObj->encoding == OBJ_ENCODING_LISTPACK_EX) { + listpackEx *lpt = hashObj->ptr; + + addReplyArrayLen(c, numFields); + for (int i = 0 ; i < numFields ; i++) { + long long expire; + sds field = c->argv[numFieldsAt+1+i]->ptr; + void *fptr = lpFirst(lpt->lp); + if (fptr != NULL) + fptr = lpFind(lpt->lp, fptr, (unsigned char *) field, sdslen(field), 2); + + if (!fptr) { + addReplyLongLong(c, HFE_GET_NO_FIELD); + continue; + } + + fptr = lpNext(lpt->lp, fptr); + serverAssert(fptr); + fptr = lpNext(lpt->lp, fptr); + serverAssert(fptr && lpGetIntegerValue(fptr, &expire)); + + if (expire == HASH_LP_NO_TTL) { + addReplyLongLong(c, HFE_GET_NO_TTL); + continue; + } + + if (expire <= commandTimeSnapshot()) { + addReplyLongLong(c, HFE_GET_NO_FIELD); + continue; + } + + if (unit == UNIT_SECONDS) + addReplyLongLong(c, (expire + 999 - basetime) / 1000); + else + addReplyLongLong(c, (expire - basetime)); + } + return; + } else if (hashObj->encoding == OBJ_ENCODING_HT) { + dict *d = hashObj->ptr; + + addReplyArrayLen(c, numFields); + for (int i = 0 ; i < numFields ; i++) { + sds field = c->argv[numFieldsAt+1+i]->ptr; + dictEntry *de = dictFind(d, field); + if (de == NULL) { + addReplyLongLong(c, HFE_GET_NO_FIELD); + continue; + } + + hfield hf = dictGetKey(de); + uint64_t expire = hfieldGetExpireTime(hf); + if (expire == EB_EXPIRE_TIME_INVALID) { + addReplyLongLong(c, HFE_GET_NO_TTL); /* no ttl */ + continue; + } + + if ( (long long) expire < commandTimeSnapshot()) { + addReplyLongLong(c, HFE_GET_NO_FIELD); + continue; + } + + if (unit == UNIT_SECONDS) + addReplyLongLong(c, (expire + 999 - basetime) / 1000); + else + addReplyLongLong(c, (expire - basetime)); + } + return; + } else { + serverPanic("Unknown encoding: %d", hashObj->encoding); + } +} + +/* This is the generic command implementation for HEXPIRE, HPEXPIRE, HEXPIREAT + * and HPEXPIREAT. Because the command second argument may be relative or absolute + * the "basetime" argument is used to signal what the base time is (either 0 + * for *AT variants of the command, or the current time for relative expires). + * + * unit is either UNIT_SECONDS or UNIT_MILLISECONDS, and is only used for + * the argv[2] parameter. The basetime is always specified in milliseconds. + * + * Additional flags are supported and parsed via parseExtendedExpireArguments */ +static void hexpireGenericCommand(client *c, const char *cmd, long long basetime, int unit) { + long numFields = 0, numFieldsAt = 4; + long long expire; /* unix time in msec */ + int expireSetCond = 0; + robj *hashObj, *keyArg = c->argv[1], *expireArg = c->argv[2]; + + /* Read the hash object */ + if ((hashObj = lookupKeyWriteOrReply(c, keyArg, shared.emptyarray)) == NULL || + checkType(c, hashObj, OBJ_HASH)) return; + + /* Read the expiry time from command */ + if (getLongLongFromObjectOrReply(c, expireArg, &expire, NULL) != C_OK) + return; + + /* Check expire overflow */ + if (expire > (long long) EB_EXPIRE_TIME_MAX) { + addReplyErrorExpireTime(c); + return; + } + + if (unit == UNIT_SECONDS) { + if (expire > (long long) EB_EXPIRE_TIME_MAX / 1000) { + addReplyErrorExpireTime(c); + return; + } + expire *= 1000; + } else { + if (expire > (long long) EB_EXPIRE_TIME_MAX) { + addReplyErrorExpireTime(c); + return; + } + } + + if (expire > (long long) EB_EXPIRE_TIME_MAX - basetime) { + addReplyErrorExpireTime(c); + return; + } + expire += basetime; + + /* Read optional expireSetCond [NX|XX|GT|LT] */ + char *optArg = c->argv[3]->ptr; + if (!strcasecmp(optArg, "nx")) { + expireSetCond = HFE_NX; ++numFieldsAt; + } else if (!strcasecmp(optArg, "xx")) { + expireSetCond = HFE_XX; ++numFieldsAt; + } else if (!strcasecmp(optArg, "gt")) { + expireSetCond = HFE_GT; ++numFieldsAt; + } else if (!strcasecmp(optArg, "lt")) { + expireSetCond = HFE_LT; ++numFieldsAt; + } + + if (strcasecmp(c->argv[numFieldsAt-1]->ptr, "FIELDS")) { + addReplyError(c, "Mandatory argument FIELDS is missing or not at the right position"); + return; + } + + /* Read number of fields */ + if (getRangeLongFromObjectOrReply(c, c->argv[numFieldsAt], 1, LONG_MAX, + &numFields, "Parameter `numFields` should be greater than 0") != C_OK) + return; + + /* Verify `numFields` is consistent with number of arguments */ + if (numFields > (c->argc - numFieldsAt - 1)) { + addReplyError(c, "Parameter `numFileds` is more than number of arguments"); + return; + } + + HashTypeSetEx exCtx; + hashTypeSetExInit(keyArg, hashObj, c, c->db, cmd, + FIELD_DONT_CREATE2, + expireSetCond, + &exCtx); + + addReplyArrayLen(c, numFields); + + for (int i = 0 ; i < numFields ; i++) { + sds field = c->argv[numFieldsAt+i+1]->ptr; + SetExRes res = hashTypeSetEx(c->db, hashObj, field, NULL, expire, &exCtx); + addReplyLongLong(c,res); + } + hashTypeSetExDone(&exCtx); + + /* rewrite command for the replica sake */ + + /* Propagate as HPEXPIREAT millisecond-timestamp. Rewrite only if not already */ + if (c->cmd->proc != hpexpireatCommand) { + rewriteClientCommandArgument(c,0,shared.hpexpireat); + } + + /* rewrite expiration time to unix time in msec */ + if (basetime != 0 || unit == UNIT_SECONDS) { + robj *expireObj = createStringObjectFromLongLong(expire); + rewriteClientCommandArgument(c, 2, expireObj); + decrRefCount(expireObj); + } +} + +/* HPEXPIRE key milliseconds [ NX | XX | GT | LT] numfields */ +void hpexpireCommand(client *c) { + hexpireGenericCommand(c,"hpexpire", commandTimeSnapshot(),UNIT_MILLISECONDS); +} + +/* HEXPIRE key seconds [NX | XX | GT | LT] numfields */ +void hexpireCommand(client *c) { + hexpireGenericCommand(c,"hexpire", commandTimeSnapshot(),UNIT_SECONDS); +} + +/* HEXPIREAT key unix-time-seconds [NX | XX | GT | LT] numfields */ +void hexpireatCommand(client *c) { + hexpireGenericCommand(c,"hexpireat", 0,UNIT_SECONDS); +} + +/* HPEXPIREAT key unix-time-milliseconds [NX | XX | GT | LT] numfields */ +void hpexpireatCommand(client *c) { + hexpireGenericCommand(c,"hpexpireat", 0,UNIT_MILLISECONDS); +} + +/* for each specified field: get the remaining time to live in seconds*/ +/* HTTL key numfields */ +void httlCommand(client *c) { + httlGenericCommand(c, "httl", commandTimeSnapshot(), UNIT_SECONDS); +} + +/* HPTTL key numfields */ +void hpttlCommand(client *c) { + httlGenericCommand(c, "hpttl", commandTimeSnapshot(), UNIT_MILLISECONDS); +} + +/* HEXPIRETIME key numFields */ +void hexpiretimeCommand(client *c) { + httlGenericCommand(c, "hexpiretime", 0, UNIT_SECONDS); +} + +/* HPEXPIRETIME key numFields */ +void hpexpiretimeCommand(client *c) { + httlGenericCommand(c, "hexpiretime", 0, UNIT_MILLISECONDS); +} + +/* HPERSIST key */ +void hpersistCommand(client *c) { + robj *hashObj; + long numFields = 0, numFieldsAt = 3; + int changed = 0; /* Used to determine whether to send a notification. */ + + /* Read the hash object */ + if ((hashObj = lookupKeyReadOrReply(c, c->argv[1], shared.emptyarray)) == NULL || + checkType(c, hashObj, OBJ_HASH)) return; + + if (strcasecmp(c->argv[numFieldsAt-1]->ptr, "FIELDS")) { + addReplyError(c, "Mandatory argument FIELDS is missing or not at the right position"); + return; + } + + /* Read number of fields */ + if (getRangeLongFromObjectOrReply(c, c->argv[numFieldsAt], 1, LONG_MAX, + &numFields, "Number of fields must be a positive integer") != C_OK) + return; + + /* Verify `numFields` is consistent with number of arguments */ + if (numFields > (c->argc - numFieldsAt - 1)) { + addReplyError(c, "Parameter `numFileds` is more than number of arguments"); + return; + } + + if (hashObj->encoding == OBJ_ENCODING_LISTPACK) { + addReplyArrayLen(c, numFields); + for (int i = 0 ; i < numFields ; i++) { + sds field = c->argv[numFieldsAt + 1 + i]->ptr; + unsigned char *fptr, *zl = hashObj->ptr; + + fptr = lpFirst(zl); + if (fptr != NULL) + fptr = lpFind(zl, fptr, (unsigned char *) field, sdslen(field), 1); + + if (!fptr) + addReplyLongLong(c, HFE_PERSIST_NO_FIELD); + else + addReplyLongLong(c, HFE_PERSIST_NO_TTL); + } + return; + } else if (hashObj->encoding == OBJ_ENCODING_LISTPACK_EX) { + long long prevExpire; + unsigned char *fptr, *vptr, *tptr; + listpackEx *lpt = hashObj->ptr; + + addReplyArrayLen(c, numFields); + for (int i = 0 ; i < numFields ; i++) { + sds field = c->argv[numFieldsAt + 1 + i]->ptr; + + fptr = lpFirst(lpt->lp); + if (fptr != NULL) + fptr = lpFind(lpt->lp, fptr, (unsigned char*)field, sdslen(field), 2); + + if (!fptr) { + addReplyLongLong(c, HFE_PERSIST_NO_FIELD); + continue; + } + + vptr = lpNext(lpt->lp, fptr); + serverAssert(vptr); + tptr = lpNext(lpt->lp, vptr); + serverAssert(tptr && lpGetIntegerValue(tptr, &prevExpire)); + + if (prevExpire == HASH_LP_NO_TTL) { + addReplyLongLong(c, HFE_PERSIST_NO_TTL); + continue; + } + + if (prevExpire < commandTimeSnapshot()) { + addReplyLongLong(c, HFE_PERSIST_NO_FIELD); + continue; + } + + listpackExUpdateExpiry(hashObj, field, fptr, vptr, HASH_LP_NO_TTL); + addReplyLongLong(c, HFE_PERSIST_OK); + changed = 1; + } + } else if (hashObj->encoding == OBJ_ENCODING_HT) { + dict *d = hashObj->ptr; + + addReplyArrayLen(c, numFields); + for (int i = 0 ; i < numFields ; i++) { + sds field = c->argv[numFieldsAt + 1 + i]->ptr; + dictEntry *de = dictFind(d, field); + if (de == NULL) { + addReplyLongLong(c, HFE_PERSIST_NO_FIELD); + continue; + } + + hfield hf = dictGetKey(de); + uint64_t expire = hfieldGetExpireTime(hf); + if (expire == EB_EXPIRE_TIME_INVALID) { + addReplyLongLong(c, HFE_PERSIST_NO_TTL); + continue; + } + + /* Already expired. Pretend there is no such field */ + if ( (long long) expire < commandTimeSnapshot()) { + addReplyLongLong(c, HFE_PERSIST_NO_FIELD); + continue; + } + + hfieldPersist(hashObj, hf); + addReplyLongLong(c, HFE_PERSIST_OK); + changed = 1; + } + } else { + serverPanic("Unknown encoding: %d", hashObj->encoding); + } + + /* Generates a hpersist event if the expiry time associated with any field + * has been successfully deleted. */ + if (changed) notifyKeyspaceEvent(NOTIFY_HASH,"hpersist",c->argv[1],c->db->id); +} diff --git a/src/t_set.c b/src/t_set.c index 5efbc535c..f16cde818 100644 --- a/src/t_set.c +++ b/src/t_set.c @@ -432,7 +432,7 @@ robj *setTypePopRandom(robj *set) { if (set->encoding == OBJ_ENCODING_LISTPACK) { /* Find random and delete it without re-seeking the listpack. */ unsigned int i = 0; - unsigned char *p = lpNextRandom(set->ptr, lpFirst(set->ptr), &i, 1, 0); + unsigned char *p = lpNextRandom(set->ptr, lpFirst(set->ptr), &i, 1, 1); unsigned int len = 0; /* initialize to silence warning */ long long llele = 0; /* initialize to silence warning */ char *str = (char *)lpGetValue(p, &len, &llele); @@ -815,7 +815,7 @@ void spopWithCountCommand(client *c) { unsigned int index = 0; unsigned char **ps = zmalloc(sizeof(char *) * count); for (unsigned long i = 0; i < count; i++) { - p = lpNextRandom(lp, p, &index, count - i, 0); + p = lpNextRandom(lp, p, &index, count - i, 1); unsigned int len; str = (char *)lpGetValue(p, &len, (long long *)&llele); @@ -877,7 +877,7 @@ void spopWithCountCommand(client *c) { unsigned int index = 0; unsigned char **ps = zmalloc(sizeof(char *) * remaining); for (unsigned long i = 0; i < remaining; i++) { - p = lpNextRandom(lp, p, &index, remaining - i, 0); + p = lpNextRandom(lp, p, &index, remaining - i, 1); unsigned int len; str = (char *)lpGetValue(p, &len, (long long *)&llele); setTypeAddAux(newset, str, len, llele, 0); @@ -1103,7 +1103,7 @@ void srandmemberWithCountCommand(client *c) { unsigned int i = 0; addReplyArrayLen(c, count); while (count) { - p = lpNextRandom(lp, p, &i, count--, 0); + p = lpNextRandom(lp, p, &i, count--, 1); unsigned int len; str = (char *)lpGetValue(p, &len, (long long *)&llele); if (str == NULL) { diff --git a/src/t_zset.c b/src/t_zset.c index ec7d5ccaf..8533ff12b 100644 --- a/src/t_zset.c +++ b/src/t_zset.c @@ -1754,7 +1754,7 @@ void zsetTypeRandomElement(robj *zsetobj, unsigned long zsetsize, listpackEntry *score = *(double*)dictGetVal(de); } else if (zsetobj->encoding == OBJ_ENCODING_LISTPACK) { listpackEntry val; - lpRandomPair(zsetobj->ptr, zsetsize, key, &val); + lpRandomPair(zsetobj->ptr, zsetsize, key, &val, 2); if (score) { if (val.sval) { *score = zzlStrtod(val.sval,val.slen); @@ -4263,7 +4263,7 @@ void zrandmemberWithCountCommand(client *c, long l, int withscores) { while (count) { sample_count = count > limit ? limit : count; count -= sample_count; - lpRandomPairs(zsetobj->ptr, sample_count, keys, vals); + lpRandomPairs(zsetobj->ptr, sample_count, keys, vals, 2); zrandmemberReplyWithListpack(c, sample_count, keys, vals); if (c->flags & CLIENT_CLOSE_ASAP) break; @@ -4317,7 +4317,7 @@ void zrandmemberWithCountCommand(client *c, long l, int withscores) { keys = zmalloc(sizeof(listpackEntry)*count); if (withscores) vals = zmalloc(sizeof(listpackEntry)*count); - serverAssert(lpRandomPairsUnique(zsetobj->ptr, count, keys, vals) == count); + serverAssert(lpRandomPairsUnique(zsetobj->ptr, count, keys, vals, 2) == count); zrandmemberReplyWithListpack(c, count, keys, vals); zfree(keys); zfree(vals); diff --git a/src/testhelp.h b/src/testhelp.h index 26c55d3f6..3304ccfee 100644 --- a/src/testhelp.h +++ b/src/testhelp.h @@ -21,6 +21,8 @@ #define REDIS_TEST_ACCURATE (1<<0) #define REDIS_TEST_LARGE_MEMORY (1<<1) #define REDIS_TEST_VALGRIND (1<<2) +#define REDIS_TEST_VERBOSE (1<<3) + extern int __failed_tests; extern int __test_num; diff --git a/tests/integration/corrupt-dump-fuzzer.tcl b/tests/integration/corrupt-dump-fuzzer.tcl index 206e503fc..84495886c 100644 --- a/tests/integration/corrupt-dump-fuzzer.tcl +++ b/tests/integration/corrupt-dump-fuzzer.tcl @@ -19,12 +19,14 @@ proc generate_collections {suffix elements} { # add both string values and integers if {$j % 2 == 0} {set val $j} else {set val "_$j"} $rd hset hash$suffix $j $val + $rd hset hashmd$suffix $j $val + $rd hexpire hashmd$suffix [expr {int(rand() * 10000)}] FIELDS 1 $j $rd lpush list$suffix $val $rd zadd zset$suffix $j $val $rd sadd set$suffix $val $rd xadd stream$suffix * item 1 value $val } - for {set j 0} {$j < $elements * 5} {incr j} { + for {set j 0} {$j < $elements * 7} {incr j} { $rd read ; # Discard replies } $rd close diff --git a/tests/integration/corrupt-dump.tcl b/tests/integration/corrupt-dump.tcl index d8537f0c5..3e644cbb9 100644 --- a/tests/integration/corrupt-dump.tcl +++ b/tests/integration/corrupt-dump.tcl @@ -59,6 +59,62 @@ test {corrupt payload: valid zipped hash header, dup records} { } } +test {corrupt payload: hash listpackex with invalid string TTL} { + start_server [list overrides [list loglevel verbose use-exit-on-panic yes crash-memcheck-enabled no] ] { + r config set sanitize-dump-payload yes + catch { + r restore key 0 "\x17\x2d\x2d\x00\x00\x00\x09\x00\x81\x61\x02\x01\x01\xf4\xa6\x96\x18\xb8\x8f\x01\x00\x00\x09\x82\x66\x31\x03\x82\x76\x31\x03\x83\x66\x6f\x6f\x04\x82\x66\x32\x03\x82\x76\x32\x03\x00\x01\xff\x0c\x00\xde\x40\xe5\x37\x51\x1c\x12\x56" replace + } err + assert_match "*Bad data format*" $err + r ping + } +} + +test {corrupt payload: hash listpackex with TTL large than EB_EXPIRE_TIME_MAX} { + start_server [list overrides [list loglevel verbose use-exit-on-panic yes crash-memcheck-enabled no] ] { + r config set sanitize-dump-payload yes + catch { + r restore key 0 "\x17\x33\x33\x00\x00\x00\x09\x00\x00\x01\x00\x01\xf4\x01\xc5\x89\x95\x8f\x01\x00\x00\x09\x01\x01\x82\x5f\x31\x03\xf4\x29\x94\x97\x95\x8f\x01\x00\x00\x09\x02\x01\x02\x01\xf4\x01\x5e\xaf\x95\x8f\x01\x33\x00\x09\xff\x0c\x00\x7e\x4f\xf4\x33\xe9\xc5\x3e\x56" replace + } err + assert_match "*Bad data format*" $err + r ping + } +} + +test {corrupt payload: hash listpackex with unordered TTL fields} { + start_server [list overrides [list loglevel verbose use-exit-on-panic yes crash-memcheck-enabled no] ] { + r config set sanitize-dump-payload yes + catch { + r restore key 0 "\x17\xc3\x30\x35\x14\x35\x00\x00\x00\t\x00\x82\x66\x32\x03\x82\x76\x32\x03\xf4\x80\x73\x16\xd1\x8f\x01\x20\x12\x02\x82\x66\x31\x20\x11\x03\x31\x03\xf4\x7f\xe0\x01\x11\x00\x33\x20\x11\x04\x33\x03\x00\x01\xff\x0c\x00\xf6\x70\x29\x57\x11\x68\x9d\xe5" replace + } err + assert_match "*Bad data format*" $err + r ping + } +} + +test {corrupt payload: hash listpackex field without TTL should not be followed by field with TTL} { + start_server [list overrides [list loglevel verbose use-exit-on-panic yes crash-memcheck-enabled no] ] { + r config set sanitize-dump-payload yes + catch { + r restore key 0 "\x17\x2d\x2d\x00\x00\x00\x09\x00\x82\x66\x31\x03\x82\x76\x31\x03\x00\x01\x82\x66\x32\x03\x82\x76\x32\x03\xf4\xe0\x59\x7a\x96\x00\x00\x00\x00\x09\x82\x66\x33\x03\x82\x76\x33\x03\x00\x01\xff\x0c\x00\x42\x66\xd4\xbe\x17\xc3\x96\x72" replace + } err + assert_match "*Bad data format*" $err + r ping + } +} + +test {corrupt payload: hash hashtable with TTL large than EB_EXPIRE_TIME_MAX} { + start_server [list overrides [list loglevel verbose use-exit-on-panic yes crash-memcheck-enabled no] ] { + r config set hash-max-listpack-entries 0 + r config set sanitize-dump-payload yes + catch { + r restore key 0 "\x16\x02\x81\x00\x01\x00\x00\x00\x00\x00\x00\x02\x66\x31\x02\x76\x31\x81\x00\x01\x00\x00\x00\x00\x00\x00\x02\x66\x32\x02\x76\x32\x0c\x00\xb9\x3c\x65\x28\x40\x94\x58\x36" replace + } err + assert_match "*Bad data format*" $err + r ping + } +} + test {corrupt payload: quicklist big ziplist prev len} { start_server [list overrides [list loglevel verbose use-exit-on-panic yes crash-memcheck-enabled no] ] { r config set sanitize-dump-payload no diff --git a/tests/integration/psync2-master-restart.tcl b/tests/integration/psync2-master-restart.tcl index 6f7a31d21..b0d394389 100644 --- a/tests/integration/psync2-master-restart.tcl +++ b/tests/integration/psync2-master-restart.tcl @@ -179,6 +179,17 @@ start_server {} { $master set $j somevalue px 10 } + ##### hash-field-expiration + # Hashes of type OBJ_ENCODING_LISTPACK_EX won't be discarded during + # RDB load, even if they are expired. + $master hset myhash1 f1 v1 f2 v2 f3 v3 + $master hpexpire myhash1 10 FIELDS 3 f1 f2 f3 + # Hashes of type RDB_TYPE_HASH_METADATA will be discarded during RDB load. + $master config set hash-max-listpack-entries 0 + $master hset myhash2 f1 v1 f2 v2 + $master hpexpire myhash2 10 FIELDS 2 f1 f2 + $master config set hash-max-listpack-entries 1 + after 20 wait_for_condition 500 100 { diff --git a/tests/integration/rdb.tcl b/tests/integration/rdb.tcl index cce21671f..9db781689 100644 --- a/tests/integration/rdb.tcl +++ b/tests/integration/rdb.tcl @@ -416,4 +416,228 @@ start_server {} { } {OK} } +set server_path [tmpdir "server.partial-hfield-exp-test"] + +# verifies writing and reading hash key with expiring and persistent fields +start_server [list overrides [list "dir" $server_path]] { + foreach {type lp_entries} {listpack 512 dict 0} { + test "HFE - save and load expired fields, expired soon after, or long after ($type)" { + r config set hash-max-listpack-entries $lp_entries + + r FLUSHALL + + r HMSET key a 1 b 2 c 3 d 4 e 5 + # expected to be expired long after restart + r HEXPIREAT key 2524600800 FIELDS 1 a + # expected long TTL value (6 bytes) is saved and loaded correctly + r HPEXPIREAT key 188900976391764 FIELDS 1 b + # expected to be already expired after restart + r HPEXPIRE key 80 FIELDS 1 d + # expected to be expired soon after restart + r HPEXPIRE key 200 FIELDS 1 e + + r save + # sleep 101 ms to make sure d will expire after restart + after 101 + restart_server 0 true false + wait_done_loading r + + assert_equal [lsort [r hgetall key]] "1 2 3 a b c" + assert_equal [r hpexpiretime key FIELDS 3 a b c] {2524600800000 188900976391764 -1} + assert_equal [s rdb_last_load_keys_loaded] 1 + + # wait until expired_hash_fields equals 2 + wait_for_condition 10 100 { + [s expired_hash_fields] == 2 + } else { + fail "Value of expired_hash_fields is not as expected" + } + } + } +} + +set server_path [tmpdir "server.all-hfield-exp-test"] + +# verifies writing hash with several expired keys, and active-expiring it on load +start_server [list overrides [list "dir" $server_path]] { + foreach {type lp_entries} {listpack 512 dict 0} { + test "HFE - save and load rdb all fields expired, ($type)" { + r config set hash-max-listpack-entries $lp_entries + + r FLUSHALL + + r HMSET key a 1 b 2 c 3 d 4 + r HPEXPIRE key 100 FIELDS 4 a b c d + + r save + # sleep 101 ms to make sure all fields will expire after restart + after 101 + + restart_server 0 true false + wait_done_loading r + + # it is expected that no field was expired on load and the key was + # loaded, even though all its fields are actually expired. + assert_equal [s rdb_last_load_keys_loaded] 1 + + assert_equal [r hgetall key] {} + } + } +} + +set server_path [tmpdir "server.listpack-to-dict-test"] + +test "save listpack, load dict" { + start_server [list overrides [list "dir" $server_path enable-debug-command yes]] { + r config set hash-max-listpack-entries 512 + + r FLUSHALL + + r HMSET key a 1 b 2 c 3 d 4 + assert_match "*encoding:listpack*" [r debug object key] + r HPEXPIRE key 100 FIELDS 1 d + r save + + # sleep 200 ms to make sure 'd' will expire after when reloading + after 200 + + # change configuration and reload - result should be dict-encoded key + r config set hash-max-listpack-entries 0 + r debug reload nosave + + # first verify d was not expired during load (no expiry when loading + # a hash that was saved listpack-encoded) + assert_equal [s rdb_last_load_keys_loaded] 1 + + # d should be lazy expired in hgetall + assert_equal [lsort [r hgetall key]] "1 2 3 a b c" + assert_match "*encoding:hashtable*" [r debug object key] + } +} + +set server_path [tmpdir "server.dict-to-listpack-test"] + +test "save dict, load listpack" { + start_server [list overrides [list "dir" $server_path enable-debug-command yes]] { + r config set hash-max-listpack-entries 0 + + r FLUSHALL + + r HMSET key a 1 b 2 c 3 d 4 + assert_match "*encoding:hashtable*" [r debug object key] + r HPEXPIRE key 200 FIELDS 1 d + r save + + # sleep 201 ms to make sure 'd' will expire during reload + after 201 + + # change configuration and reload - result should be LP-encoded key + r config set hash-max-listpack-entries 512 + r debug reload nosave + + # verify d was expired during load + assert_equal [s rdb_last_load_keys_loaded] 1 + + assert_equal [lsort [r hgetall key]] "1 2 3 a b c" + assert_match "*encoding:listpack*" [r debug object key] + } +} + +set server_path [tmpdir "server.active-expiry-after-load"] + +# verifies a field is correctly expired by active expiry AFTER loading from RDB +foreach {type lp_entries} {listpack 512 dict 0} { + start_server [list overrides [list "dir" $server_path enable-debug-command yes]] { + test "active field expiry after load, ($type)" { + r config set hash-max-listpack-entries $lp_entries + + r FLUSHALL + + r HMSET key a 1 b 2 c 3 d 4 e 5 f 6 + r HEXPIREAT key 2524600800 FIELDS 2 a b + r HPEXPIRE key 200 FIELDS 2 c d + + r save + r debug reload nosave + + # wait at most 2 secs to make sure 'c' and 'd' will active-expire + wait_for_condition 20 100 { + [s expired_hash_fields] == 2 + } else { + fail "expired hash fields is [s expired_hash_fields] != 2" + } + + assert_equal [s rdb_last_load_keys_loaded] 1 + + # hgetall might lazy expire fields, so it's only called after the stat asserts + assert_equal [lsort [r hgetall key]] "1 2 5 6 a b e f" + assert_equal [r hexpiretime key FIELDS 6 a b c d e f] {2524600800 2524600800 -2 -2 -1 -1} + } + } +} + +set server_path [tmpdir "server.lazy-expiry-after-load"] + +foreach {type lp_entries} {listpack 512 dict 0} { + start_server [list overrides [list "dir" $server_path enable-debug-command yes]] { + test "lazy field expiry after load, ($type)" { + r config set hash-max-listpack-entries $lp_entries + r debug set-active-expire 0 + + r FLUSHALL + + r HMSET key a 1 b 2 c 3 d 4 e 5 f 6 + r HEXPIREAT key 2524600800 FIELDS 2 a b + r HPEXPIRE key 200 FIELDS 2 c d + + r save + r debug reload nosave + + # sleep 500 msec to make sure 'c' and 'd' will lazy-expire when calling hgetall + after 500 + + assert_equal [s rdb_last_load_keys_loaded] 1 + assert_equal [s expired_hash_fields] 0 + + # hgetall will lazy expire fields, so it's only called after the stat asserts + assert_equal [lsort [r hgetall key]] "1 2 5 6 a b e f" + assert_equal [r hexpiretime key FIELDS 6 a b c d e f] {2524600800 2524600800 -2 -2 -1 -1} + } + } +} + +set server_path [tmpdir "server.unexpired-items-rax-list-boundary"] + +foreach {type lp_entries} {listpack 512 dict 0} { + start_server [list overrides [list "dir" $server_path enable-debug-command yes]] { + test "load un-expired items below and above rax-list boundary, ($type)" { + r config set hash-max-listpack-entries $lp_entries + + r flushall + + set hash_sizes {15 16 17 31 32 33} + foreach h $hash_sizes { + for {set i 1} {$i <= $h} {incr i} { + r hset key$h f$i v$i + r hexpireat key$h 2524600800 FIELDS 1 f$i + } + } + + r save + + restart_server 0 true false + wait_done_loading r + + set hash_sizes {15 16 17 31 32 33} + foreach h $hash_sizes { + for {set i 1} {$i <= $h} {incr i} { + # random expiration time + assert_equal [r hget key$h f$i] v$i + assert_equal [r hexpiretime key$h FIELDS 1 f$i] 2524600800 + } + } + } + } +} + } ;# tags diff --git a/tests/support/util.tcl b/tests/support/util.tcl index 0270e9222..fcdac8c94 100644 --- a/tests/support/util.tcl +++ b/tests/support/util.tcl @@ -293,6 +293,9 @@ proc findKeyWithType {r type} { proc createComplexDataset {r ops {opt {}}} { set useexpire [expr {[lsearch -exact $opt useexpire] != -1}] + # TODO: Remove usehexpire on next commit, when RDB will support replication + set usehexpire [expr {[lsearch -exact $opt usehexpire] != -1}] + if {[lsearch -exact $opt usetag] != -1} { set tag "{t}" } else { @@ -386,6 +389,10 @@ proc createComplexDataset {r ops {opt {}}} { {hash} { randpath {{*}$r hset $k $f $v} \ {{*}$r hdel $k $f} + + if { [{*}$r hexists $k $f] && $usehexpire && rand() < 0.5} { + {*}$r hexpire $k 1000 FIELDS 1 $f + } } } } @@ -438,8 +445,14 @@ proc csvdump r { hash { set fields [{*}$r hgetall $k] set newfields {} - foreach {k v} $fields { - lappend newfields [list $k $v] + foreach {f v} $fields { + set expirylist [{*}$r hexpiretime $k FIELDS 1 $f] + if {$expirylist eq (-1)} { + lappend newfields [list $f $v] + } else { + set e [lindex $expirylist 0] + lappend newfields [list $f $e $v] # TODO: extract the actual ttl value from the list in $e + } } set fields [lsort -index 0 $newfields] foreach kv $fields { diff --git a/tests/test_helper.tcl b/tests/test_helper.tcl index ffe89862f..8901e2f8a 100644 --- a/tests/test_helper.tcl +++ b/tests/test_helper.tcl @@ -34,6 +34,7 @@ set ::all_tests { unit/type/set unit/type/zset unit/type/hash + unit/type/hash-field-expire unit/type/stream unit/type/stream-cgroups unit/sort diff --git a/tests/unit/memefficiency.tcl b/tests/unit/memefficiency.tcl index 9735af3ec..788f835c2 100644 --- a/tests/unit/memefficiency.tcl +++ b/tests/unit/memefficiency.tcl @@ -503,6 +503,105 @@ run_solo {defrag} { $rd_pubsub close } + test "Active Defrag HFE: $type" { + r flushdb + r config resetstat + r config set hz 100 + r config set activedefrag no + # TODO: Lower the threshold after defraging the ebuckets. + # Now just to ensure that the reference is updated correctly. + r config set active-defrag-threshold-lower 12 + r config set active-defrag-cycle-min 65 + r config set active-defrag-cycle-max 75 + r config set active-defrag-ignore-bytes 1500kb + r config set maxmemory 0 + r config set hash-max-listpack-value 512 + r config set hash-max-listpack-entries 10 + + # Populate memory with interleaving hash field of same size + set n 3000 + set fields 16 ;# make all the fields in an eblist. + set dummy_field "[string repeat x 400]" + set rd [redis_deferring_client] + for {set i 0} {$i < $n} {incr i} { + for {set j 0} {$j < $fields} {incr j} { + $rd hset h$i f$j $dummy_field + $rd hexpire h$i 9999999 FIELDS 1 f$j + $rd set "k$i$j" $dummy_field + } + } + for {set j 0} {$j < [expr $n*$fields]} {incr j} { + $rd read ; # Discard hset replies + $rd read ; # Discard hexpire replies + $rd read ; # Discard set replies + } + + # Coverage for listpackex. + r hset h_lpex f0 $dummy_field + r hexpire h_lpex 9999999 FIELDS 1 f0 + assert_encoding listpackex h_lpex + + after 120 ;# serverCron only updates the info once in 100ms + if {$::verbose} { + puts "used [s allocator_allocated]" + puts "rss [s allocator_active]" + puts "frag [s allocator_frag_ratio]" + puts "frag_bytes [s allocator_frag_bytes]" + } + assert_lessthan [s allocator_frag_ratio] 1.05 + + # Delete all the keys to create fragmentation + for {set i 0} {$i < $n} {incr i} { + for {set j 0} {$j < $fields} {incr j} { + r del "k$i$j" + } + } + $rd close + after 120 ;# serverCron only updates the info once in 100ms + if {$::verbose} { + puts "used [s allocator_allocated]" + puts "rss [s allocator_active]" + puts "frag [s allocator_frag_ratio]" + puts "frag_bytes [s allocator_frag_bytes]" + } + assert_morethan [s allocator_frag_ratio] 1.35 + + catch {r config set activedefrag yes} e + if {[r config get activedefrag] eq "activedefrag yes"} { + + # wait for the active defrag to start working (decision once a second) + wait_for_condition 50 100 { + [s total_active_defrag_time] ne 0 + } else { + after 120 ;# serverCron only updates the info once in 100ms + puts [r info memory] + puts [r info stats] + puts [r memory malloc-stats] + fail "defrag not started." + } + + # wait for the active defrag to stop working + wait_for_condition 500 100 { + [s active_defrag_running] eq 0 + } else { + after 120 ;# serverCron only updates the info once in 100ms + puts [r info memory] + puts [r memory malloc-stats] + fail "defrag didn't stop." + } + + # test the fragmentation is lower + after 120 ;# serverCron only updates the info once in 100ms + if {$::verbose} { + puts "used [s allocator_allocated]" + puts "rss [s allocator_active]" + puts "frag [s allocator_frag_ratio]" + puts "frag_bytes [s allocator_frag_bytes]" + } + assert_lessthan_equal [s allocator_frag_ratio] 1.5 + } + } + if {$type eq "standalone"} { ;# skip in cluster mode test "Active defrag big list: $type" { r flushdb diff --git a/tests/unit/other.tcl b/tests/unit/other.tcl index 1ba0e62fa..52f1fa75c 100644 --- a/tests/unit/other.tcl +++ b/tests/unit/other.tcl @@ -124,7 +124,8 @@ start_server {tags {"other"}} { if {$::accurate} {set numops 10000} else {set numops 1000} test {Check consistency of different data types after a reload} { r flushdb - createComplexDataset r $numops usetag + # TODO: integrate usehexpire following next commit that will support replication + createComplexDataset r $numops {usetag usehexpire} if {$::ignoredigest} { set _ 1 } else { diff --git a/tests/unit/pubsub.tcl b/tests/unit/pubsub.tcl index 3797b00c7..5e911e843 100644 --- a/tests/unit/pubsub.tcl +++ b/tests/unit/pubsub.tcl @@ -353,17 +353,29 @@ start_server {tags {"pubsub network"}} { $rd1 close } - test "Keyspace notifications: hash events test" { + foreach {type max_lp_entries} {listpackex 512 hashtable 0} { + test "Keyspace notifications: hash events test ($type)" { + r config set hash-max-listpack-entries $max_lp_entries r config set notify-keyspace-events Kh r del myhash set rd1 [redis_deferring_client] assert_equal {1} [psubscribe $rd1 *] r hmset myhash yes 1 no 0 r hincrby myhash yes 10 + r hexpire myhash 999999 FIELDS 1 yes + r hexpireat myhash [expr {[clock seconds] + 999999}] NX FIELDS 1 no + r hpexpire myhash 5 FIELDS 1 yes + r hpersist myhash FIELDS 1 yes + assert_encoding $type myhash assert_equal "pmessage * __keyspace@${db}__:myhash hset" [$rd1 read] assert_equal "pmessage * __keyspace@${db}__:myhash hincrby" [$rd1 read] + assert_equal "pmessage * __keyspace@${db}__:myhash hexpire" [$rd1 read] + assert_equal "pmessage * __keyspace@${db}__:myhash hexpire" [$rd1 read] + assert_equal "pmessage * __keyspace@${db}__:myhash hexpire" [$rd1 read] + assert_equal "pmessage * __keyspace@${db}__:myhash hpersist" [$rd1 read] $rd1 close } + } ;# foreach test "Keyspace notifications: stream events test" { r config set notify-keyspace-events Kt diff --git a/tests/unit/scan.tcl b/tests/unit/scan.tcl index d980a52ad..8ab14d3b1 100644 --- a/tests/unit/scan.tcl +++ b/tests/unit/scan.tcl @@ -277,6 +277,32 @@ proc test_scan {type} { set res [r hscan hash 0 count 1000 novalues] assert_equal [lsort $keys2] [lsort [lindex $res 1]] } + + test "{$type} HSCAN with large value $enc" { + r del hash + + if {$enc eq {listpack}} { + set count 60 + } else { + set count 170 + } + + set val1 [string repeat "1" $count] + r hset hash $val1 $val1 + + set val2 [string repeat "2" $count] + r hset hash $val2 $val2 + + set res [lsort [lindex [r hscan hash 0] 1]] + assert_equal $val1 [lindex $res 0] + assert_equal $val1 [lindex $res 1] + assert_equal $val2 [lindex $res 2] + assert_equal $val2 [lindex $res 3] + + set res [lsort [lindex [r hscan hash 0 novalues] 1]] + assert_equal $val1 [lindex $res 0] + assert_equal $val2 [lindex $res 1] + } } foreach enc {listpack skiplist} { diff --git a/tests/unit/type/hash-field-expire.tcl b/tests/unit/type/hash-field-expire.tcl new file mode 100644 index 000000000..ef5217068 --- /dev/null +++ b/tests/unit/type/hash-field-expire.tcl @@ -0,0 +1,1151 @@ +######## HEXPIRE family commands +# Field does not exists +set E_NO_FIELD -2 +# Specified NX | XX | GT | LT condition not met +set E_FAIL 0 +# expiration time set/updated +set E_OK 1 +# Field deleted because the specified expiration time is in the past +set E_DELETED 2 + +######## HTTL family commands +set T_NO_FIELD -2 +set T_NO_EXPIRY -1 + +######## HPERIST +set P_NO_FIELD -2 +set P_NO_EXPIRY -1 +set P_OK 1 + +############################### AUX FUNCS ###################################### + +proc get_hashes_with_expiry_fields {r} { + set input_string [r info keyspace] + set hash_count 0 + + foreach line [split $input_string \n] { + if {[regexp {hashes_with_expiry_fields=(\d+)} $line -> value]} { + return $value + } + } + + return 0 +} + +proc create_hash {key entries} { + r del $key + foreach entry $entries { + r hset $key [lindex $entry 0] [lindex $entry 1] + } +} + +proc get_keys {l} { + set res {} + foreach entry $l { + set key [lindex $entry 0] + lappend res $key + } + return $res +} + +proc cmp_hrandfield_result {hash_name expected_result} { + # Accumulate hrandfield results + unset -nocomplain myhash + array set myhash {} + for {set i 0} {$i < 100} {incr i} { + set key [r hrandfield $hash_name] + set myhash($key) 1 + } + set res [lsort [array names myhash]] + if {$res eq $expected_result} { + return 1 + } else { + return $res + } +} + +proc dumpAllHashes {client} { + set keyAndFields(0,0) 0 + unset keyAndFields + # keep keys sorted for comparison + foreach key [lsort [$client keys *]] { + set fields [$client hgetall $key] + foreach f $fields { + set keyAndFields($key,$f) [$client hpexpiretime $key FIELDS 1 $f] + } + } + return [array get keyAndFields] +} + +proc hrandfieldTest {activeExpireConfig} { + r debug set-active-expire $activeExpireConfig + r del myhash + set contents {{field1 1} {field2 2} } + create_hash myhash $contents + + set factorValgrind [expr {$::valgrind ? 2 : 1}] + + # Set expiration time for field1 and field2 such that field1 expires first + r hpexpire myhash 1 NX FIELDS 1 field1 + r hpexpire myhash 100 NX FIELDS 1 field2 + + # On call hrandfield command lazy expire deletes field1 first + wait_for_condition 8 10 { + [cmp_hrandfield_result myhash "field2"] == 1 + } else { + fail "Expected field2 to be returned by HRANDFIELD." + } + + # On call hrandfield command lazy expire deletes field2 as well + wait_for_condition 8 20 { + [cmp_hrandfield_result myhash "{}"] == 1 + } else { + fail "Expected {} to be returned by HRANDFIELD." + } + + # restore the default value + r debug set-active-expire 1 +} + +############################### TESTS ######################################### + +start_server {tags {"external:skip needs:debug"}} { + foreach type {listpackex hashtable} { + if {$type eq "hashtable"} { + r config set hash-max-listpack-entries 0 + } else { + r config set hash-max-listpack-entries 512 + } + + test "HEXPIRE/HEXPIREAT/HPEXPIRE/HPEXPIREAT - Returns empty array if key does not exist" { + r del myhash + # Make sure we can distinguish between an empty array and a null response + r readraw 1 + assert_equal {*0} [r HEXPIRE myhash 1000 FIELDS 1 a] + assert_equal {*0} [r HEXPIREAT myhash 1000 FIELDS 1 a] + assert_equal {*0} [r HPEXPIRE myhash 1000 FIELDS 1 a] + assert_equal {*0} [r HPEXPIREAT myhash 1000 FIELDS 1 a] + r readraw 0 + } + + test "HPEXPIRE(AT) - Test 'NX' flag ($type)" { + r del myhash + r hset myhash field1 value1 field2 value2 field3 value3 + assert_equal [r hpexpire myhash 1000 NX FIELDS 1 field1] [list $E_OK] + assert_equal [r hpexpire myhash 1000 NX FIELDS 2 field1 field2] [list $E_FAIL $E_OK] + + r del myhash + r hset myhash field1 value1 field2 value2 field3 value3 + assert_equal [r hpexpireat myhash [expr {([clock seconds]+1000)*1000}] NX FIELDS 1 field1] [list $E_OK] + assert_equal [r hpexpireat myhash [expr {([clock seconds]+1000)*1000}] NX FIELDS 2 field1 field2] [list $E_FAIL $E_OK] + } + + test "HPEXPIRE(AT) - Test 'XX' flag ($type)" { + r del myhash + r hset myhash field1 value1 field2 value2 field3 value3 + assert_equal [r hpexpire myhash 1000 NX FIELDS 2 field1 field2] [list $E_OK $E_OK] + assert_equal [r hpexpire myhash 1000 XX FIELDS 2 field1 field3] [list $E_OK $E_FAIL] + + r del myhash + r hset myhash field1 value1 field2 value2 field3 value3 + assert_equal [r hpexpireat myhash [expr {([clock seconds]+1000)*1000}] NX FIELDS 2 field1 field2] [list $E_OK $E_OK] + assert_equal [r hpexpireat myhash [expr {([clock seconds]+1000)*1000}] XX FIELDS 2 field1 field3] [list $E_OK $E_FAIL] + } + + test "HPEXPIRE(AT) - Test 'GT' flag ($type)" { + r del myhash + r hset myhash field1 value1 field2 value2 + assert_equal [r hpexpire myhash 1000 NX FIELDS 1 field1] [list $E_OK] + assert_equal [r hpexpire myhash 2000 NX FIELDS 1 field2] [list $E_OK] + assert_equal [r hpexpire myhash 1500 GT FIELDS 2 field1 field2] [list $E_OK $E_FAIL] + + r del myhash + r hset myhash field1 value1 field2 value2 + assert_equal [r hpexpireat myhash [expr {([clock seconds]+1000)*1000}] NX FIELDS 1 field1] [list $E_OK] + assert_equal [r hpexpireat myhash [expr {([clock seconds]+2000)*1000}] NX FIELDS 1 field2] [list $E_OK] + assert_equal [r hpexpireat myhash [expr {([clock seconds]+1500)*1000}] GT FIELDS 2 field1 field2] [list $E_OK $E_FAIL] + } + + test "HPEXPIRE(AT) - Test 'LT' flag ($type)" { + r del myhash + r hset myhash field1 value1 field2 value2 field3 value3 + assert_equal [r hpexpire myhash 1000 NX FIELDS 1 field1] [list $E_OK] + assert_equal [r hpexpire myhash 2000 NX FIELDS 1 field2] [list $E_OK] + assert_equal [r hpexpire myhash 1500 LT FIELDS 3 field1 field2 field3] [list $E_FAIL $E_OK $E_OK] + + r del myhash + r hset myhash field1 value1 field2 value2 field3 value3 + assert_equal [r hpexpireat myhash [expr {([clock seconds]+1000)*1000}] NX FIELDS 1 field1] [list $E_OK] + assert_equal [r hpexpireat myhash [expr {([clock seconds]+2000)*1000}] NX FIELDS 1 field2] [list $E_OK] + assert_equal [r hpexpireat myhash [expr {([clock seconds]+1500)*1000}] LT FIELDS 3 field1 field2 field3] [list $E_FAIL $E_OK $E_OK] + } + + test "HPEXPIREAT - field not exists or TTL is in the past ($type)" { + r del myhash + r hset myhash f1 v1 f2 v2 f4 v4 + r hexpire myhash 1000 NX FIELDS 1 f4 + assert_equal [r hpexpireat myhash [expr {([clock seconds]-1)*1000}] NX FIELDS 4 f1 f2 f3 f4] "$E_DELETED $E_DELETED $E_NO_FIELD $E_FAIL" + assert_equal [r hexists myhash field1] 0 + } + + test "HPEXPIRE - wrong number of arguments ($type)" { + r del myhash + r hset myhash f1 v1 + assert_error {*Parameter `numFields` should be greater than 0} {r hpexpire myhash 1000 NX FIELDS 0 f1 f2 f3} + assert_error {*Parameter `numFileds` is more than number of arguments} {r hpexpire myhash 1000 NX FIELDS 4 f1 f2 f3} + } + + test "HPEXPIRE - parameter expire-time near limit of 2^48 ($type)" { + r del myhash + r hset myhash f1 v1 + # below & above + assert_equal [r hpexpire myhash [expr (1<<48) - [clock milliseconds] - 1000 ] FIELDS 1 f1] [list $E_OK] + assert_error {*invalid expire time*} {r hpexpire myhash [expr (1<<48) - [clock milliseconds] + 100 ] FIELDS 1 f1} + } + + test "Lazy Expire - fields are lazy deleted ($type)" { + + # TODO remove the SELECT once dbid will be embedded inside dict/listpack + r select 0 + r debug set-active-expire 0 + r del myhash + + r hset myhash f1 v1 f2 v2 f3 v3 + r hpexpire myhash 1 NX FIELDS 3 f1 f2 f3 + after 5 + + # Verify that still exists even if all fields are expired + assert_equal 1 [r EXISTS myhash] + + # Verify that len counts also expired fields + assert_equal 3 [r HLEN myhash] + + # Trying access to expired field should delete it. Len should be updated + assert_equal 0 [r hexists myhash f1] + assert_equal 2 [r HLEN myhash] + + # Trying access another expired field should delete it. Len should be updated + assert_equal "" [r hget myhash f2] + assert_equal 1 [r HLEN myhash] + + # Trying access last expired field should delete it. hash shouldn't exists afterward. + assert_equal 0 [r hstrlen myhash f3] + assert_equal 0 [r HLEN myhash] + assert_equal 0 [r EXISTS myhash] + + # Restore default + r debug set-active-expire 1 + } + + test "Active Expire - deletes hash that all its fields got expired ($type)" { + r flushall + + set hash_sizes {1 15 16 17 31 32 33 40} + foreach h $hash_sizes { + for {set i 1} {$i <= $h} {incr i} { + # random expiration time + r hset hrand$h f$i v$i + r hpexpire hrand$h [expr {50 + int(rand() * 50)}] FIELDS 1 f$i + assert_equal 1 [r HEXISTS hrand$h f$i] + + # same expiration time + r hset same$h f$i v$i + r hpexpire same$h 100 FIELDS 1 f$i + assert_equal 1 [r HEXISTS same$h f$i] + + # same expiration time + r hset mix$h f$i v$i fieldWithoutExpire$i v$i + r hpexpire mix$h 100 FIELDS 1 f$i + assert_equal 1 [r HEXISTS mix$h f$i] + } + } + + # Wait for active expire + wait_for_condition 50 20 { [r EXISTS same40] == 0 } else { fail "hash `same40` should be expired" } + + # Verify that all fields got expired and keys got deleted + foreach h $hash_sizes { + wait_for_condition 50 20 { + [r HLEN mix$h] == $h + } else { + fail "volatile fields of hash `mix$h` should be expired" + } + + for {set i 1} {$i <= $h} {incr i} { + assert_equal 0 [r HEXISTS mix$h f$i] + } + assert_equal 0 [r EXISTS hrand$h] + assert_equal 0 [r EXISTS same$h] + } + } + + test "HPEXPIRE - Flushall deletes all pending expired fields ($type)" { + r del myhash + r hset myhash field1 value1 field2 value2 + r hpexpire myhash 10000 NX FIELDS 1 field1 + r hpexpire myhash 10000 NX FIELDS 1 field2 + r flushall + r del myhash + r hset myhash field1 value1 field2 value2 + r hpexpire myhash 10000 NX FIELDS 1 field1 + r hpexpire myhash 10000 NX FIELDS 1 field2 + r flushall async + } + + test "HTTL/HPTTL - Returns empty array if key does not exist" { + r del myhash + # Make sure we can distinguish between an empty array and a null response + r readraw 1 + assert_equal {*0} [r HTTL myhash FIELDS 1 a] + assert_equal {*0} [r HPTTL myhash FIELDS 1 a] + r readraw 0 + } + + test "HTTL/HPTTL - Input validation gets failed on nonexists field or field without expire ($type)" { + r del myhash + r HSET myhash field1 value1 field2 value2 + r HPEXPIRE myhash 1000 NX FIELDS 1 field1 + + foreach cmd {HTTL HPTTL} { + assert_equal [r $cmd non_exists_key FIELDS 1 f] {} + assert_equal [r $cmd myhash FIELDS 2 field2 non_exists_field] "$T_NO_EXPIRY $T_NO_FIELD" + # Set numFields less than actual number of fields. Fine. + assert_equal [r $cmd myhash FIELDS 1 non_exists_field1 non_exists_field2] "$T_NO_FIELD" + } + } + + test "HTTL/HPTTL - returns time to live in seconds/msillisec ($type)" { + r del myhash + r HSET myhash field1 value1 field2 value2 + r HPEXPIRE myhash 2000 NX FIELDS 2 field1 field2 + set ttlArray [r HTTL myhash FIELDS 2 field1 field2] + assert_range [lindex $ttlArray 0] 1 2 + set ttl [r HPTTL myhash FIELDS 1 field1] + assert_range $ttl 1000 2000 + } + + test "HEXPIRETIME/HPEXPIRETIME - Returns empty array if key does not exist" { + r del myhash + # Make sure we can distinguish between an empty array and a null response + r readraw 1 + assert_equal {*0} [r HEXPIRETIME myhash FIELDS 1 a] + assert_equal {*0} [r HPEXPIRETIME myhash FIELDS 1 a] + r readraw 0 + } + + test "HEXPIRETIME - returns TTL in Unix timestamp ($type)" { + r del myhash + r HSET myhash field1 value1 + r HPEXPIRE myhash 1000 NX FIELDS 1 field1 + + set lo [expr {[clock seconds] + 1}] + set hi [expr {[clock seconds] + 2}] + assert_range [r HEXPIRETIME myhash FIELDS 1 field1] $lo $hi + assert_range [r HPEXPIRETIME myhash FIELDS 1 field1] [expr $lo*1000] [expr $hi*1000] + } + + test "HTTL/HPTTL - Verify TTL progress until expiration ($type)" { + r del myhash + r hset myhash field1 value1 field2 value2 + r hpexpire myhash 1000 NX FIELDS 1 field1 + assert_range [r HPTTL myhash FIELDS 1 field1] 100 1000 + assert_range [r HTTL myhash FIELDS 1 field1] 0 1 + after 100 + assert_range [r HPTTL myhash FIELDS 1 field1] 1 901 + after 910 + assert_equal [r HPTTL myhash FIELDS 1 field1] $T_NO_FIELD + assert_equal [r HTTL myhash FIELDS 1 field1] $T_NO_FIELD + } + + test "HPEXPIRE - DEL hash with non expired fields (valgrind test) ($type)" { + r del myhash + r hset myhash field1 value1 field2 value2 + r hpexpire myhash 10000 NX FIELDS 1 field1 + r del myhash + } + + test "HEXPIREAT - Set time in the past ($type)" { + r del myhash + r hset myhash field1 value1 + assert_equal [r hexpireat myhash [expr {[clock seconds] - 1}] NX FIELDS 1 field1] $E_DELETED + assert_equal [r hexists myhash field1] 0 + } + + test "HEXPIREAT - Set time and then get TTL ($type)" { + r del myhash + r hset myhash field1 value1 + + r hexpireat myhash [expr {[clock seconds] + 2}] NX FIELDS 1 field1 + assert_range [r hpttl myhash FIELDS 1 field1] 1000 2000 + assert_range [r httl myhash FIELDS 1 field1] 1 2 + + r hexpireat myhash [expr {[clock seconds] + 5}] XX FIELDS 1 field1 + assert_range [r httl myhash FIELDS 1 field1] 4 5 + } + + test "Lazy Expire - delete hash with expired fields ($type)" { + r del myhash + r debug set-active-expire 0 + r hset myhash k v + r hpexpire myhash 1 NX FIELDS 1 k + after 5 + r del myhash + r debug set-active-expire 1 + } + + # OPEN: To decide if to delete expired fields at start of HRANDFIELD. + # test "Test HRANDFIELD does not return expired fields ($type)" { + # hrandfieldTest 0 + # hrandfieldTest 1 + # } + + test "Test HRANDFIELD can return expired fields ($type)" { + r debug set-active-expire 0 + r del myhash + r hset myhash f1 v1 f2 v2 f3 v3 f4 v4 f5 v5 + r hpexpire myhash 1 NX FIELDS 4 f1 f2 f3 f4 + after 5 + set res [cmp_hrandfield_result myhash "f1 f2 f3 f4 f5"] + assert {$res == 1} + r debug set-active-expire 1 + + } + + test "Lazy Expire - HLEN does count expired fields ($type)" { + # Enforce only lazy expire + r debug set-active-expire 0 + + r del h1 h4 h18 h20 + r hset h1 k1 v1 + r hpexpire h1 1 NX FIELDS 1 k1 + + r hset h4 k1 v1 k2 v2 k3 v3 k4 v4 + r hpexpire h4 1 NX FIELDS 3 k1 k3 k4 + + # beyond 16 fields: HFE DS (ebuckets) converts from list to rax + + r hset h18 k1 v1 k2 v2 k3 v3 k4 v4 k5 v5 k6 v6 k7 v7 k8 v8 k9 v9 k10 v10 k11 v11 k12 v12 k13 v13 k14 v14 k15 v15 k16 v16 k17 v17 k18 v18 + r hpexpire h18 1 NX FIELDS 18 k1 k2 k3 k4 k5 k6 k7 k8 k9 k10 k11 k12 k13 k14 k15 k16 k17 k18 + + r hset h20 k1 v1 k2 v2 k3 v3 k4 v4 k5 v5 k6 v6 k7 v7 k8 v8 k9 v9 k10 v10 k11 v11 k12 v12 k13 v13 k14 v14 k15 v15 k16 v16 k17 v17 k18 v18 k19 v19 k20 v20 + r hpexpire h20 1 NX FIELDS 2 k1 k2 + + after 10 + + assert_equal [r hlen h1] 1 + assert_equal [r hlen h4] 4 + assert_equal [r hlen h18] 18 + assert_equal [r hlen h20] 20 + # Restore to support active expire + r debug set-active-expire 1 + } + + test "Lazy Expire - HSCAN does not report expired fields ($type)" { + # Enforce only lazy expire + r debug set-active-expire 0 + + r del h1 h20 h4 h18 h20 + r hset h1 01 01 + r hpexpire h1 1 NX FIELDS 1 01 + + r hset h4 01 01 02 02 03 03 04 04 + r hpexpire h4 1 NX FIELDS 3 01 03 04 + + # beyond 16 fields hash-field expiration DS (ebuckets) converts from list to rax + + r hset h18 01 01 02 02 03 03 04 04 05 05 06 06 07 07 08 08 09 09 10 10 11 11 12 12 13 13 14 14 15 15 16 16 17 17 18 18 + r hpexpire h18 1 NX FIELDS 18 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 + + r hset h20 01 01 02 02 03 03 04 04 05 05 06 06 07 07 08 08 09 09 10 10 11 11 12 12 13 13 14 14 15 15 16 16 17 17 18 18 19 19 20 20 + r hpexpire h20 1 NX FIELDS 2 01 02 + + after 10 + + # Verify SCAN does not report expired fields + assert_equal [lsort -unique [lindex [r hscan h1 0 COUNT 10] 1]] "" + assert_equal [lsort -unique [lindex [r hscan h4 0 COUNT 10] 1]] "02" + assert_equal [lsort -unique [lindex [r hscan h18 0 COUNT 10] 1]] "" + assert_equal [lsort -unique [lindex [r hscan h20 0 COUNT 100] 1]] "03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20" + # Restore to support active expire + r debug set-active-expire 1 + } + + test "Test HSCAN with mostly expired fields return empty result ($type)" { + r debug set-active-expire 0 + + # Create hash with 1000 fields and 999 of them will be expired + r del myhash + for {set i 1} {$i <= 1000} {incr i} { + r hset myhash field$i value$i + if {$i > 1} { + r hpexpire myhash 1 NX FIELDS 1 field$i + } + } + after 3 + + # Verify iterative HSCAN returns either empty result or only the first field + set countEmptyResult 0 + set cur 0 + while 1 { + set res [r hscan myhash $cur] + set cur [lindex $res 0] + # if the result is not empty, it should contain only the first field + if {[llength [lindex $res 1]] > 0} { + assert_equal [lindex $res 1] "field1 value1" + } else { + incr countEmptyResult + } + if {$cur == 0} break + } + assert {$countEmptyResult > 0} + r debug set-active-expire 1 + } + + test "Lazy Expire - verify various HASH commands handling expired fields ($type)" { + # Enforce only lazy expire + r debug set-active-expire 0 + r del h1 h2 h3 h4 h5 h18 + r hset h1 01 01 + r hset h2 01 01 02 02 + r hset h3 01 01 02 02 03 03 + r hset h4 1 99 2 99 3 99 4 99 + r hset h5 1 1 2 22 3 333 4 4444 5 55555 + r hset h6 01 01 02 02 03 03 04 04 05 05 06 06 + r hset h18 01 01 02 02 03 03 04 04 05 05 06 06 07 07 08 08 09 09 10 10 11 11 12 12 13 13 14 14 15 15 16 16 17 17 18 18 + r hpexpire h1 100 NX FIELDS 1 01 + r hpexpire h2 100 NX FIELDS 1 01 + r hpexpire h2 100 NX FIELDS 1 02 + r hpexpire h3 100 NX FIELDS 1 01 + r hpexpire h4 100 NX FIELDS 1 2 + r hpexpire h5 100 NX FIELDS 1 3 + r hpexpire h6 100 NX FIELDS 1 05 + r hpexpire h18 100 NX FIELDS 17 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 + + after 150 + + # Verify HDEL not ignore expired field. It is too much overhead to check + # if the field is expired before deletion. + assert_equal [r HDEL h1 01] "1" + + # Verify HGET ignore expired field + assert_equal [r HGET h2 01] "" + assert_equal [r HGET h2 02] "" + assert_equal [r HGET h3 01] "" + assert_equal [r HGET h3 02] "02" + assert_equal [r HGET h3 03] "03" + # Verify HINCRBY ignore expired field + assert_equal [r HINCRBY h4 2 1] "1" + assert_equal [r HINCRBY h4 3 1] "100" + # Verify HSTRLEN ignore expired field + assert_equal [r HSTRLEN h5 3] "0" + assert_equal [r HSTRLEN h5 4] "4" + assert_equal [lsort [r HKEYS h6]] "01 02 03 04 06" + # Verify HEXISTS ignore expired field + assert_equal [r HEXISTS h18 07] "0" + assert_equal [r HEXISTS h18 18] "1" + # Verify HVALS ignore expired field + assert_equal [lsort [r HVALS h18]] "18" + # Restore to support active expire + r debug set-active-expire 1 + } + + test "A field with TTL overridden with another value (TTL discarded) ($type)" { + r del myhash + r hset myhash field1 value1 field2 value2 field3 value3 + r hpexpire myhash 10000 NX FIELDS 1 field1 + r hpexpire myhash 1 NX FIELDS 1 field2 + + # field2 TTL will be discarded + r hset myhash field2 value4 + after 5 + # Expected TTL will be discarded + assert_equal [r hget myhash field2] "value4" + assert_equal [r httl myhash FIELDS 2 field2 field3] "$T_NO_EXPIRY $T_NO_EXPIRY" + assert_not_equal [r httl myhash FIELDS 1 field1] "$T_NO_EXPIRY" + } + + test "Modify TTL of a field ($type)" { + r del myhash + r hset myhash field1 value1 + r hpexpire myhash 200000 NX FIELDS 1 field1 + r hpexpire myhash 1000000 XX FIELDS 1 field1 + after 15 + assert_equal [r hget myhash field1] "value1" + assert_range [r hpttl myhash FIELDS 1 field1] 900000 1000000 + } + + test "Test return value of set operation ($type)" { + r del myhash + r hset myhash f1 v1 f2 v2 + r hexpire myhash 100000 FIELDS 1 f1 + assert_equal [r hset myhash f2 v2] 0 + assert_equal [r hset myhash f3 v3] 1 + assert_equal [r hset myhash f3 v3 f4 v4] 1 + assert_equal [r hset myhash f3 v3 f5 v5 f6 v6] 2 + } + + test "Test HGETALL not return expired fields ($type)" { + # Test with small hash + r debug set-active-expire 0 + r del myhash + r hset myhash1 f1 v1 f2 v2 f3 v3 f4 v4 f5 v5 f6 v6 + r hpexpire myhash1 1 NX FIELDS 3 f2 f4 f6 + after 10 + assert_equal [lsort [r hgetall myhash1]] "f1 f3 f5 v1 v3 v5" + + # Test with large hash + r del myhash + for {set i 1} {$i <= 600} {incr i} { + r hset myhash f$i v$i + if {$i > 3} { r hpexpire myhash 1 NX FIELDS 1 f$i } + } + after 10 + assert_equal [lsort [r hgetall myhash]] [lsort "f1 f2 f3 v1 v2 v3"] + r debug set-active-expire 1 + } + + test "Test RENAME hash with fields to be expired ($type)" { + r debug set-active-expire 0 + r del myhash + r hset myhash field1 value1 + r hpexpire myhash 20 NX FIELDS 1 field1 + r rename myhash myhash2 + assert_equal [r exists myhash] 0 + assert_range [r hpttl myhash2 FIELDS 1 field1] 1 20 + after 25 + # Verify the renamed key exists + assert_equal [r exists myhash2] 1 + r debug set-active-expire 1 + # Only active expire will delete the key + wait_for_condition 30 10 { [r exists myhash2] == 0 } else { fail "`myhash2` should be expired" } + } + + test "MOVE to another DB hash with fields to be expired ($type)" { + r select 9 + r flushall + r hset myhash field1 value1 + r hpexpire myhash 100 NX FIELDS 1 field1 + r move myhash 10 + assert_equal [r exists myhash] 0 + assert_equal [r dbsize] 0 + + # Verify the key and its field exists in the target DB + r select 10 + assert_equal [r hget myhash field1] "value1" + assert_equal [r exists myhash] 1 + + # Eventually the field will be expired and the key will be deleted + wait_for_condition 40 10 { [r hget myhash field1] == "" } else { fail "`field1` should be expired" } + wait_for_condition 40 10 { [r exists myhash] == 0 } else { fail "db should be empty" } + } {} {singledb:skip} + + test "Test COPY hash with fields to be expired ($type)" { + r flushall + r hset h1 f1 v1 f2 v2 + r hset h2 f1 v1 f2 v2 f3 v3 f4 v4 f5 v5 f6 v6 f7 v7 f8 v8 f9 v9 f10 v10 f11 v11 f12 v12 f13 v13 f14 v14 f15 v15 f16 v16 f17 v17 f18 v18 + r hpexpire h1 100 NX FIELDS 1 f1 + r hpexpire h2 100 NX FIELDS 18 f1 f2 f3 f4 f5 f6 f7 f8 f9 f10 f11 f12 f13 f14 f15 f16 f17 f18 + r COPY h1 h1copy + r COPY h2 h2copy + assert_equal [r hget h1 f1] "v1" + assert_equal [r hget h1copy f1] "v1" + assert_equal [r exists h2] 1 + assert_equal [r exists h2copy] 1 + after 105 + + # Verify lazy expire of field in h1 and its copy + assert_equal [r hget h1 f1] "" + assert_equal [r hget h1copy f1] "" + + # Verify lazy expire of field in h2 and its copy. Verify the key deleted as well. + wait_for_condition 40 10 { [r exists h2] == 0 } else { fail "`h2` should be expired" } + wait_for_condition 40 10 { [r exists h2copy] == 0 } else { fail "`h2copy` should be expired" } + + } {} {singledb:skip} + + test "Test SWAPDB hash-fields to be expired ($type)" { + r select 9 + r flushall + r hset myhash field1 value1 + r hpexpire myhash 50 NX FIELDS 1 field1 + + r swapdb 9 10 + + # Verify the key and its field doesn't exist in the source DB + assert_equal [r exists myhash] 0 + assert_equal [r dbsize] 0 + + # Verify the key and its field exists in the target DB + r select 10 + assert_equal [r hget myhash field1] "value1" + assert_equal [r dbsize] 1 + + # Eventually the field will be expired and the key will be deleted + wait_for_condition 20 10 { [r exists myhash] == 0 } else { fail "'myhash' should be expired" } + } {} {singledb:skip} + + test "HMGET - returns empty entries if fields or hash expired ($type)" { + r debug set-active-expire 0 + r del h1 h2 + r hset h1 f1 v1 f2 v2 f3 v3 + r hset h2 f1 v1 f2 v2 f3 v3 + r hpexpire h1 10000000 NX FIELDS 1 f1 + r hpexpire h1 1 NX FIELDS 2 f2 f3 + r hpexpire h2 1 NX FIELDS 3 f1 f2 f3 + after 5 + assert_equal [r hmget h1 f1 f2 f3] {v1 {} {}} + assert_equal [r hmget h2 f1 f2 f3] {{} {} {}} + r debug set-active-expire 1 + } + + test "HPERSIST - Returns empty array if key does not exist ($type)" { + r del myhash + # Make sure we can distinguish between an empty array and a null response + r readraw 1 + assert_equal {*0} [r HPERSIST myhash FIELDS 1 a] + r readraw 0 + } + + test "HPERSIST - input validation ($type)" { + # HPERSIST key + r del myhash + r hset myhash f1 v1 f2 v2 + r hexpire myhash 1000 NX FIELDS 1 f1 + assert_error {*wrong number of arguments*} {r hpersist myhash} + assert_error {*wrong number of arguments*} {r hpersist myhash FIELDS 1} + assert_equal [r hpersist not-exists-key FIELDS 1 f1] {} + assert_equal [r hpersist myhash FIELDS 2 f1 not-exists-field] "$P_OK $P_NO_FIELD" + assert_equal [r hpersist myhash FIELDS 1 f2] "$P_NO_EXPIRY" + } + + test "HPERSIST - verify fields with TTL are persisted ($type)" { + r del myhash + r hset myhash f1 v1 f2 v2 + r hexpire myhash 20 NX FIELDS 2 f1 f2 + r hpersist myhash FIELDS 2 f1 f2 + after 25 + assert_equal [r hget myhash f1] "v1" + assert_equal [r hget myhash f2] "v2" + assert_equal [r HTTL myhash FIELDS 2 f1 f2] "$T_NO_EXPIRY $T_NO_EXPIRY" + } + + test "HTTL/HPERSIST - Test expiry commands with non-volatile hash ($type)" { + r del myhash + r hset myhash field1 value1 field2 value2 field3 value3 + assert_equal [r httl myhash FIELDS 1 field1] $T_NO_EXPIRY + assert_equal [r httl myhash FIELDS 1 fieldnonexist] $E_NO_FIELD + + assert_equal [r hpersist myhash FIELDS 1 field1] $P_NO_EXPIRY + assert_equal [r hpersist myhash FIELDS 1 fieldnonexist] $P_NO_FIELD + } + + test {DUMP / RESTORE are able to serialize / unserialize a hash} { + r config set sanitize-dump-payload yes + r del myhash + r hmset myhash a 1 b 2 c 3 + r hexpireat myhash 2524600800 fields 1 a + r hexpireat myhash 2524600801 fields 1 b + set encoded [r dump myhash] + r del myhash + r restore myhash 0 $encoded + assert_equal [lsort [r hgetall myhash]] "1 2 3 a b c" + assert_equal [r hexpiretime myhash FIELDS 3 a b c] {2524600800 2524600801 -1} + } + + test {DUMP / RESTORE are able to serialize / unserialize a hash with TTL 0 for all fields} { + r config set sanitize-dump-payload yes + r del myhash + r hmset myhash a 1 b 2 c 3 + r hexpire myhash 9999999 fields 1 a ;# make all TTLs of fields to 0 + r hpersist myhash fields 1 a + assert_encoding $type myhash + set encoded [r dump myhash] + r del myhash + r restore myhash 0 $encoded + assert_equal [lsort [r hgetall myhash]] "1 2 3 a b c" + assert_equal [r hexpiretime myhash FIELDS 3 a b c] {-1 -1 -1} + } + + test {HINCRBY - discards pending expired field and reset its value} { + r debug set-active-expire 0 + r del h1 h2 + r hset h1 f1 10 f2 2 + r hset h2 f1 10 + assert_equal [r HINCRBY h1 f1 2] 12 + assert_equal [r HINCRBY h2 f1 2] 12 + r HPEXPIRE h1 10 FIELDS 1 f1 + r HPEXPIRE h2 10 FIELDS 1 f1 + after 15 + assert_equal [r HINCRBY h1 f1 1] 1 + assert_equal [r HINCRBY h2 f1 1] 1 + r debug set-active-expire 1 + } + + test {HINCRBY - preserve expiration time of the field} { + r del h1 + r hset h1 f1 10 + r hpexpire h1 20 FIELDS 1 f1 + assert_equal [r HINCRBY h1 f1 2] 12 + assert_range [r HPTTL h1 FIELDS 1 f1] 1 20 + } + + + test {HINCRBYFLOAT - discards pending expired field and reset its value} { + r debug set-active-expire 0 + r del h1 h2 + r hset h1 f1 10 f2 2 + r hset h2 f1 10 + assert_equal [r HINCRBYFLOAT h1 f1 2] 12 + assert_equal [r HINCRBYFLOAT h2 f1 2] 12 + r HPEXPIRE h1 10 FIELDS 1 f1 + r HPEXPIRE h2 10 FIELDS 1 f1 + after 15 + assert_equal [r HINCRBYFLOAT h1 f1 1] 1 + assert_equal [r HINCRBYFLOAT h2 f1 1] 1 + r debug set-active-expire 1 + } + + test {HINCRBYFLOAT - preserve expiration time of the field} { + r del h1 + r hset h1 f1 10 + r hpexpire h1 20 FIELDS 1 f1 + assert_equal [r HINCRBYFLOAT h1 f1 2.5] 12.5 + assert_range [r HPTTL h1 FIELDS 1 f1] 1 20 + } + } + + r config set hash-max-listpack-entries 512 +} + +start_server {tags {"external:skip needs:debug"}} { + + # Tests that only applies to listpack + + test "Test listpack memory usage" { + r hset myhash f1 v1 f2 v2 f3 v3 f4 v4 f5 v5 + r hpexpire myhash 5 FIELDS 2 f2 f4 + + # Just to have code coverage for the new listpack encoding + r memory usage myhash + } + + test "Test listpack object encoding" { + r hset myhash f1 v1 f2 v2 f3 v3 f4 v4 f5 v5 + r hpexpire myhash 5 FIELDS 2 f2 f4 + + # Just to have code coverage for the listpackex encoding + assert_equal [r object encoding myhash] "listpackex" + } + + test "Test listpack debug listpack" { + r hset myhash f1 v1 f2 v2 f3 v3 f4 v4 f5 v5 + + # Just to have code coverage for the listpackex encoding + r debug listpack myhash + } + + test "Test listpack converts to ht and passive expiry works" { + set prev [lindex [r config get hash-max-listpack-entries] 1] + r config set hash-max-listpack-entries 10 + r debug set-active-expire 0 + + r hset myhash f1 v1 f2 v2 f3 v3 f4 v4 f5 v5 + r hpexpire myhash 5 FIELDS 2 f2 f4 + + for {set i 6} {$i < 11} {incr i} { + r hset myhash f$i v$i + } + after 50 + assert_equal [lsort [r hgetall myhash]] [lsort "f1 f3 f5 f6 f7 f8 f9 f10 v1 v3 v5 v6 v7 v8 v9 v10"] + r config set hash-max-listpack-entries $prev + r debug set-active-expire 1 + } + + test "Test listpack converts to ht and active expiry works" { + r del myhash + r debug set-active-expire 0 + + r hset myhash f1 v1 f2 v2 f3 v3 f4 v4 f5 v5 + r hpexpire myhash 10 FIELDS 1 f1 + + for {set i 0} {$i < 2048} {incr i} { + r hset myhash f$i v$i + } + + for {set i 0} {$i < 2048} {incr i} { + r hpexpire myhash 10 FIELDS 1 f$i + } + + r debug set-active-expire 1 + wait_for_condition 50 20 { [r EXISTS myhash] == 0 } else { fail "'myhash' should be expired" } + } + + test "Test listpack converts to ht and active expiry works" { + r del myhash + r debug set-active-expire 0 + + # Check expiry works after listpack converts to ht + for {set i 0} {$i < 1024} {incr i} { + r hset myhash f1_$i v1_$i f2_$i v2_$i f3_$i v3_$i f4_$i v4_$i + r hpexpire myhash 10 FIELDS 4 f1_$i f2_$i f3_$i f4_$i + } + + assert_encoding hashtable myhash + assert_equal [r hlen myhash] 4096 + + r debug set-active-expire 1 + wait_for_condition 50 20 { [r EXISTS myhash] == 0 } else { fail "'myhash' should be expired" } + } + + test "HPERSIST/HEXPIRE - Test listpack with large values" { + r del myhash + + # Test with larger values to verify we successfully move fields in + # listpack when we are ordering according to TTL. This config change + # will make code to use temporary heap allocation when moving fields. + # See listpackExUpdateExpiry() for details. + r config set hash-max-listpack-value 2048 + + set payload1 [string repeat v3 1024] + set payload2 [string repeat v1 1024] + + # Test with single item list + r hset myhash f1 $payload1 + r hexpire myhash 2000 FIELDS 1 f1 + assert_equal [r hget myhash f1] $payload1 + r del myhash + + # Test with multiple items + r hset myhash f1 $payload2 f2 v2 f3 $payload1 f4 v4 + r hexpire myhash 100000 FIELDS 1 f3 + r hpersist myhash FIELDS 1 f3 + assert_equal [r hpersist myhash FIELDS 1 f3] $P_NO_EXPIRY + + r hpexpire myhash 10 FIELDS 1 f1 + after 20 + assert_equal [lsort [r hgetall myhash]] [lsort "f2 f3 f4 v2 $payload1 v4"] + + r config set hash-max-listpack-value 64 + } + + test "Statistics - Hashes with HFEs" { + r config resetstat + r del myhash + r hset myhash f1 v1 f2 v2 f3 v3 f4 v4 f5 v5 + r hpexpire myhash 100 FIELDS 3 f1 f2 f3 + + assert_match [get_hashes_with_expiry_fields r] 1 + r hset myhash2 f1 v1 f2 v2 f3 v3 f4 v4 f5 v5 + assert_match [get_hashes_with_expiry_fields r] 1 + r hpexpire myhash2 100 FIELDS 3 f1 f2 f3 + assert_match [get_hashes_with_expiry_fields r] 2 + + wait_for_condition 50 50 { + [get_hashes_with_expiry_fields r] == 0 + } else { + fail "Hash field expiry statistics failed" + } + } +} + +start_server {tags {"external:skip needs:debug"}} { + foreach type {listpack ht} { + if {$type eq "ht"} { + r config set hash-max-listpack-entries 0 + } else { + r config set hash-max-listpack-entries 512 + } + + test "Command rewrite and expired hash fields are propagated to replica ($type)" { + start_server {overrides {appendonly {yes} appendfsync always} tags {external:skip}} { + + set aof [get_last_incr_aof_path r] + r hset h1 f1 v1 f2 v2 + + r hpexpire h1 20 FIELDS 1 f1 + r hpexpire h1 30 FIELDS 1 f2 + r hpexpire h1 30 FIELDS 1 non_exists_field + r hset h2 f1 v1 f2 v2 f3 v3 f4 v4 + r hpexpire h2 40 FIELDS 2 f1 non_exists_field + r hpexpire h2 50 FIELDS 1 f2 + r hpexpireat h2 [expr [clock seconds]*1000+100000] LT FIELDS 1 f3 + r hexpireat h2 [expr [clock seconds]+10] NX FIELDS 1 f4 + + wait_for_condition 50 100 { + [r hlen h2] eq 2 + } else { + fail "Field f2 of hash h2 wasn't deleted" + } + + # Assert that each TTL-related command are persisted with absolute timestamps in AOF + assert_aof_content $aof { + {select *} + {hset h1 f1 v1 f2 v2} + {hpexpireat h1 * FIELDS 1 f1} + {hpexpireat h1 * FIELDS 1 f2} + {hset h2 f1 v1 f2 v2 f3 v3 f4 v4} + {hpexpireat h2 * FIELDS 2 f1 non_exists_field} + {hpexpireat h2 * FIELDS 1 f2} + {hpexpireat h2 * FIELDS 1 f3} + {hpexpireat h2 * FIELDS 1 f4} + {hdel h1 f1} + {hdel h1 f2} + {hdel h2 f1} + {hdel h2 f2} + } + } + } + + test "Lazy Expire - fields are lazy deleted and propagated to replicas ($type)" { + start_server {overrides {appendonly {yes} appendfsync always} tags {external:skip}} { + r debug set-active-expire 0 + set aof [get_last_incr_aof_path r] + + r del myhash + + r hset myhash f1 v1 f2 v2 f3 v3 + r hpexpire myhash 1 NX FIELDS 3 f1 f2 f3 + after 5 + + # Verify that still exists even if all fields are expired + assert_equal 1 [r EXISTS myhash] + + # Verify that len counts also expired fields + assert_equal 3 [r HLEN myhash] + + # Trying access to expired field should delete it. Len should be updated + assert_equal 0 [r hexists myhash f1] + assert_equal 2 [r HLEN myhash] + + # Trying access another expired field should delete it. Len should be updated + assert_equal "" [r hget myhash f2] + assert_equal 1 [r HLEN myhash] + + # Trying access last expired field should delete it. hash shouldn't exists afterward. + assert_equal 0 [r hstrlen myhash f3] + assert_equal 0 [r HLEN myhash] + assert_equal 0 [r EXISTS myhash] + + wait_for_condition 50 100 { [r exists h1] == 0 } else { fail "hash h1 wasn't deleted" } + + # HDEL are propagated as expected + assert_aof_content $aof { + {select *} + {hset myhash f1 v1 f2 v2 f3 v3} + {hpexpireat myhash * NX FIELDS 3 f1 f2 f3} + {hdel myhash f1} + {hdel myhash f2} + {hdel myhash f3} + } + r debug set-active-expire 1 + } + } + + # Start a new server with empty data and AOF file. + start_server {overrides {appendonly {yes} appendfsync always} tags {external:skip}} { + + # Based on test at expire.tcl: " All time-to-live(TTL) in commands are propagated as absolute ..." + test {All TTLs in commands are propagated as absolute timestamp in milliseconds in AOF} { + + set aof [get_last_incr_aof_path r] + + r hset h1 f1 v1 f2 v2 f3 v3 f4 v4 f5 v5 f6 v6 + r hexpireat h1 [expr [clock seconds]+100] NX FIELDS 1 f1 + r hpexpireat h1 [expr [clock seconds]*1000+100000] NX FIELDS 1 f2 + r hpexpire h1 100000 NX FIELDS 3 f3 f4 f5 + r hexpire h1 100000 FIELDS 1 f6 + r hset h5 f1 v1 + + assert_aof_content $aof { + {select *} + {hset h1 f1 v1 f2 v2 f3 v3 f4 v4 f5 v5 f6 v6} + {hpexpireat h1 * FIELDS 1 f1} + {hpexpireat h1 * FIELDS 1 f2} + {hpexpireat h1 * NX FIELDS 3 f3 f4 f5} + {hpexpireat h1 * FIELDS 1 f6} + {hset h5 f1 v1} + } + + array set keyAndFields1 [dumpAllHashes r] + # Let some time pass and reload data from AOF + after 2000 + r debug loadaof + array set keyAndFields2 [dumpAllHashes r] + + # Assert that absolute TTLs are the same + assert_equal [array get keyAndFields1] [array get keyAndFields2] + + } {} {needs:debug} + } + + # Based on test, with same name, at expire.tcl: + test {All TTL in commands are propagated as absolute timestamp in replication stream} { + # Make sure that both relative and absolute expire commands are propagated + # Consider also comment of the test, with same name, at expire.tcl + + r flushall ; # Clean up keyspace to avoid interference by keys from other tests + set repl [attach_to_replication_stream] + + r hset h1 f1 v1 + r hexpireat h1 [expr [clock seconds]+100] NX FIELDS 1 f1 + r hset h2 f2 v2 + r hpexpireat h2 [expr [clock seconds]*1000+100000] NX FIELDS 1 f2 + r hset h3 f3 v3 f4 v4 + r hexpire h3 100 FIELDS 3 f3 f4 non_exists_field + + assert_replication_stream $repl { + {select *} + {hset h1 f1 v1} + {hpexpireat h1 * NX FIELDS 1 f1} + {hset h2 f2 v2} + {hpexpireat h2 * NX FIELDS 1 f2} + {hset h3 f3 v3 f4 v4} + {hpexpireat h3 * FIELDS 3 f3 f4 non_exists_field} + } + close_replication_stream $repl + } {} {needs:repl} + + # Start another server to test replication of TTLs + start_server {tags {needs:repl external:skip}} { + # Set the outer layer server as primary + set primary [srv -1 client] + set primary_host [srv -1 host] + set primary_port [srv -1 port] + # Set this inner layer server as replica + set replica [srv 0 client] + + # Server should have role slave + $replica replicaof $primary_host $primary_port + wait_for_condition 50 100 { + [s 0 role] eq {slave} + } else { + fail "Replication not started." + } + + # Based on test, with same name, at expire.tcl + test {For all replicated TTL-related commands, absolute expire times are identical on primary and replica} { + # Apply each TTL-related command to a unique key on primary + $primary flushall + $primary hset h1 f v + $primary hexpireat h1 [expr [clock seconds]+10000] FIELDS 1 f + $primary hset h2 f v + $primary hpexpireat h2 [expr [clock milliseconds]+100000] FIELDS 1 f + $primary hset h3 f v + $primary hexpire h3 100 NX FIELDS 1 f + $primary hset h4 f v + $primary hpexpire h4 100000 NX FIELDS 1 f + $primary hset h5 f v + $primary hpexpireat h5 [expr [clock milliseconds]-100000] FIELDS 1 f + $primary hset h9 f v + + # Wait for replica to get the keys and TTLs + assert {[$primary wait 1 0] == 1} + + # Verify absolute TTLs are identical on primary and replica for all keys + # This is because TTLs are always replicated as absolute values + assert_equal [dumpAllHashes $primary] [dumpAllHashes $replica] + } + } + } +} +