Unverified Commit 7b9e9606 authored by debing.sun's avatar debing.sun Committed by GitHub
Browse files

Hash Field Expiration (#13303)

## Background

This PR introduces support for field-level expiration in Redis hashes. Previously, Redis supported expiration only at the key level, but this enhancement allows setting expiration times for individual fields within a hash.

## New commands
* HEXPIRE
* HEXPIREAT
* HEXPIRETIME
* HPERSIST
* HPEXPIRE
* HPEXPIREAT
* HPEXPIRETIME
* HPTTL
* HTTL

## Short example
from @moticless
```sh
127.0.0.1:6379>  hset myhash f1 v1 f2 v2 f3 v3                                                   
(integer) 3
127.0.0.1:6379>  hpexpire myhash 10000 NX fields 2 f2 f3                                         
1) (integer) 1
2) (integer) 1
127.0.0.1:6379>  hpttl myhash fields 3 f1 f2 f3                                                                                                                                                                         
1) (integer) -1
2) (integer) 9997
3) (integer) 9997
127.0.0.1:6379>  hgetall myhash  
1) "f3"
2) "v3"
3) "f2"
4) "v2"
5) "f1"
6) "v1"

... after 10 seconds ...

127.0.0.1:6379>  hgetall myhash  
1) "f1"
2) "v1"
127.0.0.1:6379>
```

## Expiration strategy
1. Integrate active
    Redis periodically performs active expiration and deletion of hash keys that contain expired fields, with a maximum attempt limit.
3. Lazy expiration
    When a client touches fields within a hash, Redis checks if the fields are expired. If a field is expired, it will be deleted. However, we do not delete expired fields during a traversal, we implicitly skip over them.

## RDB changes
Add two new rdb type s`RDB_TYPE_HASH_METADATA` and `RDB_TYPE_HASH_LISTPACK_EX`.

## Notification
1. Add `hpersist` notification for `HPERSIST` command.
5. Add `hexpire` notification for `HEXPIRE`, `HEXPIREAT`, `HPEXPIRE` and `HPEXPIREAT` commands.

## Internal
1. Add new data structure `ebuckets`, which is used to store TTL and keys, enabling quick retrieval of keys based on TTL.
2. Add new data structure `mstr` like sds, which is used to store a string with TTL.

This work was done by @moticless, @tezc, @ronen-kalish, @sundb, I just release it.
parents 2d1bb42c f0389f28
...@@ -354,7 +354,7 @@ endif ...@@ -354,7 +354,7 @@ endif
REDIS_SERVER_NAME=redis-server$(PROG_SUFFIX) REDIS_SERVER_NAME=redis-server$(PROG_SUFFIX)
REDIS_SENTINEL_NAME=redis-sentinel$(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_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_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) REDIS_BENCHMARK_NAME=redis-benchmark$(PROG_SUFFIX)
......
...@@ -1939,19 +1939,21 @@ int rewriteSortedSetObject(rio *r, robj *key, robj *o) { ...@@ -1939,19 +1939,21 @@ int rewriteSortedSetObject(rio *r, robj *key, robj *o) {
* *
* The function returns 0 on error, non-zero on success. */ * The function returns 0 on error, non-zero on success. */
static int rioWriteHashIteratorCursor(rio *r, hashTypeIterator *hi, int what) { 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 char *vstr = NULL;
unsigned int vlen = UINT_MAX; unsigned int vlen = UINT_MAX;
long long vll = LLONG_MAX; long long vll = LLONG_MAX;
hashTypeCurrentFromListpack(hi, what, &vstr, &vlen, &vll); hashTypeCurrentFromListpack(hi, what, &vstr, &vlen, &vll, NULL);
if (vstr) if (vstr)
return rioWriteBulkString(r, (char*)vstr, vlen); return rioWriteBulkString(r, (char*)vstr, vlen);
else else
return rioWriteBulkLongLong(r, vll); return rioWriteBulkLongLong(r, vll);
} else if (hi->encoding == OBJ_ENCODING_HT) { } else if (hi->encoding == OBJ_ENCODING_HT) {
sds value = hashTypeCurrentFromHashTable(hi, what); char *str;
return rioWriteBulkString(r, value, sdslen(value)); size_t len;
hashTypeCurrentFromHashTable(hi, what, &str, &len, NULL);
return rioWriteBulkString(r, str, len);
} }
serverPanic("Unknown hash encoding"); serverPanic("Unknown hash encoding");
...@@ -1961,37 +1963,60 @@ static int rioWriteHashIteratorCursor(rio *r, hashTypeIterator *hi, int what) { ...@@ -1961,37 +1963,60 @@ static int rioWriteHashIteratorCursor(rio *r, hashTypeIterator *hi, int what) {
/* Emit the commands needed to rebuild a hash object. /* Emit the commands needed to rebuild a hash object.
* The function returns 0 on error, 1 on success. */ * The function returns 0 on error, 1 on success. */
int rewriteHashObject(rio *r, robj *key, robj *o) { int rewriteHashObject(rio *r, robj *key, robj *o) {
int res = 0; /*fail*/
hashTypeIterator *hi; 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); hi = hashTypeInitIterator(o);
while (hashTypeNext(hi) != C_ERR) {
if (!isHFE) {
while (hashTypeNext(hi, 0) != C_ERR) {
if (count == 0) { if (count == 0) {
int cmd_items = (items > AOF_REWRITE_ITEMS_PER_CMD) ? int cmd_items = (items > AOF_REWRITE_ITEMS_PER_CMD) ?
AOF_REWRITE_ITEMS_PER_CMD : items; AOF_REWRITE_ITEMS_PER_CMD : items;
if (!rioWriteBulkCount(r, '*', 2 + cmd_items * 2) ||
if (!rioWriteBulkCount(r,'*',2+cmd_items*2) || !rioWriteBulkString(r, "HMSET", 5) ||
!rioWriteBulkString(r,"HMSET",5) || !rioWriteBulkObject(r, key))
!rioWriteBulkObject(r,key)) goto reHashEnd;
{
hashTypeReleaseIterator(hi);
return 0;
}
} }
if (!rioWriteHashIteratorCursor(r, hi, OBJ_HASH_KEY) || if (!rioWriteHashIteratorCursor(r, hi, OBJ_HASH_KEY) ||
!rioWriteHashIteratorCursor(r, hi, OBJ_HASH_VALUE)) !rioWriteHashIteratorCursor(r, hi, OBJ_HASH_VALUE))
{ goto reHashEnd;
hashTypeReleaseIterator(hi);
return 0;
}
if (++count == AOF_REWRITE_ITEMS_PER_CMD) count = 0; if (++count == AOF_REWRITE_ITEMS_PER_CMD) count = 0;
items--; items--;
} }
} else {
while (hashTypeNext(hi, 0) != C_ERR) {
hashTypeReleaseIterator(hi); 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;
return 1; 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;
}
}
}
res = 1; /* success */
reHashEnd:
hashTypeReleaseIterator(hi);
return res;
} }
/* Helper for rewriteStreamObject() that generates a bulk string into the /* Helper for rewriteStreamObject() that generates a bulk string into the
......
...@@ -176,6 +176,7 @@ void dumpCommand(client *c) { ...@@ -176,6 +176,7 @@ void dumpCommand(client *c) {
/* RESTORE key ttl serialized-value [REPLACE] [ABSTTL] [IDLETIME seconds] [FREQ frequency] */ /* RESTORE key ttl serialized-value [REPLACE] [ABSTTL] [IDLETIME seconds] [FREQ frequency] */
void restoreCommand(client *c) { void restoreCommand(client *c) {
uint64_t minExpiredField = EB_EXPIRE_TIME_INVALID;
long long ttl, lfu_freq = -1, lru_idle = -1, lru_clock = -1; long long ttl, lfu_freq = -1, lru_idle = -1, lru_clock = -1;
rio payload; rio payload;
int j, type, replace = 0, absttl = 0; int j, type, replace = 0, absttl = 0;
...@@ -239,7 +240,7 @@ void restoreCommand(client *c) { ...@@ -239,7 +240,7 @@ void restoreCommand(client *c) {
rioInitWithBuffer(&payload,c->argv[3]->ptr); rioInitWithBuffer(&payload,c->argv[3]->ptr);
if (((type = rdbLoadObjectType(&payload)) == -1) || 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"); addReplyError(c,"Bad data format");
return; return;
...@@ -265,7 +266,13 @@ void restoreCommand(client *c) { ...@@ -265,7 +266,13 @@ void restoreCommand(client *c) {
} }
/* Create the key and set the TTL if any */ /* 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) { if (ttl) {
setExpire(c,c->db,key,ttl); setExpire(c,c->db,key,ttl);
if (!absttl) { if (!absttl) {
......
...@@ -3303,6 +3303,107 @@ struct COMMAND_ARG HEXISTS_Args[] = { ...@@ -3303,6 +3303,107 @@ struct COMMAND_ARG HEXISTS_Args[] = {
{MAKE_ARG("field",ARG_TYPE_STRING,-1,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)}, {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 ********************/ /********** HGET ********************/
#ifndef SKIP_CMD_HISTORY_TABLE #ifndef SKIP_CMD_HISTORY_TABLE
...@@ -3512,6 +3613,161 @@ struct COMMAND_ARG HMSET_Args[] = { ...@@ -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}, {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 ********************/ /********** HRANDFIELD ********************/
#ifndef SKIP_CMD_HISTORY_TABLE #ifndef SKIP_CMD_HISTORY_TABLE
...@@ -3659,6 +3915,33 @@ struct COMMAND_ARG HSTRLEN_Args[] = { ...@@ -3659,6 +3915,33 @@ struct COMMAND_ARG HSTRLEN_Args[] = {
{MAKE_ARG("field",ARG_TYPE_STRING,-1,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)}, {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 ********************/ /********** HVALS ********************/
#ifndef SKIP_CMD_HISTORY_TABLE #ifndef SKIP_CMD_HISTORY_TABLE
...@@ -10710,6 +10993,9 @@ struct COMMAND_STRUCT redisCommandTable[] = { ...@@ -10710,6 +10993,9 @@ struct COMMAND_STRUCT redisCommandTable[] = {
/* hash */ /* 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("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("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("hget","Returns the value of a field in a hash.","O(1)","2.0.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HGET_History,0,HGET_Tips,0,hgetCommand,3,CMD_READONLY|CMD_FAST,ACL_CATEGORY_HASH,HGET_Keyspecs,1,NULL,2),.args=HGET_Args},
{MAKE_CMD("hgetall","Returns all fields and values in a hash.","O(N) where N is the size of the hash.","2.0.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HGETALL_History,0,HGETALL_Tips,1,hgetallCommand,2,CMD_READONLY,ACL_CATEGORY_HASH,HGETALL_Keyspecs,1,NULL,1),.args=HGETALL_Args}, {MAKE_CMD("hgetall","Returns all fields and values in a hash.","O(N) where N is the size of the hash.","2.0.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HGETALL_History,0,HGETALL_Tips,1,hgetallCommand,2,CMD_READONLY,ACL_CATEGORY_HASH,HGETALL_Keyspecs,1,NULL,1),.args=HGETALL_Args},
{MAKE_CMD("hincrby","Increments the integer value of a field in a hash by a number. Uses 0 as initial value if the field doesn't exist.","O(1)","2.0.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HINCRBY_History,0,HINCRBY_Tips,0,hincrbyCommand,4,CMD_WRITE|CMD_DENYOOM|CMD_FAST,ACL_CATEGORY_HASH,HINCRBY_Keyspecs,1,NULL,3),.args=HINCRBY_Args}, {MAKE_CMD("hincrby","Increments the integer value of a field in a hash by a number. Uses 0 as initial value if the field doesn't exist.","O(1)","2.0.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HINCRBY_History,0,HINCRBY_Tips,0,hincrbyCommand,4,CMD_WRITE|CMD_DENYOOM|CMD_FAST,ACL_CATEGORY_HASH,HINCRBY_Keyspecs,1,NULL,3),.args=HINCRBY_Args},
...@@ -10718,11 +11004,17 @@ struct COMMAND_STRUCT redisCommandTable[] = { ...@@ -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("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("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("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("hrandfield","Returns one or more random fields from a hash.","O(N) where N is the number of fields returned","6.2.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HRANDFIELD_History,0,HRANDFIELD_Tips,1,hrandfieldCommand,-2,CMD_READONLY,ACL_CATEGORY_HASH,HRANDFIELD_Keyspecs,1,NULL,2),.args=HRANDFIELD_Args},
{MAKE_CMD("hscan","Iterates over fields and values of a hash.","O(1) for every call. O(N) for a complete iteration, including enough command calls for the cursor to return back to 0. N is the number of elements inside the collection.","2.8.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HSCAN_History,0,HSCAN_Tips,1,hscanCommand,-3,CMD_READONLY,ACL_CATEGORY_HASH,HSCAN_Keyspecs,1,NULL,5),.args=HSCAN_Args}, {MAKE_CMD("hscan","Iterates over fields and values of a hash.","O(1) for every call. O(N) for a complete iteration, including enough command calls for the cursor to return back to 0. N is the number of elements inside the collection.","2.8.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HSCAN_History,0,HSCAN_Tips,1,hscanCommand,-3,CMD_READONLY,ACL_CATEGORY_HASH,HSCAN_Keyspecs,1,NULL,5),.args=HSCAN_Args},
{MAKE_CMD("hset","Creates or modifies the value of a field in a hash.","O(1) for each field/value pair added, so O(N) to add N field/value pairs when the command is called with multiple field/value pairs.","2.0.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HSET_History,1,HSET_Tips,0,hsetCommand,-4,CMD_WRITE|CMD_DENYOOM|CMD_FAST,ACL_CATEGORY_HASH,HSET_Keyspecs,1,NULL,2),.args=HSET_Args}, {MAKE_CMD("hset","Creates or modifies the value of a field in a hash.","O(1) for each field/value pair added, so O(N) to add N field/value pairs when the command is called with multiple field/value pairs.","2.0.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HSET_History,1,HSET_Tips,0,hsetCommand,-4,CMD_WRITE|CMD_DENYOOM|CMD_FAST,ACL_CATEGORY_HASH,HSET_Keyspecs,1,NULL,2),.args=HSET_Args},
{MAKE_CMD("hsetnx","Sets the value of a field in a hash only when the field doesn't exist.","O(1)","2.0.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HSETNX_History,0,HSETNX_Tips,0,hsetnxCommand,4,CMD_WRITE|CMD_DENYOOM|CMD_FAST,ACL_CATEGORY_HASH,HSETNX_Keyspecs,1,NULL,3),.args=HSETNX_Args}, {MAKE_CMD("hsetnx","Sets the value of a field in a hash only when the field doesn't exist.","O(1)","2.0.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HSETNX_History,0,HSETNX_Tips,0,hsetnxCommand,4,CMD_WRITE|CMD_DENYOOM|CMD_FAST,ACL_CATEGORY_HASH,HSETNX_Keyspecs,1,NULL,3),.args=HSETNX_Args},
{MAKE_CMD("hstrlen","Returns the length of the value of a field.","O(1)","3.2.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HSTRLEN_History,0,HSTRLEN_Tips,0,hstrlenCommand,3,CMD_READONLY|CMD_FAST,ACL_CATEGORY_HASH,HSTRLEN_Keyspecs,1,NULL,2),.args=HSTRLEN_Args}, {MAKE_CMD("hstrlen","Returns the length of the value of a field.","O(1)","3.2.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HSTRLEN_History,0,HSTRLEN_Tips,0,hstrlenCommand,3,CMD_READONLY|CMD_FAST,ACL_CATEGORY_HASH,HSTRLEN_Keyspecs,1,NULL,2),.args=HSTRLEN_Args},
{MAKE_CMD("httl","Returns the TTL in seconds of a hash field.","O(N) where N is the number of specified fields","7.4.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HTTL_History,0,HTTL_Tips,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}, {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 */ /* 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}, {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},
......
{
"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
}
]
}
}
{
"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
{
"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
}
]
}
}
{
"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
}
]
}
}
{
"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
{
"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
{
"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
}
]
}
}
{
"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
}
]
}
}
{
"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
}
]
}
}
...@@ -177,13 +177,13 @@ robj *lookupKeyWriteOrReply(client *c, robj *key, robj *reply) { ...@@ -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 update_if_existing argument is false, the program is aborted
* if the key already exists, otherwise, it can fall back to dbOverwrite. */ * 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; dictEntry *existing;
int slot = getKeySlot(key->ptr); int slot = getKeySlot(key->ptr);
dictEntry *de = kvstoreDictAddRaw(db->keys, slot, key->ptr, &existing); dictEntry *de = kvstoreDictAddRaw(db->keys, slot, key->ptr, &existing);
if (update_if_existing && existing) { if (update_if_existing && existing) {
dbSetValue(db, key, val, 1, existing); dbSetValue(db, key, val, 1, existing);
return; return existing;
} }
serverAssertWithInfo(NULL, key, de != NULL); serverAssertWithInfo(NULL, key, de != NULL);
kvstoreDictSetKey(db->keys, slot, de, sdsdup(key->ptr)); 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 ...@@ -191,10 +191,11 @@ static void dbAddInternal(redisDb *db, robj *key, robj *val, int update_if_exist
kvstoreDictSetVal(db->keys, slot, de, val); kvstoreDictSetVal(db->keys, slot, de, val);
signalKeyAsReady(db, key, val->type); signalKeyAsReady(db, key, val->type);
notifyKeyspaceEvent(NOTIFY_NEW,"new",key,db->id); notifyKeyspaceEvent(NOTIFY_NEW,"new",key,db->id);
return de;
} }
void dbAdd(redisDb *db, robj *key, robj *val) { dictEntry *dbAdd(redisDb *db, robj *key, robj *val) {
dbAddInternal(db, key, val, 0); return dbAddInternal(db, key, val, 0);
} }
/* Returns key's hash slot when cluster mode is enabled, or 0 when disabled. /* 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 ...@@ -275,6 +276,11 @@ static void dbSetValue(redisDb *db, robj *key, robj *val, int overwrite, dictEnt
old = dictGetVal(de); old = dictGetVal(de);
} }
kvstoreDictSetVal(db->keys, slot, de, val); 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) { if (server.lazyfree_lazy_server_del) {
freeObjAsync(key,old,db->id); freeObjAsync(key,old,db->id);
} else { } else {
...@@ -370,6 +376,11 @@ int dbGenericDelete(redisDb *db, robj *key, int async, int flags) { ...@@ -370,6 +376,11 @@ int dbGenericDelete(redisDb *db, robj *key, int async, int flags) {
dictEntry *de = kvstoreDictTwoPhaseUnlinkFind(db->keys, slot, key->ptr, &plink, &table); dictEntry *de = kvstoreDictTwoPhaseUnlinkFind(db->keys, slot, key->ptr, &plink, &table);
if (de) { if (de) {
robj *val = dictGetVal(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 /* RM_StringDMA may call dbUnshareStringValue which may free val, so we
* need to incr to retain val */ * need to incr to retain val */
incrRefCount(val); incrRefCount(val);
...@@ -475,6 +486,9 @@ long long emptyDbStructure(redisDb *dbarray, int dbnum, int async, ...@@ -475,6 +486,9 @@ long long emptyDbStructure(redisDb *dbarray, int dbnum, int async,
if (async) { if (async) {
emptyDbAsync(&dbarray[j]); emptyDbAsync(&dbarray[j]);
} else { } 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].keys, callback);
kvstoreEmpty(dbarray[j].expires, callback); kvstoreEmpty(dbarray[j].expires, callback);
} }
...@@ -554,6 +568,7 @@ redisDb *initTempDb(void) { ...@@ -554,6 +568,7 @@ redisDb *initTempDb(void) {
tempDb[i].id = i; tempDb[i].id = i;
tempDb[i].keys = kvstoreCreate(&dbDictType, slot_count_bits, flags); tempDb[i].keys = kvstoreCreate(&dbDictType, slot_count_bits, flags);
tempDb[i].expires = kvstoreCreate(&dbExpiresDictType, slot_count_bits, flags); tempDb[i].expires = kvstoreCreate(&dbExpiresDictType, slot_count_bits, flags);
tempDb[i].hexpires = ebCreate();
} }
return tempDb; return tempDb;
...@@ -566,6 +581,9 @@ void discardTempDb(redisDb *tempDb, void(callback)(dict*)) { ...@@ -566,6 +581,9 @@ void discardTempDb(redisDb *tempDb, void(callback)(dict*)) {
/* Release temp DBs. */ /* Release temp DBs. */
emptyDbStructure(tempDb, -1, async, callback); emptyDbStructure(tempDb, -1, async, callback);
for (int i=0; i<server.dbnum; i++) { for (int i=0; i<server.dbnum; i++) {
/* Destroy global HFE DS before deleting the hashes since ebuckets DS is
* embedded in the stored objects. */
ebDestroy(&tempDb[i].hexpires, &hashExpireBucketsType, NULL);
kvstoreRelease(tempDb[i].keys); kvstoreRelease(tempDb[i].keys);
kvstoreRelease(tempDb[i].expires); kvstoreRelease(tempDb[i].expires);
} }
...@@ -894,6 +912,7 @@ typedef struct { ...@@ -894,6 +912,7 @@ typedef struct {
sds pattern; /* pattern string, NULL means no pattern */ sds pattern; /* pattern string, NULL means no pattern */
long sampled; /* cumulative number of keys sampled */ long sampled; /* cumulative number of keys sampled */
int no_values; /* set to 1 means to return keys only */ int no_values; /* set to 1 means to return keys only */
size_t (*strlen)(char *s); /* (o->type == OBJ_HASH) ? hfieldlen : sdslen */
} scanData; } scanData;
/* Helper function to compare key type in scan commands */ /* Helper function to compare key type in scan commands */
...@@ -918,7 +937,7 @@ void scanCallback(void *privdata, const dictEntry *de) { ...@@ -918,7 +937,7 @@ void scanCallback(void *privdata, const dictEntry *de) {
list *keys = data->keys; list *keys = data->keys;
robj *o = data->o; robj *o = data->o;
sds val = NULL; sds val = NULL;
sds key = NULL; void *key = NULL; /* if OBJ_HASH then key is of type `hfield`. Otherwise, `sds` */
data->sampled++; data->sampled++;
/* o and typename can not have values at the same time. */ /* o and typename can not have values at the same time. */
...@@ -932,24 +951,29 @@ void scanCallback(void *privdata, const dictEntry *de) { ...@@ -932,24 +951,29 @@ void scanCallback(void *privdata, const dictEntry *de) {
}*/ }*/
/* Filter element if it does not match the pattern. */ /* Filter element if it does not match the pattern. */
sds keysds = dictGetKey(de); void *keyStr = dictGetKey(de);
if (data->pattern) { 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; return;
} }
} }
if (o == NULL) { if (o == NULL) {
key = keysds; key = keyStr;
} else if (o->type == OBJ_SET) { } else if (o->type == OBJ_SET) {
key = keysds; key = keyStr;
} else if (o->type == OBJ_HASH) { } else if (o->type == OBJ_HASH) {
key = keysds; key = keyStr;
val = dictGetVal(de); val = dictGetVal(de);
/* If field is expired, then ignore */
if (hfieldIsExpired(key))
return;
} else if (o->type == OBJ_ZSET) { } else if (o->type == OBJ_ZSET) {
char buf[MAX_LONG_DOUBLE_CHARS]; char buf[MAX_LONG_DOUBLE_CHARS];
int len = ld2string(buf, sizeof(buf), *(double *)dictGetVal(de), LD_STR_AUTO); int len = ld2string(buf, sizeof(buf), *(double *)dictGetVal(de), LD_STR_AUTO);
key = sdsdup(keysds); key = sdsdup(keyStr);
val = sdsnewlen(buf, len); val = sdsnewlen(buf, len);
} else { } else {
serverPanic("Type not handled in SCAN callback."); serverPanic("Type not handled in SCAN callback.");
...@@ -1023,6 +1047,7 @@ char *getObjectTypeName(robj *o) { ...@@ -1023,6 +1047,7 @@ char *getObjectTypeName(robj *o) {
* In the case of a Hash object the function returns both the field and value * In the case of a Hash object the function returns both the field and value
* of every element on the Hash. */ * of every element on the Hash. */
void scanGenericCommand(client *c, robj *o, unsigned long long cursor) { void scanGenericCommand(client *c, robj *o, unsigned long long cursor) {
int isKeysHfield = 0;
int i, j; int i, j;
listNode *node; listNode *node;
long count = 10; long count = 10;
...@@ -1103,6 +1128,7 @@ void scanGenericCommand(client *c, robj *o, unsigned long long cursor) { ...@@ -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) { } else if (o->type == OBJ_SET && o->encoding == OBJ_ENCODING_HT) {
ht = o->ptr; ht = o->ptr;
} else if (o->type == OBJ_HASH && o->encoding == OBJ_ENCODING_HT) { } else if (o->type == OBJ_HASH && o->encoding == OBJ_ENCODING_HT) {
isKeysHfield = 1;
ht = o->ptr; ht = o->ptr;
} else if (o->type == OBJ_ZSET && o->encoding == OBJ_ENCODING_SKIPLIST) { } else if (o->type == OBJ_ZSET && o->encoding == OBJ_ENCODING_SKIPLIST) {
zset *zs = o->ptr; zset *zs = o->ptr;
...@@ -1150,6 +1176,7 @@ void scanGenericCommand(client *c, robj *o, unsigned long long cursor) { ...@@ -1150,6 +1176,7 @@ void scanGenericCommand(client *c, robj *o, unsigned long long cursor) {
.pattern = use_pattern ? pat : NULL, .pattern = use_pattern ? pat : NULL,
.sampled = 0, .sampled = 0,
.no_values = no_values, .no_values = no_values,
.strlen = (isKeysHfield) ? hfieldlen : sdslen,
}; };
/* A pattern may restrict all matching keys to one cluster slot. */ /* 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) { ...@@ -1211,6 +1238,40 @@ void scanGenericCommand(client *c, robj *o, unsigned long long cursor) {
p = lpNext(o->ptr, p); p = lpNext(o->ptr, p);
} }
cursor = 0; 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 { } else {
serverPanic("Not handled encoding in SCAN."); serverPanic("Not handled encoding in SCAN.");
} }
...@@ -1243,10 +1304,14 @@ void scanGenericCommand(client *c, robj *o, unsigned long long cursor) { ...@@ -1243,10 +1304,14 @@ void scanGenericCommand(client *c, robj *o, unsigned long long cursor) {
addReplyArrayLen(c, 2); addReplyArrayLen(c, 2);
addReplyBulkLongLong(c,cursor); addReplyBulkLongLong(c,cursor);
unsigned long long idx = 0;
addReplyArrayLen(c, listLength(keys)); addReplyArrayLen(c, listLength(keys));
while ((node = listFirst(keys)) != NULL) { while ((node = listFirst(keys)) != NULL) {
sds key = listNodeValue(node); void *key = listNodeValue(node);
addReplyBulkCBuffer(c, key, sdslen(key)); /* 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); listDelNode(keys, node);
} }
...@@ -1339,6 +1404,7 @@ void renameGenericCommand(client *c, int nx) { ...@@ -1339,6 +1404,7 @@ void renameGenericCommand(client *c, int nx) {
robj *o; robj *o;
long long expire; long long expire;
int samekey = 0; int samekey = 0;
uint64_t minHashExpireTime = EB_EXPIRE_TIME_INVALID;
/* When source and dest key is the same, no operation is performed, /* 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. */ * if the key exists, however we still return an error on unexisting key. */
...@@ -1364,9 +1430,21 @@ void renameGenericCommand(client *c, int nx) { ...@@ -1364,9 +1430,21 @@ void renameGenericCommand(client *c, int nx) {
* with the same name. */ * with the same name. */
dbDelete(c->db,c->argv[2]); 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 (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]); 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[1]);
signalModifiedKey(c,c->db,c->argv[2]); signalModifiedKey(c,c->db,c->argv[2]);
notifyKeyspaceEvent(NOTIFY_GENERIC,"rename_from", notifyKeyspaceEvent(NOTIFY_GENERIC,"rename_from",
...@@ -1390,6 +1468,7 @@ void moveCommand(client *c) { ...@@ -1390,6 +1468,7 @@ void moveCommand(client *c) {
redisDb *src, *dst; redisDb *src, *dst;
int srcid, dbid; int srcid, dbid;
long long expire; long long expire;
uint64_t hashExpireTime = EB_EXPIRE_TIME_INVALID;
if (server.cluster_enabled) { if (server.cluster_enabled) {
addReplyError(c,"MOVE is not allowed in cluster mode"); addReplyError(c,"MOVE is not allowed in cluster mode");
...@@ -1430,12 +1509,25 @@ void moveCommand(client *c) { ...@@ -1430,12 +1509,25 @@ void moveCommand(client *c) {
addReply(c,shared.czero); addReply(c,shared.czero);
return; 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 (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); incrRefCount(o);
/* OK! key moved, free the entry in the source DB */ /* OK! key moved, free the entry in the source DB */
dbDelete(src,c->argv[1]); 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,src,c->argv[1]);
signalModifiedKey(c,dst,c->argv[1]); signalModifiedKey(c,dst,c->argv[1]);
notifyKeyspaceEvent(NOTIFY_GENERIC, notifyKeyspaceEvent(NOTIFY_GENERIC,
...@@ -1518,12 +1610,13 @@ void copyCommand(client *c) { ...@@ -1518,12 +1610,13 @@ void copyCommand(client *c) {
/* Duplicate object according to object's type. */ /* Duplicate object according to object's type. */
robj *newobj; robj *newobj;
uint64_t minHashExpire = EB_EXPIRE_TIME_INVALID; /* HFE feature */
switch(o->type) { switch(o->type) {
case OBJ_STRING: newobj = dupStringObject(o); break; case OBJ_STRING: newobj = dupStringObject(o); break;
case OBJ_LIST: newobj = listTypeDup(o); break; case OBJ_LIST: newobj = listTypeDup(o); break;
case OBJ_SET: newobj = setTypeDup(o); break; case OBJ_SET: newobj = setTypeDup(o); break;
case OBJ_ZSET: newobj = zsetDup(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_STREAM: newobj = streamDup(o); break;
case OBJ_MODULE: case OBJ_MODULE:
newobj = moduleTypeDupOrReply(c, key, newkey, dst->id, o); newobj = moduleTypeDupOrReply(c, key, newkey, dst->id, o);
...@@ -1538,8 +1631,16 @@ void copyCommand(client *c) { ...@@ -1538,8 +1631,16 @@ void copyCommand(client *c) {
dbDelete(dst,newkey); dbDelete(dst,newkey);
} }
dbAdd(dst,newkey,newobj); dictEntry *deCopy = dbAdd(dst,newkey,newobj);
if (expire != -1) setExpire(c, dst, newkey, expire);
/* 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 */ /* OK! key copied */
signalModifiedKey(c,dst,c->argv[2]); signalModifiedKey(c,dst,c->argv[2]);
...@@ -1629,11 +1730,13 @@ int dbSwapDatabases(int id1, int id2) { ...@@ -1629,11 +1730,13 @@ int dbSwapDatabases(int id1, int id2) {
* remain in the same DB they were. */ * remain in the same DB they were. */
db1->keys = db2->keys; db1->keys = db2->keys;
db1->expires = db2->expires; db1->expires = db2->expires;
db1->hexpires = db2->hexpires;
db1->avg_ttl = db2->avg_ttl; db1->avg_ttl = db2->avg_ttl;
db1->expires_cursor = db2->expires_cursor; db1->expires_cursor = db2->expires_cursor;
db2->keys = aux.keys; db2->keys = aux.keys;
db2->expires = aux.expires; db2->expires = aux.expires;
db2->hexpires = aux.hexpires;
db2->avg_ttl = aux.avg_ttl; db2->avg_ttl = aux.avg_ttl;
db2->expires_cursor = aux.expires_cursor; db2->expires_cursor = aux.expires_cursor;
...@@ -1671,11 +1774,13 @@ void swapMainDbWithTempDb(redisDb *tempDb) { ...@@ -1671,11 +1774,13 @@ void swapMainDbWithTempDb(redisDb *tempDb) {
* remain in the same DB they were. */ * remain in the same DB they were. */
activedb->keys = newdb->keys; activedb->keys = newdb->keys;
activedb->expires = newdb->expires; activedb->expires = newdb->expires;
activedb->hexpires = newdb->hexpires;
activedb->avg_ttl = newdb->avg_ttl; activedb->avg_ttl = newdb->avg_ttl;
activedb->expires_cursor = newdb->expires_cursor; activedb->expires_cursor = newdb->expires_cursor;
newdb->keys = aux.keys; newdb->keys = aux.keys;
newdb->expires = aux.expires; newdb->expires = aux.expires;
newdb->hexpires = aux.hexpires;
newdb->avg_ttl = aux.avg_ttl; newdb->avg_ttl = aux.avg_ttl;
newdb->expires_cursor = aux.expires_cursor; newdb->expires_cursor = aux.expires_cursor;
......
...@@ -200,17 +200,22 @@ void xorObjectDigest(redisDb *db, robj *keyobj, unsigned char *digest, robj *o) ...@@ -200,17 +200,22 @@ void xorObjectDigest(redisDb *db, robj *keyobj, unsigned char *digest, robj *o)
} }
} else if (o->type == OBJ_HASH) { } else if (o->type == OBJ_HASH) {
hashTypeIterator *hi = hashTypeInitIterator(o); hashTypeIterator *hi = hashTypeInitIterator(o);
while (hashTypeNext(hi) != C_ERR) { while (hashTypeNext(hi, 0) != C_ERR) {
unsigned char eledigest[20]; unsigned char eledigest[20];
sds sdsele; sds sdsele;
/* field */
memset(eledigest,0,20); memset(eledigest,0,20);
sdsele = hashTypeCurrentObjectNewSds(hi,OBJ_HASH_KEY); sdsele = hashTypeCurrentObjectNewSds(hi,OBJ_HASH_KEY);
mixDigest(eledigest,sdsele,sdslen(sdsele)); mixDigest(eledigest,sdsele,sdslen(sdsele));
sdsfree(sdsele); sdsfree(sdsele);
/* val */
sdsele = hashTypeCurrentObjectNewSds(hi,OBJ_HASH_VALUE); sdsele = hashTypeCurrentObjectNewSds(hi,OBJ_HASH_VALUE);
mixDigest(eledigest,sdsele,sdslen(sdsele)); mixDigest(eledigest,sdsele,sdslen(sdsele));
sdsfree(sdsele); sdsfree(sdsele);
/* hash-field expiration (HFE) */
if (hi->expire_time != EB_EXPIRE_TIME_INVALID)
xorDigest(eledigest,"!!hexpire!!",11);
xorDigest(digest,eledigest,20); xorDigest(digest,eledigest,20);
} }
hashTypeReleaseIterator(hi); hashTypeReleaseIterator(hi);
...@@ -445,9 +450,9 @@ void debugCommand(client *c) { ...@@ -445,9 +450,9 @@ void debugCommand(client *c) {
"SEGFAULT", "SEGFAULT",
" Crash the server with sigsegv.", " Crash the server with sigsegv.",
"SET-ACTIVE-EXPIRE <0|1>", "SET-ACTIVE-EXPIRE <0|1>",
" Setting it to 0 disables expiring keys in background when they are not", " Setting it to 0 disables expiring keys (and hash-fields) in background ",
" accessed (otherwise the Redis behavior). Setting it to 1 reenables back the", " when they are not accessed (otherwise the Redis behavior). Setting it",
" default.", " to 1 reenables back the default.",
"QUICKLIST-PACKED-THRESHOLD <size>", "QUICKLIST-PACKED-THRESHOLD <size>",
" Sets the threshold for elements to be inserted as plain vs packed nodes", " 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.", " Default value is 1GB, allows values up to 4GB. Setting to 0 restores to default.",
...@@ -664,10 +669,14 @@ NULL ...@@ -664,10 +669,14 @@ NULL
if ((o = objectCommandLookupOrReply(c,c->argv[2],shared.nokeyerr)) if ((o = objectCommandLookupOrReply(c,c->argv[2],shared.nokeyerr))
== NULL) return; == 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."); addReplyError(c,"Not a listpack encoded object.");
} else { } else {
if (o->encoding == OBJ_ENCODING_LISTPACK)
lpRepr(o->ptr); lpRepr(o->ptr);
else if (o->encoding == OBJ_ENCODING_LISTPACK_EX)
lpRepr(((listpackEx*)o->ptr)->lp);
addReplyStatus(c,"Listpack structure printed on stdout"); addReplyStatus(c,"Listpack structure printed on stdout");
} }
} else if (!strcasecmp(c->argv[1]->ptr,"quicklist") && (c->argc == 3 || c->argc == 4)) { } else if (!strcasecmp(c->argv[1]->ptr,"quicklist") && (c->argc == 3 || c->argc == 4)) {
...@@ -1081,7 +1090,7 @@ void serverLogObjectDebugInfo(const robj *o) { ...@@ -1081,7 +1090,7 @@ void serverLogObjectDebugInfo(const robj *o) {
} else if (o->type == OBJ_SET) { } else if (o->type == OBJ_SET) {
serverLog(LL_WARNING,"Set size: %d", (int) setTypeSize(o)); serverLog(LL_WARNING,"Set size: %d", (int) setTypeSize(o));
} else if (o->type == OBJ_HASH) { } 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) { } else if (o->type == OBJ_ZSET) {
serverLog(LL_WARNING,"Sorted set size: %d", (int) zsetLength(o)); serverLog(LL_WARNING,"Sorted set size: %d", (int) zsetLength(o));
if (o->encoding == OBJ_ENCODING_SKIPLIST) if (o->encoding == OBJ_ENCODING_SKIPLIST)
......
...@@ -70,6 +70,22 @@ sds activeDefragSds(sds sdsptr) { ...@@ -70,6 +70,22 @@ sds activeDefragSds(sds sdsptr) {
return NULL; 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. /* Defrag helper for robj and/or string objects with expected refcount.
* *
* Like activeDefragStringOb, but it requires the caller to pass in the expected * Like activeDefragStringOb, but it requires the caller to pass in the expected
...@@ -250,6 +266,31 @@ void activeDefragSdsDictCallback(void *privdata, const dictEntry *de) { ...@@ -250,6 +266,31 @@ void activeDefragSdsDictCallback(void *privdata, const dictEntry *de) {
UNUSED(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) */ /* Defrag a dict with sds key and optional value (either ptr, sds or robj string) */
void activeDefragSdsDict(dict* d, int val_type) { void activeDefragSdsDict(dict* d, int val_type) {
unsigned long cursor = 0; unsigned long cursor = 0;
...@@ -268,6 +309,20 @@ void activeDefragSdsDict(dict* d, int val_type) { ...@@ -268,6 +309,20 @@ void activeDefragSdsDict(dict* d, int val_type) {
} while (cursor != 0); } 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 */ /* Defrag a list of ptr, sds or robj string values */
void activeDefragList(list *l, int val_type) { void activeDefragList(list *l, int val_type) {
listNode *ln, *newln; listNode *ln, *newln;
...@@ -422,10 +477,10 @@ void scanLaterHash(robj *ob, unsigned long *cursor) { ...@@ -422,10 +477,10 @@ void scanLaterHash(robj *ob, unsigned long *cursor) {
dict *d = ob->ptr; dict *d = ob->ptr;
dictDefragFunctions defragfns = { dictDefragFunctions defragfns = {
.defragAlloc = activeDefragAlloc, .defragAlloc = activeDefragAlloc,
.defragKey = (dictDefragAllocFunction *)activeDefragSds, .defragKey = NULL, /* Will be defragmented in activeDefragHfieldDictCallback. */
.defragVal = (dictDefragAllocFunction *)activeDefragSds .defragVal = (dictDefragAllocFunction *)activeDefragSds
}; };
*cursor = dictScanDefrag(d, *cursor, scanCallbackCountScanned, &defragfns, NULL); *cursor = dictScanDefrag(d, *cursor, activeDefragHfieldDictCallback, &defragfns, d);
} }
void defragQuicklist(redisDb *db, dictEntry *kde) { void defragQuicklist(redisDb *db, dictEntry *kde) {
...@@ -477,7 +532,7 @@ void defragHash(redisDb *db, dictEntry *kde) { ...@@ -477,7 +532,7 @@ void defragHash(redisDb *db, dictEntry *kde) {
if (dictSize(d) > server.active_defrag_max_scan_fields) if (dictSize(d) > server.active_defrag_max_scan_fields)
defragLater(db, kde); defragLater(db, kde);
else else
activeDefragSdsDict(d, DEFRAG_SDS_DICT_VAL_IS_SDS); activeDefragHfieldDict(d);
/* defrag the dict struct and tables */ /* defrag the dict struct and tables */
if ((newd = dictDefragTables(ob->ptr))) if ((newd = dictDefragTables(ob->ptr)))
ob->ptr = newd; ob->ptr = newd;
...@@ -672,7 +727,7 @@ void defragModule(redisDb *db, dictEntry *kde) { ...@@ -672,7 +727,7 @@ void defragModule(redisDb *db, dictEntry *kde) {
* all the various pointers it has. */ * all the various pointers it has. */
void defragKey(defragCtx *ctx, dictEntry *de) { void defragKey(defragCtx *ctx, dictEntry *de) {
sds keysds = dictGetKey(de); sds keysds = dictGetKey(de);
robj *newob, *ob; robj *newob, *ob = dictGetVal(de);
unsigned char *newzl; unsigned char *newzl;
sds newsds; sds newsds;
redisDb *db = ctx->privdata; redisDb *db = ctx->privdata;
...@@ -689,11 +744,22 @@ void defragKey(defragCtx *ctx, dictEntry *de) { ...@@ -689,11 +744,22 @@ void defragKey(defragCtx *ctx, dictEntry *de) {
dictEntry *expire_de = kvstoreDictFindEntryByPtrAndHash(db->expires, slot, keysds, hash); dictEntry *expire_de = kvstoreDictFindEntryByPtrAndHash(db->expires, slot, keysds, hash);
if (expire_de) kvstoreDictSetKey(db->expires, slot, expire_de, newsds); 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. */ /* Try to defrag robj and / or string value. */
ob = dictGetVal(de); if (unlikely(ob->type == OBJ_HASH && hashTypeGetMinExpire(ob) != EB_EXPIRE_TIME_INVALID)) {
if ((newob = activeDefragStringOb(ob))) { /* 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); kvstoreDictSetVal(db->keys, slot, de, newob);
ob = newob; ob = newob;
} }
...@@ -734,6 +800,12 @@ void defragKey(defragCtx *ctx, dictEntry *de) { ...@@ -734,6 +800,12 @@ void defragKey(defragCtx *ctx, dictEntry *de) {
if (ob->encoding == OBJ_ENCODING_LISTPACK) { if (ob->encoding == OBJ_ENCODING_LISTPACK) {
if ((newzl = activeDefragAlloc(ob->ptr))) if ((newzl = activeDefragAlloc(ob->ptr)))
ob->ptr = newzl; 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) { } else if (ob->encoding == OBJ_ENCODING_HT) {
defragHash(db, de); defragHash(db, de);
} else { } else {
......
...@@ -67,6 +67,25 @@ static int _dictInit(dict *d, dictType *type); ...@@ -67,6 +67,25 @@ static int _dictInit(dict *d, dictType *type);
static dictEntry *dictGetNext(const dictEntry *de); static dictEntry *dictGetNext(const dictEntry *de);
static dictEntry **dictGetNextRef(dictEntry *de); static dictEntry **dictGetNextRef(dictEntry *de);
static void dictSetNext(dictEntry *de, dictEntry *next); 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 -------------------------------- */ /* -------------------------- hash functions -------------------------------- */
...@@ -173,6 +192,19 @@ dict *dictCreate(dictType *type) ...@@ -173,6 +192,19 @@ dict *dictCreate(dictType *type)
return d; 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 */ /* Initialize the hash table */
int _dictInit(dict *d, dictType *type) int _dictInit(dict *d, dictType *type)
{ {
...@@ -182,6 +214,7 @@ int _dictInit(dict *d, dictType *type) ...@@ -182,6 +214,7 @@ int _dictInit(dict *d, dictType *type)
d->rehashidx = -1; d->rehashidx = -1;
d->pauserehash = 0; d->pauserehash = 0;
d->pauseAutoResize = 0; d->pauseAutoResize = 0;
d->useStoredKeyApi = 0;
return DICT_OK; return DICT_OK;
} }
...@@ -285,7 +318,7 @@ static void rehashEntriesInBucketAtIndex(dict *d, uint64_t idx) { ...@@ -285,7 +318,7 @@ static void rehashEntriesInBucketAtIndex(dict *d, uint64_t idx) {
void *key = dictGetKey(de); void *key = dictGetKey(de);
/* Get the index in the new hash table */ /* Get the index in the new hash table */
if (d->ht_size_exp[1] > d->ht_size_exp[0]) { 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 { } else {
/* We're shrinking the table. The tables sizes are powers of /* We're shrinking the table. The tables sizes are powers of
* two, so we simply mask the bucket index in the larger table * 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) { ...@@ -572,7 +605,7 @@ static dictEntry *dictGenericDelete(dict *d, const void *key, int nofree) {
/* dict is empty */ /* dict is empty */
if (dictSize(d) == 0) return NULL; 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]); idx = h & DICTHT_SIZE_MASK(d->ht_size_exp[0]);
if (dictIsRehashing(d)) { if (dictIsRehashing(d)) {
...@@ -587,6 +620,8 @@ static dictEntry *dictGenericDelete(dict *d, const void *key, int nofree) { ...@@ -587,6 +620,8 @@ static dictEntry *dictGenericDelete(dict *d, const void *key, int nofree) {
} }
} }
keyCmpFunc cmpFunc = dictGetKeyCmpFunc(d);
for (table = 0; table <= 1; table++) { for (table = 0; table <= 1; table++) {
if (table == 0 && (long)idx < d->rehashidx) continue; if (table == 0 && (long)idx < d->rehashidx) continue;
idx = h & DICTHT_SIZE_MASK(d->ht_size_exp[table]); idx = h & DICTHT_SIZE_MASK(d->ht_size_exp[table]);
...@@ -594,7 +629,7 @@ static dictEntry *dictGenericDelete(dict *d, const void *key, int nofree) { ...@@ -594,7 +629,7 @@ static dictEntry *dictGenericDelete(dict *d, const void *key, int nofree) {
prevHe = NULL; prevHe = NULL;
while(he) { while(he) {
void *he_key = dictGetKey(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 */ /* Unlink the element from the list */
if (prevHe) if (prevHe)
dictSetNext(prevHe, dictGetNext(he)); dictSetNext(prevHe, dictGetNext(he));
...@@ -689,6 +724,10 @@ void dictRelease(dict *d) ...@@ -689,6 +724,10 @@ void dictRelease(dict *d)
* destroying the dict fake completion. */ * destroying the dict fake completion. */
if (dictIsRehashing(d) && d->type->rehashingCompleted) if (dictIsRehashing(d) && d->type->rehashingCompleted)
d->type->rehashingCompleted(d); d->type->rehashingCompleted(d);
if (d->type->onDictRelease)
d->type->onDictRelease(d);
_dictClear(d,0,NULL); _dictClear(d,0,NULL);
_dictClear(d,1,NULL); _dictClear(d,1,NULL);
zfree(d); zfree(d);
...@@ -701,8 +740,9 @@ dictEntry *dictFind(dict *d, const void *key) ...@@ -701,8 +740,9 @@ dictEntry *dictFind(dict *d, const void *key)
if (dictSize(d) == 0) return NULL; /* dict is empty */ 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]); idx = h & DICTHT_SIZE_MASK(d->ht_size_exp[0]);
keyCmpFunc cmpFunc = dictGetKeyCmpFunc(d);
if (dictIsRehashing(d)) { if (dictIsRehashing(d)) {
if ((long)idx >= d->rehashidx && d->ht_table[0][idx]) { if ((long)idx >= d->rehashidx && d->ht_table[0][idx]) {
...@@ -722,7 +762,7 @@ dictEntry *dictFind(dict *d, const void *key) ...@@ -722,7 +762,7 @@ dictEntry *dictFind(dict *d, const void *key)
he = d->ht_table[table][idx]; he = d->ht_table[table][idx];
while(he) { while(he) {
void *he_key = dictGetKey(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; return he;
he = dictGetNext(he); he = dictGetNext(he);
} }
...@@ -759,7 +799,9 @@ dictEntry *dictTwoPhaseUnlinkFind(dict *d, const void *key, dictEntry ***plink, ...@@ -759,7 +799,9 @@ dictEntry *dictTwoPhaseUnlinkFind(dict *d, const void *key, dictEntry ***plink,
if (dictSize(d) == 0) return NULL; /* dict is empty */ if (dictSize(d) == 0) return NULL; /* dict is empty */
if (dictIsRehashing(d)) _dictRehashStep(d); 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++) { for (table = 0; table <= 1; table++) {
idx = h & DICTHT_SIZE_MASK(d->ht_size_exp[table]); idx = h & DICTHT_SIZE_MASK(d->ht_size_exp[table]);
...@@ -767,7 +809,7 @@ dictEntry *dictTwoPhaseUnlinkFind(dict *d, const void *key, dictEntry ***plink, ...@@ -767,7 +809,7 @@ dictEntry *dictTwoPhaseUnlinkFind(dict *d, const void *key, dictEntry ***plink,
dictEntry **ref = &d->ht_table[table][idx]; dictEntry **ref = &d->ht_table[table][idx];
while (ref && *ref) { while (ref && *ref) {
void *de_key = dictGetKey(*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; *table_index = table;
*plink = ref; *plink = ref;
dictPauseRehashing(d); dictPauseRehashing(d);
...@@ -1530,8 +1572,8 @@ static signed char _dictNextExp(unsigned long size) ...@@ -1530,8 +1572,8 @@ static signed char _dictNextExp(unsigned long size)
void *dictFindPositionForInsert(dict *d, const void *key, dictEntry **existing) { void *dictFindPositionForInsert(dict *d, const void *key, dictEntry **existing) {
unsigned long idx, table; unsigned long idx, table;
dictEntry *he; dictEntry *he;
uint64_t hash = dictHashKey(d, key, d->useStoredKeyApi);
if (existing) *existing = NULL; if (existing) *existing = NULL;
uint64_t hash = dictHashKey(d, key);
idx = hash & DICTHT_SIZE_MASK(d->ht_size_exp[0]); idx = hash & DICTHT_SIZE_MASK(d->ht_size_exp[0]);
if (dictIsRehashing(d)) { if (dictIsRehashing(d)) {
...@@ -1548,6 +1590,8 @@ void *dictFindPositionForInsert(dict *d, const void *key, dictEntry **existing) ...@@ -1548,6 +1590,8 @@ void *dictFindPositionForInsert(dict *d, const void *key, dictEntry **existing)
/* Expand the hash table if needed */ /* Expand the hash table if needed */
_dictExpandIfNeeded(d); _dictExpandIfNeeded(d);
keyCmpFunc cmpFunc = dictGetKeyCmpFunc(d);
for (table = 0; table <= 1; table++) { for (table = 0; table <= 1; table++) {
if (table == 0 && (long)idx < d->rehashidx) continue; if (table == 0 && (long)idx < d->rehashidx) continue;
idx = hash & DICTHT_SIZE_MASK(d->ht_size_exp[table]); idx = hash & DICTHT_SIZE_MASK(d->ht_size_exp[table]);
...@@ -1555,7 +1599,7 @@ void *dictFindPositionForInsert(dict *d, const void *key, dictEntry **existing) ...@@ -1555,7 +1599,7 @@ void *dictFindPositionForInsert(dict *d, const void *key, dictEntry **existing)
he = d->ht_table[table][idx]; he = d->ht_table[table][idx];
while(he) { while(he) {
void *he_key = dictGetKey(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; if (existing) *existing = he;
return NULL; return NULL;
} }
...@@ -1587,7 +1631,7 @@ void dictSetResizeEnabled(dictResizeEnable enable) { ...@@ -1587,7 +1631,7 @@ void dictSetResizeEnabled(dictResizeEnable enable) {
} }
uint64_t dictGetHash(dict *d, const void *key) { 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. /* Finds the dictEntry using pointer and pre-calculated hash.
...@@ -1732,6 +1776,11 @@ void dictGetStats(char *buf, size_t bufsize, dict *d, int full) { ...@@ -1732,6 +1776,11 @@ void dictGetStats(char *buf, size_t bufsize, dict *d, int full) {
orig_buf[orig_bufsize-1] = '\0'; orig_buf[orig_bufsize-1] = '\0';
} }
static int dictDefaultCompare(dict *d, const void *key1, const void *key2) {
(void)(d); /*unused*/
return key1 == key2;
}
/* ------------------------------- Benchmark ---------------------------------*/ /* ------------------------------- Benchmark ---------------------------------*/
#ifdef REDIS_TEST #ifdef REDIS_TEST
......
...@@ -62,6 +62,32 @@ typedef struct dictType { ...@@ -62,6 +62,32 @@ typedef struct dictType {
unsigned int keys_are_odd:1; unsigned int keys_are_odd:1;
/* TODO: Add a 'keys_are_even' flag and use a similar optimization if that /* TODO: Add a 'keys_are_even' flag and use a similar optimization if that
* flag is set. */ * 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; } dictType;
#define DICTHT_SIZE(exp) ((exp) == -1 ? 0 : (unsigned long)1<<(exp)) #define DICTHT_SIZE(exp) ((exp) == -1 ? 0 : (unsigned long)1<<(exp))
...@@ -76,7 +102,9 @@ struct dict { ...@@ -76,7 +102,9 @@ struct dict {
long rehashidx; /* rehashing not in progress if rehashidx == -1 */ long rehashidx; /* rehashing not in progress if rehashidx == -1 */
/* Keep small vars at end for optimal (minimal) struct padding */ /* 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<<exp) */ signed char ht_size_exp[2]; /* exponent of size. (size = 1<<exp) */
int16_t pauseAutoResize; /* If >0 automatic resizing is disallowed (<0 indicates coding error) */ int16_t pauseAutoResize; /* If >0 automatic resizing is disallowed (<0 indicates coding error) */
void *metadata[]; void *metadata[];
...@@ -136,7 +164,6 @@ typedef struct { ...@@ -136,7 +164,6 @@ typedef struct {
#define dictMetadataSize(d) ((d)->type->dictMetadataBytes \ #define dictMetadataSize(d) ((d)->type->dictMetadataBytes \
? (d)->type->dictMetadataBytes(d) : 0) ? (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 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 dictSize(d) ((d)->ht_used[0]+(d)->ht_used[1])
#define dictIsEmpty(d) ((d)->ht_used[0] == 0 && (d)->ht_used[1] == 0) #define dictIsEmpty(d) ((d)->ht_used[0] == 0 && (d)->ht_used[1] == 0)
...@@ -146,6 +173,7 @@ typedef struct { ...@@ -146,6 +173,7 @@ typedef struct {
#define dictIsRehashingPaused(d) ((d)->pauserehash > 0) #define dictIsRehashingPaused(d) ((d)->pauserehash > 0)
#define dictPauseAutoResize(d) ((d)->pauseAutoResize++) #define dictPauseAutoResize(d) ((d)->pauseAutoResize++)
#define dictResumeAutoResize(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 our unsigned long type can store a 64 bit number, use a 64 bit PRNG. */
#if ULONG_MAX >= 0xffffffffffffffff #if ULONG_MAX >= 0xffffffffffffffff
...@@ -162,6 +190,7 @@ typedef enum { ...@@ -162,6 +190,7 @@ typedef enum {
/* API */ /* API */
dict *dictCreate(dictType *type); dict *dictCreate(dictType *type);
void dictTypeAddMeta(dict **d, dictType *typeWithMeta);
int dictExpand(dict *d, unsigned long size); int dictExpand(dict *d, unsigned long size);
int dictTryExpand(dict *d, unsigned long size); int dictTryExpand(dict *d, unsigned long size);
int dictShrink(dict *d, unsigned long size); int dictShrink(dict *d, unsigned long size);
......
/*
* 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 <stdio.h>
#include <stddef.h>
#include <stdlib.h>
#include <inttypes.h>
#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 "<n=%d,f=%d,ls=%d,lb=%d>, ",
expireTime, mIter->numItems, mIter->firstItemBucket,
mIter->lastInSegment, mIter->lastItemBucket);
else if (i == (mHead->numItems - 1) && PRINT_EXPIRE_META_FLAGS) {
printf("%" PRIu64 "<n=%d,f=%d,ls=%d,lb=%d>",
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<<EB_BUCKET_KEY_PRECISION msec ago. That is, it
* is needed to visit only the buckets with keys that are "<" than:
* EB_BUCKET_KEY(info->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<<EB_BUCKET_KEY_PRECISION-1 msec (we
* don't want to traverse the entire extended-segment since it might not
* bounded).
*/
uint64_t ebGetNextTimeToExpire(ebuckets eb, EbucketsType *type) {
if (ebIsEmpty(eb))
return EB_EXPIRE_TIME_INVALID;
if (ebIsList(eb))
return ebGetMetaExpTime(type->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<<EB_BUCKET_KEY_PRECISION)-1) ;
}
raxStop(&iter);
return minExpire;
}
/**
* Retrieves the expiration time of the item with the latest expiration
*
* However, precision loss (EB_BUCKET_KEY_PRECISION) in rax tree buckets
* may result in slight inaccuracies, up to a variation of
* 1<<EB_BUCKET_KEY_PRECISION msec.
*
* @param eb - The ebucket to be checked.
* @param type - Pointer to the EbucketsType structure defining the type of ebucket.
* @param accurate - If 1, then the function will return accurate result. Otherwise,
* it might return the upper limit with slight inaccuracy of
* 1<<EB_BUCKET_KEY_PRECISION msec.
*
* This special case is relevant only when the last bucket
* is of type extended-segment. In this case, we might don't
* want to traverse the entire bucket to find the accurate
* expiration time since there might be unbounded number of
* items in the extended-segment. If EB_BUCKET_KEY_PRECISION
* is 0, then the function will return accurate result anyway.
*
* @return The expiration time of the item with the latest expiration time in
* the ebucket. If empty, return EB_EXPIRE_TIME_INVALID.
*/
uint64_t ebGetMaxExpireTime(ebuckets eb, EbucketsType *type, int accurate) {
if (ebIsEmpty(eb))
return EB_EXPIRE_TIME_INVALID;
if (ebIsList(eb)) {
eItem item = ebGetListPtr(type, eb);
ExpireMeta *em = type->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<<EB_BUCKET_KEY_PRECISION)-1;
uint64_t expTime = ebGetMetaExpTime(type->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 <stddef.h>
#include <sys/time.h>
#include <sys/resource.h>
#include <string.h>
#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<<EB_BUCKET_KEY_PRECISION
* msec ago. That is, it is needed to visit only the buckets with keys
* that are "<" EB_BUCKET_KEY(info->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<<EB_BUCKET_KEY_PRECISION) > 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<<EB_BUCKET_KEY_PRECISION) + i;
ebAdd(&eb, &myEbucketsType2, items + i, itemExpireTime);
}
/* Now start adding more items to extended-segment and verify min/max */
for (int i = EB_SEG_MAX_ITEMS+1; i < numItems; i++) {
uint64_t itemExpireTime = (1<<EB_BUCKET_KEY_PRECISION) + i;
ebAdd(&eb, &myEbucketsType2, items + i, itemExpireTime);
assert(ebGetNextTimeToExpire(eb, &myEbucketsType2) == (uint64_t)(2<<EB_BUCKET_KEY_PRECISION));
assert(ebGetMaxExpireTime(eb, &myEbucketsType2, 0) == (uint64_t)(2<<EB_BUCKET_KEY_PRECISION));
assert(ebGetMaxExpireTime(eb, &myEbucketsType2, 1) == (uint64_t)((1<<EB_BUCKET_KEY_PRECISION) + i));
}
ebDestroy(&eb, &myEbucketsType2, NULL);
}
}
TEST("ebuckets - active-expire dry-run") {
ebuckets eb = NULL;
MyItem items[2*EB_SEG_MAX_ITEMS];
for (int numItems = 1 ; numItems < (int)ARRAY_SIZE(items) ; numItems++) {
int maxExpireKey = (numItems % 2) ? 40 : 2;
/* Allocate numItems and add to ebuckets */
for (int i = 0; i < numItems; i++) {
/* generate random expiration time */
uint64_t expireTime = (rand() % maxExpireKey) << EB_BUCKET_KEY_PRECISION;
ebAdd(&eb, &myEbucketsType2, items + i, expireTime);
}
for (int i = 0 ; i <= maxExpireKey ; ++i) {
uint64_t now = i << EB_BUCKET_KEY_PRECISION;
/* Count how much items are expired */
uint64_t expectedNumExpired = 0;
for (int j = 0; j < numItems; j++) {
if (ebGetExpireTime(&myEbucketsType2, items + j) < now)
expectedNumExpired++;
}
/* Perform dry-run and verify number of expired items */
assert(ebExpireDryRun(eb, &myEbucketsType2, now) == expectedNumExpired);
}
ebDestroy(&eb, &myEbucketsType2, NULL);
}
}
TEST("ebuckets - active expire callback returns ACT_UPDATE_EXP_ITEM") {
ebuckets eb = NULL;
MyItem items[2*EB_SEG_MAX_ITEMS];
int numItems = 2*EB_SEG_MAX_ITEMS;
/* timeline */
int expiredAt = 2,
applyActiveExpireAt = 3,
updateItemTo = 5,
expectedExpiredAt = 6;
/* Allocate numItems and add to ebuckets */
for (int i = 0; i < numItems; i++)
ebAdd(&eb, &myEbucketsType2, items + i, expiredAt << EB_BUCKET_KEY_PRECISION);
/* active-expire. Expected that all but one will be expired */
ExpireInfo info = {
.maxToExpire = 0xFFFFFFFF,
.onExpireItem = expireUpdateThirdItemCb,
.ctx = (void *) (uintptr_t) (updateItemTo << EB_BUCKET_KEY_PRECISION),
.now = applyActiveExpireAt << EB_BUCKET_KEY_PRECISION,
.itemsExpired = 0};
ebExpire(&eb, &myEbucketsType2, &info);
assert(info.itemsExpired == (uint64_t) numItems);
assert(ebGetTotalItems(eb, &myEbucketsType2) == 1);
/* active-expire. Expected that all will be expired */
ExpireInfo info2 = {
.maxToExpire = 0xFFFFFFFF,
.onExpireItem = expireUpdateThirdItemCb,
.ctx = (void *) (uintptr_t) (updateItemTo << EB_BUCKET_KEY_PRECISION),
.now = expectedExpiredAt << EB_BUCKET_KEY_PRECISION,
.itemsExpired = 0};
ebExpire(&eb, &myEbucketsType2, &info2);
assert(info2.itemsExpired == (uint64_t) 1);
assert(ebGetTotalItems(eb, &myEbucketsType2) == 0);
ebDestroy(&eb, &myEbucketsType2, NULL);
}
TEST("item defragmentation") {
for (int s = 1; s <= EB_LIST_MAX_ITEMS * 3; s++) {
ebuckets eb = NULL;
MyItem *items[s];
for (int i = 0; i < s; i++) {
items[i] = zmalloc(sizeof(MyItem));
items[i]->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
/*
* 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 <stdlib.h>
#include <sys/types.h>
#include <stdarg.h>
#include <stdint.h>
#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) msec ago.
* For example if EB_BUCKET_KEY_PRECISION is 10, then active expiration
* will work only on buckets that already got expired at least 1sec ago.
*
* The idea of it is to trim the rax tree depth, avoid having too many branches,
* and reduce frequent modifications of the tree to the minimum.
*/
#define EB_BUCKET_KEY_PRECISION 0 /* TBD: modify to 10 */
/* From expiration time to bucket-key */
#define EB_BUCKET_KEY(exptime) ((exptime) >> 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 */
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment