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.
This commit is contained in:
+1
-1
@@ -354,7 +354,7 @@ endif
|
||||
|
||||
REDIS_SERVER_NAME=redis-server$(PROG_SUFFIX)
|
||||
REDIS_SENTINEL_NAME=redis-sentinel$(PROG_SUFFIX)
|
||||
REDIS_SERVER_OBJ=threads_mngr.o adlist.o quicklist.o ae.o anet.o dict.o kvstore.o server.o sds.o zmalloc.o lzf_c.o lzf_d.o pqsort.o zipmap.o sha1.o ziplist.o release.o networking.o util.o object.o db.o replication.o rdb.o t_string.o t_list.o t_set.o t_zset.o t_hash.o config.o aof.o pubsub.o multi.o debug.o sort.o intset.o syncio.o cluster.o cluster_legacy.o crc16.o endianconv.o slowlog.o eval.o bio.o rio.o rand.o memtest.o syscheck.o crcspeed.o crc64.o bitops.o sentinel.o notify.o setproctitle.o blocked.o hyperloglog.o latency.o sparkline.o redis-check-rdb.o redis-check-aof.o geo.o lazyfree.o module.o evict.o expire.o geohash.o geohash_helper.o childinfo.o defrag.o siphash.o rax.o t_stream.o listpack.o localtime.o lolwut.o lolwut5.o lolwut6.o acl.o tracking.o socket.o tls.o sha256.o timeout.o setcpuaffinity.o monotonic.o mt19937-64.o resp_parser.o call_reply.o script_lua.o script.o functions.o function_lua.o commands.o strl.o connection.o unix.o logreqres.o
|
||||
REDIS_SERVER_OBJ=threads_mngr.o adlist.o quicklist.o ae.o anet.o dict.o ebuckets.o mstr.o kvstore.o server.o sds.o zmalloc.o lzf_c.o lzf_d.o pqsort.o zipmap.o sha1.o ziplist.o release.o networking.o util.o object.o db.o replication.o rdb.o t_string.o t_list.o t_set.o t_zset.o t_hash.o config.o aof.o pubsub.o multi.o debug.o sort.o intset.o syncio.o cluster.o cluster_legacy.o crc16.o endianconv.o slowlog.o eval.o bio.o rio.o rand.o memtest.o syscheck.o crcspeed.o crc64.o bitops.o sentinel.o notify.o setproctitle.o blocked.o hyperloglog.o latency.o sparkline.o redis-check-rdb.o redis-check-aof.o geo.o lazyfree.o module.o evict.o expire.o geohash.o geohash_helper.o childinfo.o defrag.o siphash.o rax.o t_stream.o listpack.o localtime.o lolwut.o lolwut5.o lolwut6.o acl.o tracking.o socket.o tls.o sha256.o timeout.o setcpuaffinity.o monotonic.o mt19937-64.o resp_parser.o call_reply.o script_lua.o script.o functions.o function_lua.o commands.o strl.o connection.o unix.o logreqres.o
|
||||
REDIS_CLI_NAME=redis-cli$(PROG_SUFFIX)
|
||||
REDIS_CLI_OBJ=anet.o adlist.o dict.o redis-cli.o zmalloc.o release.o ae.o redisassert.o crcspeed.o crc64.o siphash.o crc16.o monotonic.o cli_common.o mt19937-64.o strl.o cli_commands.o
|
||||
REDIS_BENCHMARK_NAME=redis-benchmark$(PROG_SUFFIX)
|
||||
|
||||
@@ -1939,19 +1939,21 @@ int rewriteSortedSetObject(rio *r, robj *key, robj *o) {
|
||||
*
|
||||
* The function returns 0 on error, non-zero on success. */
|
||||
static int rioWriteHashIteratorCursor(rio *r, hashTypeIterator *hi, int what) {
|
||||
if (hi->encoding == OBJ_ENCODING_LISTPACK) {
|
||||
if ((hi->encoding == OBJ_ENCODING_LISTPACK) || (hi->encoding == OBJ_ENCODING_LISTPACK_EX)) {
|
||||
unsigned char *vstr = NULL;
|
||||
unsigned int vlen = UINT_MAX;
|
||||
long long vll = LLONG_MAX;
|
||||
|
||||
hashTypeCurrentFromListpack(hi, what, &vstr, &vlen, &vll);
|
||||
hashTypeCurrentFromListpack(hi, what, &vstr, &vlen, &vll, NULL);
|
||||
if (vstr)
|
||||
return rioWriteBulkString(r, (char*)vstr, vlen);
|
||||
else
|
||||
return rioWriteBulkLongLong(r, vll);
|
||||
} else if (hi->encoding == OBJ_ENCODING_HT) {
|
||||
sds value = hashTypeCurrentFromHashTable(hi, what);
|
||||
return rioWriteBulkString(r, value, sdslen(value));
|
||||
char *str;
|
||||
size_t len;
|
||||
hashTypeCurrentFromHashTable(hi, what, &str, &len, NULL);
|
||||
return rioWriteBulkString(r, str, len);
|
||||
}
|
||||
|
||||
serverPanic("Unknown hash encoding");
|
||||
@@ -1961,37 +1963,60 @@ static int rioWriteHashIteratorCursor(rio *r, hashTypeIterator *hi, int what) {
|
||||
/* Emit the commands needed to rebuild a hash object.
|
||||
* The function returns 0 on error, 1 on success. */
|
||||
int rewriteHashObject(rio *r, robj *key, robj *o) {
|
||||
int res = 0; /*fail*/
|
||||
|
||||
hashTypeIterator *hi;
|
||||
long long count = 0, items = hashTypeLength(o);
|
||||
long long count = 0, items = hashTypeLength(o, 0);
|
||||
|
||||
int isHFE = hashTypeGetMinExpire(o) != EB_EXPIRE_TIME_INVALID;
|
||||
hi = hashTypeInitIterator(o);
|
||||
while (hashTypeNext(hi) != C_ERR) {
|
||||
if (count == 0) {
|
||||
int cmd_items = (items > AOF_REWRITE_ITEMS_PER_CMD) ?
|
||||
AOF_REWRITE_ITEMS_PER_CMD : items;
|
||||
|
||||
if (!rioWriteBulkCount(r,'*',2+cmd_items*2) ||
|
||||
!rioWriteBulkString(r,"HMSET",5) ||
|
||||
!rioWriteBulkObject(r,key))
|
||||
{
|
||||
hashTypeReleaseIterator(hi);
|
||||
return 0;
|
||||
if (!isHFE) {
|
||||
while (hashTypeNext(hi, 0) != C_ERR) {
|
||||
if (count == 0) {
|
||||
int cmd_items = (items > AOF_REWRITE_ITEMS_PER_CMD) ?
|
||||
AOF_REWRITE_ITEMS_PER_CMD : items;
|
||||
if (!rioWriteBulkCount(r, '*', 2 + cmd_items * 2) ||
|
||||
!rioWriteBulkString(r, "HMSET", 5) ||
|
||||
!rioWriteBulkObject(r, key))
|
||||
goto reHashEnd;
|
||||
}
|
||||
|
||||
if (!rioWriteHashIteratorCursor(r, hi, OBJ_HASH_KEY) ||
|
||||
!rioWriteHashIteratorCursor(r, hi, OBJ_HASH_VALUE))
|
||||
goto reHashEnd;
|
||||
|
||||
if (++count == AOF_REWRITE_ITEMS_PER_CMD) count = 0;
|
||||
items--;
|
||||
}
|
||||
} else {
|
||||
while (hashTypeNext(hi, 0) != C_ERR) {
|
||||
|
||||
char hmsetCmd[] = "*4\r\n$5\r\nHMSET\r\n";
|
||||
if ( (!rioWrite(r, hmsetCmd, sizeof(hmsetCmd) - 1)) ||
|
||||
(!rioWriteBulkObject(r, key)) ||
|
||||
(!rioWriteHashIteratorCursor(r, hi, OBJ_HASH_KEY)) ||
|
||||
(!rioWriteHashIteratorCursor(r, hi, OBJ_HASH_VALUE)) )
|
||||
goto reHashEnd;
|
||||
|
||||
if (hi->expire_time != EB_EXPIRE_TIME_INVALID) {
|
||||
char cmd[] = "*6\r\n$10\r\nHPEXPIREAT\r\n";
|
||||
if ( (!rioWrite(r, cmd, sizeof(cmd) - 1)) ||
|
||||
(!rioWriteBulkObject(r, key)) ||
|
||||
(!rioWriteBulkLongLong(r, hi->expire_time)) ||
|
||||
(!rioWriteBulkString(r, "FIELDS", 6)) ||
|
||||
(!rioWriteBulkString(r, "1", 1)) ||
|
||||
(!rioWriteHashIteratorCursor(r, hi, OBJ_HASH_KEY)) )
|
||||
goto reHashEnd;
|
||||
}
|
||||
}
|
||||
|
||||
if (!rioWriteHashIteratorCursor(r, hi, OBJ_HASH_KEY) ||
|
||||
!rioWriteHashIteratorCursor(r, hi, OBJ_HASH_VALUE))
|
||||
{
|
||||
hashTypeReleaseIterator(hi);
|
||||
return 0;
|
||||
}
|
||||
if (++count == AOF_REWRITE_ITEMS_PER_CMD) count = 0;
|
||||
items--;
|
||||
}
|
||||
|
||||
hashTypeReleaseIterator(hi);
|
||||
res = 1; /* success */
|
||||
|
||||
return 1;
|
||||
reHashEnd:
|
||||
hashTypeReleaseIterator(hi);
|
||||
return res;
|
||||
}
|
||||
|
||||
/* Helper for rewriteStreamObject() that generates a bulk string into the
|
||||
|
||||
+9
-2
@@ -176,6 +176,7 @@ void dumpCommand(client *c) {
|
||||
|
||||
/* RESTORE key ttl serialized-value [REPLACE] [ABSTTL] [IDLETIME seconds] [FREQ frequency] */
|
||||
void restoreCommand(client *c) {
|
||||
uint64_t minExpiredField = EB_EXPIRE_TIME_INVALID;
|
||||
long long ttl, lfu_freq = -1, lru_idle = -1, lru_clock = -1;
|
||||
rio payload;
|
||||
int j, type, replace = 0, absttl = 0;
|
||||
@@ -239,7 +240,7 @@ void restoreCommand(client *c) {
|
||||
|
||||
rioInitWithBuffer(&payload,c->argv[3]->ptr);
|
||||
if (((type = rdbLoadObjectType(&payload)) == -1) ||
|
||||
((obj = rdbLoadObject(type,&payload,key->ptr,c->db->id,NULL)) == NULL))
|
||||
((obj = rdbLoadObject(type,&payload,key->ptr,c->db,NULL, &minExpiredField)) == NULL))
|
||||
{
|
||||
addReplyError(c,"Bad data format");
|
||||
return;
|
||||
@@ -265,7 +266,13 @@ void restoreCommand(client *c) {
|
||||
}
|
||||
|
||||
/* Create the key and set the TTL if any */
|
||||
dbAdd(c->db,key,obj);
|
||||
dictEntry *de = dbAdd(c->db,key,obj);
|
||||
|
||||
/* If minExpiredField was set, then the object is hash with expiration
|
||||
* on fields and need to register it in global HFE DS */
|
||||
if (minExpiredField != EB_EXPIRE_TIME_INVALID)
|
||||
hashTypeAddToExpires(c->db, dictGetKey(de), obj, minExpiredField);
|
||||
|
||||
if (ttl) {
|
||||
setExpire(c,c->db,key,ttl);
|
||||
if (!absttl) {
|
||||
|
||||
@@ -3303,6 +3303,107 @@ struct COMMAND_ARG HEXISTS_Args[] = {
|
||||
{MAKE_ARG("field",ARG_TYPE_STRING,-1,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
};
|
||||
|
||||
/********** HEXPIRE ********************/
|
||||
|
||||
#ifndef SKIP_CMD_HISTORY_TABLE
|
||||
/* HEXPIRE history */
|
||||
#define HEXPIRE_History NULL
|
||||
#endif
|
||||
|
||||
#ifndef SKIP_CMD_TIPS_TABLE
|
||||
/* HEXPIRE tips */
|
||||
#define HEXPIRE_Tips NULL
|
||||
#endif
|
||||
|
||||
#ifndef SKIP_CMD_KEY_SPECS_TABLE
|
||||
/* HEXPIRE key specs */
|
||||
keySpec HEXPIRE_Keyspecs[1] = {
|
||||
{NULL,CMD_KEY_RW|CMD_KEY_UPDATE,KSPEC_BS_INDEX,.bs.index={1},KSPEC_FK_RANGE,.fk.range={0,1,0}}
|
||||
};
|
||||
#endif
|
||||
|
||||
/* HEXPIRE condition argument table */
|
||||
struct COMMAND_ARG HEXPIRE_condition_Subargs[] = {
|
||||
{MAKE_ARG("nx",ARG_TYPE_PURE_TOKEN,-1,"NX",NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
{MAKE_ARG("xx",ARG_TYPE_PURE_TOKEN,-1,"XX",NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
{MAKE_ARG("gt",ARG_TYPE_PURE_TOKEN,-1,"GT",NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
{MAKE_ARG("lt",ARG_TYPE_PURE_TOKEN,-1,"LT",NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
};
|
||||
|
||||
/* HEXPIRE argument table */
|
||||
struct COMMAND_ARG HEXPIRE_Args[] = {
|
||||
{MAKE_ARG("key",ARG_TYPE_KEY,0,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
{MAKE_ARG("seconds",ARG_TYPE_INTEGER,-1,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
{MAKE_ARG("condition",ARG_TYPE_ONEOF,-1,NULL,NULL,NULL,CMD_ARG_OPTIONAL,4,NULL),.subargs=HEXPIRE_condition_Subargs},
|
||||
{MAKE_ARG("fields",ARG_TYPE_STRING,-1,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
{MAKE_ARG("numfields",ARG_TYPE_INTEGER,-1,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
{MAKE_ARG("field",ARG_TYPE_STRING,-1,NULL,NULL,NULL,CMD_ARG_MULTIPLE,0,NULL)},
|
||||
};
|
||||
|
||||
/********** HEXPIREAT ********************/
|
||||
|
||||
#ifndef SKIP_CMD_HISTORY_TABLE
|
||||
/* HEXPIREAT history */
|
||||
#define HEXPIREAT_History NULL
|
||||
#endif
|
||||
|
||||
#ifndef SKIP_CMD_TIPS_TABLE
|
||||
/* HEXPIREAT tips */
|
||||
#define HEXPIREAT_Tips NULL
|
||||
#endif
|
||||
|
||||
#ifndef SKIP_CMD_KEY_SPECS_TABLE
|
||||
/* HEXPIREAT key specs */
|
||||
keySpec HEXPIREAT_Keyspecs[1] = {
|
||||
{NULL,CMD_KEY_RW|CMD_KEY_UPDATE,KSPEC_BS_INDEX,.bs.index={1},KSPEC_FK_RANGE,.fk.range={0,1,0}}
|
||||
};
|
||||
#endif
|
||||
|
||||
/* HEXPIREAT condition argument table */
|
||||
struct COMMAND_ARG HEXPIREAT_condition_Subargs[] = {
|
||||
{MAKE_ARG("nx",ARG_TYPE_PURE_TOKEN,-1,"NX",NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
{MAKE_ARG("xx",ARG_TYPE_PURE_TOKEN,-1,"XX",NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
{MAKE_ARG("gt",ARG_TYPE_PURE_TOKEN,-1,"GT",NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
{MAKE_ARG("lt",ARG_TYPE_PURE_TOKEN,-1,"LT",NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
};
|
||||
|
||||
/* HEXPIREAT argument table */
|
||||
struct COMMAND_ARG HEXPIREAT_Args[] = {
|
||||
{MAKE_ARG("key",ARG_TYPE_KEY,0,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
{MAKE_ARG("unix-time-seconds",ARG_TYPE_UNIX_TIME,-1,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
{MAKE_ARG("condition",ARG_TYPE_ONEOF,-1,NULL,NULL,NULL,CMD_ARG_OPTIONAL,4,NULL),.subargs=HEXPIREAT_condition_Subargs},
|
||||
{MAKE_ARG("fields",ARG_TYPE_STRING,-1,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
{MAKE_ARG("numfields",ARG_TYPE_INTEGER,-1,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
{MAKE_ARG("field",ARG_TYPE_STRING,-1,NULL,NULL,NULL,CMD_ARG_MULTIPLE,0,NULL)},
|
||||
};
|
||||
|
||||
/********** HEXPIRETIME ********************/
|
||||
|
||||
#ifndef SKIP_CMD_HISTORY_TABLE
|
||||
/* HEXPIRETIME history */
|
||||
#define HEXPIRETIME_History NULL
|
||||
#endif
|
||||
|
||||
#ifndef SKIP_CMD_TIPS_TABLE
|
||||
/* HEXPIRETIME tips */
|
||||
#define HEXPIRETIME_Tips NULL
|
||||
#endif
|
||||
|
||||
#ifndef SKIP_CMD_KEY_SPECS_TABLE
|
||||
/* HEXPIRETIME key specs */
|
||||
keySpec HEXPIRETIME_Keyspecs[1] = {
|
||||
{NULL,CMD_KEY_RO|CMD_KEY_ACCESS,KSPEC_BS_INDEX,.bs.index={1},KSPEC_FK_RANGE,.fk.range={0,1,0}}
|
||||
};
|
||||
#endif
|
||||
|
||||
/* HEXPIRETIME argument table */
|
||||
struct COMMAND_ARG HEXPIRETIME_Args[] = {
|
||||
{MAKE_ARG("key",ARG_TYPE_KEY,0,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
{MAKE_ARG("fields",ARG_TYPE_STRING,-1,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
{MAKE_ARG("numfields",ARG_TYPE_INTEGER,-1,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
{MAKE_ARG("field",ARG_TYPE_STRING,-1,NULL,NULL,NULL,CMD_ARG_MULTIPLE,0,NULL)},
|
||||
};
|
||||
|
||||
/********** HGET ********************/
|
||||
|
||||
#ifndef SKIP_CMD_HISTORY_TABLE
|
||||
@@ -3512,6 +3613,161 @@ struct COMMAND_ARG HMSET_Args[] = {
|
||||
{MAKE_ARG("data",ARG_TYPE_BLOCK,-1,NULL,NULL,NULL,CMD_ARG_MULTIPLE,2,NULL),.subargs=HMSET_data_Subargs},
|
||||
};
|
||||
|
||||
/********** HPERSIST ********************/
|
||||
|
||||
#ifndef SKIP_CMD_HISTORY_TABLE
|
||||
/* HPERSIST history */
|
||||
#define HPERSIST_History NULL
|
||||
#endif
|
||||
|
||||
#ifndef SKIP_CMD_TIPS_TABLE
|
||||
/* HPERSIST tips */
|
||||
#define HPERSIST_Tips NULL
|
||||
#endif
|
||||
|
||||
#ifndef SKIP_CMD_KEY_SPECS_TABLE
|
||||
/* HPERSIST key specs */
|
||||
keySpec HPERSIST_Keyspecs[1] = {
|
||||
{NULL,CMD_KEY_RW|CMD_KEY_UPDATE,KSPEC_BS_INDEX,.bs.index={1},KSPEC_FK_RANGE,.fk.range={0,1,0}}
|
||||
};
|
||||
#endif
|
||||
|
||||
/* HPERSIST argument table */
|
||||
struct COMMAND_ARG HPERSIST_Args[] = {
|
||||
{MAKE_ARG("key",ARG_TYPE_KEY,0,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
{MAKE_ARG("fields",ARG_TYPE_STRING,-1,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
{MAKE_ARG("numfields",ARG_TYPE_INTEGER,-1,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
{MAKE_ARG("field",ARG_TYPE_STRING,-1,NULL,NULL,NULL,CMD_ARG_MULTIPLE,0,NULL)},
|
||||
};
|
||||
|
||||
/********** HPEXPIRE ********************/
|
||||
|
||||
#ifndef SKIP_CMD_HISTORY_TABLE
|
||||
/* HPEXPIRE history */
|
||||
#define HPEXPIRE_History NULL
|
||||
#endif
|
||||
|
||||
#ifndef SKIP_CMD_TIPS_TABLE
|
||||
/* HPEXPIRE tips */
|
||||
#define HPEXPIRE_Tips NULL
|
||||
#endif
|
||||
|
||||
#ifndef SKIP_CMD_KEY_SPECS_TABLE
|
||||
/* HPEXPIRE key specs */
|
||||
keySpec HPEXPIRE_Keyspecs[1] = {
|
||||
{NULL,CMD_KEY_RW|CMD_KEY_UPDATE,KSPEC_BS_INDEX,.bs.index={1},KSPEC_FK_RANGE,.fk.range={0,1,0}}
|
||||
};
|
||||
#endif
|
||||
|
||||
/* HPEXPIRE condition argument table */
|
||||
struct COMMAND_ARG HPEXPIRE_condition_Subargs[] = {
|
||||
{MAKE_ARG("nx",ARG_TYPE_PURE_TOKEN,-1,"NX",NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
{MAKE_ARG("xx",ARG_TYPE_PURE_TOKEN,-1,"XX",NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
{MAKE_ARG("gt",ARG_TYPE_PURE_TOKEN,-1,"GT",NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
{MAKE_ARG("lt",ARG_TYPE_PURE_TOKEN,-1,"LT",NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
};
|
||||
|
||||
/* HPEXPIRE argument table */
|
||||
struct COMMAND_ARG HPEXPIRE_Args[] = {
|
||||
{MAKE_ARG("key",ARG_TYPE_KEY,0,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
{MAKE_ARG("milliseconds",ARG_TYPE_INTEGER,-1,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
{MAKE_ARG("condition",ARG_TYPE_ONEOF,-1,NULL,NULL,NULL,CMD_ARG_OPTIONAL,4,NULL),.subargs=HPEXPIRE_condition_Subargs},
|
||||
{MAKE_ARG("fields",ARG_TYPE_STRING,-1,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
{MAKE_ARG("numfields",ARG_TYPE_INTEGER,-1,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
{MAKE_ARG("field",ARG_TYPE_STRING,-1,NULL,NULL,NULL,CMD_ARG_MULTIPLE,0,NULL)},
|
||||
};
|
||||
|
||||
/********** HPEXPIREAT ********************/
|
||||
|
||||
#ifndef SKIP_CMD_HISTORY_TABLE
|
||||
/* HPEXPIREAT history */
|
||||
#define HPEXPIREAT_History NULL
|
||||
#endif
|
||||
|
||||
#ifndef SKIP_CMD_TIPS_TABLE
|
||||
/* HPEXPIREAT tips */
|
||||
#define HPEXPIREAT_Tips NULL
|
||||
#endif
|
||||
|
||||
#ifndef SKIP_CMD_KEY_SPECS_TABLE
|
||||
/* HPEXPIREAT key specs */
|
||||
keySpec HPEXPIREAT_Keyspecs[1] = {
|
||||
{NULL,CMD_KEY_RW|CMD_KEY_UPDATE,KSPEC_BS_INDEX,.bs.index={1},KSPEC_FK_RANGE,.fk.range={0,1,0}}
|
||||
};
|
||||
#endif
|
||||
|
||||
/* HPEXPIREAT condition argument table */
|
||||
struct COMMAND_ARG HPEXPIREAT_condition_Subargs[] = {
|
||||
{MAKE_ARG("nx",ARG_TYPE_PURE_TOKEN,-1,"NX",NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
{MAKE_ARG("xx",ARG_TYPE_PURE_TOKEN,-1,"XX",NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
{MAKE_ARG("gt",ARG_TYPE_PURE_TOKEN,-1,"GT",NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
{MAKE_ARG("lt",ARG_TYPE_PURE_TOKEN,-1,"LT",NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
};
|
||||
|
||||
/* HPEXPIREAT argument table */
|
||||
struct COMMAND_ARG HPEXPIREAT_Args[] = {
|
||||
{MAKE_ARG("key",ARG_TYPE_KEY,0,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
{MAKE_ARG("unix-time-milliseconds",ARG_TYPE_UNIX_TIME,-1,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
{MAKE_ARG("condition",ARG_TYPE_ONEOF,-1,NULL,NULL,NULL,CMD_ARG_OPTIONAL,4,NULL),.subargs=HPEXPIREAT_condition_Subargs},
|
||||
{MAKE_ARG("fields",ARG_TYPE_STRING,-1,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
{MAKE_ARG("numfields",ARG_TYPE_INTEGER,-1,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
{MAKE_ARG("field",ARG_TYPE_STRING,-1,NULL,NULL,NULL,CMD_ARG_MULTIPLE,0,NULL)},
|
||||
};
|
||||
|
||||
/********** HPEXPIRETIME ********************/
|
||||
|
||||
#ifndef SKIP_CMD_HISTORY_TABLE
|
||||
/* HPEXPIRETIME history */
|
||||
#define HPEXPIRETIME_History NULL
|
||||
#endif
|
||||
|
||||
#ifndef SKIP_CMD_TIPS_TABLE
|
||||
/* HPEXPIRETIME tips */
|
||||
#define HPEXPIRETIME_Tips NULL
|
||||
#endif
|
||||
|
||||
#ifndef SKIP_CMD_KEY_SPECS_TABLE
|
||||
/* HPEXPIRETIME key specs */
|
||||
keySpec HPEXPIRETIME_Keyspecs[1] = {
|
||||
{NULL,CMD_KEY_RO|CMD_KEY_ACCESS,KSPEC_BS_INDEX,.bs.index={1},KSPEC_FK_RANGE,.fk.range={0,1,0}}
|
||||
};
|
||||
#endif
|
||||
|
||||
/* HPEXPIRETIME argument table */
|
||||
struct COMMAND_ARG HPEXPIRETIME_Args[] = {
|
||||
{MAKE_ARG("key",ARG_TYPE_KEY,0,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
{MAKE_ARG("fields",ARG_TYPE_STRING,-1,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
{MAKE_ARG("numfields",ARG_TYPE_INTEGER,-1,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
{MAKE_ARG("field",ARG_TYPE_STRING,-1,NULL,NULL,NULL,CMD_ARG_MULTIPLE,0,NULL)},
|
||||
};
|
||||
|
||||
/********** HPTTL ********************/
|
||||
|
||||
#ifndef SKIP_CMD_HISTORY_TABLE
|
||||
/* HPTTL history */
|
||||
#define HPTTL_History NULL
|
||||
#endif
|
||||
|
||||
#ifndef SKIP_CMD_TIPS_TABLE
|
||||
/* HPTTL tips */
|
||||
#define HPTTL_Tips NULL
|
||||
#endif
|
||||
|
||||
#ifndef SKIP_CMD_KEY_SPECS_TABLE
|
||||
/* HPTTL key specs */
|
||||
keySpec HPTTL_Keyspecs[1] = {
|
||||
{NULL,CMD_KEY_RO|CMD_KEY_ACCESS,KSPEC_BS_INDEX,.bs.index={1},KSPEC_FK_RANGE,.fk.range={0,1,0}}
|
||||
};
|
||||
#endif
|
||||
|
||||
/* HPTTL argument table */
|
||||
struct COMMAND_ARG HPTTL_Args[] = {
|
||||
{MAKE_ARG("key",ARG_TYPE_KEY,0,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
{MAKE_ARG("fields",ARG_TYPE_STRING,-1,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
{MAKE_ARG("numfields",ARG_TYPE_INTEGER,-1,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
{MAKE_ARG("field",ARG_TYPE_STRING,-1,NULL,NULL,NULL,CMD_ARG_MULTIPLE,0,NULL)},
|
||||
};
|
||||
|
||||
/********** HRANDFIELD ********************/
|
||||
|
||||
#ifndef SKIP_CMD_HISTORY_TABLE
|
||||
@@ -3659,6 +3915,33 @@ struct COMMAND_ARG HSTRLEN_Args[] = {
|
||||
{MAKE_ARG("field",ARG_TYPE_STRING,-1,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
};
|
||||
|
||||
/********** HTTL ********************/
|
||||
|
||||
#ifndef SKIP_CMD_HISTORY_TABLE
|
||||
/* HTTL history */
|
||||
#define HTTL_History NULL
|
||||
#endif
|
||||
|
||||
#ifndef SKIP_CMD_TIPS_TABLE
|
||||
/* HTTL tips */
|
||||
#define HTTL_Tips NULL
|
||||
#endif
|
||||
|
||||
#ifndef SKIP_CMD_KEY_SPECS_TABLE
|
||||
/* HTTL key specs */
|
||||
keySpec HTTL_Keyspecs[1] = {
|
||||
{NULL,CMD_KEY_RO|CMD_KEY_ACCESS,KSPEC_BS_INDEX,.bs.index={1},KSPEC_FK_RANGE,.fk.range={0,1,0}}
|
||||
};
|
||||
#endif
|
||||
|
||||
/* HTTL argument table */
|
||||
struct COMMAND_ARG HTTL_Args[] = {
|
||||
{MAKE_ARG("key",ARG_TYPE_KEY,0,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
{MAKE_ARG("fields",ARG_TYPE_STRING,-1,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
{MAKE_ARG("numfields",ARG_TYPE_INTEGER,-1,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
{MAKE_ARG("field",ARG_TYPE_STRING,-1,NULL,NULL,NULL,CMD_ARG_MULTIPLE,0,NULL)},
|
||||
};
|
||||
|
||||
/********** HVALS ********************/
|
||||
|
||||
#ifndef SKIP_CMD_HISTORY_TABLE
|
||||
@@ -10710,6 +10993,9 @@ struct COMMAND_STRUCT redisCommandTable[] = {
|
||||
/* hash */
|
||||
{MAKE_CMD("hdel","Deletes one or more fields and their values from a hash. Deletes the hash if no fields remain.","O(N) where N is the number of fields to be removed.","2.0.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HDEL_History,1,HDEL_Tips,0,hdelCommand,-3,CMD_WRITE|CMD_FAST,ACL_CATEGORY_HASH,HDEL_Keyspecs,1,NULL,2),.args=HDEL_Args},
|
||||
{MAKE_CMD("hexists","Determines whether a field exists in a hash.","O(1)","2.0.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HEXISTS_History,0,HEXISTS_Tips,0,hexistsCommand,3,CMD_READONLY|CMD_FAST,ACL_CATEGORY_HASH,HEXISTS_Keyspecs,1,NULL,2),.args=HEXISTS_Args},
|
||||
{MAKE_CMD("hexpire","Set expiry for hash field using relative time to expire (seconds)","O(N) where N is the number of specified fields","7.4.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HEXPIRE_History,0,HEXPIRE_Tips,0,hexpireCommand,-6,CMD_WRITE|CMD_DENYOOM|CMD_FAST,ACL_CATEGORY_HASH,HEXPIRE_Keyspecs,1,NULL,6),.args=HEXPIRE_Args},
|
||||
{MAKE_CMD("hexpireat","Set expiry for hash field using an absolute Unix timestamp (seconds)","O(N) where N is the number of specified fields","7.4.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HEXPIREAT_History,0,HEXPIREAT_Tips,0,hexpireatCommand,-6,CMD_WRITE|CMD_DENYOOM|CMD_FAST,ACL_CATEGORY_HASH,HEXPIREAT_Keyspecs,1,NULL,6),.args=HEXPIREAT_Args},
|
||||
{MAKE_CMD("hexpiretime","Returns the expiration time of a hash field as a Unix timestamp, in seconds.","O(N) where N is the number of specified fields","7.4.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HEXPIRETIME_History,0,HEXPIRETIME_Tips,0,hexpiretimeCommand,-5,CMD_READONLY|CMD_FAST,ACL_CATEGORY_HASH,HEXPIRETIME_Keyspecs,1,NULL,4),.args=HEXPIRETIME_Args},
|
||||
{MAKE_CMD("hget","Returns the value of a field in a hash.","O(1)","2.0.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HGET_History,0,HGET_Tips,0,hgetCommand,3,CMD_READONLY|CMD_FAST,ACL_CATEGORY_HASH,HGET_Keyspecs,1,NULL,2),.args=HGET_Args},
|
||||
{MAKE_CMD("hgetall","Returns all fields and values in a hash.","O(N) where N is the size of the hash.","2.0.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HGETALL_History,0,HGETALL_Tips,1,hgetallCommand,2,CMD_READONLY,ACL_CATEGORY_HASH,HGETALL_Keyspecs,1,NULL,1),.args=HGETALL_Args},
|
||||
{MAKE_CMD("hincrby","Increments the integer value of a field in a hash by a number. Uses 0 as initial value if the field doesn't exist.","O(1)","2.0.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HINCRBY_History,0,HINCRBY_Tips,0,hincrbyCommand,4,CMD_WRITE|CMD_DENYOOM|CMD_FAST,ACL_CATEGORY_HASH,HINCRBY_Keyspecs,1,NULL,3),.args=HINCRBY_Args},
|
||||
@@ -10718,11 +11004,17 @@ struct COMMAND_STRUCT redisCommandTable[] = {
|
||||
{MAKE_CMD("hlen","Returns the number of fields in a hash.","O(1)","2.0.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HLEN_History,0,HLEN_Tips,0,hlenCommand,2,CMD_READONLY|CMD_FAST,ACL_CATEGORY_HASH,HLEN_Keyspecs,1,NULL,1),.args=HLEN_Args},
|
||||
{MAKE_CMD("hmget","Returns the values of all fields in a hash.","O(N) where N is the number of fields being requested.","2.0.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HMGET_History,0,HMGET_Tips,0,hmgetCommand,-3,CMD_READONLY|CMD_FAST,ACL_CATEGORY_HASH,HMGET_Keyspecs,1,NULL,2),.args=HMGET_Args},
|
||||
{MAKE_CMD("hmset","Sets the values of multiple fields.","O(N) where N is the number of fields being set.","2.0.0",CMD_DOC_DEPRECATED,"`HSET` with multiple field-value pairs","4.0.0","hash",COMMAND_GROUP_HASH,HMSET_History,0,HMSET_Tips,0,hsetCommand,-4,CMD_WRITE|CMD_DENYOOM|CMD_FAST,ACL_CATEGORY_HASH,HMSET_Keyspecs,1,NULL,2),.args=HMSET_Args},
|
||||
{MAKE_CMD("hpersist","Removes the expiration time for each specified field","O(N) where N is the number of specified fields","7.4.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HPERSIST_History,0,HPERSIST_Tips,0,hpersistCommand,-5,CMD_WRITE|CMD_FAST,ACL_CATEGORY_HASH,HPERSIST_Keyspecs,1,NULL,4),.args=HPERSIST_Args},
|
||||
{MAKE_CMD("hpexpire","Set expiry for hash field using relative time to expire (milliseconds)","O(N) where N is the number of specified fields","7.4.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HPEXPIRE_History,0,HPEXPIRE_Tips,0,hpexpireCommand,-6,CMD_WRITE|CMD_DENYOOM|CMD_FAST,ACL_CATEGORY_HASH,HPEXPIRE_Keyspecs,1,NULL,6),.args=HPEXPIRE_Args},
|
||||
{MAKE_CMD("hpexpireat","Set expiry for hash field using an absolute Unix timestamp (milliseconds)","O(N) where N is the number of specified fields","7.4.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HPEXPIREAT_History,0,HPEXPIREAT_Tips,0,hpexpireatCommand,-6,CMD_WRITE|CMD_DENYOOM|CMD_FAST,ACL_CATEGORY_HASH,HPEXPIREAT_Keyspecs,1,NULL,6),.args=HPEXPIREAT_Args},
|
||||
{MAKE_CMD("hpexpiretime","Returns the expiration time of a hash field as a Unix timestamp, in msec.","O(N) where N is the number of specified fields","7.4.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HPEXPIRETIME_History,0,HPEXPIRETIME_Tips,0,hpexpiretimeCommand,-5,CMD_READONLY|CMD_FAST,ACL_CATEGORY_HASH,HPEXPIRETIME_Keyspecs,1,NULL,4),.args=HPEXPIRETIME_Args},
|
||||
{MAKE_CMD("hpttl","Returns the TTL in milliseconds of a hash field.","O(N) where N is the number of specified fields","7.4.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HPTTL_History,0,HPTTL_Tips,0,hpttlCommand,-5,CMD_READONLY|CMD_FAST,ACL_CATEGORY_HASH,HPTTL_Keyspecs,1,NULL,4),.args=HPTTL_Args},
|
||||
{MAKE_CMD("hrandfield","Returns one or more random fields from a hash.","O(N) where N is the number of fields returned","6.2.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HRANDFIELD_History,0,HRANDFIELD_Tips,1,hrandfieldCommand,-2,CMD_READONLY,ACL_CATEGORY_HASH,HRANDFIELD_Keyspecs,1,NULL,2),.args=HRANDFIELD_Args},
|
||||
{MAKE_CMD("hscan","Iterates over fields and values of a hash.","O(1) for every call. O(N) for a complete iteration, including enough command calls for the cursor to return back to 0. N is the number of elements inside the collection.","2.8.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HSCAN_History,0,HSCAN_Tips,1,hscanCommand,-3,CMD_READONLY,ACL_CATEGORY_HASH,HSCAN_Keyspecs,1,NULL,5),.args=HSCAN_Args},
|
||||
{MAKE_CMD("hset","Creates or modifies the value of a field in a hash.","O(1) for each field/value pair added, so O(N) to add N field/value pairs when the command is called with multiple field/value pairs.","2.0.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HSET_History,1,HSET_Tips,0,hsetCommand,-4,CMD_WRITE|CMD_DENYOOM|CMD_FAST,ACL_CATEGORY_HASH,HSET_Keyspecs,1,NULL,2),.args=HSET_Args},
|
||||
{MAKE_CMD("hsetnx","Sets the value of a field in a hash only when the field doesn't exist.","O(1)","2.0.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HSETNX_History,0,HSETNX_Tips,0,hsetnxCommand,4,CMD_WRITE|CMD_DENYOOM|CMD_FAST,ACL_CATEGORY_HASH,HSETNX_Keyspecs,1,NULL,3),.args=HSETNX_Args},
|
||||
{MAKE_CMD("hstrlen","Returns the length of the value of a field.","O(1)","3.2.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HSTRLEN_History,0,HSTRLEN_Tips,0,hstrlenCommand,3,CMD_READONLY|CMD_FAST,ACL_CATEGORY_HASH,HSTRLEN_Keyspecs,1,NULL,2),.args=HSTRLEN_Args},
|
||||
{MAKE_CMD("httl","Returns the TTL in seconds of a hash field.","O(N) where N is the number of specified fields","7.4.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HTTL_History,0,HTTL_Tips,0,httlCommand,-5,CMD_READONLY|CMD_FAST,ACL_CATEGORY_HASH,HTTL_Keyspecs,1,NULL,4),.args=HTTL_Args},
|
||||
{MAKE_CMD("hvals","Returns all values in a hash.","O(N) where N is the size of the hash.","2.0.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HVALS_History,0,HVALS_Tips,1,hvalsCommand,2,CMD_READONLY,ACL_CATEGORY_HASH,HVALS_Keyspecs,1,NULL,1),.args=HVALS_Args},
|
||||
/* hyperloglog */
|
||||
{MAKE_CMD("pfadd","Adds elements to a HyperLogLog key. Creates the key if it doesn't exist.","O(1) to add every element.","2.8.9",CMD_DOC_NONE,NULL,NULL,"hyperloglog",COMMAND_GROUP_HYPERLOGLOG,PFADD_History,0,PFADD_Tips,0,pfaddCommand,-2,CMD_WRITE|CMD_DENYOOM|CMD_FAST,ACL_CATEGORY_HYPERLOGLOG,PFADD_Keyspecs,1,NULL,2),.args=PFADD_Args},
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
{
|
||||
"HEXPIRE": {
|
||||
"summary": "Set expiry for hash field using relative time to expire (seconds)",
|
||||
"complexity": "O(N) where N is the number of specified fields",
|
||||
"group": "hash",
|
||||
"since": "7.4.0",
|
||||
"arity": -6,
|
||||
"function": "hexpireCommand",
|
||||
"history": [],
|
||||
"command_flags": [
|
||||
"WRITE",
|
||||
"DENYOOM",
|
||||
"FAST"
|
||||
],
|
||||
"acl_categories": [
|
||||
"HASH"
|
||||
],
|
||||
"key_specs": [
|
||||
{
|
||||
"flags": [
|
||||
"RW",
|
||||
"UPDATE"
|
||||
],
|
||||
"begin_search": {
|
||||
"index": {
|
||||
"pos": 1
|
||||
}
|
||||
},
|
||||
"find_keys": {
|
||||
"range": {
|
||||
"lastkey": 0,
|
||||
"step": 1,
|
||||
"limit": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"reply_schema": {
|
||||
"description": "Array of results. Returns empty array if the key does not exist.",
|
||||
"type": "array",
|
||||
"minItems": 0,
|
||||
"maxItems": 4294967295,
|
||||
"items": {
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "The field does not exist.",
|
||||
"const": -2
|
||||
},
|
||||
{
|
||||
"description": "Specified NX | XX | GT | LT condition not met",
|
||||
"const": 0
|
||||
},
|
||||
{
|
||||
"description": "Expiration time was set or updated.",
|
||||
"const": 1
|
||||
},
|
||||
{
|
||||
"description": "Field deleted because the specified expiration time is in the past.",
|
||||
"const": 2
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"arguments": [
|
||||
{
|
||||
"name": "key",
|
||||
"type": "key",
|
||||
"key_spec_index": 0
|
||||
},
|
||||
{
|
||||
"name": "seconds",
|
||||
"type": "integer"
|
||||
},
|
||||
{
|
||||
"name": "condition",
|
||||
"type": "oneof",
|
||||
"optional": true,
|
||||
"arguments": [
|
||||
{
|
||||
"name": "nx",
|
||||
"type": "pure-token",
|
||||
"token": "NX"
|
||||
},
|
||||
{
|
||||
"name": "xx",
|
||||
"type": "pure-token",
|
||||
"token": "XX"
|
||||
},
|
||||
{
|
||||
"name": "gt",
|
||||
"type": "pure-token",
|
||||
"token": "GT"
|
||||
},
|
||||
{
|
||||
"name": "lt",
|
||||
"type": "pure-token",
|
||||
"token": "LT"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "FIELDS",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "numfields",
|
||||
"type": "integer"
|
||||
},
|
||||
{
|
||||
"name": "field",
|
||||
"type": "string",
|
||||
"multiple": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
{
|
||||
"HEXPIREAT": {
|
||||
"summary": "Set expiry for hash field using an absolute Unix timestamp (seconds)",
|
||||
"complexity": "O(N) where N is the number of specified fields",
|
||||
"group": "hash",
|
||||
"since": "7.4.0",
|
||||
"arity": -6,
|
||||
"function": "hexpireatCommand",
|
||||
"history": [],
|
||||
"command_flags": [
|
||||
"WRITE",
|
||||
"DENYOOM",
|
||||
"FAST"
|
||||
],
|
||||
"acl_categories": [
|
||||
"HASH"
|
||||
],
|
||||
"key_specs": [
|
||||
{
|
||||
"flags": [
|
||||
"RW",
|
||||
"UPDATE"
|
||||
],
|
||||
"begin_search": {
|
||||
"index": {
|
||||
"pos": 1
|
||||
}
|
||||
},
|
||||
"find_keys": {
|
||||
"range": {
|
||||
"lastkey": 0,
|
||||
"step": 1,
|
||||
"limit": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"reply_schema": {
|
||||
"description": "Array of results. Returns empty array if the key does not exist.",
|
||||
"type": "array",
|
||||
"minItems": 0,
|
||||
"maxItems": 4294967295,
|
||||
"items": {
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "The field does not exist.",
|
||||
"const": -2
|
||||
},
|
||||
{
|
||||
"description": "Specified NX | XX | GT | LT condition not met",
|
||||
"const": 0
|
||||
},
|
||||
{
|
||||
"description": "Expiration time was set or updated.",
|
||||
"const": 1
|
||||
},
|
||||
{
|
||||
"description": "Field deleted because the specified expiration time is in the past.",
|
||||
"const": 2
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"arguments": [
|
||||
{
|
||||
"name": "key",
|
||||
"type": "key",
|
||||
"key_spec_index": 0
|
||||
},
|
||||
{
|
||||
"name": "unix-time-seconds",
|
||||
"type": "unix-time"
|
||||
},
|
||||
{
|
||||
"name": "condition",
|
||||
"type": "oneof",
|
||||
"optional": true,
|
||||
"arguments": [
|
||||
{
|
||||
"name": "nx",
|
||||
"type": "pure-token",
|
||||
"token": "NX"
|
||||
},
|
||||
{
|
||||
"name": "xx",
|
||||
"type": "pure-token",
|
||||
"token": "XX"
|
||||
},
|
||||
{
|
||||
"name": "gt",
|
||||
"type": "pure-token",
|
||||
"token": "GT"
|
||||
},
|
||||
{
|
||||
"name": "lt",
|
||||
"type": "pure-token",
|
||||
"token": "LT"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "FIELDS",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "numfields",
|
||||
"type": "integer"
|
||||
},
|
||||
{
|
||||
"name": "field",
|
||||
"type": "string",
|
||||
"multiple": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
{
|
||||
"HEXPIRETIME": {
|
||||
"summary": "Returns the expiration time of a hash field as a Unix timestamp, in seconds.",
|
||||
"complexity": "O(N) where N is the number of specified fields",
|
||||
"group": "hash",
|
||||
"since": "7.4.0",
|
||||
"arity": -5,
|
||||
"function": "hexpiretimeCommand",
|
||||
"history": [],
|
||||
"command_flags": [
|
||||
"READONLY",
|
||||
"FAST"
|
||||
],
|
||||
"acl_categories": [
|
||||
"HASH"
|
||||
],
|
||||
"key_specs": [
|
||||
{
|
||||
"flags": [
|
||||
"RO",
|
||||
"ACCESS"
|
||||
],
|
||||
"begin_search": {
|
||||
"index": {
|
||||
"pos": 1
|
||||
}
|
||||
},
|
||||
"find_keys": {
|
||||
"range": {
|
||||
"lastkey": 0,
|
||||
"step": 1,
|
||||
"limit": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"reply_schema": {
|
||||
"description": "Array of results. Returns empty array if the key does not exist.",
|
||||
"type": "array",
|
||||
"minItems": 0,
|
||||
"maxItems": 4294967295,
|
||||
"items": {
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "The field does not exist.",
|
||||
"const": -2
|
||||
},
|
||||
{
|
||||
"description": "The field exists but has no associated expire.",
|
||||
"const": -1
|
||||
},
|
||||
{
|
||||
"description": "Expiration Unix timestamp in seconds.",
|
||||
"type": "integer",
|
||||
"minimum": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"arguments": [
|
||||
{
|
||||
"name": "key",
|
||||
"type": "key",
|
||||
"key_spec_index": 0
|
||||
},
|
||||
{
|
||||
"name": "FIELDS",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "numfields",
|
||||
"type": "integer"
|
||||
},
|
||||
{
|
||||
"name": "field",
|
||||
"type": "string",
|
||||
"multiple": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
{
|
||||
"HPERSIST": {
|
||||
"summary": "Removes the expiration time for each specified field",
|
||||
"complexity": "O(N) where N is the number of specified fields",
|
||||
"group": "hash",
|
||||
"since": "7.4.0",
|
||||
"arity": -5,
|
||||
"function": "hpersistCommand",
|
||||
"history": [],
|
||||
"command_flags": [
|
||||
"WRITE",
|
||||
"FAST"
|
||||
],
|
||||
"acl_categories": [
|
||||
"HASH"
|
||||
],
|
||||
"key_specs": [
|
||||
{
|
||||
"flags": [
|
||||
"RW",
|
||||
"UPDATE"
|
||||
],
|
||||
"begin_search": {
|
||||
"index": {
|
||||
"pos": 1
|
||||
}
|
||||
},
|
||||
"find_keys": {
|
||||
"range": {
|
||||
"lastkey": 0,
|
||||
"step": 1,
|
||||
"limit": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"reply_schema": {
|
||||
"description": "Array of results. Returns empty array if the key does not exist.",
|
||||
"type": "array",
|
||||
"minItems": 0,
|
||||
"maxItems": 4294967295,
|
||||
"items": {
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "The field does not exist.",
|
||||
"const": -2
|
||||
},
|
||||
{
|
||||
"description": "The field exists but has no associated expire.",
|
||||
"const": -1
|
||||
},
|
||||
{
|
||||
"description": "Expiration time was removed",
|
||||
"const": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"arguments": [
|
||||
{
|
||||
"name": "key",
|
||||
"type": "key",
|
||||
"key_spec_index": 0
|
||||
},
|
||||
{
|
||||
"name": "FIELDS",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "numfields",
|
||||
"type": "integer"
|
||||
},
|
||||
{
|
||||
"name": "field",
|
||||
"type": "string",
|
||||
"multiple": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
{
|
||||
"HPEXPIRE": {
|
||||
"summary": "Set expiry for hash field using relative time to expire (milliseconds)",
|
||||
"complexity": "O(N) where N is the number of specified fields",
|
||||
"group": "hash",
|
||||
"since": "7.4.0",
|
||||
"arity": -6,
|
||||
"function": "hpexpireCommand",
|
||||
"history": [],
|
||||
"command_flags": [
|
||||
"WRITE",
|
||||
"DENYOOM",
|
||||
"FAST"
|
||||
],
|
||||
"acl_categories": [
|
||||
"HASH"
|
||||
],
|
||||
"key_specs": [
|
||||
{
|
||||
"flags": [
|
||||
"RW",
|
||||
"UPDATE"
|
||||
],
|
||||
"begin_search": {
|
||||
"index": {
|
||||
"pos": 1
|
||||
}
|
||||
},
|
||||
"find_keys": {
|
||||
"range": {
|
||||
"lastkey": 0,
|
||||
"step": 1,
|
||||
"limit": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"reply_schema": {
|
||||
"description": "Array of results. Returns empty array if the key does not exist.",
|
||||
"type": "array",
|
||||
"minItems": 0,
|
||||
"maxItems": 4294967295,
|
||||
"items": {
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "The field does not exist.",
|
||||
"const": -2
|
||||
},
|
||||
{
|
||||
"description": "Specified NX | XX | GT | LT condition not met",
|
||||
"const": 0
|
||||
},
|
||||
{
|
||||
"description": "Expiration time was set or updated.",
|
||||
"const": 1
|
||||
},
|
||||
{
|
||||
"description": "Field deleted because the specified expiration time is in the past.",
|
||||
"const": 2
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"arguments": [
|
||||
{
|
||||
"name": "key",
|
||||
"type": "key",
|
||||
"key_spec_index": 0
|
||||
},
|
||||
{
|
||||
"name": "milliseconds",
|
||||
"type": "integer"
|
||||
},
|
||||
{
|
||||
"name": "condition",
|
||||
"type": "oneof",
|
||||
"optional": true,
|
||||
"arguments": [
|
||||
{
|
||||
"name": "nx",
|
||||
"type": "pure-token",
|
||||
"token": "NX"
|
||||
},
|
||||
{
|
||||
"name": "xx",
|
||||
"type": "pure-token",
|
||||
"token": "XX"
|
||||
},
|
||||
{
|
||||
"name": "gt",
|
||||
"type": "pure-token",
|
||||
"token": "GT"
|
||||
},
|
||||
{
|
||||
"name": "lt",
|
||||
"type": "pure-token",
|
||||
"token": "LT"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "FIELDS",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "numfields",
|
||||
"type": "integer"
|
||||
},
|
||||
{
|
||||
"name": "field",
|
||||
"type": "string",
|
||||
"multiple": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
{
|
||||
"HPEXPIREAT": {
|
||||
"summary": "Set expiry for hash field using an absolute Unix timestamp (milliseconds)",
|
||||
"complexity": "O(N) where N is the number of specified fields",
|
||||
"group": "hash",
|
||||
"since": "7.4.0",
|
||||
"arity": -6,
|
||||
"function": "hpexpireatCommand",
|
||||
"history": [],
|
||||
"command_flags": [
|
||||
"WRITE",
|
||||
"DENYOOM",
|
||||
"FAST"
|
||||
],
|
||||
"acl_categories": [
|
||||
"HASH"
|
||||
],
|
||||
"key_specs": [
|
||||
{
|
||||
"flags": [
|
||||
"RW",
|
||||
"UPDATE"
|
||||
],
|
||||
"begin_search": {
|
||||
"index": {
|
||||
"pos": 1
|
||||
}
|
||||
},
|
||||
"find_keys": {
|
||||
"range": {
|
||||
"lastkey": 0,
|
||||
"step": 1,
|
||||
"limit": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"reply_schema": {
|
||||
"description": "Array of results. Returns empty array if the key does not exist.",
|
||||
"type": "array",
|
||||
"minItems": 0,
|
||||
"maxItems": 4294967295,
|
||||
"items": {
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "The field does not exist.",
|
||||
"const": -2
|
||||
},
|
||||
{
|
||||
"description": "Specified NX | XX | GT | LT condition not met",
|
||||
"const": 0
|
||||
},
|
||||
{
|
||||
"description": "Expiration time was set or updated.",
|
||||
"const": 1
|
||||
},
|
||||
{
|
||||
"description": "Field deleted because the specified expiration time is in the past.",
|
||||
"const": 2
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"arguments": [
|
||||
{
|
||||
"name": "key",
|
||||
"type": "key",
|
||||
"key_spec_index": 0
|
||||
},
|
||||
{
|
||||
"name": "unix-time-milliseconds",
|
||||
"type": "unix-time"
|
||||
},
|
||||
{
|
||||
"name": "condition",
|
||||
"type": "oneof",
|
||||
"optional": true,
|
||||
"arguments": [
|
||||
{
|
||||
"name": "nx",
|
||||
"type": "pure-token",
|
||||
"token": "NX"
|
||||
},
|
||||
{
|
||||
"name": "xx",
|
||||
"type": "pure-token",
|
||||
"token": "XX"
|
||||
},
|
||||
{
|
||||
"name": "gt",
|
||||
"type": "pure-token",
|
||||
"token": "GT"
|
||||
},
|
||||
{
|
||||
"name": "lt",
|
||||
"type": "pure-token",
|
||||
"token": "LT"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "FIELDS",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "numfields",
|
||||
"type": "integer"
|
||||
},
|
||||
{
|
||||
"name": "field",
|
||||
"type": "string",
|
||||
"multiple": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
{
|
||||
"HPEXPIRETIME": {
|
||||
"summary": "Returns the expiration time of a hash field as a Unix timestamp, in msec.",
|
||||
"complexity": "O(N) where N is the number of specified fields",
|
||||
"group": "hash",
|
||||
"since": "7.4.0",
|
||||
"arity": -5,
|
||||
"function": "hpexpiretimeCommand",
|
||||
"history": [],
|
||||
"command_flags": [
|
||||
"READONLY",
|
||||
"FAST"
|
||||
],
|
||||
"acl_categories": [
|
||||
"HASH"
|
||||
],
|
||||
"key_specs": [
|
||||
{
|
||||
"flags": [
|
||||
"RO",
|
||||
"ACCESS"
|
||||
],
|
||||
"begin_search": {
|
||||
"index": {
|
||||
"pos": 1
|
||||
}
|
||||
},
|
||||
"find_keys": {
|
||||
"range": {
|
||||
"lastkey": 0,
|
||||
"step": 1,
|
||||
"limit": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"reply_schema": {
|
||||
"description": "Array of results. Returns empty array if the key does not exist.",
|
||||
"type": "array",
|
||||
"minItems": 0,
|
||||
"maxItems": 4294967295,
|
||||
"items": {
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "The field does not exist.",
|
||||
"const": -2
|
||||
},
|
||||
{
|
||||
"description": "The field exists but has no associated expire.",
|
||||
"const": -1
|
||||
},
|
||||
{
|
||||
"description": "Expiration Unix timestamp in milliseconds.",
|
||||
"type": "integer",
|
||||
"minimum": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"arguments": [
|
||||
{
|
||||
"name": "key",
|
||||
"type": "key",
|
||||
"key_spec_index": 0
|
||||
},
|
||||
{
|
||||
"name": "FIELDS",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "numfields",
|
||||
"type": "integer"
|
||||
},
|
||||
{
|
||||
"name": "field",
|
||||
"type": "string",
|
||||
"multiple": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
{
|
||||
"HPTTL": {
|
||||
"summary": "Returns the TTL in milliseconds of a hash field.",
|
||||
"complexity": "O(N) where N is the number of specified fields",
|
||||
"group": "hash",
|
||||
"since": "7.4.0",
|
||||
"arity": -5,
|
||||
"function": "hpttlCommand",
|
||||
"history": [],
|
||||
"command_flags": [
|
||||
"READONLY",
|
||||
"FAST"
|
||||
],
|
||||
"acl_categories": [
|
||||
"HASH"
|
||||
],
|
||||
"key_specs": [
|
||||
{
|
||||
"flags": [
|
||||
"RO",
|
||||
"ACCESS"
|
||||
],
|
||||
"begin_search": {
|
||||
"index": {
|
||||
"pos": 1
|
||||
}
|
||||
},
|
||||
"find_keys": {
|
||||
"range": {
|
||||
"lastkey": 0,
|
||||
"step": 1,
|
||||
"limit": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"reply_schema": {
|
||||
"description": "Array of results. Returns empty array if the key does not exist.",
|
||||
"type": "array",
|
||||
"minItems": 0,
|
||||
"maxItems": 4294967295,
|
||||
"items": {
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "The field does not exist.",
|
||||
"const": -2
|
||||
},
|
||||
{
|
||||
"description": "The field exists but has no associated expire.",
|
||||
"const": -1
|
||||
},
|
||||
{
|
||||
"description": "TTL in milliseconds.",
|
||||
"type": "integer",
|
||||
"minimum": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"arguments": [
|
||||
{
|
||||
"name": "key",
|
||||
"type": "key",
|
||||
"key_spec_index": 0
|
||||
},
|
||||
{
|
||||
"name": "FIELDS",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "numfields",
|
||||
"type": "integer"
|
||||
},
|
||||
{
|
||||
"name": "field",
|
||||
"type": "string",
|
||||
"multiple": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
{
|
||||
"HTTL": {
|
||||
"summary": "Returns the TTL in seconds of a hash field.",
|
||||
"complexity": "O(N) where N is the number of specified fields",
|
||||
"group": "hash",
|
||||
"since": "7.4.0",
|
||||
"arity": -5,
|
||||
"function": "httlCommand",
|
||||
"history": [],
|
||||
"command_flags": [
|
||||
"READONLY",
|
||||
"FAST"
|
||||
],
|
||||
"acl_categories": [
|
||||
"HASH"
|
||||
],
|
||||
"key_specs": [
|
||||
{
|
||||
"flags": [
|
||||
"RO",
|
||||
"ACCESS"
|
||||
],
|
||||
"begin_search": {
|
||||
"index": {
|
||||
"pos": 1
|
||||
}
|
||||
},
|
||||
"find_keys": {
|
||||
"range": {
|
||||
"lastkey": 0,
|
||||
"step": 1,
|
||||
"limit": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"reply_schema": {
|
||||
"description": "Array of results. Returns empty array if the key does not exist.",
|
||||
"type": "array",
|
||||
"minItems": 0,
|
||||
"maxItems": 4294967295,
|
||||
"items": {
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "The field does not exist.",
|
||||
"const": -2
|
||||
},
|
||||
{
|
||||
"description": "The field exists but has no associated expire.",
|
||||
"const": -1
|
||||
},
|
||||
{
|
||||
"description": "TTL in seconds.",
|
||||
"type": "integer",
|
||||
"minimum": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"arguments": [
|
||||
{
|
||||
"name": "key",
|
||||
"type": "key",
|
||||
"key_spec_index": 0
|
||||
},
|
||||
{
|
||||
"name": "FIELDS",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "numfields",
|
||||
"type": "integer"
|
||||
},
|
||||
{
|
||||
"name": "field",
|
||||
"type": "string",
|
||||
"multiple": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -177,13 +177,13 @@ robj *lookupKeyWriteOrReply(client *c, robj *key, robj *reply) {
|
||||
*
|
||||
* If the update_if_existing argument is false, the program is aborted
|
||||
* if the key already exists, otherwise, it can fall back to dbOverwrite. */
|
||||
static void dbAddInternal(redisDb *db, robj *key, robj *val, int update_if_existing) {
|
||||
static dictEntry *dbAddInternal(redisDb *db, robj *key, robj *val, int update_if_existing) {
|
||||
dictEntry *existing;
|
||||
int slot = getKeySlot(key->ptr);
|
||||
dictEntry *de = kvstoreDictAddRaw(db->keys, slot, key->ptr, &existing);
|
||||
if (update_if_existing && existing) {
|
||||
dbSetValue(db, key, val, 1, existing);
|
||||
return;
|
||||
return existing;
|
||||
}
|
||||
serverAssertWithInfo(NULL, key, de != NULL);
|
||||
kvstoreDictSetKey(db->keys, slot, de, sdsdup(key->ptr));
|
||||
@@ -191,10 +191,11 @@ static void dbAddInternal(redisDb *db, robj *key, robj *val, int update_if_exist
|
||||
kvstoreDictSetVal(db->keys, slot, de, val);
|
||||
signalKeyAsReady(db, key, val->type);
|
||||
notifyKeyspaceEvent(NOTIFY_NEW,"new",key,db->id);
|
||||
return de;
|
||||
}
|
||||
|
||||
void dbAdd(redisDb *db, robj *key, robj *val) {
|
||||
dbAddInternal(db, key, val, 0);
|
||||
dictEntry *dbAdd(redisDb *db, robj *key, robj *val) {
|
||||
return dbAddInternal(db, key, val, 0);
|
||||
}
|
||||
|
||||
/* Returns key's hash slot when cluster mode is enabled, or 0 when disabled.
|
||||
@@ -275,6 +276,11 @@ static void dbSetValue(redisDb *db, robj *key, robj *val, int overwrite, dictEnt
|
||||
old = dictGetVal(de);
|
||||
}
|
||||
kvstoreDictSetVal(db->keys, slot, de, val);
|
||||
|
||||
/* if hash with HFEs, take care to remove from global HFE DS */
|
||||
if (old->type == OBJ_HASH)
|
||||
hashTypeRemoveFromExpires(&db->hexpires, old);
|
||||
|
||||
if (server.lazyfree_lazy_server_del) {
|
||||
freeObjAsync(key,old,db->id);
|
||||
} else {
|
||||
@@ -370,6 +376,11 @@ int dbGenericDelete(redisDb *db, robj *key, int async, int flags) {
|
||||
dictEntry *de = kvstoreDictTwoPhaseUnlinkFind(db->keys, slot, key->ptr, &plink, &table);
|
||||
if (de) {
|
||||
robj *val = dictGetVal(de);
|
||||
|
||||
/* If hash object with expiry on fields, remove it from HFE DS of DB */
|
||||
if (val->type == OBJ_HASH)
|
||||
hashTypeRemoveFromExpires(&db->hexpires, val);
|
||||
|
||||
/* RM_StringDMA may call dbUnshareStringValue which may free val, so we
|
||||
* need to incr to retain val */
|
||||
incrRefCount(val);
|
||||
@@ -475,6 +486,9 @@ long long emptyDbStructure(redisDb *dbarray, int dbnum, int async,
|
||||
if (async) {
|
||||
emptyDbAsync(&dbarray[j]);
|
||||
} else {
|
||||
/* Destroy global HFE DS before deleting the hashes since ebuckets
|
||||
* DS is embedded in the stored objects. */
|
||||
ebDestroy(&dbarray[j].hexpires, &hashExpireBucketsType, NULL);
|
||||
kvstoreEmpty(dbarray[j].keys, callback);
|
||||
kvstoreEmpty(dbarray[j].expires, callback);
|
||||
}
|
||||
@@ -554,6 +568,7 @@ redisDb *initTempDb(void) {
|
||||
tempDb[i].id = i;
|
||||
tempDb[i].keys = kvstoreCreate(&dbDictType, slot_count_bits, flags);
|
||||
tempDb[i].expires = kvstoreCreate(&dbExpiresDictType, slot_count_bits, flags);
|
||||
tempDb[i].hexpires = ebCreate();
|
||||
}
|
||||
|
||||
return tempDb;
|
||||
@@ -566,6 +581,9 @@ void discardTempDb(redisDb *tempDb, void(callback)(dict*)) {
|
||||
/* Release temp DBs. */
|
||||
emptyDbStructure(tempDb, -1, async, callback);
|
||||
for (int i=0; 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].expires);
|
||||
}
|
||||
@@ -894,6 +912,7 @@ typedef struct {
|
||||
sds pattern; /* pattern string, NULL means no pattern */
|
||||
long sampled; /* cumulative number of keys sampled */
|
||||
int no_values; /* set to 1 means to return keys only */
|
||||
size_t (*strlen)(char *s); /* (o->type == OBJ_HASH) ? hfieldlen : sdslen */
|
||||
} scanData;
|
||||
|
||||
/* Helper function to compare key type in scan commands */
|
||||
@@ -918,7 +937,7 @@ void scanCallback(void *privdata, const dictEntry *de) {
|
||||
list *keys = data->keys;
|
||||
robj *o = data->o;
|
||||
sds val = NULL;
|
||||
sds key = NULL;
|
||||
void *key = NULL; /* if OBJ_HASH then key is of type `hfield`. Otherwise, `sds` */
|
||||
data->sampled++;
|
||||
|
||||
/* o and typename can not have values at the same time. */
|
||||
@@ -932,24 +951,29 @@ void scanCallback(void *privdata, const dictEntry *de) {
|
||||
}*/
|
||||
|
||||
/* Filter element if it does not match the pattern. */
|
||||
sds keysds = dictGetKey(de);
|
||||
void *keyStr = dictGetKey(de);
|
||||
if (data->pattern) {
|
||||
if (!stringmatchlen(data->pattern, sdslen(data->pattern), keysds, sdslen(keysds), 0)) {
|
||||
if (!stringmatchlen(data->pattern, sdslen(data->pattern), keyStr, data->strlen(keyStr), 0)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (o == NULL) {
|
||||
key = keysds;
|
||||
key = keyStr;
|
||||
} else if (o->type == OBJ_SET) {
|
||||
key = keysds;
|
||||
key = keyStr;
|
||||
} else if (o->type == OBJ_HASH) {
|
||||
key = keysds;
|
||||
key = keyStr;
|
||||
val = dictGetVal(de);
|
||||
|
||||
/* If field is expired, then ignore */
|
||||
if (hfieldIsExpired(key))
|
||||
return;
|
||||
|
||||
} else if (o->type == OBJ_ZSET) {
|
||||
char buf[MAX_LONG_DOUBLE_CHARS];
|
||||
int len = ld2string(buf, sizeof(buf), *(double *)dictGetVal(de), LD_STR_AUTO);
|
||||
key = sdsdup(keysds);
|
||||
key = sdsdup(keyStr);
|
||||
val = sdsnewlen(buf, len);
|
||||
} else {
|
||||
serverPanic("Type not handled in SCAN callback.");
|
||||
@@ -1023,6 +1047,7 @@ char *getObjectTypeName(robj *o) {
|
||||
* In the case of a Hash object the function returns both the field and value
|
||||
* of every element on the Hash. */
|
||||
void scanGenericCommand(client *c, robj *o, unsigned long long cursor) {
|
||||
int isKeysHfield = 0;
|
||||
int i, j;
|
||||
listNode *node;
|
||||
long count = 10;
|
||||
@@ -1103,6 +1128,7 @@ void scanGenericCommand(client *c, robj *o, unsigned long long cursor) {
|
||||
} else if (o->type == OBJ_SET && o->encoding == OBJ_ENCODING_HT) {
|
||||
ht = o->ptr;
|
||||
} else if (o->type == OBJ_HASH && o->encoding == OBJ_ENCODING_HT) {
|
||||
isKeysHfield = 1;
|
||||
ht = o->ptr;
|
||||
} else if (o->type == OBJ_ZSET && o->encoding == OBJ_ENCODING_SKIPLIST) {
|
||||
zset *zs = o->ptr;
|
||||
@@ -1141,7 +1167,7 @@ void scanGenericCommand(client *c, robj *o, unsigned long long cursor) {
|
||||
* working on an empty dict, one with a lot of empty buckets, and
|
||||
* for the buckets are not empty, we need to limit the spampled number
|
||||
* to prevent a long hang time caused by filtering too many keys;
|
||||
* 6. data.no_values: to control whether values will be returned or
|
||||
* 6. data.no_values: to control whether values will be returned or
|
||||
* only keys are returned. */
|
||||
scanData data = {
|
||||
.keys = keys,
|
||||
@@ -1150,6 +1176,7 @@ void scanGenericCommand(client *c, robj *o, unsigned long long cursor) {
|
||||
.pattern = use_pattern ? pat : NULL,
|
||||
.sampled = 0,
|
||||
.no_values = no_values,
|
||||
.strlen = (isKeysHfield) ? hfieldlen : sdslen,
|
||||
};
|
||||
|
||||
/* A pattern may restrict all matching keys to one cluster slot. */
|
||||
@@ -1211,6 +1238,40 @@ void scanGenericCommand(client *c, robj *o, unsigned long long cursor) {
|
||||
p = lpNext(o->ptr, p);
|
||||
}
|
||||
cursor = 0;
|
||||
} else if (o->type == OBJ_HASH && o->encoding == OBJ_ENCODING_LISTPACK_EX) {
|
||||
int64_t len;
|
||||
long long expire_at;
|
||||
unsigned char *lp = hashTypeListpackGetLp(o);
|
||||
unsigned char *p = lpFirst(lp);
|
||||
unsigned char *str, *val;
|
||||
unsigned char intbuf[LP_INTBUF_SIZE];
|
||||
|
||||
while (p) {
|
||||
str = lpGet(p, &len, intbuf);
|
||||
p = lpNext(lp, p);
|
||||
val = p; /* Keep pointer to value */
|
||||
|
||||
p = lpNext(lp, p);
|
||||
serverAssert(p && lpGetIntegerValue(p, &expire_at));
|
||||
|
||||
if (hashTypeIsExpired(o, expire_at) ||
|
||||
(use_pattern && !stringmatchlen(pat, sdslen(pat), (char *)str, len, 0)))
|
||||
{
|
||||
/* jump to the next key/val pair */
|
||||
p = lpNext(lp, p);
|
||||
continue;
|
||||
}
|
||||
|
||||
/* add key object */
|
||||
listAddNodeTail(keys, sdsnewlen(str, len));
|
||||
/* add value object */
|
||||
if (!no_values) {
|
||||
str = lpGet(val, &len, intbuf);
|
||||
listAddNodeTail(keys, sdsnewlen(str, len));
|
||||
}
|
||||
p = lpNext(lp, p);
|
||||
}
|
||||
cursor = 0;
|
||||
} else {
|
||||
serverPanic("Not handled encoding in SCAN.");
|
||||
}
|
||||
@@ -1243,10 +1304,14 @@ void scanGenericCommand(client *c, robj *o, unsigned long long cursor) {
|
||||
addReplyArrayLen(c, 2);
|
||||
addReplyBulkLongLong(c,cursor);
|
||||
|
||||
unsigned long long idx = 0;
|
||||
addReplyArrayLen(c, listLength(keys));
|
||||
while ((node = listFirst(keys)) != NULL) {
|
||||
sds key = listNodeValue(node);
|
||||
addReplyBulkCBuffer(c, key, sdslen(key));
|
||||
void *key = listNodeValue(node);
|
||||
/* For HSCAN, list will contain keys value pairs unless no_values arg
|
||||
* was given. We should call mstrlen for the keys only. */
|
||||
int hfieldkey = isKeysHfield && (no_values || (idx++ % 2 == 0));
|
||||
addReplyBulkCBuffer(c, key, hfieldkey ? mstrlen(key) : sdslen(key));
|
||||
listDelNode(keys, node);
|
||||
}
|
||||
|
||||
@@ -1339,6 +1404,7 @@ void renameGenericCommand(client *c, int nx) {
|
||||
robj *o;
|
||||
long long expire;
|
||||
int samekey = 0;
|
||||
uint64_t minHashExpireTime = EB_EXPIRE_TIME_INVALID;
|
||||
|
||||
/* When source and dest key is the same, no operation is performed,
|
||||
* if the key exists, however we still return an error on unexisting key. */
|
||||
@@ -1364,9 +1430,21 @@ void renameGenericCommand(client *c, int nx) {
|
||||
* with the same name. */
|
||||
dbDelete(c->db,c->argv[2]);
|
||||
}
|
||||
dbAdd(c->db,c->argv[2],o);
|
||||
dictEntry *de = dbAdd(c->db, c->argv[2], o);
|
||||
if (expire != -1) setExpire(c,c->db,c->argv[2],expire);
|
||||
|
||||
/* If hash with expiration on fields then remove it from global HFE DS and
|
||||
* keep next expiration time. Otherwise, dbDelete() will remove it from the
|
||||
* global HFE DS and we will lose the expiration time. */
|
||||
if (o->type == OBJ_HASH)
|
||||
minHashExpireTime = hashTypeRemoveFromExpires(&c->db->hexpires, o);
|
||||
|
||||
dbDelete(c->db,c->argv[1]);
|
||||
|
||||
/* If hash with HFEs, register in db->hexpires */
|
||||
if (minHashExpireTime != EB_EXPIRE_TIME_INVALID)
|
||||
hashTypeAddToExpires(c->db, dictGetKey(de), o, minHashExpireTime);
|
||||
|
||||
signalModifiedKey(c,c->db,c->argv[1]);
|
||||
signalModifiedKey(c,c->db,c->argv[2]);
|
||||
notifyKeyspaceEvent(NOTIFY_GENERIC,"rename_from",
|
||||
@@ -1390,6 +1468,7 @@ void moveCommand(client *c) {
|
||||
redisDb *src, *dst;
|
||||
int srcid, dbid;
|
||||
long long expire;
|
||||
uint64_t hashExpireTime = EB_EXPIRE_TIME_INVALID;
|
||||
|
||||
if (server.cluster_enabled) {
|
||||
addReplyError(c,"MOVE is not allowed in cluster mode");
|
||||
@@ -1430,12 +1509,25 @@ void moveCommand(client *c) {
|
||||
addReply(c,shared.czero);
|
||||
return;
|
||||
}
|
||||
dbAdd(dst,c->argv[1],o);
|
||||
dictEntry *dstDictEntry = dbAdd(dst,c->argv[1],o);
|
||||
if (expire != -1) setExpire(c,dst,c->argv[1],expire);
|
||||
|
||||
/* If hash with expiration on fields, remove it from global HFE DS and keep
|
||||
* aside registered expiration time. Must be before deletion of the object.
|
||||
* hexpires (ebuckets) embed in stored items its structure. */
|
||||
if (o->type == OBJ_HASH)
|
||||
hashExpireTime = hashTypeRemoveFromExpires(&src->hexpires, o);
|
||||
|
||||
incrRefCount(o);
|
||||
|
||||
/* OK! key moved, free the entry in the source DB */
|
||||
dbDelete(src,c->argv[1]);
|
||||
|
||||
/* If object of type hash with expiration on fields. Taken care to add the
|
||||
* hash to hexpires of `dst` only after dbDelete(). */
|
||||
if (hashExpireTime != EB_EXPIRE_TIME_INVALID)
|
||||
hashTypeAddToExpires(dst, dictGetKey(dstDictEntry), o, hashExpireTime);
|
||||
|
||||
signalModifiedKey(c,src,c->argv[1]);
|
||||
signalModifiedKey(c,dst,c->argv[1]);
|
||||
notifyKeyspaceEvent(NOTIFY_GENERIC,
|
||||
@@ -1518,12 +1610,13 @@ void copyCommand(client *c) {
|
||||
|
||||
/* Duplicate object according to object's type. */
|
||||
robj *newobj;
|
||||
uint64_t minHashExpire = EB_EXPIRE_TIME_INVALID; /* HFE feature */
|
||||
switch(o->type) {
|
||||
case OBJ_STRING: newobj = dupStringObject(o); break;
|
||||
case OBJ_LIST: newobj = listTypeDup(o); break;
|
||||
case OBJ_SET: newobj = setTypeDup(o); break;
|
||||
case OBJ_ZSET: newobj = zsetDup(o); break;
|
||||
case OBJ_HASH: newobj = hashTypeDup(o); break;
|
||||
case OBJ_HASH: newobj = hashTypeDup(o, newkey->ptr, &minHashExpire); break;
|
||||
case OBJ_STREAM: newobj = streamDup(o); break;
|
||||
case OBJ_MODULE:
|
||||
newobj = moduleTypeDupOrReply(c, key, newkey, dst->id, o);
|
||||
@@ -1538,8 +1631,16 @@ void copyCommand(client *c) {
|
||||
dbDelete(dst,newkey);
|
||||
}
|
||||
|
||||
dbAdd(dst,newkey,newobj);
|
||||
if (expire != -1) setExpire(c, dst, newkey, expire);
|
||||
dictEntry *deCopy = dbAdd(dst,newkey,newobj);
|
||||
|
||||
/* if key with expiration then set it */
|
||||
if (expire != -1)
|
||||
setExpire(c, dst, newkey, expire);
|
||||
|
||||
/* If minExpiredField was set, then the object is hash with expiration
|
||||
* on fields and need to register it in global HFE DS */
|
||||
if (minHashExpire != EB_EXPIRE_TIME_INVALID)
|
||||
hashTypeAddToExpires(dst, dictGetKey(deCopy), newobj, minHashExpire);
|
||||
|
||||
/* OK! key copied */
|
||||
signalModifiedKey(c,dst,c->argv[2]);
|
||||
@@ -1629,11 +1730,13 @@ int dbSwapDatabases(int id1, int id2) {
|
||||
* remain in the same DB they were. */
|
||||
db1->keys = db2->keys;
|
||||
db1->expires = db2->expires;
|
||||
db1->hexpires = db2->hexpires;
|
||||
db1->avg_ttl = db2->avg_ttl;
|
||||
db1->expires_cursor = db2->expires_cursor;
|
||||
|
||||
db2->keys = aux.keys;
|
||||
db2->expires = aux.expires;
|
||||
db2->hexpires = aux.hexpires;
|
||||
db2->avg_ttl = aux.avg_ttl;
|
||||
db2->expires_cursor = aux.expires_cursor;
|
||||
|
||||
@@ -1671,11 +1774,13 @@ void swapMainDbWithTempDb(redisDb *tempDb) {
|
||||
* remain in the same DB they were. */
|
||||
activedb->keys = newdb->keys;
|
||||
activedb->expires = newdb->expires;
|
||||
activedb->hexpires = newdb->hexpires;
|
||||
activedb->avg_ttl = newdb->avg_ttl;
|
||||
activedb->expires_cursor = newdb->expires_cursor;
|
||||
|
||||
newdb->keys = aux.keys;
|
||||
newdb->expires = aux.expires;
|
||||
newdb->hexpires = aux.hexpires;
|
||||
newdb->avg_ttl = aux.avg_ttl;
|
||||
newdb->expires_cursor = aux.expires_cursor;
|
||||
|
||||
@@ -1864,7 +1969,7 @@ int keyIsExpired(redisDb *db, robj *key) {
|
||||
* EXPIRE_AVOID_DELETE_EXPIRED flag.
|
||||
*
|
||||
* The return value of the function is KEY_VALID if the key is still valid.
|
||||
* The function returns KEY_EXPIRED if the key is expired BUT not deleted,
|
||||
* The function returns KEY_EXPIRED if the key is expired BUT not deleted,
|
||||
* or returns KEY_DELETED if the key is expired and deleted. */
|
||||
keyStatus expireIfNeeded(redisDb *db, robj *key, int flags) {
|
||||
if (server.lazy_expire_disabled) return KEY_VALID;
|
||||
@@ -1878,7 +1983,7 @@ keyStatus expireIfNeeded(redisDb *db, robj *key, int flags) {
|
||||
* replicas.
|
||||
*
|
||||
* Still we try to return the right information to the caller,
|
||||
* that is, KEY_VALID if we think the key should still be valid,
|
||||
* that is, KEY_VALID if we think the key should still be valid,
|
||||
* KEY_EXPIRED if we think the key is expired but don't want to delete it at this time.
|
||||
*
|
||||
* When replicating commands from the master, keys are never considered
|
||||
|
||||
+16
-7
@@ -200,17 +200,22 @@ void xorObjectDigest(redisDb *db, robj *keyobj, unsigned char *digest, robj *o)
|
||||
}
|
||||
} else if (o->type == OBJ_HASH) {
|
||||
hashTypeIterator *hi = hashTypeInitIterator(o);
|
||||
while (hashTypeNext(hi) != C_ERR) {
|
||||
while (hashTypeNext(hi, 0) != C_ERR) {
|
||||
unsigned char eledigest[20];
|
||||
sds sdsele;
|
||||
|
||||
/* field */
|
||||
memset(eledigest,0,20);
|
||||
sdsele = hashTypeCurrentObjectNewSds(hi,OBJ_HASH_KEY);
|
||||
mixDigest(eledigest,sdsele,sdslen(sdsele));
|
||||
sdsfree(sdsele);
|
||||
/* val */
|
||||
sdsele = hashTypeCurrentObjectNewSds(hi,OBJ_HASH_VALUE);
|
||||
mixDigest(eledigest,sdsele,sdslen(sdsele));
|
||||
sdsfree(sdsele);
|
||||
/* hash-field expiration (HFE) */
|
||||
if (hi->expire_time != EB_EXPIRE_TIME_INVALID)
|
||||
xorDigest(eledigest,"!!hexpire!!",11);
|
||||
xorDigest(digest,eledigest,20);
|
||||
}
|
||||
hashTypeReleaseIterator(hi);
|
||||
@@ -445,9 +450,9 @@ void debugCommand(client *c) {
|
||||
"SEGFAULT",
|
||||
" Crash the server with sigsegv.",
|
||||
"SET-ACTIVE-EXPIRE <0|1>",
|
||||
" Setting it to 0 disables expiring keys in background when they are not",
|
||||
" accessed (otherwise the Redis behavior). Setting it to 1 reenables back the",
|
||||
" default.",
|
||||
" Setting it to 0 disables expiring keys (and hash-fields) in background ",
|
||||
" when they are not accessed (otherwise the Redis behavior). Setting it",
|
||||
" to 1 reenables back the default.",
|
||||
"QUICKLIST-PACKED-THRESHOLD <size>",
|
||||
" Sets the threshold for elements to be inserted as plain vs packed nodes",
|
||||
" Default value is 1GB, allows values up to 4GB. Setting to 0 restores to default.",
|
||||
@@ -664,10 +669,14 @@ NULL
|
||||
if ((o = objectCommandLookupOrReply(c,c->argv[2],shared.nokeyerr))
|
||||
== NULL) return;
|
||||
|
||||
if (o->encoding != OBJ_ENCODING_LISTPACK) {
|
||||
if (o->encoding != OBJ_ENCODING_LISTPACK && o->encoding != OBJ_ENCODING_LISTPACK_EX) {
|
||||
addReplyError(c,"Not a listpack encoded object.");
|
||||
} else {
|
||||
lpRepr(o->ptr);
|
||||
if (o->encoding == OBJ_ENCODING_LISTPACK)
|
||||
lpRepr(o->ptr);
|
||||
else if (o->encoding == OBJ_ENCODING_LISTPACK_EX)
|
||||
lpRepr(((listpackEx*)o->ptr)->lp);
|
||||
|
||||
addReplyStatus(c,"Listpack structure printed on stdout");
|
||||
}
|
||||
} else if (!strcasecmp(c->argv[1]->ptr,"quicklist") && (c->argc == 3 || c->argc == 4)) {
|
||||
@@ -1081,7 +1090,7 @@ void serverLogObjectDebugInfo(const robj *o) {
|
||||
} else if (o->type == OBJ_SET) {
|
||||
serverLog(LL_WARNING,"Set size: %d", (int) setTypeSize(o));
|
||||
} else if (o->type == OBJ_HASH) {
|
||||
serverLog(LL_WARNING,"Hash size: %d", (int) hashTypeLength(o));
|
||||
serverLog(LL_WARNING,"Hash size: %d", (int) hashTypeLength(o, 0));
|
||||
} else if (o->type == OBJ_ZSET) {
|
||||
serverLog(LL_WARNING,"Sorted set size: %d", (int) zsetLength(o));
|
||||
if (o->encoding == OBJ_ENCODING_SKIPLIST)
|
||||
|
||||
+78
-6
@@ -70,6 +70,22 @@ sds activeDefragSds(sds sdsptr) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Defrag helper for hfield strings
|
||||
*
|
||||
* returns NULL in case the allocation wasn't moved.
|
||||
* when it returns a non-null value, the old pointer was already released
|
||||
* and should NOT be accessed. */
|
||||
hfield activeDefragHfield(hfield hf) {
|
||||
void *ptr = hfieldGetAllocPtr(hf);
|
||||
void *newptr = activeDefragAlloc(ptr);
|
||||
if (newptr) {
|
||||
size_t offset = hf - (char*)ptr;
|
||||
hf = (char*)newptr + offset;
|
||||
return hf;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Defrag helper for robj and/or string objects with expected refcount.
|
||||
*
|
||||
* Like activeDefragStringOb, but it requires the caller to pass in the expected
|
||||
@@ -250,6 +266,31 @@ void activeDefragSdsDictCallback(void *privdata, const dictEntry *de) {
|
||||
UNUSED(de);
|
||||
}
|
||||
|
||||
void activeDefragHfieldDictCallback(void *privdata, const dictEntry *de) {
|
||||
dict *d = privdata;
|
||||
hfield newhf, hf = dictGetKey(de);
|
||||
|
||||
if (hfieldGetExpireTime(hf) == EB_EXPIRE_TIME_INVALID) {
|
||||
/* If the hfield does not have TTL, we directly defrag it. */
|
||||
newhf = activeDefragHfield(hf);
|
||||
} else {
|
||||
/* Update its reference in the ebucket while defragging it. */
|
||||
ebuckets *eb = hashTypeGetDictMetaHFE(d);
|
||||
newhf = ebDefragItem(eb, &hashFieldExpireBucketsType, hf, (ebDefragFunction *)activeDefragHfield);
|
||||
}
|
||||
if (newhf) {
|
||||
/* We can't search in dict for that key after we've released
|
||||
* the pointer it holds, since it won't be able to do the string
|
||||
* compare, but we can find the entry using key hash and pointer. */
|
||||
dictUseStoredKeyApi(d, 1);
|
||||
uint64_t hash = dictGetHash(d, newhf);
|
||||
dictUseStoredKeyApi(d, 0);
|
||||
dictEntry *de = dictFindEntryByPtrAndHash(d, hf, hash);
|
||||
serverAssert(de);
|
||||
dictSetKey(d, de, newhf);
|
||||
}
|
||||
}
|
||||
|
||||
/* Defrag a dict with sds key and optional value (either ptr, sds or robj string) */
|
||||
void activeDefragSdsDict(dict* d, int val_type) {
|
||||
unsigned long cursor = 0;
|
||||
@@ -268,6 +309,20 @@ void activeDefragSdsDict(dict* d, int val_type) {
|
||||
} while (cursor != 0);
|
||||
}
|
||||
|
||||
/* Defrag a dict with hfield key and sds value. */
|
||||
void activeDefragHfieldDict(dict *d) {
|
||||
unsigned long cursor = 0;
|
||||
dictDefragFunctions defragfns = {
|
||||
.defragAlloc = activeDefragAlloc,
|
||||
.defragKey = NULL, /* Will be defragmented in activeDefragHfieldDictCallback. */
|
||||
.defragVal = (dictDefragAllocFunction *)activeDefragSds
|
||||
};
|
||||
do {
|
||||
cursor = dictScanDefrag(d, cursor, activeDefragHfieldDictCallback,
|
||||
&defragfns, d);
|
||||
} while (cursor != 0);
|
||||
}
|
||||
|
||||
/* Defrag a list of ptr, sds or robj string values */
|
||||
void activeDefragList(list *l, int val_type) {
|
||||
listNode *ln, *newln;
|
||||
@@ -422,10 +477,10 @@ void scanLaterHash(robj *ob, unsigned long *cursor) {
|
||||
dict *d = ob->ptr;
|
||||
dictDefragFunctions defragfns = {
|
||||
.defragAlloc = activeDefragAlloc,
|
||||
.defragKey = (dictDefragAllocFunction *)activeDefragSds,
|
||||
.defragKey = NULL, /* Will be defragmented in activeDefragHfieldDictCallback. */
|
||||
.defragVal = (dictDefragAllocFunction *)activeDefragSds
|
||||
};
|
||||
*cursor = dictScanDefrag(d, *cursor, scanCallbackCountScanned, &defragfns, NULL);
|
||||
*cursor = dictScanDefrag(d, *cursor, activeDefragHfieldDictCallback, &defragfns, d);
|
||||
}
|
||||
|
||||
void defragQuicklist(redisDb *db, dictEntry *kde) {
|
||||
@@ -477,7 +532,7 @@ void defragHash(redisDb *db, dictEntry *kde) {
|
||||
if (dictSize(d) > server.active_defrag_max_scan_fields)
|
||||
defragLater(db, kde);
|
||||
else
|
||||
activeDefragSdsDict(d, DEFRAG_SDS_DICT_VAL_IS_SDS);
|
||||
activeDefragHfieldDict(d);
|
||||
/* defrag the dict struct and tables */
|
||||
if ((newd = dictDefragTables(ob->ptr)))
|
||||
ob->ptr = newd;
|
||||
@@ -672,7 +727,7 @@ void defragModule(redisDb *db, dictEntry *kde) {
|
||||
* all the various pointers it has. */
|
||||
void defragKey(defragCtx *ctx, dictEntry *de) {
|
||||
sds keysds = dictGetKey(de);
|
||||
robj *newob, *ob;
|
||||
robj *newob, *ob = dictGetVal(de);
|
||||
unsigned char *newzl;
|
||||
sds newsds;
|
||||
redisDb *db = ctx->privdata;
|
||||
@@ -689,11 +744,22 @@ void defragKey(defragCtx *ctx, dictEntry *de) {
|
||||
dictEntry *expire_de = kvstoreDictFindEntryByPtrAndHash(db->expires, slot, keysds, hash);
|
||||
if (expire_de) kvstoreDictSetKey(db->expires, slot, expire_de, newsds);
|
||||
}
|
||||
|
||||
/* Update the key's reference in the dict's metadata or the listpackEx. */
|
||||
if (unlikely(ob->type == OBJ_HASH))
|
||||
hashTypeUpdateKeyRef(ob, newsds);
|
||||
}
|
||||
|
||||
/* Try to defrag robj and / or string value. */
|
||||
ob = dictGetVal(de);
|
||||
if ((newob = activeDefragStringOb(ob))) {
|
||||
if (unlikely(ob->type == OBJ_HASH && hashTypeGetMinExpire(ob) != EB_EXPIRE_TIME_INVALID)) {
|
||||
/* Update its reference in the ebucket while defragging it. */
|
||||
newob = ebDefragItem(&db->hexpires, &hashExpireBucketsType, ob,
|
||||
(ebDefragFunction *)activeDefragStringOb);
|
||||
} else {
|
||||
/* If the dict doesn't have metadata, we directly defrag it. */
|
||||
newob = activeDefragStringOb(ob);
|
||||
}
|
||||
if (newob) {
|
||||
kvstoreDictSetVal(db->keys, slot, de, newob);
|
||||
ob = newob;
|
||||
}
|
||||
@@ -734,6 +800,12 @@ void defragKey(defragCtx *ctx, dictEntry *de) {
|
||||
if (ob->encoding == OBJ_ENCODING_LISTPACK) {
|
||||
if ((newzl = activeDefragAlloc(ob->ptr)))
|
||||
ob->ptr = newzl;
|
||||
} else if (ob->encoding == OBJ_ENCODING_LISTPACK_EX) {
|
||||
listpackEx *newlpt, *lpt = (listpackEx*)ob->ptr;
|
||||
if ((newlpt = activeDefragAlloc(lpt)))
|
||||
ob->ptr = lpt = newlpt;
|
||||
if ((newzl = activeDefragAlloc(lpt->lp)))
|
||||
lpt->lp = newzl;
|
||||
} else if (ob->encoding == OBJ_ENCODING_HT) {
|
||||
defragHash(db, de);
|
||||
} else {
|
||||
|
||||
+59
-10
@@ -67,6 +67,25 @@ static int _dictInit(dict *d, dictType *type);
|
||||
static dictEntry *dictGetNext(const dictEntry *de);
|
||||
static dictEntry **dictGetNextRef(dictEntry *de);
|
||||
static void dictSetNext(dictEntry *de, dictEntry *next);
|
||||
static int dictDefaultCompare(dict *d, const void *key1, const void *key2);
|
||||
|
||||
/* -------------------------- misc inline functions -------------------------------- */
|
||||
|
||||
typedef int (*keyCmpFunc)(dict *d, const void *key1, const void *key2);
|
||||
static inline keyCmpFunc dictGetKeyCmpFunc(dict *d) {
|
||||
if (d->useStoredKeyApi && d->type->storedKeyCompare)
|
||||
return d->type->storedKeyCompare;
|
||||
if (d->type->keyCompare)
|
||||
return d->type->keyCompare;
|
||||
return dictDefaultCompare;
|
||||
}
|
||||
|
||||
static inline uint64_t dictHashKey(dict *d, const void *key, int isStoredKey) {
|
||||
if (isStoredKey && d->type->storedHashFunction)
|
||||
return d->type->storedHashFunction(key);
|
||||
else
|
||||
return d->type->hashFunction(key);
|
||||
}
|
||||
|
||||
/* -------------------------- hash functions -------------------------------- */
|
||||
|
||||
@@ -173,6 +192,19 @@ dict *dictCreate(dictType *type)
|
||||
return d;
|
||||
}
|
||||
|
||||
/* Change dictType of dict to another one with metadata support
|
||||
* Rest of dictType's values must stay the same */
|
||||
void dictTypeAddMeta(dict **d, dictType *typeWithMeta) {
|
||||
/* Verify new dictType is compatible with the old one */
|
||||
dictType toCmp = *typeWithMeta;
|
||||
toCmp.dictMetadataBytes = NULL; /* Expected old one not to have metadata */
|
||||
toCmp.onDictRelease = (*d)->type->onDictRelease; /* Ignore 'onDictRelease' in comparison */
|
||||
assert(memcmp((*d)->type, &toCmp, sizeof(dictType)) == 0); /* The rest of the dictType fields must be the same */
|
||||
|
||||
*d = zrealloc(*d, sizeof(dict) + typeWithMeta->dictMetadataBytes(*d));
|
||||
(*d)->type = typeWithMeta;
|
||||
}
|
||||
|
||||
/* Initialize the hash table */
|
||||
int _dictInit(dict *d, dictType *type)
|
||||
{
|
||||
@@ -182,6 +214,7 @@ int _dictInit(dict *d, dictType *type)
|
||||
d->rehashidx = -1;
|
||||
d->pauserehash = 0;
|
||||
d->pauseAutoResize = 0;
|
||||
d->useStoredKeyApi = 0;
|
||||
return DICT_OK;
|
||||
}
|
||||
|
||||
@@ -285,7 +318,7 @@ static void rehashEntriesInBucketAtIndex(dict *d, uint64_t idx) {
|
||||
void *key = dictGetKey(de);
|
||||
/* Get the index in the new hash table */
|
||||
if (d->ht_size_exp[1] > d->ht_size_exp[0]) {
|
||||
h = dictHashKey(d, key) & DICTHT_SIZE_MASK(d->ht_size_exp[1]);
|
||||
h = dictHashKey(d, key, 1) & DICTHT_SIZE_MASK(d->ht_size_exp[1]);
|
||||
} else {
|
||||
/* We're shrinking the table. The tables sizes are powers of
|
||||
* two, so we simply mask the bucket index in the larger table
|
||||
@@ -572,7 +605,7 @@ static dictEntry *dictGenericDelete(dict *d, const void *key, int nofree) {
|
||||
/* dict is empty */
|
||||
if (dictSize(d) == 0) return NULL;
|
||||
|
||||
h = dictHashKey(d, key);
|
||||
h = dictHashKey(d, key, d->useStoredKeyApi);
|
||||
idx = h & DICTHT_SIZE_MASK(d->ht_size_exp[0]);
|
||||
|
||||
if (dictIsRehashing(d)) {
|
||||
@@ -587,6 +620,8 @@ static dictEntry *dictGenericDelete(dict *d, const void *key, int nofree) {
|
||||
}
|
||||
}
|
||||
|
||||
keyCmpFunc cmpFunc = dictGetKeyCmpFunc(d);
|
||||
|
||||
for (table = 0; table <= 1; table++) {
|
||||
if (table == 0 && (long)idx < d->rehashidx) continue;
|
||||
idx = h & DICTHT_SIZE_MASK(d->ht_size_exp[table]);
|
||||
@@ -594,7 +629,7 @@ static dictEntry *dictGenericDelete(dict *d, const void *key, int nofree) {
|
||||
prevHe = NULL;
|
||||
while(he) {
|
||||
void *he_key = dictGetKey(he);
|
||||
if (key == he_key || dictCompareKeys(d, key, he_key)) {
|
||||
if (key == he_key || cmpFunc(d, key, he_key)) {
|
||||
/* Unlink the element from the list */
|
||||
if (prevHe)
|
||||
dictSetNext(prevHe, dictGetNext(he));
|
||||
@@ -689,6 +724,10 @@ void dictRelease(dict *d)
|
||||
* destroying the dict fake completion. */
|
||||
if (dictIsRehashing(d) && d->type->rehashingCompleted)
|
||||
d->type->rehashingCompleted(d);
|
||||
|
||||
if (d->type->onDictRelease)
|
||||
d->type->onDictRelease(d);
|
||||
|
||||
_dictClear(d,0,NULL);
|
||||
_dictClear(d,1,NULL);
|
||||
zfree(d);
|
||||
@@ -701,8 +740,9 @@ dictEntry *dictFind(dict *d, const void *key)
|
||||
|
||||
if (dictSize(d) == 0) return NULL; /* dict is empty */
|
||||
|
||||
h = dictHashKey(d, key);
|
||||
h = dictHashKey(d, key, d->useStoredKeyApi);
|
||||
idx = h & DICTHT_SIZE_MASK(d->ht_size_exp[0]);
|
||||
keyCmpFunc cmpFunc = dictGetKeyCmpFunc(d);
|
||||
|
||||
if (dictIsRehashing(d)) {
|
||||
if ((long)idx >= d->rehashidx && d->ht_table[0][idx]) {
|
||||
@@ -722,7 +762,7 @@ dictEntry *dictFind(dict *d, const void *key)
|
||||
he = d->ht_table[table][idx];
|
||||
while(he) {
|
||||
void *he_key = dictGetKey(he);
|
||||
if (key == he_key || dictCompareKeys(d, key, he_key))
|
||||
if (key == he_key || cmpFunc(d, key, he_key))
|
||||
return he;
|
||||
he = dictGetNext(he);
|
||||
}
|
||||
@@ -759,7 +799,9 @@ dictEntry *dictTwoPhaseUnlinkFind(dict *d, const void *key, dictEntry ***plink,
|
||||
|
||||
if (dictSize(d) == 0) return NULL; /* dict is empty */
|
||||
if (dictIsRehashing(d)) _dictRehashStep(d);
|
||||
h = dictHashKey(d, key);
|
||||
|
||||
h = dictHashKey(d, key, d->useStoredKeyApi);
|
||||
keyCmpFunc cmpFunc = dictGetKeyCmpFunc(d);
|
||||
|
||||
for (table = 0; table <= 1; table++) {
|
||||
idx = h & DICTHT_SIZE_MASK(d->ht_size_exp[table]);
|
||||
@@ -767,7 +809,7 @@ dictEntry *dictTwoPhaseUnlinkFind(dict *d, const void *key, dictEntry ***plink,
|
||||
dictEntry **ref = &d->ht_table[table][idx];
|
||||
while (ref && *ref) {
|
||||
void *de_key = dictGetKey(*ref);
|
||||
if (key == de_key || dictCompareKeys(d, key, de_key)) {
|
||||
if (key == de_key || cmpFunc(d, key, de_key)) {
|
||||
*table_index = table;
|
||||
*plink = ref;
|
||||
dictPauseRehashing(d);
|
||||
@@ -1530,8 +1572,8 @@ static signed char _dictNextExp(unsigned long size)
|
||||
void *dictFindPositionForInsert(dict *d, const void *key, dictEntry **existing) {
|
||||
unsigned long idx, table;
|
||||
dictEntry *he;
|
||||
uint64_t hash = dictHashKey(d, key, d->useStoredKeyApi);
|
||||
if (existing) *existing = NULL;
|
||||
uint64_t hash = dictHashKey(d, key);
|
||||
idx = hash & DICTHT_SIZE_MASK(d->ht_size_exp[0]);
|
||||
|
||||
if (dictIsRehashing(d)) {
|
||||
@@ -1548,6 +1590,8 @@ void *dictFindPositionForInsert(dict *d, const void *key, dictEntry **existing)
|
||||
|
||||
/* Expand the hash table if needed */
|
||||
_dictExpandIfNeeded(d);
|
||||
keyCmpFunc cmpFunc = dictGetKeyCmpFunc(d);
|
||||
|
||||
for (table = 0; table <= 1; table++) {
|
||||
if (table == 0 && (long)idx < d->rehashidx) continue;
|
||||
idx = hash & DICTHT_SIZE_MASK(d->ht_size_exp[table]);
|
||||
@@ -1555,7 +1599,7 @@ void *dictFindPositionForInsert(dict *d, const void *key, dictEntry **existing)
|
||||
he = d->ht_table[table][idx];
|
||||
while(he) {
|
||||
void *he_key = dictGetKey(he);
|
||||
if (key == he_key || dictCompareKeys(d, key, he_key)) {
|
||||
if (key == he_key || cmpFunc(d, key, he_key)) {
|
||||
if (existing) *existing = he;
|
||||
return NULL;
|
||||
}
|
||||
@@ -1587,7 +1631,7 @@ void dictSetResizeEnabled(dictResizeEnable enable) {
|
||||
}
|
||||
|
||||
uint64_t dictGetHash(dict *d, const void *key) {
|
||||
return dictHashKey(d, key);
|
||||
return dictHashKey(d, key, d->useStoredKeyApi);
|
||||
}
|
||||
|
||||
/* Finds the dictEntry using pointer and pre-calculated hash.
|
||||
@@ -1732,6 +1776,11 @@ void dictGetStats(char *buf, size_t bufsize, dict *d, int full) {
|
||||
orig_buf[orig_bufsize-1] = '\0';
|
||||
}
|
||||
|
||||
static int dictDefaultCompare(dict *d, const void *key1, const void *key2) {
|
||||
(void)(d); /*unused*/
|
||||
return key1 == key2;
|
||||
}
|
||||
|
||||
/* ------------------------------- Benchmark ---------------------------------*/
|
||||
|
||||
#ifdef REDIS_TEST
|
||||
|
||||
+31
-2
@@ -62,6 +62,32 @@ typedef struct dictType {
|
||||
unsigned int keys_are_odd:1;
|
||||
/* TODO: Add a 'keys_are_even' flag and use a similar optimization if that
|
||||
* flag is set. */
|
||||
/* Sometimes we want the ability to store a key in a given way inside the hash
|
||||
* function, and lookup it in some other way without resorting to any kind of
|
||||
* conversion. For instance the key may be stored as a structure also
|
||||
* representing other things, but the lookup happens via just a pointer to a
|
||||
* null terminated string. Optionally providing additional hash/cmp functions,
|
||||
* dict supports such usage. In that case we'll have a hashFunction() that will
|
||||
* expect a null terminated C string, and a storedHashFunction() that will
|
||||
* instead expect the structure. Similarly, the two comparison functions will
|
||||
* work differently. The keyCompare() will treat the first argument as a pointer
|
||||
* to a C string and the other as a structure (this way we can directly lookup
|
||||
* the structure key using the C string). While the storedKeyCompare() will
|
||||
* check if two pointers to the key in structure form are the same.
|
||||
*
|
||||
* However, functions of dict that gets key as argument (void *key) don't get
|
||||
* any indication whether it is a lookup or stored key. To indicate that
|
||||
* you intend to use key of type stored-key, and, consequently, use
|
||||
* dedicated compare and hash functions of stored-key, is by calling
|
||||
* dictUseStoredKeyApi(1) before using any of the dict functions that gets
|
||||
* key as a parameter and then call again dictUseStoredKeyApi(0) once done.
|
||||
*
|
||||
* Set to NULL both functions, if you don't want to support this feature. */
|
||||
uint64_t (*storedHashFunction)(const void *key);
|
||||
int (*storedKeyCompare)(dict *d, const void *key1, const void *key2);
|
||||
|
||||
/* Optional callback called when the dict is destroyed. */
|
||||
void (*onDictRelease)(dict *d);
|
||||
} dictType;
|
||||
|
||||
#define DICTHT_SIZE(exp) ((exp) == -1 ? 0 : (unsigned long)1<<(exp))
|
||||
@@ -76,7 +102,9 @@ struct dict {
|
||||
long rehashidx; /* rehashing not in progress if rehashidx == -1 */
|
||||
|
||||
/* Keep small vars at end for optimal (minimal) struct padding */
|
||||
int16_t pauserehash; /* If >0 rehashing is paused (<0 indicates coding error) */
|
||||
unsigned pauserehash : 15; /* If >0 rehashing is paused */
|
||||
|
||||
unsigned useStoredKeyApi : 1; /* See comment of storedHashFunction above */
|
||||
signed char ht_size_exp[2]; /* exponent of size. (size = 1<<exp) */
|
||||
int16_t pauseAutoResize; /* If >0 automatic resizing is disallowed (<0 indicates coding error) */
|
||||
void *metadata[];
|
||||
@@ -136,7 +164,6 @@ typedef struct {
|
||||
#define dictMetadataSize(d) ((d)->type->dictMetadataBytes \
|
||||
? (d)->type->dictMetadataBytes(d) : 0)
|
||||
|
||||
#define dictHashKey(d, key) ((d)->type->hashFunction(key))
|
||||
#define dictBuckets(d) (DICTHT_SIZE((d)->ht_size_exp[0])+DICTHT_SIZE((d)->ht_size_exp[1]))
|
||||
#define dictSize(d) ((d)->ht_used[0]+(d)->ht_used[1])
|
||||
#define dictIsEmpty(d) ((d)->ht_used[0] == 0 && (d)->ht_used[1] == 0)
|
||||
@@ -146,6 +173,7 @@ typedef struct {
|
||||
#define dictIsRehashingPaused(d) ((d)->pauserehash > 0)
|
||||
#define dictPauseAutoResize(d) ((d)->pauseAutoResize++)
|
||||
#define dictResumeAutoResize(d) ((d)->pauseAutoResize--)
|
||||
#define dictUseStoredKeyApi(d, flag) ((d)->useStoredKeyApi = (flag))
|
||||
|
||||
/* If our unsigned long type can store a 64 bit number, use a 64 bit PRNG. */
|
||||
#if ULONG_MAX >= 0xffffffffffffffff
|
||||
@@ -162,6 +190,7 @@ typedef enum {
|
||||
|
||||
/* API */
|
||||
dict *dictCreate(dictType *type);
|
||||
void dictTypeAddMeta(dict **d, dictType *typeWithMeta);
|
||||
int dictExpand(dict *d, unsigned long size);
|
||||
int dictTryExpand(dict *d, unsigned long size);
|
||||
int dictShrink(dict *d, unsigned long size);
|
||||
|
||||
+2422
File diff suppressed because it is too large
Load Diff
+308
@@ -0,0 +1,308 @@
|
||||
/*
|
||||
* Copyright Redis Ltd. 2024 - present
|
||||
*
|
||||
* Licensed under your choice of the Redis Source Available License 2.0 (RSALv2)
|
||||
* or the Server Side Public License v1 (SSPLv1).
|
||||
*
|
||||
*
|
||||
* WHAT IS EBUCKETS?
|
||||
* -----------------
|
||||
* ebuckets is being used to store items that are set with expiration-time. It
|
||||
* supports the basic API of add, remove and active expiration. The implementation
|
||||
* of it is based on rax-tree, or plain linked-list when small. The expiration time
|
||||
* of the items are used as the key to traverse rax-tree.
|
||||
*
|
||||
* Instead of holding a distinct item in each leaf of the rax-tree we can aggregate
|
||||
* items into small segments and hold it in each leaf. This way we can avoid
|
||||
* frequent modification of the rax-tree, since many of the modifications
|
||||
* will be done only at the segment level. It will also save memory because
|
||||
* rax-tree can be costly, around 40 bytes per leaf (with rax-key limited to 6
|
||||
* bytes). Whereas each additional item in the segment will cost the size of the
|
||||
* 'next' pointer in a list (8 bytes) and few more bytes for maintenance of the
|
||||
* segment.
|
||||
*
|
||||
* EBUCKETS STRUCTURE
|
||||
* ------------------
|
||||
* The ebuckets data structure is organized in a hierarchical manner as follows:
|
||||
*
|
||||
* 1. ebuckets: This is the top-level data structure. It can be either a rax tree
|
||||
* or a plain linked list. It contains one or more buckets, each representing
|
||||
* an interval in time.
|
||||
*
|
||||
* 2. bucket: Each bucket represents an interval in time and contains one or more
|
||||
* segments. The key in the rax-tree for each bucket represents low
|
||||
* bound expiration-time for the items within this bucket. The key of the
|
||||
* following bucket represents the upper bound expiration-time.
|
||||
*
|
||||
* 3. segment: Each segment within a bucket can hold up to `EB_SEG_MAX_ITEMS`
|
||||
* items as a linked list. If there are more, the segment will try to
|
||||
* split the bucket. To avoid wasting memory, it is a singly linked list (only
|
||||
* next-item pointer). It is a cyclic linked-list to allow efficient removal of
|
||||
* items from the middle of the segment without traversing the rax tree.
|
||||
*
|
||||
* 4. item: Each item that is stored in ebuckets should embed the ExpireMeta
|
||||
* struct and supply getter function (see EbucketsType.getExpireMeta). This
|
||||
* struct holds the expire-time of the item and few more fields that are used
|
||||
* to maintain the segments data-structure.
|
||||
*
|
||||
* SPLITTING BUCKET
|
||||
* ----------------
|
||||
* Each segment can hold up-to `EB_SEG_MAX_ITEMS` items. On insertion of new
|
||||
* item, it will try to split the segment. Here is an example For adding item
|
||||
* with expiration of 42 to a segment that already reached its maximum capacity
|
||||
* which will cause to split of the segment and in turn split of the bucket as
|
||||
* well to a finer grained ranges:
|
||||
*
|
||||
* BUCKETS BUCKETS
|
||||
* [ 00-10 ] -> size(Seg0) = 11 ==> [ 00-10 ] -> size(Seg0) = 11
|
||||
* [ 11-76 ] -> size(Seg1) = 16 [ 11-36 ] -> size(Seg1) = 9
|
||||
* [ 37-76 ] -> size(Seg2) = 7
|
||||
*
|
||||
* EXTENDING BUCKET
|
||||
* ----------------
|
||||
* In the example above, the reason it wasn't split evenly is that Seg1 must have
|
||||
* been holding items with same TTL and they must reside together in the same
|
||||
* bucket after the split. Which brings us to another important point. If there
|
||||
* is a segment that reached its maximum capacity and all the items have same
|
||||
* expiration-time key, then we cannot split the bucket but aggregate all the
|
||||
* items, with same expiration time key, by allocating an extended-segment and
|
||||
* chain it to the first segment in visited bucket. In that sense, extended
|
||||
* segments will only hold items with same expiration-time key.
|
||||
*
|
||||
* BUCKETS BUCKETS
|
||||
* [ 00-10 ] -> size(Seg0)=11 ==> [ 00-10 ] -> size(Seg0)=11
|
||||
* [ 11-12 ] -> size(Seg1)=16 [ 11-12 ] -> size(Seg1)=1 -> size(Seg2)=16
|
||||
*
|
||||
* LIMITING RAX TREE DEPTH
|
||||
* -----------------------
|
||||
* The rax tree is basically a B-tree and its depth is bounded by the sizeof of
|
||||
* the key. Holding 6 bytes for expiration-time key is more than enough to represent
|
||||
* unix-time in msec, and in turn the depth of the tree is limited to 6 levels.
|
||||
* At a first glance it might look sufficient but we need take into consideration
|
||||
* the heavyweight maintenance and traversal of each node in the B-tree.
|
||||
*
|
||||
* And so, we can further prune the tree such that holding keys with msec precision
|
||||
* in the tree doesn't bring with it much value. The active-expiration operation can
|
||||
* live with deletion of expired items, say, older than 1 sec, which means the size
|
||||
* of time-expiration keys to the rax tree become no more than ~4.5 bytes and we
|
||||
* also get rid of the "noisy" bits which most probably will cause to yet another
|
||||
* branching and modification of the rax tree in case of items with time-expiration
|
||||
* difference of less than 1 second. The lazy expiration will still be precise and
|
||||
* without compromise on accuracy because the exact expiration-time is kept
|
||||
* attached as well to each item, in `ExpireMeta`, and each traversal of item with
|
||||
* expiration will behave as expected down to the msec. Take care to configure
|
||||
* `EB_BUCKET_KEY_PRECISION` according to your needs.
|
||||
*
|
||||
* EBUCKET KEY
|
||||
* -----------
|
||||
* Taking into account configured value of `EB_BUCKET_KEY_PRECISION`, two items
|
||||
* with expiration-time t1 and t2 will be considered to have the same key in the
|
||||
* rax-tree/buckets if and only if:
|
||||
*
|
||||
* EB_BUCKET_KEY(t1) == EB_BUCKET_KEY(t2)
|
||||
*
|
||||
* EBUCKETS CREATION
|
||||
* -----------------
|
||||
* To avoid the cost of allocating rax data-structure for only few elements,
|
||||
* ebuckets will start as a simple linked-list and only when it reaches some
|
||||
* threshold, it will be converted to rax.
|
||||
*
|
||||
* TODO
|
||||
* ----
|
||||
* - ebRemove() optimize to merge small segments into one segment.
|
||||
* - ebAdd() Fix pathological case of cascade addition of items into rax such
|
||||
* that their values are smaller/bigger than visited extended-segment which ends
|
||||
* up with multiple segments with a single item in each segment.
|
||||
*/
|
||||
|
||||
#ifndef __EBUCKETS_H
|
||||
#define __EBUCKETS_H
|
||||
|
||||
#include <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 */
|
||||
@@ -94,6 +94,7 @@ int activeExpireCycleTryExpire(redisDb *db, dictEntry *de, long long now) {
|
||||
#define ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC 25 /* Max % of CPU to use. */
|
||||
#define ACTIVE_EXPIRE_CYCLE_ACCEPTABLE_STALE 10 /* % of stale keys after which
|
||||
we do extra efforts. */
|
||||
#define HFE_ACTIVE_EXPIRE_CYCLE_FIELDS 1000
|
||||
|
||||
/* Data used by the expire dict scan callback. */
|
||||
typedef struct {
|
||||
@@ -134,6 +135,53 @@ static inline int isExpiryDictValidForSamplingCb(dict *d) {
|
||||
return C_OK;
|
||||
}
|
||||
|
||||
/* Active expiration Cycle for hash-fields.
|
||||
*
|
||||
* Note that releasing fields is expected to be more predictable and rewarding
|
||||
* than releasing keys because it is stored in `ebuckets` DS which optimized for
|
||||
* active expiration and in addition the deletion of fields is simple to handle. */
|
||||
static inline void activeExpireHashFieldCycle(int type) {
|
||||
/* Remember current db across calls */
|
||||
static unsigned int currentDb = 0;
|
||||
|
||||
/* Tracks the count of fields actively expired for the current database.
|
||||
* This count continues as long as it fails to actively expire all expired
|
||||
* fields of currentDb, indicating a possible need to adjust the value of
|
||||
* maxToExpire. */
|
||||
static uint64_t activeExpirySequence = 0;
|
||||
/* Threshold for adjusting maxToExpire */
|
||||
const uint32_t EXPIRED_FIELDS_TH = 1000000;
|
||||
/* Maximum number of fields to actively expire in a single call */
|
||||
uint32_t maxToExpire = HFE_ACTIVE_EXPIRE_CYCLE_FIELDS;
|
||||
|
||||
redisDb *db = server.db + currentDb;
|
||||
|
||||
/* If db is empty, move to next db and return */
|
||||
if (ebIsEmpty(db->hexpires)) {
|
||||
activeExpirySequence = 0;
|
||||
currentDb = (currentDb + 1) % server.dbnum;
|
||||
return;
|
||||
}
|
||||
|
||||
/* If running for a while and didn't manage to active-expire all expired fields of
|
||||
* currentDb (i.e. activeExpirySequence becomes significant) then adjust maxToExpire */
|
||||
if ((activeExpirySequence > EXPIRED_FIELDS_TH) && (type == ACTIVE_EXPIRE_CYCLE_SLOW)) {
|
||||
/* maxToExpire is multiplied by a factor between 1 and 32, proportional to
|
||||
* the number of times activeExpirySequence exceeded EXPIRED_FIELDS_TH */
|
||||
uint64_t factor = activeExpirySequence / EXPIRED_FIELDS_TH;
|
||||
maxToExpire *= (factor<32) ? factor : 32;
|
||||
}
|
||||
|
||||
if (hashTypeDbActiveExpire(db, maxToExpire) == maxToExpire) {
|
||||
/* active-expire reached maxToExpire limit */
|
||||
activeExpirySequence += maxToExpire;
|
||||
} else {
|
||||
/* Managed to active-expire all expired fields of currentDb */
|
||||
activeExpirySequence = 0;
|
||||
currentDb = (currentDb + 1) % server.dbnum;
|
||||
}
|
||||
}
|
||||
|
||||
void activeExpireCycle(int type) {
|
||||
/* Adjust the running parameters according to the configured expire
|
||||
* effort. The default effort is 1, and the maximum configurable effort
|
||||
@@ -232,6 +280,11 @@ void activeExpireCycle(int type) {
|
||||
* distribute the time evenly across DBs. */
|
||||
current_db++;
|
||||
|
||||
/* Interleaving hash-field expiration with key expiration. Better
|
||||
* call it before handling expired keys because HFE DS is optimized for
|
||||
* active expiration */
|
||||
activeExpireHashFieldCycle(type);
|
||||
|
||||
if (kvstoreSize(db->expires))
|
||||
dbs_performed++;
|
||||
|
||||
|
||||
+6
-2
@@ -3,6 +3,7 @@
|
||||
#include "atomicvar.h"
|
||||
#include "functions.h"
|
||||
#include "cluster.h"
|
||||
#include "ebuckets.h"
|
||||
|
||||
static redisAtomic size_t lazyfree_objects = 0;
|
||||
static redisAtomic size_t lazyfreed_objects = 0;
|
||||
@@ -22,7 +23,8 @@ void lazyfreeFreeObject(void *args[]) {
|
||||
void lazyfreeFreeDatabase(void *args[]) {
|
||||
kvstore *da1 = args[0];
|
||||
kvstore *da2 = args[1];
|
||||
|
||||
ebuckets oldHfe = args[2];
|
||||
ebDestroy(&oldHfe, &hashExpireBucketsType, NULL);
|
||||
size_t numkeys = kvstoreSize(da1);
|
||||
kvstoreRelease(da1);
|
||||
kvstoreRelease(da2);
|
||||
@@ -201,10 +203,12 @@ void emptyDbAsync(redisDb *db) {
|
||||
flags |= KVSTORE_FREE_EMPTY_DICTS;
|
||||
}
|
||||
kvstore *oldkeys = db->keys, *oldexpires = db->expires;
|
||||
ebuckets oldHfe = db->hexpires;
|
||||
db->keys = kvstoreCreate(&dbDictType, slot_count_bits, flags);
|
||||
db->expires = kvstoreCreate(&dbExpiresDictType, slot_count_bits, flags);
|
||||
db->hexpires = ebCreate();
|
||||
atomicIncr(lazyfree_objects, kvstoreSize(oldkeys));
|
||||
bioCreateLazyFreeJob(lazyfreeFreeDatabase, 2, oldkeys, oldexpires);
|
||||
bioCreateLazyFreeJob(lazyfreeFreeDatabase, 3, oldkeys, oldexpires, oldHfe);
|
||||
}
|
||||
|
||||
/* Free the key tracking table.
|
||||
|
||||
+614
-102
@@ -245,51 +245,61 @@ unsigned char* lpShrinkToFit(unsigned char *lp) {
|
||||
static inline void lpEncodeIntegerGetType(int64_t v, unsigned char *intenc, uint64_t *enclen) {
|
||||
if (v >= 0 && v <= 127) {
|
||||
/* Single byte 0-127 integer. */
|
||||
intenc[0] = v;
|
||||
*enclen = 1;
|
||||
if (intenc != NULL) intenc[0] = v;
|
||||
if (enclen != NULL) *enclen = 1;
|
||||
} else if (v >= -4096 && v <= 4095) {
|
||||
/* 13 bit integer. */
|
||||
if (v < 0) v = ((int64_t)1<<13)+v;
|
||||
intenc[0] = (v>>8)|LP_ENCODING_13BIT_INT;
|
||||
intenc[1] = v&0xff;
|
||||
*enclen = 2;
|
||||
if (intenc != NULL) {
|
||||
intenc[0] = (v>>8)|LP_ENCODING_13BIT_INT;
|
||||
intenc[1] = v&0xff;
|
||||
}
|
||||
if (enclen != NULL) *enclen = 2;
|
||||
} else if (v >= -32768 && v <= 32767) {
|
||||
/* 16 bit integer. */
|
||||
if (v < 0) v = ((int64_t)1<<16)+v;
|
||||
intenc[0] = LP_ENCODING_16BIT_INT;
|
||||
intenc[1] = v&0xff;
|
||||
intenc[2] = v>>8;
|
||||
*enclen = 3;
|
||||
if (intenc != NULL) {
|
||||
intenc[0] = LP_ENCODING_16BIT_INT;
|
||||
intenc[1] = v&0xff;
|
||||
intenc[2] = v>>8;
|
||||
}
|
||||
if (enclen != NULL) *enclen = 3;
|
||||
} else if (v >= -8388608 && v <= 8388607) {
|
||||
/* 24 bit integer. */
|
||||
if (v < 0) v = ((int64_t)1<<24)+v;
|
||||
intenc[0] = LP_ENCODING_24BIT_INT;
|
||||
intenc[1] = v&0xff;
|
||||
intenc[2] = (v>>8)&0xff;
|
||||
intenc[3] = v>>16;
|
||||
*enclen = 4;
|
||||
if (intenc != NULL) {
|
||||
intenc[0] = LP_ENCODING_24BIT_INT;
|
||||
intenc[1] = v&0xff;
|
||||
intenc[2] = (v>>8)&0xff;
|
||||
intenc[3] = v>>16;
|
||||
}
|
||||
if (enclen != NULL) *enclen = 4;
|
||||
} else if (v >= -2147483648 && v <= 2147483647) {
|
||||
/* 32 bit integer. */
|
||||
if (v < 0) v = ((int64_t)1<<32)+v;
|
||||
intenc[0] = LP_ENCODING_32BIT_INT;
|
||||
intenc[1] = v&0xff;
|
||||
intenc[2] = (v>>8)&0xff;
|
||||
intenc[3] = (v>>16)&0xff;
|
||||
intenc[4] = v>>24;
|
||||
*enclen = 5;
|
||||
if (intenc != NULL) {
|
||||
intenc[0] = LP_ENCODING_32BIT_INT;
|
||||
intenc[1] = v&0xff;
|
||||
intenc[2] = (v>>8)&0xff;
|
||||
intenc[3] = (v>>16)&0xff;
|
||||
intenc[4] = v>>24;
|
||||
}
|
||||
if (enclen != NULL) *enclen = 5;
|
||||
} else {
|
||||
/* 64 bit integer. */
|
||||
uint64_t uv = v;
|
||||
intenc[0] = LP_ENCODING_64BIT_INT;
|
||||
intenc[1] = uv&0xff;
|
||||
intenc[2] = (uv>>8)&0xff;
|
||||
intenc[3] = (uv>>16)&0xff;
|
||||
intenc[4] = (uv>>24)&0xff;
|
||||
intenc[5] = (uv>>32)&0xff;
|
||||
intenc[6] = (uv>>40)&0xff;
|
||||
intenc[7] = (uv>>48)&0xff;
|
||||
intenc[8] = uv>>56;
|
||||
*enclen = 9;
|
||||
if (intenc != NULL) {
|
||||
intenc[0] = LP_ENCODING_64BIT_INT;
|
||||
intenc[1] = uv&0xff;
|
||||
intenc[2] = (uv>>8)&0xff;
|
||||
intenc[3] = (uv>>16)&0xff;
|
||||
intenc[4] = (uv>>24)&0xff;
|
||||
intenc[5] = (uv>>32)&0xff;
|
||||
intenc[6] = (uv>>40)&0xff;
|
||||
intenc[7] = (uv>>48)&0xff;
|
||||
intenc[8] = uv>>56;
|
||||
}
|
||||
if (enclen != NULL) *enclen = 9;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -659,50 +669,47 @@ unsigned char *lpGetValue(unsigned char *p, unsigned int *slen, long long *lval)
|
||||
return vstr;
|
||||
}
|
||||
|
||||
/* Find pointer to the entry equal to the specified entry. Skip 'skip' entries
|
||||
* between every comparison. Returns NULL when the field could not be found. */
|
||||
unsigned char *lpFind(unsigned char *lp, unsigned char *p, unsigned char *s,
|
||||
uint32_t slen, unsigned int skip) {
|
||||
/* This is just a wrapper to lpGet() that is able to get an integer from an entry directly.
|
||||
* Returns 1 and stores the integer in 'lval' if the entry is an integer.
|
||||
* Returns 0 if the entry is a string. */
|
||||
int lpGetIntegerValue(unsigned char *p, long long *lval) {
|
||||
int64_t ele_len;
|
||||
if (!lpGet(p, &ele_len, NULL)) {
|
||||
*lval = ele_len;
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Find pointer to the entry with a comparator callback.
|
||||
*
|
||||
* 'cmp' is a comparator callback. If it returns zero, current entry pointer
|
||||
* will be returned. 'user' is passed to this callback.
|
||||
* Skip 'skip' entries between every comparison.
|
||||
* Returns NULL when the field could not be found. */
|
||||
unsigned char *lpFindCb(unsigned char *lp, unsigned char *p,
|
||||
void *user, lpCmp cmp, unsigned int skip)
|
||||
{
|
||||
int skipcnt = 0;
|
||||
unsigned char vencoding = 0;
|
||||
unsigned char *value;
|
||||
int64_t ll, vll;
|
||||
int64_t ll;
|
||||
uint64_t entry_size = 123456789; /* initialized to avoid warning. */
|
||||
uint32_t lp_bytes = lpBytes(lp);
|
||||
|
||||
assert(p);
|
||||
if (!p)
|
||||
p = lpFirst(lp);
|
||||
|
||||
while (p) {
|
||||
if (skipcnt == 0) {
|
||||
value = lpGetWithSize(p, &ll, NULL, &entry_size);
|
||||
if (value) {
|
||||
/* check the value doesn't reach outside the listpack before accessing it */
|
||||
assert(p >= lp + LP_HDR_SIZE && p + entry_size < lp + lp_bytes);
|
||||
if (slen == ll && memcmp(value, s, slen) == 0) {
|
||||
return p;
|
||||
}
|
||||
} else {
|
||||
/* Find out if the searched field can be encoded. Note that
|
||||
* we do it only the first time, once done vencoding is set
|
||||
* to non-zero and vll is set to the integer value. */
|
||||
if (vencoding == 0) {
|
||||
/* If the entry can be encoded as integer we set it to
|
||||
* 1, else set it to UCHAR_MAX, so that we don't retry
|
||||
* again the next time. */
|
||||
if (slen >= 32 || slen == 0 || !lpStringToInt64((const char*)s, slen, &vll)) {
|
||||
vencoding = UCHAR_MAX;
|
||||
} else {
|
||||
vencoding = 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Compare current entry with specified entry, do it only
|
||||
* if vencoding != UCHAR_MAX because if there is no encoding
|
||||
* possible for the field it can't be a valid integer. */
|
||||
if (vencoding != UCHAR_MAX && ll == vll) {
|
||||
return p;
|
||||
}
|
||||
}
|
||||
|
||||
if (cmp(lp, p, user, value, ll) == 0)
|
||||
return p;
|
||||
|
||||
/* Reset skip count */
|
||||
skipcnt = skip;
|
||||
p += entry_size;
|
||||
@@ -727,6 +734,62 @@ unsigned char *lpFind(unsigned char *lp, unsigned char *p, unsigned char *s,
|
||||
return NULL;
|
||||
}
|
||||
|
||||
struct lpFindArg {
|
||||
unsigned char *s; /* Item to search */
|
||||
uint32_t slen; /* Item len */
|
||||
int vencoding;
|
||||
int64_t vll;
|
||||
};
|
||||
|
||||
/* Comparator function to find item */
|
||||
static inline int lpFindCmp(const unsigned char *lp, unsigned char *p,
|
||||
void *user, unsigned char *s, long long slen) {
|
||||
(void) lp;
|
||||
(void) p;
|
||||
struct lpFindArg *arg = user;
|
||||
|
||||
if (s) {
|
||||
if (slen == arg->slen && memcmp(arg->s, s, slen) == 0) {
|
||||
return 0;
|
||||
}
|
||||
} else {
|
||||
/* Find out if the searched field can be encoded. Note that
|
||||
* we do it only the first time, once done vencoding is set
|
||||
* to non-zero and vll is set to the integer value. */
|
||||
if (arg->vencoding == 0) {
|
||||
/* If the entry can be encoded as integer we set it to
|
||||
* 1, else set it to UCHAR_MAX, so that we don't retry
|
||||
* again the next time. */
|
||||
if (arg->slen >= 32 || arg->slen == 0 || !lpStringToInt64((const char*)arg->s, arg->slen, &arg->vll)) {
|
||||
arg->vencoding = UCHAR_MAX;
|
||||
} else {
|
||||
arg->vencoding = 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Compare current entry with specified entry, do it only
|
||||
* if vencoding != UCHAR_MAX because if there is no encoding
|
||||
* possible for the field it can't be a valid integer. */
|
||||
if (arg->vencoding != UCHAR_MAX && slen == arg->vll) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Find pointer to the entry equal to the specified entry. Skip 'skip' entries
|
||||
* between every comparison. Returns NULL when the field could not be found. */
|
||||
unsigned char *lpFind(unsigned char *lp, unsigned char *p, unsigned char *s,
|
||||
uint32_t slen, unsigned int skip)
|
||||
{
|
||||
struct lpFindArg arg = {
|
||||
.s = s,
|
||||
.slen = slen
|
||||
};
|
||||
return lpFindCb(lp, p, &arg, lpFindCmp, skip);
|
||||
}
|
||||
|
||||
/* Insert, delete or replace the specified string element 'elestr' of length
|
||||
* 'size' or integer element 'eleint' at the specified position 'p', with 'p'
|
||||
* being a listpack element pointer obtained with lpFirst(), lpLast(), lpNext(),
|
||||
@@ -904,6 +967,140 @@ unsigned char *lpInsert(unsigned char *lp, unsigned char *elestr, unsigned char
|
||||
return lp;
|
||||
}
|
||||
|
||||
/* Insert the specified elements with 'entries' and 'len' at the specified
|
||||
* position 'p', with 'p' being a listpack element pointer obtained with
|
||||
* lpFirst(), lpLast(), lpNext(), lpPrev() or lpSeek().
|
||||
*
|
||||
* This is similar to lpInsert() but allows you to insert batch of entries in
|
||||
* one call. This function is more efficient than inserting entries one by one
|
||||
* as it does single realloc()/memmove() calls for all the entries.
|
||||
*
|
||||
* In each listpackEntry, if 'sval' is not null, it is assumed entry is string
|
||||
* and 'sval' and 'slen' will be used. Otherwise, 'lval' will be used to append
|
||||
* the integer entry.
|
||||
*
|
||||
* The elements are inserted before or after the element pointed by 'p'
|
||||
* depending on the 'where' argument, that can be LP_BEFORE or LP_AFTER.
|
||||
*
|
||||
* If 'newp' is not NULL, at the end of a successful call '*newp' will be set
|
||||
* to the address of the element just added, so that it will be possible to
|
||||
* continue an interaction with lpNext() and lpPrev().
|
||||
*
|
||||
* Returns NULL on out of memory or when the listpack total length would exceed
|
||||
* the max allowed size of 2^32-1, otherwise the new pointer to the listpack
|
||||
* holding the new element is returned (and the old pointer passed is no longer
|
||||
* considered valid). */
|
||||
unsigned char *lpBatchInsert(unsigned char *lp, unsigned char *p, int where,
|
||||
listpackEntry *entries, unsigned int len,
|
||||
unsigned char **newp)
|
||||
{
|
||||
assert(where == LP_BEFORE || where == LP_AFTER);
|
||||
assert(entries != NULL && len > 0);
|
||||
|
||||
struct listpackInsertEntry {
|
||||
int enctype;
|
||||
uint64_t enclen;
|
||||
unsigned char intenc[LP_MAX_INT_ENCODING_LEN];
|
||||
unsigned char backlen[LP_MAX_BACKLEN_SIZE];
|
||||
unsigned long backlen_size;
|
||||
};
|
||||
|
||||
uint64_t addedlen = 0; /* The encoded length of the added elements. */
|
||||
struct listpackInsertEntry tmp[3]; /* Encoded entries */
|
||||
struct listpackInsertEntry *enc = tmp;
|
||||
|
||||
if (len > sizeof(tmp) / sizeof(struct listpackInsertEntry)) {
|
||||
/* If 'len' is larger than local buffer size, allocate on heap. */
|
||||
enc = zmalloc(len * sizeof(struct listpackInsertEntry));
|
||||
}
|
||||
|
||||
/* If we need to insert after the current element, we just jump to the
|
||||
* next element (that could be the EOF one) and handle the case of
|
||||
* inserting before. So the function will actually deal with just one
|
||||
* case: LP_BEFORE. */
|
||||
if (where == LP_AFTER) {
|
||||
p = lpSkip(p);
|
||||
where = LP_BEFORE;
|
||||
ASSERT_INTEGRITY(lp, p);
|
||||
}
|
||||
|
||||
for (unsigned int i = 0; i < len; i++) {
|
||||
listpackEntry *e = &entries[i];
|
||||
if (e->sval) {
|
||||
/* Calling lpEncodeGetType() results into the encoded version of the
|
||||
* element to be stored into 'intenc' in case it is representable as
|
||||
* an integer: in that case, the function returns LP_ENCODING_INT.
|
||||
* Otherwise, if LP_ENCODING_STR is returned, we'll have to call
|
||||
* lpEncodeString() to actually write the encoded string on place
|
||||
* later.
|
||||
*
|
||||
* Whatever the returned encoding is, 'enclen' is populated with the
|
||||
* length of the encoded element. */
|
||||
enc[i].enctype = lpEncodeGetType(e->sval, e->slen,
|
||||
enc[i].intenc, &enc[i].enclen);
|
||||
} else {
|
||||
enc[i].enctype = LP_ENCODING_INT;
|
||||
lpEncodeIntegerGetType(e->lval, enc[i].intenc, &enc[i].enclen);
|
||||
}
|
||||
addedlen += enc[i].enclen;
|
||||
|
||||
/* We need to also encode the backward-parsable length of the element
|
||||
* and append it to the end: this allows to traverse the listpack from
|
||||
* the end to the start. */
|
||||
enc[i].backlen_size = lpEncodeBacklen(enc[i].backlen, enc[i].enclen);
|
||||
addedlen += enc[i].backlen_size;
|
||||
}
|
||||
|
||||
uint64_t old_listpack_bytes = lpGetTotalBytes(lp);
|
||||
uint64_t new_listpack_bytes = old_listpack_bytes + addedlen;
|
||||
if (new_listpack_bytes > UINT32_MAX) return NULL;
|
||||
|
||||
/* Store the offset of the element 'p', so that we can obtain its
|
||||
* address again after a reallocation. */
|
||||
unsigned long poff = p-lp;
|
||||
unsigned char *dst = lp + poff; /* May be updated after reallocation. */
|
||||
|
||||
/* Realloc before: we need more room. */
|
||||
if (new_listpack_bytes > old_listpack_bytes &&
|
||||
new_listpack_bytes > lp_malloc_size(lp)) {
|
||||
if ((lp = lp_realloc(lp,new_listpack_bytes)) == NULL) return NULL;
|
||||
dst = lp + poff;
|
||||
}
|
||||
|
||||
/* Setup the listpack relocating the elements to make the exact room
|
||||
* we need to store the new ones. */
|
||||
memmove(dst+addedlen,dst,old_listpack_bytes-poff);
|
||||
|
||||
for (unsigned int i = 0; i < len; i++) {
|
||||
listpackEntry *ent = &entries[i];
|
||||
|
||||
if (newp)
|
||||
*newp = dst;
|
||||
|
||||
if (enc[i].enctype == LP_ENCODING_INT)
|
||||
memcpy(dst, enc[i].intenc, enc[i].enclen);
|
||||
else
|
||||
lpEncodeString(dst, ent->sval, ent->slen);
|
||||
|
||||
dst += enc[i].enclen;
|
||||
memcpy(dst, enc[i].backlen, enc[i].backlen_size);
|
||||
dst += enc[i].backlen_size;
|
||||
}
|
||||
|
||||
/* Update header. */
|
||||
uint32_t num_elements = lpGetNumElements(lp);
|
||||
if (num_elements != LP_HDR_NUMELE_UNKNOWN) {
|
||||
if ((int64_t) len > (int64_t) LP_HDR_NUMELE_UNKNOWN - (int64_t) num_elements)
|
||||
lpSetNumElements(lp, LP_HDR_NUMELE_UNKNOWN);
|
||||
else
|
||||
lpSetNumElements(lp,num_elements + len);
|
||||
}
|
||||
lpSetTotalBytes(lp,new_listpack_bytes);
|
||||
if (enc != tmp) lp_free(enc);
|
||||
|
||||
return lp;
|
||||
}
|
||||
|
||||
/* This is just a wrapper for lpInsert() to directly use a string. */
|
||||
unsigned char *lpInsertString(unsigned char *lp, unsigned char *s, uint32_t slen,
|
||||
unsigned char *p, int where, unsigned char **newp)
|
||||
@@ -951,6 +1148,20 @@ unsigned char *lpAppendInteger(unsigned char *lp, long long lval) {
|
||||
return lpInsertInteger(lp, lval, eofptr, LP_BEFORE, NULL);
|
||||
}
|
||||
|
||||
/* Append batch of entries to the listpack.
|
||||
*
|
||||
* This call is more efficient than multiple lpAppend() calls as it only does
|
||||
* a single realloc() for all the given entries.
|
||||
*
|
||||
* In each listpackEntry, if 'sval' is not null, it is assumed entry is string
|
||||
* and 'sval' and 'slen' will be used. Otherwise, 'lval' will be used to append
|
||||
* the integer entry. */
|
||||
unsigned char *lpBatchAppend(unsigned char *lp, listpackEntry *entries, unsigned long len) {
|
||||
uint64_t listpack_bytes = lpGetTotalBytes(lp);
|
||||
unsigned char *eofptr = lp + listpack_bytes - 1;
|
||||
return lpBatchInsert(lp, eofptr, LP_BEFORE, entries, len, NULL);
|
||||
}
|
||||
|
||||
/* This is just a wrapper for lpInsert() to directly use a string to replace
|
||||
* the current element. The function returns the new listpack as return
|
||||
* value, and also updates the current cursor by updating '*p'. */
|
||||
@@ -1199,13 +1410,17 @@ size_t lpBytes(unsigned char *lp) {
|
||||
return lpGetTotalBytes(lp);
|
||||
}
|
||||
|
||||
/* Returns the size 'lval' will require when encoded, in bytes */
|
||||
size_t lpEntrySizeInteger(long long lval) {
|
||||
uint64_t enclen;
|
||||
lpEncodeIntegerGetType(lval, NULL, &enclen);
|
||||
unsigned long backlen = lpEncodeBacklen(NULL, enclen);
|
||||
return enclen + backlen;
|
||||
}
|
||||
|
||||
/* Returns the size of a listpack consisting of an integer repeated 'rep' times. */
|
||||
size_t lpEstimateBytesRepeatedInteger(long long lval, unsigned long rep) {
|
||||
uint64_t enclen;
|
||||
unsigned char intenc[LP_MAX_INT_ENCODING_LEN];
|
||||
lpEncodeIntegerGetType(lval, intenc, &enclen);
|
||||
unsigned long backlen = lpEncodeBacklen(NULL, enclen);
|
||||
return LP_HDR_SIZE + (enclen + backlen) * rep + 1;
|
||||
return LP_HDR_SIZE + lpEntrySizeInteger(lval) * rep + 1;
|
||||
}
|
||||
|
||||
/* Seek the specified element and returns the pointer to the seeked element.
|
||||
@@ -1408,15 +1623,20 @@ static inline void lpSaveValue(unsigned char *val, unsigned int len, int64_t lva
|
||||
/* Randomly select a pair of key and value.
|
||||
* total_count is a pre-computed length/2 of the listpack (to avoid calls to lpLength)
|
||||
* 'key' and 'val' are used to store the result key value pair.
|
||||
* 'val' can be NULL if the value is not needed. */
|
||||
void lpRandomPair(unsigned char *lp, unsigned long total_count, listpackEntry *key, listpackEntry *val) {
|
||||
* 'val' can be NULL if the value is not needed.
|
||||
* 'tuple_len' indicates entry count of a single logical item. It should be 2
|
||||
* if listpack was saved as key-value pair or more for key-value-...(n_entries). */
|
||||
void lpRandomPair(unsigned char *lp, unsigned long total_count,
|
||||
listpackEntry *key, listpackEntry *val, int tuple_len)
|
||||
{
|
||||
unsigned char *p;
|
||||
|
||||
assert(tuple_len >= 2);
|
||||
|
||||
/* Avoid div by zero on corrupt listpack */
|
||||
assert(total_count);
|
||||
|
||||
/* Generate even numbers, because listpack saved K-V pair */
|
||||
int r = (rand() % total_count) * 2;
|
||||
int r = (rand() % total_count) * tuple_len;
|
||||
assert((p = lpSeek(lp, r)));
|
||||
key->sval = lpGetValue(p, &(key->slen), &(key->lval));
|
||||
|
||||
@@ -1466,26 +1686,31 @@ void lpRandomEntries(unsigned char *lp, unsigned int count, listpackEntry *entri
|
||||
/* Randomly select count of key value pairs and store into 'keys' and
|
||||
* 'vals' args. The order of the picked entries is random, and the selections
|
||||
* are non-unique (repetitions are possible).
|
||||
* The 'vals' arg can be NULL in which case we skip these. */
|
||||
void lpRandomPairs(unsigned char *lp, unsigned int count, listpackEntry *keys, listpackEntry *vals) {
|
||||
* The 'vals' arg can be NULL in which case we skip these.
|
||||
* 'tuple_len' indicates entry count of a single logical item. It should be 2
|
||||
* if listpack was saved as key-value pair or more for key-value-...(n_entries). */
|
||||
void lpRandomPairs(unsigned char *lp, unsigned int count, listpackEntry *keys, listpackEntry *vals, int tuple_len) {
|
||||
unsigned char *p, *key, *value;
|
||||
unsigned int klen = 0, vlen = 0;
|
||||
long long klval = 0, vlval = 0;
|
||||
|
||||
assert(tuple_len >= 2);
|
||||
|
||||
/* Notice: the index member must be first due to the use in uintCompare */
|
||||
typedef struct {
|
||||
unsigned int index;
|
||||
unsigned int order;
|
||||
} rand_pick;
|
||||
rand_pick *picks = lp_malloc(sizeof(rand_pick)*count);
|
||||
unsigned int total_size = lpLength(lp)/2;
|
||||
unsigned int total_size = lpLength(lp)/tuple_len;
|
||||
|
||||
/* Avoid div by zero on corrupt listpack */
|
||||
assert(total_size);
|
||||
|
||||
/* create a pool of random indexes (some may be duplicate). */
|
||||
for (unsigned int i = 0; i < count; i++) {
|
||||
picks[i].index = (rand() % total_size) * 2; /* Generate even indexes */
|
||||
/* Generate indexes that key exist at */
|
||||
picks[i].index = (rand() % total_size) * tuple_len;
|
||||
/* keep track of the order we picked them */
|
||||
picks[i].order = i;
|
||||
}
|
||||
@@ -1507,8 +1732,11 @@ void lpRandomPairs(unsigned char *lp, unsigned int count, listpackEntry *keys, l
|
||||
lpSaveValue(value, vlen, vlval, &vals[storeorder]);
|
||||
pickindex++;
|
||||
}
|
||||
lpindex += 2;
|
||||
p = lpNext(lp, p);
|
||||
lpindex += tuple_len;
|
||||
|
||||
for (int i = 0; i < tuple_len - 1; i++) {
|
||||
p = lpNext(lp, p);
|
||||
}
|
||||
}
|
||||
|
||||
lp_free(picks);
|
||||
@@ -1518,13 +1746,20 @@ void lpRandomPairs(unsigned char *lp, unsigned int count, listpackEntry *keys, l
|
||||
* 'vals' args. The selections are unique (no repetitions), and the order of
|
||||
* the picked entries is NOT-random.
|
||||
* The 'vals' arg can be NULL in which case we skip these.
|
||||
* 'tuple_len' indicates entry count of a single logical item. It should be 2
|
||||
* if listpack was saved as key-value pair or more for key-value-...(n_entries).
|
||||
* The return value is the number of items picked which can be lower than the
|
||||
* requested count if the listpack doesn't hold enough pairs. */
|
||||
unsigned int lpRandomPairsUnique(unsigned char *lp, unsigned int count, listpackEntry *keys, listpackEntry *vals) {
|
||||
unsigned int lpRandomPairsUnique(unsigned char *lp, unsigned int count,
|
||||
listpackEntry *keys, listpackEntry *vals,
|
||||
int tuple_len)
|
||||
{
|
||||
assert(tuple_len >= 2);
|
||||
|
||||
unsigned char *p, *key;
|
||||
unsigned int klen = 0;
|
||||
long long klval = 0;
|
||||
unsigned int total_size = lpLength(lp)/2;
|
||||
unsigned int total_size = lpLength(lp)/tuple_len;
|
||||
unsigned int index = 0;
|
||||
if (count > total_size)
|
||||
count = total_size;
|
||||
@@ -1532,7 +1767,7 @@ unsigned int lpRandomPairsUnique(unsigned char *lp, unsigned int count, listpack
|
||||
p = lpFirst(lp);
|
||||
unsigned int picked = 0, remaining = count;
|
||||
while (picked < count && p) {
|
||||
assert((p = lpNextRandom(lp, p, &index, remaining, 1)));
|
||||
assert((p = lpNextRandom(lp, p, &index, remaining, tuple_len)));
|
||||
key = lpGetValue(p, &klen, &klval);
|
||||
lpSaveValue(key, klen, klval, &keys[picked]);
|
||||
assert((p = lpNext(lp, p)));
|
||||
@@ -1554,8 +1789,9 @@ unsigned int lpRandomPairsUnique(unsigned char *lp, unsigned int count, listpack
|
||||
* the end of the list. The 'index' needs to be initialized according to the
|
||||
* current zero-based index matching the position of the starting element 'p'
|
||||
* and is updated to match the returned element's zero-based index. If
|
||||
* 'even_only' is nonzero, an element with an even index is picked, which is
|
||||
* useful if the listpack represents a key-value pair sequence.
|
||||
* 'tuple_len' indicates entry count of a single logical item. e.g. This is
|
||||
* useful if listpack represents key-value pairs. In this case, tuple_len should
|
||||
* be two and even indexes will be picked.
|
||||
*
|
||||
* Note that this function can return p. In order to skip the previously
|
||||
* returned element, you need to call lpNext() or lpDelete() after each call to
|
||||
@@ -1565,7 +1801,7 @@ unsigned int lpRandomPairsUnique(unsigned char *lp, unsigned int count, listpack
|
||||
* p = lpFirst(lp);
|
||||
* i = 0;
|
||||
* while (remaining > 0) {
|
||||
* p = lpNextRandom(lp, p, &i, remaining--, 0);
|
||||
* p = lpNextRandom(lp, p, &i, remaining--, 1);
|
||||
*
|
||||
* // ... Do stuff with p ...
|
||||
*
|
||||
@@ -1574,8 +1810,9 @@ unsigned int lpRandomPairsUnique(unsigned char *lp, unsigned int count, listpack
|
||||
* }
|
||||
*/
|
||||
unsigned char *lpNextRandom(unsigned char *lp, unsigned char *p, unsigned int *index,
|
||||
unsigned int remaining, int even_only)
|
||||
unsigned int remaining, int tuple_len)
|
||||
{
|
||||
assert(tuple_len > 0);
|
||||
/* To only iterate once, every time we try to pick a member, the probability
|
||||
* we pick it is the quotient of the count left we want to pick and the
|
||||
* count still we haven't visited. This way, we could make every member be
|
||||
@@ -1583,15 +1820,14 @@ unsigned char *lpNextRandom(unsigned char *lp, unsigned char *p, unsigned int *i
|
||||
unsigned int i = *index;
|
||||
unsigned int total_size = lpLength(lp);
|
||||
while (i < total_size && p != NULL) {
|
||||
if (even_only && i % 2 != 0) {
|
||||
if (i % tuple_len != 0) {
|
||||
p = lpNext(lp, p);
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Do we pick this element? */
|
||||
unsigned int available = total_size - i;
|
||||
if (even_only) available /= 2;
|
||||
unsigned int available = (total_size - i) / tuple_len;
|
||||
double randomDouble = ((double)rand()) / RAND_MAX;
|
||||
double threshold = ((double)remaining) / available;
|
||||
if (randomDouble <= threshold) {
|
||||
@@ -1787,6 +2023,24 @@ static int lpValidation(unsigned char *p, unsigned int head_count, void *userdat
|
||||
return ret;
|
||||
}
|
||||
|
||||
static int lpFindCbCmp(const unsigned char *lp, unsigned char *p, void *user, unsigned char *s, long long slen) {
|
||||
assert(lp);
|
||||
assert(p);
|
||||
|
||||
char *n = user;
|
||||
|
||||
if (!s) {
|
||||
int64_t sval;
|
||||
if (lpStringToInt64((const char*)n, strlen(n), &sval))
|
||||
return slen == sval ? 0 : 1;
|
||||
} else {
|
||||
if (strlen(n) == (size_t) slen && memcmp(n, s, slen) == 0)
|
||||
return 0;
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
int listpackTest(int argc, char *argv[], int flags) {
|
||||
UNUSED(argc);
|
||||
UNUSED(argv);
|
||||
@@ -2031,6 +2285,111 @@ int listpackTest(int argc, char *argv[], int flags) {
|
||||
zfree(lp);
|
||||
}
|
||||
|
||||
TEST("Batch append") {
|
||||
listpackEntry ent[6] = {
|
||||
{.sval = (unsigned char*)mixlist[0], .slen = strlen(mixlist[0])},
|
||||
{.sval = (unsigned char*)mixlist[1], .slen = strlen(mixlist[1])},
|
||||
{.sval = (unsigned char*)mixlist[2], .slen = strlen(mixlist[2])},
|
||||
{.lval = 4294967296},
|
||||
{.sval = (unsigned char*)mixlist[3], .slen = strlen(mixlist[3])},
|
||||
{.lval = -100}
|
||||
};
|
||||
|
||||
lp = lpNew(0);
|
||||
lp = lpBatchAppend(lp, ent, 2);
|
||||
verifyEntry(lpSeek(lp, 0), ent[0].sval, ent[0].slen);
|
||||
verifyEntry(lpSeek(lp, 1), ent[1].sval, ent[1].slen);
|
||||
assert(lpLength(lp) == 2);
|
||||
|
||||
lp = lpBatchAppend(lp, &ent[2], 1);
|
||||
verifyEntry(lpSeek(lp, 0), ent[0].sval, ent[0].slen);
|
||||
verifyEntry(lpSeek(lp, 1), ent[1].sval, ent[1].slen);
|
||||
verifyEntry(lpSeek(lp, 2), ent[2].sval, ent[2].slen);
|
||||
assert(lpLength(lp) == 3);
|
||||
|
||||
lp = lpDeleteRange(lp, 1, 1);
|
||||
verifyEntry(lpSeek(lp, 0), ent[0].sval, ent[0].slen);
|
||||
verifyEntry(lpSeek(lp, 1), ent[2].sval, ent[2].slen);
|
||||
assert(lpLength(lp) == 2);
|
||||
|
||||
lp = lpBatchAppend(lp, &ent[3], 3);
|
||||
verifyEntry(lpSeek(lp, 0), ent[0].sval, ent[0].slen);
|
||||
verifyEntry(lpSeek(lp, 1), ent[2].sval, ent[2].slen);
|
||||
verifyEntry(lpSeek(lp, 2), (unsigned char*) "4294967296", 10);
|
||||
verifyEntry(lpSeek(lp, 3), ent[4].sval, ent[4].slen);
|
||||
verifyEntry(lpSeek(lp, 4), (unsigned char*) "-100", 4);
|
||||
assert(lpLength(lp) == 5);
|
||||
|
||||
lp = lpDeleteRange(lp, 1, 3);
|
||||
verifyEntry(lpSeek(lp, 0), ent[0].sval, ent[0].slen);
|
||||
verifyEntry(lpSeek(lp, 1), (unsigned char*) "-100", 4);
|
||||
assert(lpLength(lp) == 2);
|
||||
|
||||
lpFree(lp);
|
||||
}
|
||||
|
||||
TEST("Batch insert") {
|
||||
lp = lpNew(0);
|
||||
listpackEntry ent[6] = {
|
||||
{.sval = (unsigned char*)mixlist[0], .slen = strlen(mixlist[0])},
|
||||
{.sval = (unsigned char*)mixlist[1], .slen = strlen(mixlist[1])},
|
||||
{.sval = (unsigned char*)mixlist[2], .slen = strlen(mixlist[2])},
|
||||
{.lval = 4294967296},
|
||||
{.sval = (unsigned char*)mixlist[3], .slen = strlen(mixlist[3])},
|
||||
{.lval = -100}
|
||||
};
|
||||
|
||||
lp = lpBatchAppend(lp, ent, 4);
|
||||
assert(lpLength(lp) == 4);
|
||||
verifyEntry(lpSeek(lp, 0), ent[0].sval, ent[0].slen);
|
||||
verifyEntry(lpSeek(lp, 1), ent[1].sval, ent[1].slen);
|
||||
verifyEntry(lpSeek(lp, 2), ent[2].sval, ent[2].slen);
|
||||
verifyEntry(lpSeek(lp, 3), (unsigned char*)"4294967296", 10);
|
||||
|
||||
/* Insert with LP_BEFORE */
|
||||
p = lpSeek(lp, 3);
|
||||
lp = lpBatchInsert(lp, p, LP_BEFORE, &ent[4], 2, &p);
|
||||
verifyEntry(p, (unsigned char*)"-100", 4);
|
||||
assert(lpLength(lp) == 6);
|
||||
verifyEntry(lpSeek(lp, 0), ent[0].sval, ent[0].slen);
|
||||
verifyEntry(lpSeek(lp, 1), ent[1].sval, ent[1].slen);
|
||||
verifyEntry(lpSeek(lp, 2), ent[2].sval, ent[2].slen);
|
||||
verifyEntry(lpSeek(lp, 3), ent[4].sval, ent[4].slen);
|
||||
verifyEntry(lpSeek(lp, 4), (unsigned char*)"-100", 4);
|
||||
verifyEntry(lpSeek(lp, 5), (unsigned char*)"4294967296", 10);
|
||||
|
||||
lp = lpDeleteRange(lp, 1, 2);
|
||||
assert(lpLength(lp) == 4);
|
||||
verifyEntry(lpSeek(lp, 0), ent[0].sval, ent[0].slen);
|
||||
verifyEntry(lpSeek(lp, 1), ent[4].sval, ent[4].slen);
|
||||
verifyEntry(lpSeek(lp, 2), (unsigned char*)"-100", 4);
|
||||
verifyEntry(lpSeek(lp, 3), (unsigned char*)"4294967296", 10);
|
||||
|
||||
/* Insert with LP_AFTER */
|
||||
p = lpSeek(lp, 0);
|
||||
lp = lpBatchInsert(lp, p, LP_AFTER, &ent[1], 2, &p);
|
||||
verifyEntry(p, ent[2].sval, ent[2].slen);
|
||||
assert(lpLength(lp) == 6);
|
||||
verifyEntry(lpSeek(lp, 0), ent[0].sval, ent[0].slen);
|
||||
verifyEntry(lpSeek(lp, 1), ent[1].sval, ent[1].slen);
|
||||
verifyEntry(lpSeek(lp, 2), ent[2].sval, ent[2].slen);
|
||||
verifyEntry(lpSeek(lp, 3), ent[4].sval, ent[4].slen);
|
||||
verifyEntry(lpSeek(lp, 4), (unsigned char*)"-100", 4);
|
||||
verifyEntry(lpSeek(lp, 5), (unsigned char*)"4294967296", 10);
|
||||
|
||||
lp = lpDeleteRange(lp, 2, 4);
|
||||
assert(lpLength(lp) == 2);
|
||||
p = lpSeek(lp, 1);
|
||||
lp = lpBatchInsert(lp, p, LP_AFTER, &ent[2], 1, &p);
|
||||
verifyEntry(p, ent[2].sval, ent[2].slen);
|
||||
assert(lpLength(lp) == 3);
|
||||
verifyEntry(lpSeek(lp, 0), ent[0].sval, ent[0].slen);
|
||||
verifyEntry(lpSeek(lp, 1), ent[1].sval, ent[1].slen);
|
||||
verifyEntry(lpSeek(lp, 2), ent[2].sval, ent[2].slen);
|
||||
|
||||
lpFree(lp);
|
||||
}
|
||||
|
||||
TEST("Batch delete") {
|
||||
unsigned char *lp = createList(); /* char *mixlist[] = {"hello", "foo", "quux", "1024"} */
|
||||
assert(lpLength(lp) == 4); /* Pre-condition */
|
||||
@@ -2210,7 +2569,7 @@ int listpackTest(int argc, char *argv[], int flags) {
|
||||
unsigned index = 0;
|
||||
while (remaining > 0) {
|
||||
assert(p != NULL);
|
||||
p = lpNextRandom(lp, p, &index, remaining--, 0);
|
||||
p = lpNextRandom(lp, p, &index, remaining--, 1);
|
||||
assert(p != NULL);
|
||||
assert(p != prev);
|
||||
prev = p;
|
||||
@@ -2226,7 +2585,7 @@ int listpackTest(int argc, char *argv[], int flags) {
|
||||
unsigned i = 0;
|
||||
|
||||
/* Pick from empty listpack returns NULL. */
|
||||
assert(lpNextRandom(lp, NULL, &i, 2, 0) == NULL);
|
||||
assert(lpNextRandom(lp, NULL, &i, 2, 1) == NULL);
|
||||
|
||||
/* Add some elements and find their pointers within the listpack. */
|
||||
lp = lpAppend(lp, (unsigned char *)"abc", 3);
|
||||
@@ -2239,19 +2598,19 @@ int listpackTest(int argc, char *argv[], int flags) {
|
||||
assert(lpNext(lp, p2) == NULL);
|
||||
|
||||
/* Pick zero elements returns NULL. */
|
||||
i = 0; assert(lpNextRandom(lp, lpFirst(lp), &i, 0, 0) == NULL);
|
||||
i = 0; assert(lpNextRandom(lp, lpFirst(lp), &i, 0, 1) == NULL);
|
||||
|
||||
/* Pick all returns all. */
|
||||
i = 0; assert(lpNextRandom(lp, p0, &i, 3, 0) == p0 && i == 0);
|
||||
i = 1; assert(lpNextRandom(lp, p1, &i, 2, 0) == p1 && i == 1);
|
||||
i = 2; assert(lpNextRandom(lp, p2, &i, 1, 0) == p2 && i == 2);
|
||||
i = 0; assert(lpNextRandom(lp, p0, &i, 3, 1) == p0 && i == 0);
|
||||
i = 1; assert(lpNextRandom(lp, p1, &i, 2, 1) == p1 && i == 1);
|
||||
i = 2; assert(lpNextRandom(lp, p2, &i, 1, 1) == p2 && i == 2);
|
||||
|
||||
/* Pick more than one when there's only one left returns the last one. */
|
||||
i = 2; assert(lpNextRandom(lp, p2, &i, 42, 0) == p2 && i == 2);
|
||||
i = 2; assert(lpNextRandom(lp, p2, &i, 42, 1) == p2 && i == 2);
|
||||
|
||||
/* Pick all even elements returns p0 and p2. */
|
||||
i = 0; assert(lpNextRandom(lp, p0, &i, 10, 1) == p0 && i == 0);
|
||||
i = 1; assert(lpNextRandom(lp, p1, &i, 10, 1) == p2 && i == 2);
|
||||
i = 0; assert(lpNextRandom(lp, p0, &i, 10, 2) == p0 && i == 0);
|
||||
i = 1; assert(lpNextRandom(lp, p1, &i, 10, 2) == p2 && i == 2);
|
||||
|
||||
/* Don't crash even for bad index. */
|
||||
for (int j = 0; j < 100; j++) {
|
||||
@@ -2264,7 +2623,7 @@ int listpackTest(int argc, char *argv[], int flags) {
|
||||
}
|
||||
i = j % 7;
|
||||
unsigned int remaining = j % 5;
|
||||
p = lpNextRandom(lp, p, &i, remaining, 0);
|
||||
p = lpNextRandom(lp, p, &i, remaining, 1);
|
||||
assert(p == p0 || p == p1 || p == p2 || p == NULL);
|
||||
}
|
||||
lpFree(lp);
|
||||
@@ -2275,7 +2634,7 @@ int listpackTest(int argc, char *argv[], int flags) {
|
||||
unsigned char *lp = lpNew(0);
|
||||
lp = lpAppend(lp, (unsigned char*)"abc", 3);
|
||||
lp = lpAppend(lp, (unsigned char*)"123", 3);
|
||||
lpRandomPair(lp, 1, &key, &val);
|
||||
lpRandomPair(lp, 1, &key, &val, 2);
|
||||
assert(memcmp(key.sval, "abc", key.slen) == 0);
|
||||
assert(val.lval == 123);
|
||||
lpFree(lp);
|
||||
@@ -2288,7 +2647,7 @@ int listpackTest(int argc, char *argv[], int flags) {
|
||||
lp = lpAppend(lp, (unsigned char*)"123", 3);
|
||||
lp = lpAppend(lp, (unsigned char*)"456", 3);
|
||||
lp = lpAppend(lp, (unsigned char*)"def", 3);
|
||||
lpRandomPair(lp, 2, &key, &val);
|
||||
lpRandomPair(lp, 2, &key, &val, 2);
|
||||
if (key.sval) {
|
||||
assert(!memcmp(key.sval, "abc", key.slen));
|
||||
assert(key.slen == 3);
|
||||
@@ -2301,6 +2660,42 @@ int listpackTest(int argc, char *argv[], int flags) {
|
||||
lpFree(lp);
|
||||
}
|
||||
|
||||
TEST("Random pair with tuple_len 3") {
|
||||
listpackEntry key, val;
|
||||
unsigned char *lp = lpNew(0);
|
||||
lp = lpAppend(lp, (unsigned char*)"abc", 3);
|
||||
lp = lpAppend(lp, (unsigned char*)"123", 3);
|
||||
lp = lpAppend(lp, (unsigned char*)"xxx", 3);
|
||||
lp = lpAppend(lp, (unsigned char*)"456", 3);
|
||||
lp = lpAppend(lp, (unsigned char*)"def", 3);
|
||||
lp = lpAppend(lp, (unsigned char*)"xxx", 3);
|
||||
lp = lpAppend(lp, (unsigned char*)"281474976710655", 15);
|
||||
lp = lpAppend(lp, (unsigned char*)"789", 3);
|
||||
lp = lpAppend(lp, (unsigned char*)"xxx", 3);
|
||||
|
||||
for (int i = 0; i < 5; i++) {
|
||||
lpRandomPair(lp, 3, &key, &val, 3);
|
||||
if (key.sval) {
|
||||
if (!memcmp(key.sval, "abc", key.slen)) {
|
||||
assert(key.slen == 3);
|
||||
assert(val.lval == 123);
|
||||
} else {
|
||||
assert(0);
|
||||
};
|
||||
}
|
||||
if (!key.sval) {
|
||||
if (key.lval == 456)
|
||||
assert(!memcmp(val.sval, "def", val.slen));
|
||||
else if (key.lval == 281474976710655LL)
|
||||
assert(val.lval == 789);
|
||||
else
|
||||
assert(0);
|
||||
}
|
||||
}
|
||||
|
||||
lpFree(lp);
|
||||
}
|
||||
|
||||
TEST("Random pairs with one element") {
|
||||
int count = 5;
|
||||
unsigned char *lp = lpNew(0);
|
||||
@@ -2309,7 +2704,7 @@ int listpackTest(int argc, char *argv[], int flags) {
|
||||
|
||||
lp = lpAppend(lp, (unsigned char*)"abc", 3);
|
||||
lp = lpAppend(lp, (unsigned char*)"123", 3);
|
||||
lpRandomPairs(lp, count, keys, vals);
|
||||
lpRandomPairs(lp, count, keys, vals, 2);
|
||||
assert(memcmp(keys[4].sval, "abc", keys[4].slen) == 0);
|
||||
assert(vals[4].lval == 123);
|
||||
zfree(keys);
|
||||
@@ -2327,7 +2722,7 @@ int listpackTest(int argc, char *argv[], int flags) {
|
||||
lp = lpAppend(lp, (unsigned char*)"123", 3);
|
||||
lp = lpAppend(lp, (unsigned char*)"456", 3);
|
||||
lp = lpAppend(lp, (unsigned char*)"def", 3);
|
||||
lpRandomPairs(lp, count, keys, vals);
|
||||
lpRandomPairs(lp, count, keys, vals, 2);
|
||||
for (int i = 0; i < count; i++) {
|
||||
if (keys[i].sval) {
|
||||
assert(!memcmp(keys[i].sval, "abc", keys[i].slen));
|
||||
@@ -2344,6 +2739,47 @@ int listpackTest(int argc, char *argv[], int flags) {
|
||||
lpFree(lp);
|
||||
}
|
||||
|
||||
TEST("Random pairs with many elements and tuple_len 3") {
|
||||
int count = 5;
|
||||
lp = lpNew(0);
|
||||
listpackEntry *keys = zcalloc(sizeof(listpackEntry) * count);
|
||||
listpackEntry *vals = zcalloc(sizeof(listpackEntry) * count);
|
||||
|
||||
lp = lpAppend(lp, (unsigned char*)"abc", 3);
|
||||
lp = lpAppend(lp, (unsigned char*)"123", 3);
|
||||
lp = lpAppend(lp, (unsigned char*)"xxx", 3);
|
||||
lp = lpAppend(lp, (unsigned char*)"456", 3);
|
||||
lp = lpAppend(lp, (unsigned char*)"def", 3);
|
||||
lp = lpAppend(lp, (unsigned char*)"xxx", 3);
|
||||
lp = lpAppend(lp, (unsigned char*)"281474976710655", 15);
|
||||
lp = lpAppend(lp, (unsigned char*)"789", 3);
|
||||
lp = lpAppend(lp, (unsigned char*)"xxx", 3);
|
||||
|
||||
lpRandomPairs(lp, count, keys, vals, 3);
|
||||
for (int i = 0; i < count; i++) {
|
||||
if (keys[i].sval) {
|
||||
if (!memcmp(keys[i].sval, "abc", keys[i].slen)) {
|
||||
assert(keys[i].slen == 3);
|
||||
assert(vals[i].lval == 123);
|
||||
} else {
|
||||
assert(0);
|
||||
};
|
||||
}
|
||||
if (!keys[i].sval) {
|
||||
if (keys[i].lval == 456)
|
||||
assert(!memcmp(vals[i].sval, "def", vals[i].slen));
|
||||
else if (keys[i].lval == 281474976710655LL)
|
||||
assert(vals[i].lval == 789);
|
||||
else
|
||||
assert(0);
|
||||
}
|
||||
}
|
||||
|
||||
zfree(keys);
|
||||
zfree(vals);
|
||||
lpFree(lp);
|
||||
}
|
||||
|
||||
TEST("Random pairs unique with one element") {
|
||||
unsigned picked;
|
||||
int count = 5;
|
||||
@@ -2353,7 +2789,7 @@ int listpackTest(int argc, char *argv[], int flags) {
|
||||
|
||||
lp = lpAppend(lp, (unsigned char*)"abc", 3);
|
||||
lp = lpAppend(lp, (unsigned char*)"123", 3);
|
||||
picked = lpRandomPairsUnique(lp, count, keys, vals);
|
||||
picked = lpRandomPairsUnique(lp, count, keys, vals, 2);
|
||||
assert(picked == 1);
|
||||
assert(memcmp(keys[0].sval, "abc", keys[0].slen) == 0);
|
||||
assert(vals[0].lval == 123);
|
||||
@@ -2373,7 +2809,7 @@ int listpackTest(int argc, char *argv[], int flags) {
|
||||
lp = lpAppend(lp, (unsigned char*)"123", 3);
|
||||
lp = lpAppend(lp, (unsigned char*)"456", 3);
|
||||
lp = lpAppend(lp, (unsigned char*)"def", 3);
|
||||
picked = lpRandomPairsUnique(lp, count, keys, vals);
|
||||
picked = lpRandomPairsUnique(lp, count, keys, vals, 2);
|
||||
assert(picked == 2);
|
||||
for (int i = 0; i < 2; i++) {
|
||||
if (keys[i].sval) {
|
||||
@@ -2391,6 +2827,47 @@ int listpackTest(int argc, char *argv[], int flags) {
|
||||
lpFree(lp);
|
||||
}
|
||||
|
||||
TEST("Random pairs unique with many elements and tuple_len 3") {
|
||||
unsigned picked;
|
||||
int count = 5;
|
||||
lp = lpNew(0);
|
||||
listpackEntry *keys = zmalloc(sizeof(listpackEntry) * count);
|
||||
listpackEntry *vals = zmalloc(sizeof(listpackEntry) * count);
|
||||
|
||||
lp = lpAppend(lp, (unsigned char*)"abc", 3);
|
||||
lp = lpAppend(lp, (unsigned char*)"123", 3);
|
||||
lp = lpAppend(lp, (unsigned char*)"xxx", 3);
|
||||
lp = lpAppend(lp, (unsigned char*)"456", 3);
|
||||
lp = lpAppend(lp, (unsigned char*)"def", 3);
|
||||
lp = lpAppend(lp, (unsigned char*)"xxx", 3);
|
||||
lp = lpAppend(lp, (unsigned char*)"281474976710655", 15);
|
||||
lp = lpAppend(lp, (unsigned char*)"789", 3);
|
||||
lp = lpAppend(lp, (unsigned char*)"xxx", 3);
|
||||
picked = lpRandomPairsUnique(lp, count, keys, vals, 3);
|
||||
assert(picked == 3);
|
||||
for (int i = 0; i < 3; i++) {
|
||||
if (keys[i].sval) {
|
||||
if (!memcmp(keys[i].sval, "abc", keys[i].slen)) {
|
||||
assert(keys[i].slen == 3);
|
||||
assert(vals[i].lval == 123);
|
||||
} else {
|
||||
assert(0);
|
||||
};
|
||||
}
|
||||
if (!keys[i].sval) {
|
||||
if (keys[i].lval == 456)
|
||||
assert(!memcmp(vals[i].sval, "def", vals[i].slen));
|
||||
else if (keys[i].lval == 281474976710655LL)
|
||||
assert(vals[i].lval == 789);
|
||||
else
|
||||
assert(0);
|
||||
}
|
||||
}
|
||||
zfree(keys);
|
||||
zfree(vals);
|
||||
lpFree(lp);
|
||||
}
|
||||
|
||||
TEST("push various encodings") {
|
||||
lp = lpNew(0);
|
||||
|
||||
@@ -2449,6 +2926,21 @@ int listpackTest(int argc, char *argv[], int flags) {
|
||||
lpFree(lp);
|
||||
}
|
||||
|
||||
TEST("Test lpFindCb") {
|
||||
lp = createList(); /* "hello", "foo", "quux", "1024" */
|
||||
assert(lpFindCb(lp, lpFirst(lp), "abc", lpFindCbCmp, 0) == NULL);
|
||||
verifyEntry(lpFindCb(lp, NULL, "hello", lpFindCbCmp, 0), (unsigned char*)"hello", 5);
|
||||
verifyEntry(lpFindCb(lp, NULL, "1024", lpFindCbCmp, 0), (unsigned char*)"1024", 4);
|
||||
verifyEntry(lpFindCb(lp, NULL, "quux", lpFindCbCmp, 0), (unsigned char*)"quux", 4);
|
||||
verifyEntry(lpFindCb(lp, NULL, "foo", lpFindCbCmp, 0), (unsigned char*)"foo", 3);
|
||||
lpFree(lp);
|
||||
|
||||
lp = lpNew(0);
|
||||
assert(lpFindCb(lp, lpFirst(lp), "hello", lpFindCbCmp, 0) == NULL);
|
||||
assert(lpFindCb(lp, lpFirst(lp), "1024", lpFindCbCmp, 0) == NULL);
|
||||
lpFree(lp);
|
||||
}
|
||||
|
||||
TEST("Test lpValidateIntegrity") {
|
||||
lp = createList();
|
||||
long count = 0;
|
||||
@@ -2471,6 +2963,26 @@ int listpackTest(int argc, char *argv[], int flags) {
|
||||
lpFree(lp);
|
||||
}
|
||||
|
||||
TEST("Test number of elements exceeds LP_HDR_NUMELE_UNKNOWN with batch insert") {
|
||||
listpackEntry ent[2] = {
|
||||
{.sval = (unsigned char*)mixlist[0], .slen = strlen(mixlist[0])},
|
||||
{.sval = (unsigned char*)mixlist[1], .slen = strlen(mixlist[1])}
|
||||
};
|
||||
|
||||
lp = lpNew(0);
|
||||
for (int i = 0; i < (LP_HDR_NUMELE_UNKNOWN/2) + 1; i++)
|
||||
lp = lpBatchAppend(lp, ent, 2);
|
||||
|
||||
assert(lpGetNumElements(lp) == LP_HDR_NUMELE_UNKNOWN);
|
||||
assert(lpLength(lp) == LP_HDR_NUMELE_UNKNOWN+1);
|
||||
|
||||
lp = lpDeleteRange(lp, -2, 2);
|
||||
assert(lpGetNumElements(lp) == LP_HDR_NUMELE_UNKNOWN);
|
||||
assert(lpLength(lp) == LP_HDR_NUMELE_UNKNOWN-1);
|
||||
assert(lpGetNumElements(lp) == LP_HDR_NUMELE_UNKNOWN-1); /* update length after lpLength */
|
||||
lpFree(lp);
|
||||
}
|
||||
|
||||
TEST("Stress with random payloads of different encoding") {
|
||||
unsigned long long start = usec();
|
||||
int i,j,len,where;
|
||||
|
||||
+14
-4
@@ -49,18 +49,25 @@ unsigned char *lpReplaceInteger(unsigned char *lp, unsigned char **p, long long
|
||||
unsigned char *lpDelete(unsigned char *lp, unsigned char *p, unsigned char **newp);
|
||||
unsigned char *lpDeleteRangeWithEntry(unsigned char *lp, unsigned char **p, unsigned long num);
|
||||
unsigned char *lpDeleteRange(unsigned char *lp, long index, unsigned long num);
|
||||
unsigned char *lpBatchAppend(unsigned char *lp, listpackEntry *entries, unsigned long len);
|
||||
unsigned char *lpBatchInsert(unsigned char *lp, unsigned char *p, int where,
|
||||
listpackEntry *entries, unsigned int len, unsigned char **newp);
|
||||
unsigned char *lpBatchDelete(unsigned char *lp, unsigned char **ps, unsigned long count);
|
||||
unsigned char *lpMerge(unsigned char **first, unsigned char **second);
|
||||
unsigned char *lpDup(unsigned char *lp);
|
||||
unsigned long lpLength(unsigned char *lp);
|
||||
unsigned char *lpGet(unsigned char *p, int64_t *count, unsigned char *intbuf);
|
||||
unsigned char *lpGetValue(unsigned char *p, unsigned int *slen, long long *lval);
|
||||
int lpGetIntegerValue(unsigned char *p, long long *lval);
|
||||
unsigned char *lpFind(unsigned char *lp, unsigned char *p, unsigned char *s, uint32_t slen, unsigned int skip);
|
||||
typedef int (*lpCmp)(const unsigned char *lp, unsigned char *p, void *user, unsigned char *s, long long slen);
|
||||
unsigned char *lpFindCb(unsigned char *lp, unsigned char *p, void *user, lpCmp cmp, unsigned int skip);
|
||||
unsigned char *lpFirst(unsigned char *lp);
|
||||
unsigned char *lpLast(unsigned char *lp);
|
||||
unsigned char *lpNext(unsigned char *lp, unsigned char *p);
|
||||
unsigned char *lpPrev(unsigned char *lp, unsigned char *p);
|
||||
size_t lpBytes(unsigned char *lp);
|
||||
size_t lpEntrySizeInteger(long long lval);
|
||||
size_t lpEstimateBytesRepeatedInteger(long long lval, unsigned long rep);
|
||||
unsigned char *lpSeek(unsigned char *lp, long index);
|
||||
typedef int (*listpackValidateEntryCB)(unsigned char *p, unsigned int head_count, void *userdata);
|
||||
@@ -69,12 +76,15 @@ int lpValidateIntegrity(unsigned char *lp, size_t size, int deep,
|
||||
unsigned char *lpValidateFirst(unsigned char *lp);
|
||||
int lpValidateNext(unsigned char *lp, unsigned char **pp, size_t lpbytes);
|
||||
unsigned int lpCompare(unsigned char *p, unsigned char *s, uint32_t slen);
|
||||
void lpRandomPair(unsigned char *lp, unsigned long total_count, listpackEntry *key, listpackEntry *val);
|
||||
void lpRandomPairs(unsigned char *lp, unsigned int count, listpackEntry *keys, listpackEntry *vals);
|
||||
unsigned int lpRandomPairsUnique(unsigned char *lp, unsigned int count, listpackEntry *keys, listpackEntry *vals);
|
||||
void lpRandomPair(unsigned char *lp, unsigned long total_count,
|
||||
listpackEntry *key, listpackEntry *val, int tuple_len);
|
||||
void lpRandomPairs(unsigned char *lp, unsigned int count,
|
||||
listpackEntry *keys, listpackEntry *vals, int tuple_len);
|
||||
unsigned int lpRandomPairsUnique(unsigned char *lp, unsigned int count,
|
||||
listpackEntry *keys, listpackEntry *vals, int tuple_len);
|
||||
void lpRandomEntries(unsigned char *lp, unsigned int count, listpackEntry *entries);
|
||||
unsigned char *lpNextRandom(unsigned char *lp, unsigned char *p, unsigned int *index,
|
||||
unsigned int remaining, int even_only);
|
||||
unsigned int remaining, int tuple_len);
|
||||
int lpSafeToAdd(unsigned char* lp, size_t add);
|
||||
void lpRepr(unsigned char *lp);
|
||||
|
||||
|
||||
+26
-11
@@ -745,7 +745,7 @@ int moduleDelKeyIfEmpty(RedisModuleKey *key) {
|
||||
case OBJ_LIST: isempty = listTypeLength(o) == 0; break;
|
||||
case OBJ_SET: isempty = setTypeSize(o) == 0; break;
|
||||
case OBJ_ZSET: isempty = zsetLength(o) == 0; break;
|
||||
case OBJ_HASH: isempty = hashTypeLength(o) == 0; break;
|
||||
case OBJ_HASH: isempty = hashTypeLength(o, 0) == 0; break;
|
||||
case OBJ_STREAM: isempty = streamLength(o) == 0; break;
|
||||
default: isempty = 0;
|
||||
}
|
||||
@@ -4168,7 +4168,7 @@ size_t RM_ValueLength(RedisModuleKey *key) {
|
||||
case OBJ_LIST: return listTypeLength(key->value);
|
||||
case OBJ_SET: return setTypeSize(key->value);
|
||||
case OBJ_ZSET: return zsetLength(key->value);
|
||||
case OBJ_HASH: return hashTypeLength(key->value);
|
||||
case OBJ_HASH: return hashTypeLength(key->value, 0); /* OPEN: To subtract expired fields? */
|
||||
case OBJ_STREAM: return streamLength(key->value);
|
||||
default: return 0;
|
||||
}
|
||||
@@ -5271,7 +5271,10 @@ int RM_HashSet(RedisModuleKey *key, int flags, ...) {
|
||||
|
||||
/* Handle XX and NX */
|
||||
if (flags & (REDISMODULE_HASH_XX|REDISMODULE_HASH_NX)) {
|
||||
int exists = hashTypeExists(key->value, field->ptr);
|
||||
int isHashDeleted;
|
||||
int exists = hashTypeExists(key->db, key->value, field->ptr, &isHashDeleted);
|
||||
/* hash-field-expiration is not exposed to modules */
|
||||
serverAssert(isHashDeleted == 0);
|
||||
if (((flags & REDISMODULE_HASH_XX) && !exists) ||
|
||||
((flags & REDISMODULE_HASH_NX) && exists))
|
||||
{
|
||||
@@ -5282,7 +5285,7 @@ int RM_HashSet(RedisModuleKey *key, int flags, ...) {
|
||||
|
||||
/* Handle deletion if value is REDISMODULE_HASH_DELETE. */
|
||||
if (value == REDISMODULE_HASH_DELETE) {
|
||||
count += hashTypeDelete(key->value, field->ptr);
|
||||
count += hashTypeDelete(key->value, field->ptr, 1);
|
||||
if (flags & REDISMODULE_HASH_CFIELDS) decrRefCount(field);
|
||||
continue;
|
||||
}
|
||||
@@ -5295,8 +5298,8 @@ int RM_HashSet(RedisModuleKey *key, int flags, ...) {
|
||||
low_flags |= HASH_SET_TAKE_FIELD;
|
||||
|
||||
robj *argv[2] = {field,value};
|
||||
hashTypeTryConversion(key->value,argv,0,1);
|
||||
int updated = hashTypeSet(key->value, field->ptr, value->ptr, low_flags);
|
||||
hashTypeTryConversion(key->db,key->value,argv,0,1);
|
||||
int updated = hashTypeSet(key->db, key->value, field->ptr, value->ptr, low_flags);
|
||||
count += (flags & REDISMODULE_HASH_COUNT_ALL) ? 1 : updated;
|
||||
|
||||
/* If CFIELDS is active, SDS string ownership is now of hashTypeSet(),
|
||||
@@ -5374,14 +5377,22 @@ int RM_HashGet(RedisModuleKey *key, int flags, ...) {
|
||||
/* Query the hash for existence or value object. */
|
||||
if (flags & REDISMODULE_HASH_EXISTS) {
|
||||
existsptr = va_arg(ap,int*);
|
||||
if (key->value)
|
||||
*existsptr = hashTypeExists(key->value,field->ptr);
|
||||
else
|
||||
if (key->value) {
|
||||
int isHashDeleted;
|
||||
*existsptr = hashTypeExists(key->db, key->value, field->ptr, &isHashDeleted);
|
||||
/* hash-field-expiration is not exposed to modules */
|
||||
serverAssert(isHashDeleted == 0);
|
||||
} else {
|
||||
*existsptr = 0;
|
||||
}
|
||||
} else {
|
||||
int isHashDeleted;
|
||||
valueptr = va_arg(ap,RedisModuleString**);
|
||||
if (key->value) {
|
||||
*valueptr = hashTypeGetValueObject(key->value,field->ptr);
|
||||
*valueptr = hashTypeGetValueObject(key->db,key->value,field->ptr, &isHashDeleted);
|
||||
|
||||
/* Currently hash-field-expiration is not exposed to modules */
|
||||
serverAssert(isHashDeleted == 0);
|
||||
if (*valueptr) {
|
||||
robj *decoded = getDecodedObject(*valueptr);
|
||||
decrRefCount(*valueptr);
|
||||
@@ -11071,18 +11082,22 @@ static void moduleScanKeyCallback(void *privdata, const dictEntry *de) {
|
||||
ScanKeyCBData *data = privdata;
|
||||
sds key = dictGetKey(de);
|
||||
robj *o = data->key->value;
|
||||
robj *field = createStringObject(key, sdslen(key));
|
||||
robj *field = NULL;
|
||||
robj *value = NULL;
|
||||
if (o->type == OBJ_SET) {
|
||||
value = NULL;
|
||||
} else if (o->type == OBJ_HASH) {
|
||||
sds val = dictGetVal(de);
|
||||
field = createStringObject(key, hfieldlen(key));
|
||||
value = createStringObject(val, sdslen(val));
|
||||
} else if (o->type == OBJ_ZSET) {
|
||||
double *val = (double*)dictGetVal(de);
|
||||
value = createStringObjectFromLongDouble(*val, 0);
|
||||
}
|
||||
|
||||
/* if type is OBJ_HASH then key is of type hfield. Otherwise sds. */
|
||||
if (!field) field = createStringObject(key, sdslen(key));
|
||||
|
||||
data->fn(data->key, field, value, data->user_data);
|
||||
decrRefCount(field);
|
||||
if (value) decrRefCount(value);
|
||||
|
||||
+524
@@ -0,0 +1,524 @@
|
||||
/*
|
||||
* Copyright Redis Ltd. 2024 - present
|
||||
*
|
||||
* Licensed under your choice of the Redis Source Available License 2.0 (RSALv2)
|
||||
* or the Server Side Public License v1 (SSPLv1).
|
||||
*/
|
||||
|
||||
#include <string.h>
|
||||
#include <assert.h>
|
||||
#include "sdsalloc.h"
|
||||
#include "mstr.h"
|
||||
#include "stdio.h"
|
||||
|
||||
#define NULL_SIZE 1
|
||||
|
||||
static inline char mstrReqType(size_t string_size);
|
||||
static inline int mstrHdrSize(char type);
|
||||
static inline int mstrSumMetaLen(mstrKind *k, mstrFlags flags);
|
||||
static inline size_t mstrAllocLen(const mstr s, struct mstrKind *kind);
|
||||
|
||||
/*** mstr API ***/
|
||||
|
||||
/* Create mstr without any metadata attached, based on string 'initStr'.
|
||||
* - If initStr equals NULL, then only allocation will be made.
|
||||
* - string of mstr is always null-terminated.
|
||||
*/
|
||||
mstr mstrNew(const char *initStr, size_t lenStr, int trymalloc) {
|
||||
unsigned char *pInfo; /* pointer to mstr info field */
|
||||
void *sh;
|
||||
mstr s;
|
||||
char type = mstrReqType(lenStr);
|
||||
int mstrHdr = mstrHdrSize(type);
|
||||
|
||||
assert(lenStr + mstrHdr + 1 > lenStr); /* Catch size_t overflow */
|
||||
|
||||
size_t len = mstrHdr + lenStr + NULL_SIZE;
|
||||
sh = trymalloc? s_trymalloc(len) : s_malloc(len);
|
||||
|
||||
if (sh == NULL) return NULL;
|
||||
|
||||
s = (char*)sh + mstrHdr;
|
||||
pInfo = ((unsigned char*)s) - 1;
|
||||
|
||||
switch(type) {
|
||||
case MSTR_TYPE_5: {
|
||||
*pInfo = CREATE_MSTR_INFO(lenStr, 0 /*ismeta*/, type);
|
||||
break;
|
||||
}
|
||||
case MSTR_TYPE_8: {
|
||||
MSTR_HDR_VAR(8,s);
|
||||
*pInfo = CREATE_MSTR_INFO(0 /*unused*/, 0 /*ismeta*/, type);
|
||||
sh->len = lenStr;
|
||||
break;
|
||||
}
|
||||
case MSTR_TYPE_16: {
|
||||
MSTR_HDR_VAR(16,s);
|
||||
*pInfo = CREATE_MSTR_INFO(0 /*unused*/, 0 /*ismeta*/, type);
|
||||
sh->len = lenStr;
|
||||
break;
|
||||
}
|
||||
case MSTR_TYPE_64: {
|
||||
MSTR_HDR_VAR(64,s);
|
||||
*pInfo = CREATE_MSTR_INFO(0 /*unused*/, 0 /*ismeta*/, type);
|
||||
sh->len = lenStr;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (initStr && lenStr)
|
||||
memcpy(s, initStr, lenStr);
|
||||
|
||||
s[lenStr] = '\0';
|
||||
return s;
|
||||
}
|
||||
|
||||
/* Creates mstr with given string. Reserve space for metadata.
|
||||
*
|
||||
* Note: mstrNew(s,l) and mstrNewWithMeta(s,l,0) are not the same. The first allocates
|
||||
* just string. The second allocates a string with flags (yet without any metadata
|
||||
* structures allocated).
|
||||
*/
|
||||
mstr mstrNewWithMeta(struct mstrKind *kind, const char *initStr, size_t lenStr, mstrFlags metaFlags, int trymalloc) {
|
||||
unsigned char *pInfo; /* pointer to mstr info field */
|
||||
char *allocMstr;
|
||||
mstr mstrPtr;
|
||||
char type = mstrReqType(lenStr);
|
||||
int mstrHdr = mstrHdrSize(type);
|
||||
int sumMetaLen = mstrSumMetaLen(kind, metaFlags);
|
||||
|
||||
|
||||
/* mstrSumMetaLen() + sizeof(mstrFlags) + sizeof(mstrhdrX) + lenStr */
|
||||
|
||||
size_t allocLen = sumMetaLen + sizeof(mstrFlags) + mstrHdr + lenStr + NULL_SIZE;
|
||||
allocMstr = trymalloc? s_trymalloc(allocLen) : s_malloc(allocLen);
|
||||
|
||||
if (allocMstr == NULL) return NULL;
|
||||
|
||||
/* metadata is located at the beginning of the allocation, then meta-flags and lastly the string */
|
||||
mstrFlags *pMetaFlags = (mstrFlags *) (allocMstr + sumMetaLen) ;
|
||||
mstrPtr = ((char*) pMetaFlags) + sizeof(mstrFlags) + mstrHdr;
|
||||
pInfo = ((unsigned char*)mstrPtr) - 1;
|
||||
|
||||
switch(type) {
|
||||
case MSTR_TYPE_5: {
|
||||
*pInfo = CREATE_MSTR_INFO(lenStr, 1 /*ismeta*/, type);
|
||||
break;
|
||||
}
|
||||
case MSTR_TYPE_8: {
|
||||
MSTR_HDR_VAR(8, mstrPtr);
|
||||
sh->len = lenStr;
|
||||
*pInfo = CREATE_MSTR_INFO(0 /*unused*/, 1 /*ismeta*/, type);
|
||||
break;
|
||||
}
|
||||
case MSTR_TYPE_16: {
|
||||
MSTR_HDR_VAR(16, mstrPtr);
|
||||
sh->len = lenStr;
|
||||
*pInfo = CREATE_MSTR_INFO(0 /*unused*/, 1 /*ismeta*/, type);
|
||||
break;
|
||||
}
|
||||
case MSTR_TYPE_64: {
|
||||
MSTR_HDR_VAR(64, mstrPtr);
|
||||
sh->len = lenStr;
|
||||
*pInfo = CREATE_MSTR_INFO(0 /*unused*/, 1 /*ismeta*/, type);
|
||||
break;
|
||||
}
|
||||
}
|
||||
*pMetaFlags = metaFlags;
|
||||
if (initStr != NULL) memcpy(mstrPtr, initStr, lenStr);
|
||||
mstrPtr[lenStr] = '\0';
|
||||
|
||||
return mstrPtr;
|
||||
}
|
||||
|
||||
/* Create copy of mstr. Flags can be modified. For each metadata flag, if
|
||||
* same flag is set on both, then copy its metadata. */
|
||||
mstr mstrNewCopy(struct mstrKind *kind, mstr src, mstrFlags newFlags) {
|
||||
mstr dst;
|
||||
|
||||
/* if no flags are set, then just copy the string */
|
||||
if (newFlags == 0) return mstrNew(src, mstrlen(src), 0);
|
||||
|
||||
dst = mstrNewWithMeta(kind, src, mstrlen(src), newFlags, 0);
|
||||
memcpy(dst, src, mstrlen(src) + 1);
|
||||
|
||||
/* if metadata is attached to src, then selectively copy metadata */
|
||||
if (mstrIsMetaAttached(src)) {
|
||||
mstrFlags *pFlags1 = mstrFlagsRef(src),
|
||||
*pFlags2 = mstrFlagsRef(dst);
|
||||
|
||||
mstrFlags flags1Shift = *pFlags1,
|
||||
flags2Shift = *pFlags2;
|
||||
|
||||
unsigned char *at1 = ((unsigned char *) pFlags1),
|
||||
*at2 = ((unsigned char *) pFlags2);
|
||||
|
||||
/* if the flag is set on both, then copy the metadata */
|
||||
for (int i = 0; flags1Shift != 0; ++i) {
|
||||
int isFlag1Set = flags1Shift & 0x1;
|
||||
int isFlag2Set = flags2Shift & 0x1;
|
||||
|
||||
if (isFlag1Set) at1 -= kind->metaSize[i];
|
||||
if (isFlag2Set) at2 -= kind->metaSize[i];
|
||||
|
||||
if (isFlag1Set && isFlag2Set)
|
||||
memcpy(at2, at1, kind->metaSize[i]);
|
||||
flags1Shift >>= 1;
|
||||
flags2Shift >>= 1;
|
||||
}
|
||||
}
|
||||
return dst;
|
||||
}
|
||||
|
||||
/* Free mstring. Note, mstrKind is required to eval sizeof metadata and find start
|
||||
* of allocation but if mstrIsMetaAttached(s) is false, you can pass NULL as well.
|
||||
*/
|
||||
void mstrFree(struct mstrKind *kind, mstr s) {
|
||||
if (s != NULL)
|
||||
s_free(mstrGetAllocPtr(kind, s));
|
||||
}
|
||||
|
||||
/* return ref to metadata flags. Useful to modify directly flags which doesn't
|
||||
* include metadata payload */
|
||||
mstrFlags *mstrFlagsRef(mstr s) {
|
||||
switch(s[-1]&MSTR_TYPE_MASK) {
|
||||
case MSTR_TYPE_5:
|
||||
return ((mstrFlags *) (s - sizeof(struct mstrhdr5))) - 1;
|
||||
case MSTR_TYPE_8:
|
||||
return ((mstrFlags *) (s - sizeof(struct mstrhdr8))) - 1;
|
||||
case MSTR_TYPE_16:
|
||||
return ((mstrFlags *) (s - sizeof(struct mstrhdr16))) - 1;
|
||||
default: /* MSTR_TYPE_64: */
|
||||
return ((mstrFlags *) (s - sizeof(struct mstrhdr64))) - 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Return a reference to corresponding metadata of the specified metadata flag
|
||||
* index (flagIdx). If the metadata doesn't exist, it still returns a reference
|
||||
* to the starting location where it would have been written among other metadatas.
|
||||
* To verify if `flagIdx` of some metadata is attached, use `mstrGetFlag(s, flagIdx)`.
|
||||
*/
|
||||
void *mstrMetaRef(mstr s, struct mstrKind *kind, int flagIdx) {
|
||||
int metaOffset = 0;
|
||||
/* start iterating from flags backward */
|
||||
mstrFlags *pFlags = mstrFlagsRef(s);
|
||||
mstrFlags tmp = *pFlags;
|
||||
|
||||
for (int i = 0 ; i <= flagIdx ; ++i) {
|
||||
if (tmp & 0x1) metaOffset += kind->metaSize[i];
|
||||
tmp >>= 1;
|
||||
}
|
||||
return ((char *)pFlags) - metaOffset;
|
||||
}
|
||||
|
||||
/* mstr layout: [meta-data#N]...[meta-data#0][mstrFlags][mstrhdr][string][null] */
|
||||
void *mstrGetAllocPtr(struct mstrKind *kind, mstr str) {
|
||||
if (!mstrIsMetaAttached(str))
|
||||
return (char*)str - mstrHdrSize(str[-1]);
|
||||
|
||||
int totalMetaLen = mstrSumMetaLen(kind, *mstrFlagsRef(str));
|
||||
return (char*)str - mstrHdrSize(str[-1]) - sizeof(mstrFlags) - totalMetaLen;
|
||||
}
|
||||
|
||||
/* Prints in the following fashion:
|
||||
* [0x7f8bd8816017] my_mstr: foo (strLen=3, mstrLen=11, isMeta=1, metaFlags=0x1)
|
||||
* [0x7f8bd8816010] >> meta[0]: 0x78 0x56 0x34 0x12 (metaLen=4)
|
||||
*/
|
||||
void mstrPrint(mstr s, struct mstrKind *kind, int verbose) {
|
||||
mstrFlags mflags, tmp;
|
||||
int isMeta = mstrIsMetaAttached(s);
|
||||
|
||||
tmp = mflags = (isMeta) ? *mstrFlagsRef(s) : 0;
|
||||
|
||||
if (!isMeta) {
|
||||
printf("[%p] %s: %s (strLen=%zu, mstrLen=%zu, isMeta=0)\n",
|
||||
(void *)s, kind->name, s, mstrlen(s), mstrAllocLen(s, kind));
|
||||
return;
|
||||
}
|
||||
|
||||
printf("[%p] %s: %s (strLen=%zu, mstrLen=%zu, isMeta=1, metaFlags=0x%x)\n",
|
||||
(void *)s, kind->name, s, mstrlen(s), mstrAllocLen(s, kind), mflags);
|
||||
|
||||
if (verbose) {
|
||||
for (unsigned int i = 0 ; i < NUM_MSTR_FLAGS ; ++i) {
|
||||
if (tmp & 0x1) {
|
||||
int mSize = kind->metaSize[i];
|
||||
void *mRef = mstrMetaRef(s, kind, i);
|
||||
printf("[%p] >> meta[%d]:", mRef, i);
|
||||
for (int j = 0 ; j < mSize ; ++j) {
|
||||
printf(" 0x%02x", ((unsigned char *) mRef)[j]);
|
||||
}
|
||||
printf(" (metaLen=%d)\n", mSize);
|
||||
}
|
||||
tmp >>= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* return length of the string (ignoring metadata attached) */
|
||||
size_t mstrlen(const mstr s) {
|
||||
unsigned char info = s[-1];
|
||||
switch(info & MSTR_TYPE_MASK) {
|
||||
case MSTR_TYPE_5:
|
||||
return MSTR_TYPE_5_LEN(info);
|
||||
case MSTR_TYPE_8:
|
||||
return MSTR_HDR(8,s)->len;
|
||||
case MSTR_TYPE_16:
|
||||
return MSTR_HDR(16,s)->len;
|
||||
default: /* MSTR_TYPE_64: */
|
||||
return MSTR_HDR(64,s)->len;
|
||||
}
|
||||
}
|
||||
|
||||
/*** mstr internals ***/
|
||||
|
||||
static inline int mstrSumMetaLen(mstrKind *k, mstrFlags flags) {
|
||||
int total = 0;
|
||||
int i = 0 ;
|
||||
while (flags) {
|
||||
total += (flags & 0x1) ? k->metaSize[i] : 0;
|
||||
flags >>= 1;
|
||||
++i;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
/* mstrSumMetaLen() + sizeof(mstrFlags) + sizeof(mstrhdrX) + strlen + '\0' */
|
||||
static inline size_t mstrAllocLen(const mstr s, struct mstrKind *kind) {
|
||||
int hdrlen;
|
||||
mstrFlags *pMetaFlags;
|
||||
size_t strlen = 0;
|
||||
|
||||
int isMeta = mstrIsMetaAttached(s);
|
||||
unsigned char info = s[-1];
|
||||
|
||||
switch(info & MSTR_TYPE_MASK) {
|
||||
case MSTR_TYPE_5:
|
||||
strlen = MSTR_TYPE_5_LEN(info);
|
||||
hdrlen = sizeof(struct mstrhdr5);
|
||||
pMetaFlags = ((mstrFlags *) MSTR_HDR(5, s)) - 1;
|
||||
break;
|
||||
case MSTR_TYPE_8:
|
||||
strlen = MSTR_HDR(8,s)->len;
|
||||
hdrlen = sizeof(struct mstrhdr8);
|
||||
pMetaFlags = ((mstrFlags *) MSTR_HDR(8, s)) - 1;
|
||||
break;
|
||||
case MSTR_TYPE_16:
|
||||
strlen = MSTR_HDR(16,s)->len;
|
||||
hdrlen = sizeof(struct mstrhdr16);
|
||||
pMetaFlags = ((mstrFlags *) MSTR_HDR(16, s)) - 1;
|
||||
break;
|
||||
default: /* MSTR_TYPE_64: */
|
||||
strlen = MSTR_HDR(64,s)->len;
|
||||
hdrlen = sizeof(struct mstrhdr64);
|
||||
pMetaFlags = ((mstrFlags *) MSTR_HDR(64, s)) - 1;
|
||||
break;
|
||||
}
|
||||
return hdrlen + strlen + NULL_SIZE + ((isMeta) ? (mstrSumMetaLen(kind, *pMetaFlags) + sizeof(mstrFlags)) : 0);
|
||||
}
|
||||
|
||||
/* returns pointer to the beginning of malloc() of mstr */
|
||||
void *mstrGetStartAlloc(mstr s, struct mstrKind *kind) {
|
||||
int hdrlen;
|
||||
mstrFlags *pMetaFlags;
|
||||
|
||||
int isMeta = mstrIsMetaAttached(s);
|
||||
|
||||
switch(s[-1]&MSTR_TYPE_MASK) {
|
||||
case MSTR_TYPE_5:
|
||||
hdrlen = sizeof(struct mstrhdr5);
|
||||
pMetaFlags = ((mstrFlags *) MSTR_HDR(5, s)) - 1;
|
||||
break;
|
||||
case MSTR_TYPE_8:
|
||||
hdrlen = sizeof(struct mstrhdr8);
|
||||
pMetaFlags = ((mstrFlags *) MSTR_HDR(8, s)) - 1;
|
||||
break;
|
||||
case MSTR_TYPE_16:
|
||||
hdrlen = sizeof(struct mstrhdr16);
|
||||
pMetaFlags = ((mstrFlags *) MSTR_HDR(16, s)) - 1;
|
||||
break;
|
||||
default: /* MSTR_TYPE_64: */
|
||||
hdrlen = sizeof(struct mstrhdr64);
|
||||
pMetaFlags = ((mstrFlags *) MSTR_HDR(64, s)) - 1;
|
||||
break;
|
||||
}
|
||||
return (char *) s - hdrlen - ((isMeta) ? (mstrSumMetaLen(kind, *pMetaFlags) + sizeof(mstrFlags)) : 0);
|
||||
}
|
||||
|
||||
static inline int mstrHdrSize(char type) {
|
||||
switch(type&MSTR_TYPE_MASK) {
|
||||
case MSTR_TYPE_5:
|
||||
return sizeof(struct mstrhdr5);
|
||||
case MSTR_TYPE_8:
|
||||
return sizeof(struct mstrhdr8);
|
||||
case MSTR_TYPE_16:
|
||||
return sizeof(struct mstrhdr16);
|
||||
case MSTR_TYPE_64:
|
||||
return sizeof(struct mstrhdr64);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static inline char mstrReqType(size_t string_size) {
|
||||
if (string_size < 1<<5)
|
||||
return MSTR_TYPE_5;
|
||||
if (string_size < 1<<8)
|
||||
return MSTR_TYPE_8;
|
||||
if (string_size < 1<<16)
|
||||
return MSTR_TYPE_16;
|
||||
return MSTR_TYPE_64;
|
||||
}
|
||||
|
||||
#ifdef REDIS_TEST
|
||||
#include <stdlib.h>
|
||||
#include <assert.h>
|
||||
#include "testhelp.h"
|
||||
#include "limits.h"
|
||||
|
||||
#ifndef UNUSED
|
||||
#define UNUSED(x) (void)(x)
|
||||
#endif
|
||||
|
||||
/* Challenge mstr with metadata interesting enough that can include the case of hfield and hkey and more */
|
||||
#define B(idx) (1<<(idx))
|
||||
|
||||
#define META_IDX_MYMSTR_TTL4 0
|
||||
#define META_IDX_MYMSTR_TTL8 1
|
||||
#define META_IDX_MYMSTR_TYPE_ENC_LRU 2 // 4Bbit type, 4bit encoding, 24bits lru
|
||||
#define META_IDX_MYMSTR_VALUE_PTR 3
|
||||
#define META_IDX_MYMSTR_FLAG_NO_META 4
|
||||
|
||||
#define TEST_CONTEXT(context) printf("\nContext: %s \n", context);
|
||||
|
||||
int mstrTest(int argc, char **argv, int flags) {
|
||||
UNUSED(argc);
|
||||
UNUSED(argv);
|
||||
UNUSED(flags);
|
||||
|
||||
struct mstrKind kind_mymstr = {
|
||||
.name = "my_mstr",
|
||||
.metaSize[META_IDX_MYMSTR_TTL4] = 4,
|
||||
.metaSize[META_IDX_MYMSTR_TTL8] = 8,
|
||||
.metaSize[META_IDX_MYMSTR_TYPE_ENC_LRU] = 4,
|
||||
.metaSize[META_IDX_MYMSTR_VALUE_PTR] = 8,
|
||||
.metaSize[META_IDX_MYMSTR_FLAG_NO_META] = 0,
|
||||
};
|
||||
|
||||
TEST_CONTEXT("Create simple short mstr")
|
||||
{
|
||||
char *str = "foo";
|
||||
mstr s = mstrNew(str, strlen(str), 0);
|
||||
size_t expStrLen = strlen(str);
|
||||
|
||||
test_cond("Verify str length and alloc length",
|
||||
mstrAllocLen(s, NULL) == (1 + expStrLen + 1) && /* mstrhdr5 + str + null */
|
||||
mstrlen(s) == expStrLen && /* expected strlen(str) */
|
||||
memcmp(s, str, expStrLen + 1) == 0);
|
||||
mstrFree(&kind_mymstr, s);
|
||||
}
|
||||
|
||||
TEST_CONTEXT("Create simple 40 bytes mstr")
|
||||
{
|
||||
char *str = "0123456789012345678901234567890123456789"; // 40 bytes
|
||||
mstr s = mstrNew(str, strlen(str), 0);
|
||||
|
||||
test_cond("Verify str length and alloc length",
|
||||
mstrAllocLen(s, NULL) == (3 + 40 + 1) && /* mstrhdr8 + str + null */
|
||||
mstrlen(s) == 40 &&
|
||||
memcmp(s,str,40) == 0);
|
||||
mstrFree(&kind_mymstr, s);
|
||||
}
|
||||
|
||||
TEST_CONTEXT("Create mstr with random characters")
|
||||
{
|
||||
long unsigned int i;
|
||||
char str[66000];
|
||||
for (i = 0 ; i < sizeof(str) ; ++i) str[i] = rand() % 256;
|
||||
|
||||
size_t len[] = { 31, 32, 33, 255, 256, 257, 65535, 65536, 65537, 66000};
|
||||
for (i = 0 ; i < sizeof(len) / sizeof(len[0]) ; ++i) {
|
||||
char title[100];
|
||||
mstr s = mstrNew(str, len[i], 0);
|
||||
size_t mstrhdrSize = (len[i] < 1<<5) ? sizeof(struct mstrhdr5) :
|
||||
(len[i] < 1<<8) ? sizeof(struct mstrhdr8) :
|
||||
(len[i] < 1<<16) ? sizeof(struct mstrhdr16) :
|
||||
sizeof(struct mstrhdr64);
|
||||
|
||||
snprintf(title, sizeof(title), "Verify string of length %zu", len[i]);
|
||||
test_cond(title,
|
||||
mstrAllocLen(s, NULL) == (mstrhdrSize + len[i] + 1) && /* mstrhdrX + str + null */
|
||||
mstrlen(s) == len[i] &&
|
||||
memcmp(s,str,len[i]) == 0);
|
||||
mstrFree(&kind_mymstr, s);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CONTEXT("Create short mstr with TTL4")
|
||||
{
|
||||
uint32_t *ttl;
|
||||
mstr s = mstrNewWithMeta(&kind_mymstr,
|
||||
"foo",
|
||||
strlen("foo"),
|
||||
B(META_IDX_MYMSTR_TTL4), /* allocate with TTL4 metadata */
|
||||
0);
|
||||
|
||||
ttl = mstrMetaRef(s, &kind_mymstr, META_IDX_MYMSTR_TTL4);
|
||||
*ttl = 0x12345678;
|
||||
|
||||
test_cond("Verify memory-allocation and string lengths",
|
||||
mstrAllocLen(s, &kind_mymstr) == (1 + 3 + 2 + 1 + 4) && /* mstrhdr5 + str + null + mstrFlags + TLL */
|
||||
mstrlen(s) == 3);
|
||||
|
||||
unsigned char expMem[] = {0xFF, 0xFF, 0xFF, 0xFF, 0x01, 0x00, 0x1c, 'f', 'o', 'o', '\0' };
|
||||
uint32_t value = 0x12345678;
|
||||
memcpy(expMem, &value, sizeof(uint32_t));
|
||||
test_cond("Verify string and TTL4 payload", memcmp(
|
||||
mstrMetaRef(s, &kind_mymstr, 0) , expMem, sizeof(expMem)) == 0);
|
||||
|
||||
test_cond("Verify mstrIsMetaAttached() function works", mstrIsMetaAttached(s) != 0);
|
||||
|
||||
mstrFree(&kind_mymstr, s);
|
||||
}
|
||||
|
||||
TEST_CONTEXT("Create short mstr with TTL4 and value ptr ")
|
||||
{
|
||||
mstr s = mstrNewWithMeta(&kind_mymstr, "foo", strlen("foo"),
|
||||
B(META_IDX_MYMSTR_TTL4) | B(META_IDX_MYMSTR_VALUE_PTR), 0);
|
||||
*((uint32_t *) (mstrMetaRef(s, &kind_mymstr,
|
||||
META_IDX_MYMSTR_TTL4))) = 0x12345678;
|
||||
|
||||
test_cond("Verify length and alloc length",
|
||||
mstrAllocLen(s, &kind_mymstr) == (1 + 3 + 1 + 2 + 4 + 8) && /* mstrhdr5 + str + null + mstrFlags + TLL + PTR */
|
||||
mstrlen(s) == 3);
|
||||
mstrFree(&kind_mymstr, s);
|
||||
}
|
||||
|
||||
TEST_CONTEXT("Copy mstr and add it TTL4")
|
||||
{
|
||||
mstr s1 = mstrNew("foo", strlen("foo"), 0);
|
||||
mstr s2 = mstrNewCopy(&kind_mymstr, s1, B(META_IDX_MYMSTR_TTL4));
|
||||
*((uint32_t *) (mstrMetaRef(s2, &kind_mymstr, META_IDX_MYMSTR_TTL4))) = 0x12345678;
|
||||
|
||||
test_cond("Verify new mstr includes TTL4",
|
||||
mstrAllocLen(s2, &kind_mymstr) == (1 + 3 + 1 + 2 + 4) && /* mstrhdr5 + str + null + mstrFlags + TTL4 */
|
||||
mstrlen(s2) == 3 && /* 'foo' = 3bytes */
|
||||
memcmp(s2, "foo\0", 4) == 0);
|
||||
|
||||
mstr s3 = mstrNewCopy(&kind_mymstr, s2, B(META_IDX_MYMSTR_TTL4));
|
||||
unsigned char expMem[] = { 0xFF, 0xFF, 0xFF, 0xFF, 0x1, 0x0, 0x1c, 'f', 'o', 'o', '\0' };
|
||||
uint32_t value = 0x12345678;
|
||||
memcpy(expMem, &value, sizeof(uint32_t));
|
||||
|
||||
char *ppp = mstrGetStartAlloc(s3, &kind_mymstr);
|
||||
test_cond("Verify string and TTL4 payload",
|
||||
memcmp(ppp, expMem, sizeof(expMem)) == 0);
|
||||
|
||||
mstrPrint(s3, &kind_mymstr, 1);
|
||||
mstrFree(&kind_mymstr, s1);
|
||||
mstrFree(&kind_mymstr, s2);
|
||||
mstrFree(&kind_mymstr, s3);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
#endif
|
||||
+226
@@ -0,0 +1,226 @@
|
||||
/*
|
||||
* Copyright Redis Ltd. 2024 - present
|
||||
*
|
||||
* Licensed under your choice of the Redis Source Available License 2.0 (RSALv2)
|
||||
* or the Server Side Public License v1 (SSPLv1).
|
||||
*
|
||||
*
|
||||
* WHAT IS MSTR (M-STRING)?
|
||||
* ------------------------
|
||||
* mstr stands for immutable string with optional metadata attached.
|
||||
*
|
||||
* sds string is widely used across the system and serves as a general purpose
|
||||
* container to hold data. The need to optimize memory and aggregate strings
|
||||
* along with metadata and store it into Redis data-structures as single bulk keep
|
||||
* reoccur. One thought might be, why not to extend sds to support metadata. The
|
||||
* answer is that sds is mutable string in its nature, with wide API (split, join,
|
||||
* etc.). Pushing metadata logic into sds will make it very fragile, and complex
|
||||
* to maintain.
|
||||
*
|
||||
* Another idea involved using a simple struct with flags and a dynamic buf[] at the
|
||||
* end. While this could be viable, it introduces considerable complexity and would
|
||||
* need maintenance across different contexts.
|
||||
*
|
||||
* As an alternative, we introduce a new implementation of immutable strings,
|
||||
* with limited API, and with the option to attach metadata. The representation
|
||||
* of the string, without any metadata, in its basic form, resembles SDS but
|
||||
* without the API to manipulate the string. Only to attach metadata to it. The
|
||||
* following diagram shows the memory layout of mstring (mstrhdr8) when no
|
||||
* metadata is attached:
|
||||
*
|
||||
* +----------------------------------------------+
|
||||
* | mstrhdr8 | c-string | |
|
||||
* +--------------------------------+-------------+
|
||||
* |8b |2b |1b |5b |?bytes |8b|
|
||||
* | Len | Type |m-bit=0 | Unused | String |\0|
|
||||
* +----------------------------------------------+
|
||||
* ^
|
||||
* |
|
||||
* mstrNew() returns pointer to here --+
|
||||
*
|
||||
* If metadata-flag is set, depicted in diagram above as m-bit in the diagram,
|
||||
* then the header will be preceded with additional 16 bits of metadata flags such
|
||||
* that if i'th bit is set, then the i'th metadata structure is attached to the
|
||||
* mstring. The metadata layout and their sizes are defined by mstrKind structure
|
||||
* (More below).
|
||||
*
|
||||
* The following diagram shows the memory layout of mstr (mstrhdr8) when 3 bits in mFlags
|
||||
* are set to indicate that 3 fields of metadata are attached to the mstring at the
|
||||
* beginning.
|
||||
*
|
||||
* +-------------------------------------------------------------------------------+
|
||||
* | METADATA FIELDS | mflags | mstrhdr8 | c-string | |
|
||||
* +-----------------------+--------+--------------------------------+-------------+
|
||||
* |?bytes |?bytes |?bytes |16b |8b |2b |1b |5b |?bytes |8b|
|
||||
* | Meta3 | Meta2 | Meta0 | 0x1101 | Len | Type |m-bit=1 | Unused | String |\0|
|
||||
* +-------------------------------------------------------------------------------+
|
||||
* ^
|
||||
* |
|
||||
* mstrNewWithMeta() returns pointer to here --+
|
||||
*
|
||||
* mstr allows to define different kinds (groups) of mstrings, each with its
|
||||
* own unique metadata layout. For example, in case of hash-fields, all instances of
|
||||
* it can optionally have TTL metadata attached to it. This is achieved by first
|
||||
* prototyping a single mstrKind structure that defines the metadata layout and sizes
|
||||
* of this specific kind. Now each hash-field instance has still the freedom to
|
||||
* attach or not attach the metadata to it, and metadata flags (mFlags) of the
|
||||
* instance will reflect this decision.
|
||||
*
|
||||
* In the future, the keys of Redis keyspace can be another kind of mstring that
|
||||
* has TTL, LRU or even dictEntry metadata embedded into. Unlike vptr in c++, this
|
||||
* struct won't be attached to mstring but will be passed as yet another argument
|
||||
* to API, to save memory. In addition, each instance of a given mstrkind can hold
|
||||
* any subset of metadata and the 8 bits of metadata-flags will reflect it.
|
||||
*
|
||||
* The following example shows how to define mstrKind for possible future keyspace
|
||||
* that aggregates several keyspace related metadata into one compact, singly
|
||||
* allocated, mstring.
|
||||
*
|
||||
* typedef enum HkeyMetaFlags {
|
||||
* HKEY_META_VAL_REF_COUNT = 0, // refcount
|
||||
* HKEY_META_VAL_REF = 1, // Val referenced
|
||||
* HKEY_META_EXPIRE = 2, // TTL and more
|
||||
* HKEY_META_TYPE_ENC_LRU = 3, // TYPE + LRU + ENC
|
||||
* HKEY_META_DICT_ENT_NEXT = 4, // Next dict entry
|
||||
* // Following two must be together and in this order
|
||||
* HKEY_META_VAL_EMBED8 = 5, // Val embedded, max 7 bytes
|
||||
* HKEY_META_VAL_EMBED16 = 6, // Val embedded, max 15 bytes (23 with EMBED8)
|
||||
* } HkeyMetaFlags;
|
||||
*
|
||||
* mstrKind hkeyKind = {
|
||||
* .name = "hkey",
|
||||
* .metaSize[HKEY_META_VAL_REF_COUNT] = 4,
|
||||
* .metaSize[HKEY_META_VAL_REF] = 8,
|
||||
* .metaSize[HKEY_META_EXPIRE] = sizeof(ExpireMeta),
|
||||
* .metaSize[HKEY_META_TYPE_ENC_LRU] = 8,
|
||||
* .metaSize[HKEY_META_DICT_ENT_NEXT] = 8,
|
||||
* .metaSize[HKEY_META_VAL_EMBED8] = 8,
|
||||
* .metaSize[HKEY_META_VAL_EMBED16] = 16,
|
||||
* };
|
||||
*
|
||||
* MSTR-ALIGNMENT
|
||||
* --------------
|
||||
* There are two types of alignments to take into consideration:
|
||||
* 1. Alignment of the metadata.
|
||||
* 2. Alignment of returned mstr pointer
|
||||
*
|
||||
* 1) As the metadatas layout are reversed to their enumeration, it is recommended
|
||||
* to put metadata with "better" alignment first in memory layout (enumerated
|
||||
* last) and the worst, or those that simply don't require any alignment will be
|
||||
* last in memory layout (enumerated first). This is similar the to the applied
|
||||
* consideration when defining new struct in C. Note also that each metadata
|
||||
* might either be attached to mstr or not which complicates the design phase
|
||||
* of a new mstrKind a little.
|
||||
*
|
||||
* In the example above, HKEY_META_VAL_REF_COUNT, with worst alignment of 4
|
||||
* bytes, is enumerated first, and therefore, will be last in memory layout.
|
||||
*
|
||||
* 2) Few optimizations in Redis rely on the fact that sds address is always an odd
|
||||
* pointer. We can achieve the same with a little effort. It was already taken
|
||||
* care that all headers of type mstrhdrX has odd size. With that in mind, if
|
||||
* a new kind of mstr is required to be limited to odd addresses, then we must
|
||||
* make sure that sizes of all related metadatas that are defined in mstrKind
|
||||
* are even in size.
|
||||
*/
|
||||
|
||||
#ifndef __MSTR_H
|
||||
#define __MSTR_H
|
||||
|
||||
#include <sys/types.h>
|
||||
#include <stdarg.h>
|
||||
#include <stdint.h>
|
||||
|
||||
/* Selective copy of ifndef from server.h instead of including it */
|
||||
#ifndef static_assert
|
||||
#define static_assert(expr, lit) extern char __static_assert_failure[(expr) ? 1:-1]
|
||||
#endif
|
||||
|
||||
#define MSTR_TYPE_5 0
|
||||
#define MSTR_TYPE_8 1
|
||||
#define MSTR_TYPE_16 2
|
||||
#define MSTR_TYPE_64 3
|
||||
#define MSTR_TYPE_MASK 3
|
||||
#define MSTR_TYPE_BITS 2
|
||||
|
||||
#define MSTR_META_MASK 4
|
||||
|
||||
#define MSTR_HDR(T,s) ((struct mstrhdr##T *)((s)-(sizeof(struct mstrhdr##T))))
|
||||
#define MSTR_HDR_VAR(T,s) struct mstrhdr##T *sh = (void*)((s)-(sizeof(struct mstrhdr##T)));
|
||||
|
||||
#define MSTR_META_BITS 1 /* is metadata attached? */
|
||||
#define MSTR_TYPE_5_LEN(f) ((f) >> (MSTR_TYPE_BITS + MSTR_META_BITS))
|
||||
#define CREATE_MSTR_INFO(len, ismeta, type) ( (((len<<MSTR_META_BITS) + ismeta) << (MSTR_TYPE_BITS)) | type )
|
||||
|
||||
/* mimic plain c-string */
|
||||
typedef char *mstr;
|
||||
|
||||
/* Flags that can be set on mstring to indicate for attached metadata. It is
|
||||
* */
|
||||
typedef uint16_t mstrFlags;
|
||||
|
||||
struct __attribute__ ((__packed__)) mstrhdr5 {
|
||||
unsigned char info; /* 2 lsb of type, 1 metadata, and 5 msb of string length */
|
||||
char buf[];
|
||||
};
|
||||
struct __attribute__ ((__packed__)) mstrhdr8 {
|
||||
uint8_t unused; /* To achieve odd size header (See comment above) */
|
||||
uint8_t len;
|
||||
unsigned char info; /* 2 lsb of type, 6 unused bits */
|
||||
char buf[];
|
||||
};
|
||||
struct __attribute__ ((__packed__)) mstrhdr16 {
|
||||
uint16_t len;
|
||||
unsigned char info; /* 2 lsb of type, 6 unused bits */
|
||||
char buf[];
|
||||
};
|
||||
struct __attribute__ ((__packed__)) mstrhdr64 {
|
||||
uint64_t len;
|
||||
unsigned char info; /* 2 lsb of type, 6 unused bits */
|
||||
char buf[];
|
||||
};
|
||||
|
||||
#define NUM_MSTR_FLAGS (sizeof(mstrFlags)*8)
|
||||
|
||||
/* mstrKind is used to define a kind (a group) of mstring with its own metadata layout */
|
||||
typedef struct mstrKind {
|
||||
const char *name;
|
||||
int metaSize[NUM_MSTR_FLAGS];
|
||||
} mstrKind;
|
||||
|
||||
mstr mstrNew(const char *initStr, size_t lenStr, int trymalloc);
|
||||
|
||||
mstr mstrNewWithMeta(struct mstrKind *kind, const char *initStr, size_t lenStr, mstrFlags flags, int trymalloc);
|
||||
|
||||
mstr mstrNewCopy(struct mstrKind *kind, mstr src, mstrFlags newFlags);
|
||||
|
||||
void *mstrGetAllocPtr(struct mstrKind *kind, mstr str);
|
||||
|
||||
void mstrFree(struct mstrKind *kind, mstr s);
|
||||
|
||||
mstrFlags *mstrFlagsRef(mstr s);
|
||||
|
||||
void *mstrMetaRef(mstr s, struct mstrKind *kind, int flagIdx);
|
||||
|
||||
size_t mstrlen(const mstr s);
|
||||
|
||||
/* return non-zero if metadata is attached to mstring */
|
||||
static inline int mstrIsMetaAttached(mstr s) { return s[-1] & MSTR_META_MASK; }
|
||||
|
||||
/* return whether if a specific flag-index is set */
|
||||
static inline int mstrGetFlag(mstr s, int flagIdx) { return *mstrFlagsRef(s) & (1 << flagIdx); }
|
||||
|
||||
/* DEBUG */
|
||||
void mstrPrint(mstr s, struct mstrKind *kind, int verbose);
|
||||
|
||||
/* See comment above about MSTR-ALIGNMENT(2) */
|
||||
static_assert(sizeof(struct mstrhdr5 ) % 2 == 1, "must be odd");
|
||||
static_assert(sizeof(struct mstrhdr8 ) % 2 == 1, "must be odd");
|
||||
static_assert(sizeof(struct mstrhdr16 ) % 2 == 1, "must be odd");
|
||||
static_assert(sizeof(struct mstrhdr64 ) % 2 == 1, "must be odd");
|
||||
static_assert(sizeof(mstrFlags ) % 2 == 0, "must be even to keep mstr pointer odd");
|
||||
|
||||
#ifdef REDIS_TEST
|
||||
int mstrTest(int argc, char *argv[], int flags);
|
||||
#endif
|
||||
|
||||
#endif
|
||||
+23
-4
@@ -31,6 +31,14 @@ size_t sdsZmallocSize(sds s) {
|
||||
return zmalloc_size(sh);
|
||||
}
|
||||
|
||||
/* Return the size consumed from the allocator, for the specified hfield with
|
||||
* metadata (mstr), including internal fragmentation. This function is used in
|
||||
* order to compute the client output buffer size. */
|
||||
size_t hfieldZmallocSize(hfield s) {
|
||||
void *sh = hfieldGetAllocPtr(s);
|
||||
return zmalloc_size(sh);
|
||||
}
|
||||
|
||||
/* Return the amount of memory used by the sds string at object->ptr
|
||||
* for a string object. This includes internal fragmentation. */
|
||||
size_t getStringObjectSdsUsedMemory(robj *o) {
|
||||
@@ -3749,7 +3757,9 @@ void replaceClientCommandVector(client *c, int argc, robj **argv) {
|
||||
* 1. Make sure there are no "holes" and all the arguments are set.
|
||||
* 2. If the original argument vector was longer than the one we
|
||||
* want to end with, it's up to the caller to set c->argc and
|
||||
* free the no longer used objects on c->argv. */
|
||||
* free the no longer used objects on c->argv.
|
||||
* 3. To remove argument at i'th index, pass NULL as new value
|
||||
*/
|
||||
void rewriteClientCommandArgument(client *c, int i, robj *newval) {
|
||||
robj *oldval;
|
||||
retainOriginalCommandVector(c);
|
||||
@@ -3767,9 +3777,18 @@ void rewriteClientCommandArgument(client *c, int i, robj *newval) {
|
||||
}
|
||||
oldval = c->argv[i];
|
||||
if (oldval) c->argv_len_sum -= getStringObjectLen(oldval);
|
||||
if (newval) c->argv_len_sum += getStringObjectLen(newval);
|
||||
c->argv[i] = newval;
|
||||
incrRefCount(newval);
|
||||
|
||||
if (newval) {
|
||||
c->argv[i] = newval;
|
||||
incrRefCount(newval);
|
||||
c->argv_len_sum += getStringObjectLen(newval);
|
||||
} else {
|
||||
/* move the remaining arguments one step left */
|
||||
for (int j = i+1; j < c->argc; j++) {
|
||||
c->argv[j-1] = c->argv[j];
|
||||
}
|
||||
c->argv[--c->argc] = NULL;
|
||||
}
|
||||
if (oldval) decrRefCount(oldval);
|
||||
|
||||
/* If this is the command name make sure to fix c->cmd. */
|
||||
|
||||
+1
-1
@@ -80,7 +80,7 @@ sds keyspaceEventsFlagsToString(int flags) {
|
||||
* 'event' is a C string representing the event name.
|
||||
* 'key' is a Redis object representing the key name.
|
||||
* 'dbid' is the database ID where the key lives. */
|
||||
void notifyKeyspaceEvent(int type, char *event, robj *key, int dbid) {
|
||||
void notifyKeyspaceEvent(int type, const char *event, robj *key, int dbid) {
|
||||
sds chan;
|
||||
robj *chanobj, *eventobj;
|
||||
int len = -1;
|
||||
|
||||
+12
-16
@@ -333,17 +333,7 @@ void freeZsetObject(robj *o) {
|
||||
}
|
||||
|
||||
void freeHashObject(robj *o) {
|
||||
switch (o->encoding) {
|
||||
case OBJ_ENCODING_HT:
|
||||
dictRelease((dict*) o->ptr);
|
||||
break;
|
||||
case OBJ_ENCODING_LISTPACK:
|
||||
lpFree(o->ptr);
|
||||
break;
|
||||
default:
|
||||
serverPanic("Unknown hash encoding type");
|
||||
break;
|
||||
}
|
||||
hashTypeFree(o);
|
||||
}
|
||||
|
||||
void freeModuleObject(robj *o) {
|
||||
@@ -502,6 +492,9 @@ void dismissHashObject(robj *o, size_t size_hint) {
|
||||
dismissMemory(d->ht_table[1], DICTHT_SIZE(d->ht_size_exp[1])*sizeof(dictEntry*));
|
||||
} else if (o->encoding == OBJ_ENCODING_LISTPACK) {
|
||||
dismissMemory(o->ptr, lpBytes((unsigned char*)o->ptr));
|
||||
} else if (o->encoding == OBJ_ENCODING_LISTPACK_EX) {
|
||||
listpackEx *lpt = o->ptr;
|
||||
dismissMemory(lpt->lp, lpBytes((unsigned char*)lpt->lp));
|
||||
} else {
|
||||
serverPanic("Unknown hash encoding type");
|
||||
}
|
||||
@@ -939,6 +932,7 @@ char *strEncoding(int encoding) {
|
||||
case OBJ_ENCODING_HT: return "hashtable";
|
||||
case OBJ_ENCODING_QUICKLIST: return "quicklist";
|
||||
case OBJ_ENCODING_LISTPACK: return "listpack";
|
||||
case OBJ_ENCODING_LISTPACK_EX: return "listpackex";
|
||||
case OBJ_ENCODING_INTSET: return "intset";
|
||||
case OBJ_ENCODING_SKIPLIST: return "skiplist";
|
||||
case OBJ_ENCODING_EMBSTR: return "embstr";
|
||||
@@ -979,7 +973,6 @@ size_t streamRadixTreeMemoryUsage(rax *rax) {
|
||||
* are checked and averaged to estimate the total size. */
|
||||
#define OBJ_COMPUTE_SIZE_DEF_SAMPLES 5 /* Default sample size. */
|
||||
size_t objectComputeSize(robj *key, robj *o, size_t sample_size, int dbid) {
|
||||
sds ele, ele2;
|
||||
dict *d;
|
||||
dictIterator *di;
|
||||
struct dictEntry *de;
|
||||
@@ -1016,7 +1009,7 @@ size_t objectComputeSize(robj *key, robj *o, size_t sample_size, int dbid) {
|
||||
di = dictGetIterator(d);
|
||||
asize = sizeof(*o)+sizeof(dict)+(sizeof(struct dictEntry*)*dictBuckets(d));
|
||||
while((de = dictNext(di)) != NULL && samples < sample_size) {
|
||||
ele = dictGetKey(de);
|
||||
sds ele = dictGetKey(de);
|
||||
elesize += dictEntryMemUsage() + sdsZmallocSize(ele);
|
||||
samples++;
|
||||
}
|
||||
@@ -1052,14 +1045,17 @@ size_t objectComputeSize(robj *key, robj *o, size_t sample_size, int dbid) {
|
||||
} else if (o->type == OBJ_HASH) {
|
||||
if (o->encoding == OBJ_ENCODING_LISTPACK) {
|
||||
asize = sizeof(*o)+zmalloc_size(o->ptr);
|
||||
} else if (o->encoding == OBJ_ENCODING_LISTPACK_EX) {
|
||||
listpackEx *lpt = o->ptr;
|
||||
asize = sizeof(*o) + zmalloc_size(lpt) + zmalloc_size(lpt->lp);
|
||||
} else if (o->encoding == OBJ_ENCODING_HT) {
|
||||
d = o->ptr;
|
||||
di = dictGetIterator(d);
|
||||
asize = sizeof(*o)+sizeof(dict)+(sizeof(struct dictEntry*)*dictBuckets(d));
|
||||
while((de = dictNext(di)) != NULL && samples < sample_size) {
|
||||
ele = dictGetKey(de);
|
||||
ele2 = dictGetVal(de);
|
||||
elesize += sdsZmallocSize(ele) + sdsZmallocSize(ele2);
|
||||
hfield ele = dictGetKey(de);
|
||||
sds ele2 = dictGetVal(de);
|
||||
elesize += hfieldZmallocSize(ele) + sdsZmallocSize(ele2);
|
||||
elesize += dictEntryMemUsage();
|
||||
samples++;
|
||||
}
|
||||
|
||||
@@ -173,11 +173,16 @@ raxNode *raxNewNode(size_t children, int datafield) {
|
||||
/* Allocate a new rax and return its pointer. On out of memory the function
|
||||
* returns NULL. */
|
||||
rax *raxNew(void) {
|
||||
rax *rax = rax_malloc(sizeof(*rax));
|
||||
return raxNewWithMetadata(0);
|
||||
}
|
||||
|
||||
/* Allocate a new rax with metadata */
|
||||
rax *raxNewWithMetadata(int metaSize) {
|
||||
rax *rax = rax_malloc(sizeof(*rax) + metaSize);
|
||||
if (rax == NULL) return NULL;
|
||||
rax->numele = 0;
|
||||
rax->numnodes = 1;
|
||||
rax->head = raxNewNode(0,0);
|
||||
rax->head = raxNewNode(0, 0);
|
||||
if (rax->head == NULL) {
|
||||
rax_free(rax);
|
||||
return NULL;
|
||||
@@ -1210,6 +1215,25 @@ void raxRecursiveFree(rax *rax, raxNode *n, void (*free_callback)(void*)) {
|
||||
rax->numnodes--;
|
||||
}
|
||||
|
||||
/* Same as raxRecursiveFree() with context argument */
|
||||
void raxRecursiveFreeWithCtx(rax *rax, raxNode *n,
|
||||
void (*free_callback)(void *item, void *ctx), void *ctx) {
|
||||
debugnode("free traversing",n);
|
||||
int numchildren = n->iscompr ? 1 : n->size;
|
||||
raxNode **cp = raxNodeLastChildPtr(n);
|
||||
while(numchildren--) {
|
||||
raxNode *child;
|
||||
memcpy(&child,cp,sizeof(child));
|
||||
raxRecursiveFreeWithCtx(rax,child,free_callback, ctx);
|
||||
cp--;
|
||||
}
|
||||
debugnode("free depth-first",n);
|
||||
if (free_callback && n->iskey && !n->isnull)
|
||||
free_callback(raxGetData(n), ctx);
|
||||
rax_free(n);
|
||||
rax->numnodes--;
|
||||
}
|
||||
|
||||
/* Free a whole radix tree, calling the specified callback in order to
|
||||
* free the auxiliary data. */
|
||||
void raxFreeWithCallback(rax *rax, void (*free_callback)(void*)) {
|
||||
@@ -1218,6 +1242,15 @@ void raxFreeWithCallback(rax *rax, void (*free_callback)(void*)) {
|
||||
rax_free(rax);
|
||||
}
|
||||
|
||||
/* Free a whole radix tree, calling the specified callback in order to
|
||||
* free the auxiliary data. */
|
||||
void raxFreeWithCbAndContext(rax *rax,
|
||||
void (*free_callback)(void *item, void *ctx), void *ctx) {
|
||||
raxRecursiveFreeWithCtx(rax,rax->head,free_callback,ctx);
|
||||
assert(rax->numnodes == 0);
|
||||
rax_free(rax);
|
||||
}
|
||||
|
||||
/* Free a whole radix tree. */
|
||||
void raxFree(rax *rax) {
|
||||
raxFreeWithCallback(rax,NULL);
|
||||
|
||||
@@ -113,6 +113,7 @@ typedef struct rax {
|
||||
raxNode *head;
|
||||
uint64_t numele;
|
||||
uint64_t numnodes;
|
||||
void *metadata[];
|
||||
} rax;
|
||||
|
||||
/* Stack data structure used by raxLowWalk() in order to, optionally, return
|
||||
@@ -166,12 +167,16 @@ typedef struct raxIterator {
|
||||
|
||||
/* Exported API. */
|
||||
rax *raxNew(void);
|
||||
rax *raxNewWithMetadata(int metaSize);
|
||||
int raxInsert(rax *rax, unsigned char *s, size_t len, void *data, void **old);
|
||||
int raxTryInsert(rax *rax, unsigned char *s, size_t len, void *data, void **old);
|
||||
int raxRemove(rax *rax, unsigned char *s, size_t len, void **old);
|
||||
int raxFind(rax *rax, unsigned char *s, size_t len, void **value);
|
||||
void raxFree(rax *rax);
|
||||
void raxFreeWithCallback(rax *rax, void (*free_callback)(void*));
|
||||
void raxFreeWithCbAndContext(rax *rax,
|
||||
void (*free_callback)(void *item, void *ctx),
|
||||
void *ctx);
|
||||
void raxStart(raxIterator *it, rax *rt);
|
||||
int raxSeek(raxIterator *it, const char *op, unsigned char *ele, size_t len);
|
||||
int raxNext(raxIterator *it);
|
||||
|
||||
@@ -268,8 +268,9 @@ int rdbEncodeInteger(long long value, unsigned char *enc) {
|
||||
* The returned value changes according to the flags, see
|
||||
* rdbGenericLoadStringObject() for more info. */
|
||||
void *rdbLoadIntegerObject(rio *rdb, int enctype, int flags, size_t *lenptr) {
|
||||
int plain = flags & RDB_LOAD_PLAIN;
|
||||
int sds = flags & RDB_LOAD_SDS;
|
||||
int plainFlag = flags & RDB_LOAD_PLAIN;
|
||||
int sdsFlag = flags & RDB_LOAD_SDS;
|
||||
int hfldFlag = flags & (RDB_LOAD_HFLD|RDB_LOAD_HFLD_TTL);
|
||||
int encode = flags & RDB_LOAD_ENC;
|
||||
unsigned char enc[4];
|
||||
long long val;
|
||||
@@ -295,11 +296,17 @@ void *rdbLoadIntegerObject(rio *rdb, int enctype, int flags, size_t *lenptr) {
|
||||
rdbReportCorruptRDB("Unknown RDB integer encoding type %d",enctype);
|
||||
return NULL; /* Never reached. */
|
||||
}
|
||||
if (plain || sds) {
|
||||
if (plainFlag || sdsFlag || hfldFlag) {
|
||||
char buf[LONG_STR_SIZE], *p;
|
||||
int len = ll2string(buf,sizeof(buf),val);
|
||||
if (lenptr) *lenptr = len;
|
||||
p = plain ? zmalloc(len) : sdsnewlen(SDS_NOINIT,len);
|
||||
if (plainFlag) {
|
||||
p = zmalloc(len);
|
||||
} else if (sdsFlag) {
|
||||
p = sdsnewlen(SDS_NOINIT,len);
|
||||
} else { /* hfldFlag */
|
||||
p = hfieldNew(NULL, len, (flags&RDB_LOAD_HFLD) ? 0 : 1);
|
||||
}
|
||||
memcpy(p,buf,len);
|
||||
return p;
|
||||
} else if (encode) {
|
||||
@@ -368,8 +375,11 @@ ssize_t rdbSaveLzfStringObject(rio *rdb, unsigned char *s, size_t len) {
|
||||
* changes according to 'flags'. For more info check the
|
||||
* rdbGenericLoadStringObject() function. */
|
||||
void *rdbLoadLzfStringObject(rio *rdb, int flags, size_t *lenptr) {
|
||||
int plain = flags & RDB_LOAD_PLAIN;
|
||||
int sds = flags & RDB_LOAD_SDS;
|
||||
int plainFlag = flags & RDB_LOAD_PLAIN;
|
||||
int sdsFlag = flags & RDB_LOAD_SDS;
|
||||
int hfldFlag = flags & (RDB_LOAD_HFLD | RDB_LOAD_HFLD_TTL);
|
||||
int robjFlag = (!(plainFlag || sdsFlag || hfldFlag)); /* not plain/sds/hfld */
|
||||
|
||||
uint64_t len, clen;
|
||||
unsigned char *c = NULL;
|
||||
char *val = NULL;
|
||||
@@ -382,11 +392,14 @@ void *rdbLoadLzfStringObject(rio *rdb, int flags, size_t *lenptr) {
|
||||
}
|
||||
|
||||
/* Allocate our target according to the uncompressed size. */
|
||||
if (plain) {
|
||||
if (plainFlag) {
|
||||
val = ztrymalloc(len);
|
||||
} else {
|
||||
} else if (sdsFlag || robjFlag) {
|
||||
val = sdstrynewlen(SDS_NOINIT,len);
|
||||
} else { /* hfldFlag */
|
||||
val = hfieldTryNew(NULL, len, (flags&RDB_LOAD_HFLD) ? 0 : 1);
|
||||
}
|
||||
|
||||
if (!val) {
|
||||
serverLog(isRestoreContext()? LL_VERBOSE: LL_WARNING, "rdbLoadLzfStringObject failed allocating %llu bytes", (unsigned long long)len);
|
||||
goto err;
|
||||
@@ -402,17 +415,17 @@ void *rdbLoadLzfStringObject(rio *rdb, int flags, size_t *lenptr) {
|
||||
}
|
||||
zfree(c);
|
||||
|
||||
if (plain || sds) {
|
||||
return val;
|
||||
} else {
|
||||
return createObject(OBJ_STRING,val);
|
||||
}
|
||||
return (robjFlag) ? createObject(OBJ_STRING,val) : (void *) val;
|
||||
|
||||
err:
|
||||
zfree(c);
|
||||
if (plain)
|
||||
if (plainFlag) {
|
||||
zfree(val);
|
||||
else
|
||||
} else if (sdsFlag || robjFlag) {
|
||||
sdsfree(val);
|
||||
} else { /* hfldFlag*/
|
||||
hfieldFree(val);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
@@ -491,12 +504,18 @@ ssize_t rdbSaveStringObject(rio *rdb, robj *obj) {
|
||||
* RDB_LOAD_PLAIN: Return a plain string allocated with zmalloc()
|
||||
* instead of a Redis object with an sds in it.
|
||||
* RDB_LOAD_SDS: Return an SDS string instead of a Redis object.
|
||||
* RDB_LOAD_HFLD: Return a hash field object (mstr)
|
||||
* RDB_LOAD_HFLD_TTL: Return a hash field with TTL metadata reserved
|
||||
*
|
||||
* On I/O error NULL is returned.
|
||||
*/
|
||||
void *rdbGenericLoadStringObject(rio *rdb, int flags, size_t *lenptr) {
|
||||
int plain = flags & RDB_LOAD_PLAIN;
|
||||
int sds = flags & RDB_LOAD_SDS;
|
||||
void *buf;
|
||||
int plainFlag = flags & RDB_LOAD_PLAIN;
|
||||
int sdsFlag = flags & RDB_LOAD_SDS;
|
||||
int hfldFlag = flags & (RDB_LOAD_HFLD|RDB_LOAD_HFLD_TTL);
|
||||
int robjFlag = (!(plainFlag || sdsFlag || hfldFlag)); /* not plain/sds/hfld */
|
||||
|
||||
int isencoded;
|
||||
unsigned long long len;
|
||||
|
||||
@@ -517,22 +536,8 @@ void *rdbGenericLoadStringObject(rio *rdb, int flags, size_t *lenptr) {
|
||||
}
|
||||
}
|
||||
|
||||
if (plain || sds) {
|
||||
void *buf = plain ? ztrymalloc(len) : sdstrynewlen(SDS_NOINIT,len);
|
||||
if (!buf) {
|
||||
serverLog(isRestoreContext()? LL_VERBOSE: LL_WARNING, "rdbGenericLoadStringObject failed allocating %llu bytes", len);
|
||||
return NULL;
|
||||
}
|
||||
if (lenptr) *lenptr = len;
|
||||
if (len && rioRead(rdb,buf,len) == 0) {
|
||||
if (plain)
|
||||
zfree(buf);
|
||||
else
|
||||
sdsfree(buf);
|
||||
return NULL;
|
||||
}
|
||||
return buf;
|
||||
} else {
|
||||
/* return robj */
|
||||
if (robjFlag) {
|
||||
robj *o = tryCreateStringObject(SDS_NOINIT,len);
|
||||
if (!o) {
|
||||
serverLog(isRestoreContext()? LL_VERBOSE: LL_WARNING, "rdbGenericLoadStringObject failed allocating %llu bytes", len);
|
||||
@@ -544,6 +549,32 @@ void *rdbGenericLoadStringObject(rio *rdb, int flags, size_t *lenptr) {
|
||||
}
|
||||
return o;
|
||||
}
|
||||
|
||||
/* plain/sds/hfld */
|
||||
if (plainFlag) {
|
||||
buf = ztrymalloc(len);
|
||||
} else if (sdsFlag) {
|
||||
buf = sdstrynewlen(SDS_NOINIT,len);
|
||||
} else { /* hfldFlag */
|
||||
buf = hfieldTryNew(NULL, len, (flags&RDB_LOAD_HFLD) ? 0 : 1);
|
||||
}
|
||||
if (!buf) {
|
||||
serverLog(isRestoreContext()? LL_VERBOSE: LL_WARNING, "rdbGenericLoadStringObject failed allocating %llu bytes", len);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (lenptr) *lenptr = len;
|
||||
if (len && rioRead(rdb,buf,len) == 0) {
|
||||
if (plainFlag)
|
||||
zfree(buf);
|
||||
else if (sdsFlag) {
|
||||
sdsfree(buf);
|
||||
} else { /* hfldFlag */
|
||||
hfieldFree(buf);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
return buf;
|
||||
}
|
||||
|
||||
robj *rdbLoadStringObject(rio *rdb) {
|
||||
@@ -665,9 +696,14 @@ int rdbSaveObjectType(rio *rdb, robj *o) {
|
||||
case OBJ_HASH:
|
||||
if (o->encoding == OBJ_ENCODING_LISTPACK)
|
||||
return rdbSaveType(rdb,RDB_TYPE_HASH_LISTPACK);
|
||||
else if (o->encoding == OBJ_ENCODING_HT)
|
||||
return rdbSaveType(rdb,RDB_TYPE_HASH);
|
||||
else
|
||||
else if (o->encoding == OBJ_ENCODING_LISTPACK_EX)
|
||||
return rdbSaveType(rdb,RDB_TYPE_HASH_LISTPACK_EX);
|
||||
else if (o->encoding == OBJ_ENCODING_HT) {
|
||||
if (hashTypeGetMinExpire(o) == EB_EXPIRE_TIME_INVALID)
|
||||
return rdbSaveType(rdb,RDB_TYPE_HASH);
|
||||
else
|
||||
return rdbSaveType(rdb,RDB_TYPE_HASH_METADATA);
|
||||
} else
|
||||
serverPanic("Unknown hash encoding");
|
||||
case OBJ_STREAM:
|
||||
return rdbSaveType(rdb,RDB_TYPE_STREAM_LISTPACKS_3);
|
||||
@@ -908,32 +944,58 @@ ssize_t rdbSaveObject(rio *rdb, robj *o, robj *key, int dbid) {
|
||||
}
|
||||
} else if (o->type == OBJ_HASH) {
|
||||
/* Save a hash value */
|
||||
if (o->encoding == OBJ_ENCODING_LISTPACK) {
|
||||
size_t l = lpBytes((unsigned char*)o->ptr);
|
||||
if ((o->encoding == OBJ_ENCODING_LISTPACK) ||
|
||||
(o->encoding == OBJ_ENCODING_LISTPACK_EX))
|
||||
{
|
||||
unsigned char *lp_ptr = hashTypeListpackGetLp(o);
|
||||
size_t l = lpBytes(lp_ptr);
|
||||
|
||||
if ((n = rdbSaveRawString(rdb,o->ptr,l)) == -1) return -1;
|
||||
if ((n = rdbSaveRawString(rdb,lp_ptr,l)) == -1) return -1;
|
||||
nwritten += n;
|
||||
} else if (o->encoding == OBJ_ENCODING_HT) {
|
||||
dictIterator *di = dictGetIterator(o->ptr);
|
||||
dictEntry *de;
|
||||
/* Determine the hash layout to use based on the presence of at least
|
||||
* one field with a valid TTL. If such a field exists, employ the
|
||||
* RDB_TYPE_HASH_METADATA layout, including tuples of [ttl][field][value].
|
||||
* Otherwise, use the standard RDB_TYPE_HASH layout containing only
|
||||
* the tuples [field][value]. */
|
||||
int with_ttl = (hashTypeGetMinExpire(o) != EB_EXPIRE_TIME_INVALID);
|
||||
|
||||
/* save number of fields in hash */
|
||||
if ((n = rdbSaveLen(rdb,dictSize((dict*)o->ptr))) == -1) {
|
||||
dictReleaseIterator(di);
|
||||
return -1;
|
||||
}
|
||||
nwritten += n;
|
||||
|
||||
/* save all hash fields */
|
||||
while((de = dictNext(di)) != NULL) {
|
||||
sds field = dictGetKey(de);
|
||||
hfield field = dictGetKey(de);
|
||||
sds value = dictGetVal(de);
|
||||
|
||||
/* save the TTL */
|
||||
if (with_ttl) {
|
||||
uint64_t ttl = hfieldGetExpireTime(field);
|
||||
/* 0 is used to indicate no TTL is set for this field */
|
||||
if (ttl == EB_EXPIRE_TIME_INVALID) ttl = 0;
|
||||
if ((n = rdbSaveLen(rdb, ttl)) == -1) {
|
||||
dictReleaseIterator(di);
|
||||
return -1;
|
||||
}
|
||||
nwritten += n;
|
||||
}
|
||||
|
||||
/* save the key */
|
||||
if ((n = rdbSaveRawString(rdb,(unsigned char*)field,
|
||||
sdslen(field))) == -1)
|
||||
hfieldlen(field))) == -1)
|
||||
{
|
||||
dictReleaseIterator(di);
|
||||
return -1;
|
||||
}
|
||||
nwritten += n;
|
||||
|
||||
/* save the value */
|
||||
if ((n = rdbSaveRawString(rdb,(unsigned char*)value,
|
||||
sdslen(value))) == -1)
|
||||
{
|
||||
@@ -1403,7 +1465,7 @@ werr:
|
||||
return C_ERR;
|
||||
}
|
||||
|
||||
/* This helper function is only used for diskless replication.
|
||||
/* This helper function is only used for diskless replication.
|
||||
* This is just a wrapper to rdbSaveRio() that additionally adds a prefix
|
||||
* and a suffix to the generated RDB dump. The prefix is:
|
||||
*
|
||||
@@ -1753,19 +1815,20 @@ static int _listZiplistEntryConvertAndValidate(unsigned char *p, unsigned int he
|
||||
/* callback for to check the listpack doesn't have duplicate records */
|
||||
static int _lpEntryValidation(unsigned char *p, unsigned int head_count, void *userdata) {
|
||||
struct {
|
||||
int pairs;
|
||||
int tuple_len;
|
||||
long count;
|
||||
dict *fields;
|
||||
long long last_expireat;
|
||||
} *data = userdata;
|
||||
|
||||
if (data->fields == NULL) {
|
||||
data->fields = dictCreate(&hashDictType);
|
||||
dictExpand(data->fields, data->pairs ? head_count/2 : head_count);
|
||||
dictExpand(data->fields, head_count/data->tuple_len);
|
||||
}
|
||||
|
||||
/* If we're checking pairs, then even records are field names. Otherwise
|
||||
* we're checking all elements. Add to dict and check that's not a dup */
|
||||
if (!data->pairs || ((data->count) & 1) == 0) {
|
||||
if (data->count % data->tuple_len == 0) {
|
||||
unsigned char *str;
|
||||
int64_t slen;
|
||||
unsigned char buf[LP_INTBUF_SIZE];
|
||||
@@ -1779,6 +1842,19 @@ static int _lpEntryValidation(unsigned char *p, unsigned int head_count, void *u
|
||||
}
|
||||
}
|
||||
|
||||
/* Validate TTL field, only for listpackex. */
|
||||
if (data->count % data->tuple_len == 2) {
|
||||
long long expire_at;
|
||||
/* Must be an integer. */
|
||||
if (!lpGetIntegerValue(p, &expire_at)) return 0;
|
||||
/* Must be less than EB_EXPIRE_TIME_MAX. */
|
||||
if (expire_at < 0 || (unsigned long long)expire_at > EB_EXPIRE_TIME_MAX) return 0;
|
||||
/* TTL fields are ordered. If the current field has TTL, the previous field must
|
||||
* also have one, and the current TTL must be greater than the previous one. */
|
||||
if (expire_at != 0 && (data->last_expireat == 0 || expire_at < data->last_expireat)) return 0;
|
||||
data->last_expireat = expire_at;
|
||||
}
|
||||
|
||||
(data->count)++;
|
||||
return 1;
|
||||
}
|
||||
@@ -1786,23 +1862,25 @@ static int _lpEntryValidation(unsigned char *p, unsigned int head_count, void *u
|
||||
/* Validate the integrity of the listpack structure.
|
||||
* when `deep` is 0, only the integrity of the header is validated.
|
||||
* when `deep` is 1, we scan all the entries one by one.
|
||||
* when `pairs` is 0, all elements need to be unique (it's a set)
|
||||
* when `pairs` is 1, odd elements need to be unique (it's a key-value map) */
|
||||
int lpValidateIntegrityAndDups(unsigned char *lp, size_t size, int deep, int pairs) {
|
||||
* tuple_len indicates what is a logical entry tuple size.
|
||||
* Whether tuple is of size 1 (set), 2 (feild-value) or 3 (field-value[-ttl]),
|
||||
* first element in the tuple must be unique */
|
||||
int lpValidateIntegrityAndDups(unsigned char *lp, size_t size, int deep, int tuple_len) {
|
||||
if (!deep)
|
||||
return lpValidateIntegrity(lp, size, 0, NULL, NULL);
|
||||
|
||||
/* Keep track of the field names to locate duplicate ones */
|
||||
struct {
|
||||
int pairs;
|
||||
int tuple_len;
|
||||
long count;
|
||||
dict *fields; /* Initialisation at the first callback. */
|
||||
} data = {pairs, 0, NULL};
|
||||
long long last_expireat; /* Last field's expiry time to ensure order in TTL fields. */
|
||||
} data = {tuple_len, 0, NULL, -1};
|
||||
|
||||
int ret = lpValidateIntegrity(lp, size, 1, _lpEntryValidation, &data);
|
||||
|
||||
/* make sure we have an even number of records. */
|
||||
if (pairs && data.count & 1)
|
||||
/* the number of records should be a multiple of the tuple length */
|
||||
if (data.count % tuple_len != 0)
|
||||
ret = 0;
|
||||
|
||||
if (data.fields) dictRelease(data.fields);
|
||||
@@ -1811,9 +1889,18 @@ int lpValidateIntegrityAndDups(unsigned char *lp, size_t size, int deep, int pai
|
||||
|
||||
/* Load a Redis object of the specified type from the specified file.
|
||||
* On success a newly allocated object is returned, otherwise NULL.
|
||||
* When the function returns NULL and if 'error' is not NULL, the
|
||||
* integer pointed by 'error' is set to the type of error that occurred */
|
||||
robj *rdbLoadObject(int rdbtype, rio *rdb, sds key, int dbid, int *error) {
|
||||
*
|
||||
* error - When the function returns NULL and if 'error' is not NULL, the
|
||||
* integer pointed by 'error' is set to the type of error that occurred
|
||||
* minExpiredField - If loading a hash with expiration on fields, then this value
|
||||
* will be set to the minimum expire time found in the hash fields. If there are
|
||||
* no fields with expiration or it is not a hash, then it will set be to
|
||||
* EB_EXPIRE_TIME_INVALID.
|
||||
*/
|
||||
robj *rdbLoadObject(int rdbtype, rio *rdb, sds key, redisDb* db, int *error,
|
||||
uint64_t *minExpiredField)
|
||||
{
|
||||
uint64_t minExpField = EB_EXPIRE_TIME_INVALID;
|
||||
robj *o = NULL, *ele, *dec;
|
||||
uint64_t len;
|
||||
unsigned int i;
|
||||
@@ -1856,7 +1943,7 @@ robj *rdbLoadObject(int rdbtype, rio *rdb, sds key, int dbid, int *error) {
|
||||
decrRefCount(ele);
|
||||
}
|
||||
|
||||
listTypeTryConversion(o,LIST_CONV_AUTO,NULL,NULL);
|
||||
listTypeTryConversion(o, LIST_CONV_AUTO, NULL, NULL);
|
||||
} else if (rdbtype == RDB_TYPE_SET) {
|
||||
/* Read Set value */
|
||||
if ((len = rdbLoadLen(rdb,NULL)) == RDB_LENERR) return NULL;
|
||||
@@ -1869,7 +1956,7 @@ robj *rdbLoadObject(int rdbtype, rio *rdb, sds key, int dbid, int *error) {
|
||||
o = createSetObject();
|
||||
/* It's faster to expand the dict to the right size asap in order
|
||||
* to avoid rehashing */
|
||||
if (len > DICT_HT_INITIAL_SIZE && dictTryExpand(o->ptr,len) != DICT_OK) {
|
||||
if (len > DICT_HT_INITIAL_SIZE && dictTryExpand(o->ptr, len) != DICT_OK) {
|
||||
rdbReportCorruptRDB("OOM in dictTryExpand %llu", (unsigned long long)len);
|
||||
decrRefCount(o);
|
||||
return NULL;
|
||||
@@ -1896,7 +1983,7 @@ robj *rdbLoadObject(int rdbtype, rio *rdb, sds key, int dbid, int *error) {
|
||||
/* Fetch integer value from element. */
|
||||
if (isSdsRepresentableAsLongLong(sdsele,&llval) == C_OK) {
|
||||
uint8_t success;
|
||||
o->ptr = intsetAdd(o->ptr,llval,&success);
|
||||
o->ptr = intsetAdd(o->ptr, llval, &success);
|
||||
if (!success) {
|
||||
rdbReportCorruptRDB("Duplicate set members detected");
|
||||
decrRefCount(o);
|
||||
@@ -1946,7 +2033,7 @@ robj *rdbLoadObject(int rdbtype, rio *rdb, sds key, int dbid, int *error) {
|
||||
/* This will also be called when the set was just converted
|
||||
* to a regular hash table encoded set. */
|
||||
if (o->encoding == OBJ_ENCODING_HT) {
|
||||
if (dictAdd((dict*)o->ptr,sdsele,NULL) != DICT_OK) {
|
||||
if (dictAdd((dict*)o->ptr, sdsele, NULL) != DICT_OK) {
|
||||
rdbReportCorruptRDB("Duplicate set members detected");
|
||||
decrRefCount(o);
|
||||
sdsfree(sdsele);
|
||||
@@ -2024,12 +2111,13 @@ robj *rdbLoadObject(int rdbtype, rio *rdb, sds key, int dbid, int *error) {
|
||||
maxelelen <= server.zset_max_listpack_value &&
|
||||
lpSafeToAdd(NULL, totelelen))
|
||||
{
|
||||
zsetConvert(o,OBJ_ENCODING_LISTPACK);
|
||||
zsetConvert(o, OBJ_ENCODING_LISTPACK);
|
||||
}
|
||||
} else if (rdbtype == RDB_TYPE_HASH) {
|
||||
uint64_t len;
|
||||
int ret;
|
||||
sds field, value;
|
||||
sds value;
|
||||
hfield field;
|
||||
dict *dupSearchDict = NULL;
|
||||
|
||||
len = rdbLoadLen(rdb, NULL);
|
||||
@@ -2040,7 +2128,7 @@ robj *rdbLoadObject(int rdbtype, rio *rdb, sds key, int dbid, int *error) {
|
||||
|
||||
/* Too many entries? Use a hash table right from the start. */
|
||||
if (len > server.hash_max_listpack_entries)
|
||||
hashTypeConvert(o, OBJ_ENCODING_HT);
|
||||
hashTypeConvert(o, OBJ_ENCODING_HT, NULL);
|
||||
else if (deep_integrity_validation) {
|
||||
/* In this mode, we need to guarantee that the server won't crash
|
||||
* later when the ziplist is converted to a dict.
|
||||
@@ -2049,48 +2137,50 @@ robj *rdbLoadObject(int rdbtype, rio *rdb, sds key, int dbid, int *error) {
|
||||
dupSearchDict = dictCreate(&hashDictType);
|
||||
}
|
||||
|
||||
|
||||
/* Load every field and value into the ziplist */
|
||||
/* Load every field and value into the listpack */
|
||||
while (o->encoding == OBJ_ENCODING_LISTPACK && len > 0) {
|
||||
len--;
|
||||
/* Load raw strings */
|
||||
if ((field = rdbGenericLoadStringObject(rdb,RDB_LOAD_SDS,NULL)) == NULL) {
|
||||
if ((field = rdbGenericLoadStringObject(rdb,RDB_LOAD_HFLD,NULL)) == NULL) {
|
||||
decrRefCount(o);
|
||||
if (dupSearchDict) dictRelease(dupSearchDict);
|
||||
return NULL;
|
||||
}
|
||||
if ((value = rdbGenericLoadStringObject(rdb,RDB_LOAD_SDS,NULL)) == NULL) {
|
||||
sdsfree(field);
|
||||
hfieldFree(field);
|
||||
decrRefCount(o);
|
||||
if (dupSearchDict) dictRelease(dupSearchDict);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (dupSearchDict) {
|
||||
sds field_dup = sdsdup(field);
|
||||
sds field_dup = sdsnewlen(field, hfieldlen(field));
|
||||
|
||||
if (dictAdd(dupSearchDict, field_dup, NULL) != DICT_OK) {
|
||||
rdbReportCorruptRDB("Hash with dup elements");
|
||||
dictRelease(dupSearchDict);
|
||||
decrRefCount(o);
|
||||
sdsfree(field_dup);
|
||||
sdsfree(field);
|
||||
hfieldFree(field);
|
||||
sdsfree(value);
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
/* Convert to hash table if size threshold is exceeded */
|
||||
if (sdslen(field) > server.hash_max_listpack_value ||
|
||||
if (hfieldlen(field) > server.hash_max_listpack_value ||
|
||||
sdslen(value) > server.hash_max_listpack_value ||
|
||||
!lpSafeToAdd(o->ptr, sdslen(field)+sdslen(value)))
|
||||
!lpSafeToAdd(o->ptr, hfieldlen(field) + sdslen(value)))
|
||||
{
|
||||
hashTypeConvert(o, OBJ_ENCODING_HT);
|
||||
hashTypeConvert(o, OBJ_ENCODING_HT, NULL);
|
||||
dictUseStoredKeyApi((dict *)o->ptr, 1);
|
||||
ret = dictAdd((dict*)o->ptr, field, value);
|
||||
dictUseStoredKeyApi((dict *)o->ptr, 0);
|
||||
if (ret == DICT_ERR) {
|
||||
rdbReportCorruptRDB("Duplicate hash fields detected");
|
||||
if (dupSearchDict) dictRelease(dupSearchDict);
|
||||
sdsfree(value);
|
||||
sdsfree(field);
|
||||
hfieldFree(field);
|
||||
decrRefCount(o);
|
||||
return NULL;
|
||||
}
|
||||
@@ -2098,10 +2188,10 @@ robj *rdbLoadObject(int rdbtype, rio *rdb, sds key, int dbid, int *error) {
|
||||
}
|
||||
|
||||
/* Add pair to listpack */
|
||||
o->ptr = lpAppend(o->ptr, (unsigned char*)field, sdslen(field));
|
||||
o->ptr = lpAppend(o->ptr, (unsigned char*)field, hfieldlen(field));
|
||||
o->ptr = lpAppend(o->ptr, (unsigned char*)value, sdslen(value));
|
||||
|
||||
sdsfree(field);
|
||||
hfieldFree(field);
|
||||
sdsfree(value);
|
||||
}
|
||||
|
||||
@@ -2113,7 +2203,7 @@ robj *rdbLoadObject(int rdbtype, rio *rdb, sds key, int dbid, int *error) {
|
||||
}
|
||||
|
||||
if (o->encoding == OBJ_ENCODING_HT && len > DICT_HT_INITIAL_SIZE) {
|
||||
if (dictTryExpand(o->ptr,len) != DICT_OK) {
|
||||
if (dictTryExpand(o->ptr, len) != DICT_OK) {
|
||||
rdbReportCorruptRDB("OOM in dictTryExpand %llu", (unsigned long long)len);
|
||||
decrRefCount(o);
|
||||
return NULL;
|
||||
@@ -2124,22 +2214,25 @@ robj *rdbLoadObject(int rdbtype, rio *rdb, sds key, int dbid, int *error) {
|
||||
while (o->encoding == OBJ_ENCODING_HT && len > 0) {
|
||||
len--;
|
||||
/* Load encoded strings */
|
||||
if ((field = rdbGenericLoadStringObject(rdb,RDB_LOAD_SDS,NULL)) == NULL) {
|
||||
if ((field = rdbGenericLoadStringObject(rdb,RDB_LOAD_HFLD,NULL)) == NULL) {
|
||||
decrRefCount(o);
|
||||
return NULL;
|
||||
}
|
||||
if ((value = rdbGenericLoadStringObject(rdb,RDB_LOAD_SDS,NULL)) == NULL) {
|
||||
sdsfree(field);
|
||||
hfieldFree(field);
|
||||
decrRefCount(o);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Add pair to hash table */
|
||||
ret = dictAdd((dict*)o->ptr, field, value);
|
||||
dict *d = o->ptr;
|
||||
dictUseStoredKeyApi(d, 1);
|
||||
ret = dictAdd(d, field, value);
|
||||
dictUseStoredKeyApi(d, 0);
|
||||
if (ret == DICT_ERR) {
|
||||
rdbReportCorruptRDB("Duplicate hash fields detected");
|
||||
sdsfree(value);
|
||||
sdsfree(field);
|
||||
hfieldFree(field);
|
||||
decrRefCount(o);
|
||||
return NULL;
|
||||
}
|
||||
@@ -2147,6 +2240,149 @@ robj *rdbLoadObject(int rdbtype, rio *rdb, sds key, int dbid, int *error) {
|
||||
|
||||
/* All pairs should be read by now */
|
||||
serverAssert(len == 0);
|
||||
} else if (rdbtype == RDB_TYPE_HASH_METADATA) {
|
||||
size_t fieldLen;
|
||||
sds value, field;
|
||||
uint64_t expireAt;
|
||||
dict *dupSearchDict = NULL;
|
||||
|
||||
len = rdbLoadLen(rdb, NULL);
|
||||
if (len == RDB_LENERR) return NULL;
|
||||
if (len == 0) goto emptykey;
|
||||
/* TODO: create listpackEx or HT directly*/
|
||||
o = createHashObject();
|
||||
/* Too many entries? Use a hash table right from the start. */
|
||||
if (len > server.hash_max_listpack_entries) {
|
||||
hashTypeConvert(o, OBJ_ENCODING_HT, NULL);
|
||||
dictTypeAddMeta((dict**)&o->ptr, &mstrHashDictTypeWithHFE);
|
||||
initDictExpireMetadata(key, o);
|
||||
} else {
|
||||
hashTypeConvert(o, OBJ_ENCODING_LISTPACK_EX, NULL);
|
||||
if (deep_integrity_validation) {
|
||||
/* In this mode, we need to guarantee that the server won't crash
|
||||
* later when the listpack is converted to a dict.
|
||||
* Create a set (dict with no values) for dup search.
|
||||
* We can dismiss it as soon as we convert the listpack to a hash. */
|
||||
dupSearchDict = dictCreate(&hashDictType);
|
||||
}
|
||||
}
|
||||
|
||||
while (len > 0) {
|
||||
len--;
|
||||
|
||||
/* read the TTL */
|
||||
if (rdbLoadLenByRef(rdb, NULL, &expireAt) == -1) {
|
||||
serverLog(LL_WARNING, "failed reading hash TTL");
|
||||
decrRefCount(o);
|
||||
if (dupSearchDict != NULL) dictRelease(dupSearchDict);
|
||||
return NULL;
|
||||
}
|
||||
if (expireAt > EB_EXPIRE_TIME_MAX) {
|
||||
rdbReportCorruptRDB("invalid expireAt time: %llu", (unsigned long long)expireAt);
|
||||
decrRefCount(o);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* if needed create field with TTL metadata */
|
||||
if (expireAt !=0)
|
||||
field = rdbGenericLoadStringObject(rdb, RDB_LOAD_HFLD_TTL, &fieldLen);
|
||||
else
|
||||
field = rdbGenericLoadStringObject(rdb, RDB_LOAD_HFLD, &fieldLen);
|
||||
|
||||
if (field == NULL) {
|
||||
serverLog(LL_WARNING, "failed reading hash field");
|
||||
decrRefCount(o);
|
||||
if (dupSearchDict != NULL) dictRelease(dupSearchDict);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* read the value */
|
||||
if ((value = rdbGenericLoadStringObject(rdb,RDB_LOAD_SDS,NULL)) == NULL) {
|
||||
serverLog(LL_WARNING, "failed reading hash value");
|
||||
decrRefCount(o);
|
||||
if (dupSearchDict != NULL) dictRelease(dupSearchDict);
|
||||
hfieldFree(field);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* keep the nearest expiration to connect listpack object to db expiry */
|
||||
if ((expireAt != 0) && (expireAt < minExpField)) minExpField = expireAt;
|
||||
|
||||
/* store the values read - either to listpack or dict */
|
||||
if (o->encoding == OBJ_ENCODING_LISTPACK_EX) {
|
||||
/* integrity - check for key duplication (if required) */
|
||||
if (dupSearchDict) {
|
||||
sds field_dup = sdsnewlen(field, hfieldlen(field));
|
||||
|
||||
if (dictAdd(dupSearchDict, field_dup, NULL) != DICT_OK) {
|
||||
rdbReportCorruptRDB("Hash with dup elements");
|
||||
dictRelease(dupSearchDict);
|
||||
decrRefCount(o);
|
||||
sdsfree(field_dup);
|
||||
sdsfree(value);
|
||||
hfieldFree(field);
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
/* check if the values can be saved to listpack (or should convert to dict encoding) */
|
||||
if (hfieldlen(field) > server.hash_max_listpack_value ||
|
||||
sdslen(value) > server.hash_max_listpack_value ||
|
||||
!lpSafeToAdd(((listpackEx*)o->ptr)->lp, hfieldlen(field) + sdslen(value) + lpEntrySizeInteger(expireAt)))
|
||||
{
|
||||
/* convert to hash */
|
||||
hashTypeConvert(o, OBJ_ENCODING_HT, NULL);
|
||||
|
||||
if (len > DICT_HT_INITIAL_SIZE) { /* TODO: this is NOT the original len, but this is also the case for simple hash, is this a bug? */
|
||||
if (dictTryExpand(o->ptr, len) != DICT_OK) {
|
||||
rdbReportCorruptRDB("OOM in dictTryExpand %llu", (unsigned long long)len);
|
||||
decrRefCount(o);
|
||||
if (dupSearchDict != NULL) dictRelease(dupSearchDict);
|
||||
sdsfree(value);
|
||||
hfieldFree(field);
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
/* don't add the values to the new hash: the next if will catch and the values will be added there */
|
||||
} else {
|
||||
listpackExAddNew(o, field, hfieldlen(field),
|
||||
value, sdslen(value), expireAt);
|
||||
hfieldFree(field);
|
||||
sdsfree(value);
|
||||
}
|
||||
}
|
||||
|
||||
if (o->encoding == OBJ_ENCODING_HT) {
|
||||
/* Add pair to hash table */
|
||||
dict *d = o->ptr;
|
||||
dictUseStoredKeyApi(d, 1);
|
||||
int ret = dictAdd(d, field, value);
|
||||
dictUseStoredKeyApi(d, 0);
|
||||
|
||||
/* Attach expiry to the hash field and register in hash private HFE DS */
|
||||
if ((ret != DICT_ERR) && expireAt) {
|
||||
dictExpireMetadata *m = (dictExpireMetadata *) dictMetadata(d);
|
||||
ret = ebAdd(&m->hfe, &hashFieldExpireBucketsType, field, expireAt);
|
||||
}
|
||||
|
||||
if (ret == DICT_ERR) {
|
||||
rdbReportCorruptRDB("Duplicate hash fields detected");
|
||||
sdsfree(value);
|
||||
hfieldFree(field);
|
||||
decrRefCount(o);
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (dupSearchDict != NULL) dictRelease(dupSearchDict);
|
||||
|
||||
/* check for empty key (if all fields were expired) */
|
||||
if (hashTypeLength(o, 0) == 0) {
|
||||
decrRefCount(o);
|
||||
goto expiredHash;
|
||||
}
|
||||
} else if (rdbtype == RDB_TYPE_LIST_QUICKLIST || rdbtype == RDB_TYPE_LIST_QUICKLIST_2) {
|
||||
if ((len = rdbLoadLen(rdb,NULL)) == RDB_LENERR) return NULL;
|
||||
if (len == 0) goto emptykey;
|
||||
@@ -2221,7 +2457,7 @@ robj *rdbLoadObject(int rdbtype, rio *rdb, sds key, int dbid, int *error) {
|
||||
goto emptykey;
|
||||
}
|
||||
|
||||
listTypeTryConversion(o,LIST_CONV_AUTO,NULL,NULL);
|
||||
listTypeTryConversion(o, LIST_CONV_AUTO, NULL, NULL);
|
||||
} else if (rdbtype == RDB_TYPE_HASH_ZIPMAP ||
|
||||
rdbtype == RDB_TYPE_LIST_ZIPLIST ||
|
||||
rdbtype == RDB_TYPE_SET_INTSET ||
|
||||
@@ -2229,14 +2465,15 @@ robj *rdbLoadObject(int rdbtype, rio *rdb, sds key, int dbid, int *error) {
|
||||
rdbtype == RDB_TYPE_ZSET_ZIPLIST ||
|
||||
rdbtype == RDB_TYPE_ZSET_LISTPACK ||
|
||||
rdbtype == RDB_TYPE_HASH_ZIPLIST ||
|
||||
rdbtype == RDB_TYPE_HASH_LISTPACK)
|
||||
rdbtype == RDB_TYPE_HASH_LISTPACK ||
|
||||
rdbtype == RDB_TYPE_HASH_LISTPACK_EX)
|
||||
{
|
||||
size_t encoded_len;
|
||||
unsigned char *encoded =
|
||||
rdbGenericLoadStringObject(rdb,RDB_LOAD_PLAIN,&encoded_len);
|
||||
if (encoded == NULL) return NULL;
|
||||
|
||||
o = createObject(OBJ_STRING,encoded); /* Obj type fixed below. */
|
||||
o = createObject(OBJ_STRING, encoded); /* Obj type fixed below. */
|
||||
|
||||
/* Fix the object encoding, and make sure to convert the encoded
|
||||
* data type into the base type if accordingly to the current
|
||||
@@ -2292,14 +2529,14 @@ robj *rdbLoadObject(int rdbtype, rio *rdb, sds key, int dbid, int *error) {
|
||||
o->type = OBJ_HASH;
|
||||
o->encoding = OBJ_ENCODING_LISTPACK;
|
||||
|
||||
if (hashTypeLength(o) > server.hash_max_listpack_entries ||
|
||||
if (hashTypeLength(o, 0) > server.hash_max_listpack_entries ||
|
||||
maxlen > server.hash_max_listpack_value)
|
||||
{
|
||||
hashTypeConvert(o, OBJ_ENCODING_HT);
|
||||
hashTypeConvert(o, OBJ_ENCODING_HT, NULL);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case RDB_TYPE_LIST_ZIPLIST:
|
||||
case RDB_TYPE_LIST_ZIPLIST:
|
||||
{
|
||||
quicklist *ql = quicklistNew(server.list_max_listpack_size,
|
||||
server.list_compress_depth);
|
||||
@@ -2341,11 +2578,11 @@ robj *rdbLoadObject(int rdbtype, rio *rdb, sds key, int dbid, int *error) {
|
||||
o->type = OBJ_SET;
|
||||
o->encoding = OBJ_ENCODING_INTSET;
|
||||
if (intsetLen(o->ptr) > server.set_max_intset_entries)
|
||||
setTypeConvert(o,OBJ_ENCODING_HT);
|
||||
setTypeConvert(o, OBJ_ENCODING_HT);
|
||||
break;
|
||||
case RDB_TYPE_SET_LISTPACK:
|
||||
if (deep_integrity_validation) server.stat_dump_payload_sanitizations++;
|
||||
if (!lpValidateIntegrityAndDups(encoded, encoded_len, deep_integrity_validation, 0)) {
|
||||
if (!lpValidateIntegrityAndDups(encoded, encoded_len, deep_integrity_validation, 1)) {
|
||||
rdbReportCorruptRDB("Set listpack integrity check failed.");
|
||||
zfree(encoded);
|
||||
o->ptr = NULL;
|
||||
@@ -2386,14 +2623,14 @@ robj *rdbLoadObject(int rdbtype, rio *rdb, sds key, int dbid, int *error) {
|
||||
}
|
||||
|
||||
if (zsetLength(o) > server.zset_max_listpack_entries)
|
||||
zsetConvert(o,OBJ_ENCODING_SKIPLIST);
|
||||
zsetConvert(o, OBJ_ENCODING_SKIPLIST);
|
||||
else
|
||||
o->ptr = lpShrinkToFit(o->ptr);
|
||||
break;
|
||||
}
|
||||
case RDB_TYPE_ZSET_LISTPACK:
|
||||
if (deep_integrity_validation) server.stat_dump_payload_sanitizations++;
|
||||
if (!lpValidateIntegrityAndDups(encoded, encoded_len, deep_integrity_validation, 1)) {
|
||||
if (!lpValidateIntegrityAndDups(encoded, encoded_len, deep_integrity_validation, 2)) {
|
||||
rdbReportCorruptRDB("Zset listpack integrity check failed.");
|
||||
zfree(encoded);
|
||||
o->ptr = NULL;
|
||||
@@ -2408,7 +2645,7 @@ robj *rdbLoadObject(int rdbtype, rio *rdb, sds key, int dbid, int *error) {
|
||||
}
|
||||
|
||||
if (zsetLength(o) > server.zset_max_listpack_entries)
|
||||
zsetConvert(o,OBJ_ENCODING_SKIPLIST);
|
||||
zsetConvert(o, OBJ_ENCODING_SKIPLIST);
|
||||
break;
|
||||
case RDB_TYPE_HASH_ZIPLIST:
|
||||
{
|
||||
@@ -2426,35 +2663,57 @@ robj *rdbLoadObject(int rdbtype, rio *rdb, sds key, int dbid, int *error) {
|
||||
o->ptr = lp;
|
||||
o->type = OBJ_HASH;
|
||||
o->encoding = OBJ_ENCODING_LISTPACK;
|
||||
if (hashTypeLength(o) == 0) {
|
||||
if (hashTypeLength(o, 0) == 0) {
|
||||
decrRefCount(o);
|
||||
goto emptykey;
|
||||
}
|
||||
|
||||
if (hashTypeLength(o) > server.hash_max_listpack_entries)
|
||||
hashTypeConvert(o, OBJ_ENCODING_HT);
|
||||
if (hashTypeLength(o, 0) > server.hash_max_listpack_entries)
|
||||
hashTypeConvert(o, OBJ_ENCODING_HT, NULL);
|
||||
else
|
||||
o->ptr = lpShrinkToFit(o->ptr);
|
||||
break;
|
||||
}
|
||||
case RDB_TYPE_HASH_LISTPACK:
|
||||
case RDB_TYPE_HASH_LISTPACK_EX:
|
||||
/* listpack-encoded hash with TTL requires its own struct
|
||||
* pointed to by o->ptr */
|
||||
o->type = OBJ_HASH;
|
||||
if (rdbtype == RDB_TYPE_HASH_LISTPACK_EX) {
|
||||
listpackEx *lpt = listpackExCreate();
|
||||
lpt->lp = encoded;
|
||||
lpt->key = key;
|
||||
o->ptr = lpt;
|
||||
o->encoding = OBJ_ENCODING_LISTPACK_EX;
|
||||
} else
|
||||
o->encoding = OBJ_ENCODING_LISTPACK;
|
||||
|
||||
/* tuple_len is the number of elements for each key:
|
||||
* key + value for simple hash, key + value + tll for hash with TTL*/
|
||||
int tuple_len = (rdbtype == RDB_TYPE_HASH_LISTPACK ? 2 : 3);
|
||||
/* validate read data */
|
||||
if (deep_integrity_validation) server.stat_dump_payload_sanitizations++;
|
||||
if (!lpValidateIntegrityAndDups(encoded, encoded_len, deep_integrity_validation, 1)) {
|
||||
if (!lpValidateIntegrityAndDups(encoded, encoded_len,
|
||||
deep_integrity_validation, tuple_len)) {
|
||||
rdbReportCorruptRDB("Hash listpack integrity check failed.");
|
||||
zfree(encoded);
|
||||
o->ptr = NULL;
|
||||
decrRefCount(o);
|
||||
return NULL;
|
||||
}
|
||||
o->type = OBJ_HASH;
|
||||
o->encoding = OBJ_ENCODING_LISTPACK;
|
||||
if (hashTypeLength(o) == 0) {
|
||||
|
||||
/* if listpack is empty, delete it */
|
||||
if (hashTypeLength(o, 0) == 0) {
|
||||
decrRefCount(o);
|
||||
goto emptykey;
|
||||
}
|
||||
|
||||
if (hashTypeLength(o) > server.hash_max_listpack_entries)
|
||||
hashTypeConvert(o, OBJ_ENCODING_HT);
|
||||
/* for TTL listpack, find the minimum expiry */
|
||||
minExpField = hashTypeGetNextTimeToExpire(o);
|
||||
|
||||
/* Convert listpack to hash table without registering in global HFE DS,
|
||||
* if has HFEs, since the listpack is not connected yet to the DB */
|
||||
if (hashTypeLength(o, 0) > server.hash_max_listpack_entries)
|
||||
hashTypeConvert(o, OBJ_ENCODING_HT, NULL /*db->hexpires*/);
|
||||
|
||||
break;
|
||||
default:
|
||||
/* totally unreachable */
|
||||
@@ -2540,7 +2799,7 @@ robj *rdbLoadObject(int rdbtype, rio *rdb, sds key, int dbid, int *error) {
|
||||
/* Load the last entry ID. */
|
||||
s->last_id.ms = rdbLoadLen(rdb,NULL);
|
||||
s->last_id.seq = rdbLoadLen(rdb,NULL);
|
||||
|
||||
|
||||
if (rdbtype >= RDB_TYPE_STREAM_LISTPACKS_2) {
|
||||
/* Load the first entry ID. */
|
||||
s->first_id.ms = rdbLoadLen(rdb,NULL);
|
||||
@@ -2559,9 +2818,9 @@ robj *rdbLoadObject(int rdbtype, rio *rdb, sds key, int dbid, int *error) {
|
||||
s->max_deleted_entry_id.ms = 0;
|
||||
s->max_deleted_entry_id.seq = 0;
|
||||
s->entries_added = s->length;
|
||||
|
||||
|
||||
/* Since the rax is already loaded, we can find the first entry's
|
||||
* ID. */
|
||||
* ID. */
|
||||
streamGetEdgeID(s,1,1,&s->first_id);
|
||||
}
|
||||
|
||||
@@ -2794,7 +3053,13 @@ robj *rdbLoadObject(int rdbtype, rio *rdb, sds key, int dbid, int *error) {
|
||||
RedisModuleIO io;
|
||||
robj keyobj;
|
||||
initStaticStringObject(keyobj,key);
|
||||
moduleInitIOContext(io,mt,rdb,&keyobj,dbid);
|
||||
/* shouldn't happen since db is NULL only in RDB check mode, and
|
||||
* in this mode the module load code returns few lines above after
|
||||
* checking module name, few lines above. So this check is only
|
||||
* for safety.
|
||||
*/
|
||||
if (db == NULL) return NULL;
|
||||
moduleInitIOContext(io,mt,rdb,&keyobj,db->id);
|
||||
/* Call the rdb_load method of the module providing the 10 bit
|
||||
* encoding version in the lower 10 bits of the module ID. */
|
||||
void *ptr = mt->rdb_load(&io,moduleid&1023);
|
||||
@@ -2807,7 +3072,7 @@ robj *rdbLoadObject(int rdbtype, rio *rdb, sds key, int dbid, int *error) {
|
||||
uint64_t eof = rdbLoadLen(rdb,NULL);
|
||||
if (eof == RDB_LENERR) {
|
||||
if (ptr) {
|
||||
o = createModuleObject(mt,ptr); /* creating just in order to easily destroy */
|
||||
o = createModuleObject(mt, ptr); /* creating just in order to easily destroy */
|
||||
decrRefCount(o);
|
||||
}
|
||||
return NULL;
|
||||
@@ -2816,7 +3081,7 @@ robj *rdbLoadObject(int rdbtype, rio *rdb, sds key, int dbid, int *error) {
|
||||
rdbReportCorruptRDB("The RDB file contains module data for the module '%s' that is not terminated by "
|
||||
"the proper module value EOF marker", moduleTypeModuleName(mt));
|
||||
if (ptr) {
|
||||
o = createModuleObject(mt,ptr); /* creating just in order to easily destroy */
|
||||
o = createModuleObject(mt, ptr); /* creating just in order to easily destroy */
|
||||
decrRefCount(o);
|
||||
}
|
||||
return NULL;
|
||||
@@ -2828,17 +3093,23 @@ robj *rdbLoadObject(int rdbtype, rio *rdb, sds key, int dbid, int *error) {
|
||||
moduleTypeModuleName(mt));
|
||||
return NULL;
|
||||
}
|
||||
o = createModuleObject(mt,ptr);
|
||||
o = createModuleObject(mt, ptr);
|
||||
} else {
|
||||
rdbReportReadError("Unknown RDB encoding type %d",rdbtype);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (minExpiredField) *minExpiredField = minExpField;
|
||||
|
||||
if (error) *error = 0;
|
||||
return o;
|
||||
|
||||
emptykey:
|
||||
if (error) *error = RDB_LOAD_ERR_EMPTY_KEY;
|
||||
return NULL;
|
||||
expiredHash:
|
||||
if (error) *error = RDB_LOAD_ERR_EXPIRED_HASH;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Mark that we are loading in the global state and setup the fields
|
||||
@@ -3008,6 +3279,7 @@ int rdbLoadRio(rio *rdb, int rdbflags, rdbSaveInfo *rsi) {
|
||||
* currently it only allow to set db object and functionLibCtx to which the data
|
||||
* will be loaded (in the future it might contains more such objects). */
|
||||
int rdbLoadRioWithLoadingCtx(rio *rdb, int rdbflags, rdbSaveInfo *rsi, rdbLoadingCtx *rdb_loading_ctx) {
|
||||
uint64_t minExpiredField = EB_EXPIRE_TIME_INVALID;
|
||||
uint64_t dbid = 0;
|
||||
int type, rdbver;
|
||||
uint64_t db_size = 0, expires_size = 0;
|
||||
@@ -3249,15 +3521,15 @@ int rdbLoadRioWithLoadingCtx(rio *rdb, int rdbflags, rdbSaveInfo *rsi, rdbLoadin
|
||||
if ((key = rdbGenericLoadStringObject(rdb,RDB_LOAD_SDS,NULL)) == NULL)
|
||||
goto eoferr;
|
||||
/* Read value */
|
||||
val = rdbLoadObject(type,rdb,key,db->id,&error);
|
||||
val = rdbLoadObject(type,rdb,key,db,&error, &minExpiredField);
|
||||
|
||||
/* Check if the key already expired. This function is used when loading
|
||||
* an RDB file from disk, either at startup, or when an RDB was
|
||||
* received from the master. In the latter case, the master is
|
||||
* responsible for key expiry. If we would expire keys here, the
|
||||
* snapshot taken by the master may not be reflected on the slave.
|
||||
* Similarly, if the base AOF is RDB format, we want to load all
|
||||
* the keys they are, since the log of operations in the incr AOF
|
||||
* Similarly, if the base AOF is RDB format, we want to load all
|
||||
* the keys they are, since the log of operations in the incr AOF
|
||||
* is assumed to work in the exact keyspace state. */
|
||||
if (val == NULL) {
|
||||
/* Since we used to have bug that could lead to empty keys
|
||||
@@ -3268,6 +3540,9 @@ int rdbLoadRioWithLoadingCtx(rio *rdb, int rdbflags, rdbSaveInfo *rsi, rdbLoadin
|
||||
if(empty_keys_skipped++ < 10)
|
||||
serverLog(LL_NOTICE, "rdbLoadObject skipping empty key: %s", key);
|
||||
sdsfree(key);
|
||||
} else if (error == RDB_LOAD_ERR_EXPIRED_HASH) {
|
||||
/* Valid flow. Continue. */
|
||||
sdsfree(key);
|
||||
} else {
|
||||
sdsfree(key);
|
||||
goto eoferr;
|
||||
@@ -3312,6 +3587,11 @@ int rdbLoadRioWithLoadingCtx(rio *rdb, int rdbflags, rdbSaveInfo *rsi, rdbLoadin
|
||||
}
|
||||
}
|
||||
|
||||
/* If minExpiredField was set, then the object is hash with expiration
|
||||
* on fields and need to register it in global HFE DS */
|
||||
if (minExpiredField != EB_EXPIRE_TIME_INVALID)
|
||||
hashTypeAddToExpires(db, key, val, minExpiredField);
|
||||
|
||||
/* Set the expire time if needed */
|
||||
if (expiretime != -1) {
|
||||
setExpire(NULL,db,&keyobj,expiretime);
|
||||
|
||||
@@ -73,10 +73,12 @@
|
||||
#define RDB_TYPE_STREAM_LISTPACKS_2 19
|
||||
#define RDB_TYPE_SET_LISTPACK 20
|
||||
#define RDB_TYPE_STREAM_LISTPACKS_3 21
|
||||
#define RDB_TYPE_HASH_METADATA 22
|
||||
#define RDB_TYPE_HASH_LISTPACK_EX 23
|
||||
/* NOTE: WHEN ADDING NEW RDB TYPE, UPDATE rdbIsObjectType(), and rdb_type_string[] */
|
||||
|
||||
/* Test if a type is an object type. */
|
||||
#define rdbIsObjectType(t) (((t) >= 0 && (t) <= 7) || ((t) >= 9 && (t) <= 21))
|
||||
#define rdbIsObjectType(t) (((t) >= 0 && (t) <= 7) || ((t) >= 9 && (t) <= 23))
|
||||
|
||||
/* Special RDB opcodes (saved/loaded with rdbSaveType/rdbLoadType). */
|
||||
#define RDB_OPCODE_SLOT_INFO 244 /* Individual slot info, such as slot id and size (cluster mode only). */
|
||||
@@ -101,10 +103,12 @@
|
||||
#define RDB_MODULE_OPCODE_STRING 5 /* String. */
|
||||
|
||||
/* rdbLoad...() functions flags. */
|
||||
#define RDB_LOAD_NONE 0
|
||||
#define RDB_LOAD_ENC (1<<0)
|
||||
#define RDB_LOAD_PLAIN (1<<1)
|
||||
#define RDB_LOAD_SDS (1<<2)
|
||||
#define RDB_LOAD_NONE 0
|
||||
#define RDB_LOAD_ENC (1<<0)
|
||||
#define RDB_LOAD_PLAIN (1<<1)
|
||||
#define RDB_LOAD_SDS (1<<2)
|
||||
#define RDB_LOAD_HFLD (1<<3)
|
||||
#define RDB_LOAD_HFLD_TTL (1<<4)
|
||||
|
||||
/* flags on the purpose of rdb save or load */
|
||||
#define RDBFLAGS_NONE 0 /* No special RDB loading or saving. */
|
||||
@@ -116,8 +120,9 @@
|
||||
|
||||
/* When rdbLoadObject() returns NULL, the err flag is
|
||||
* set to hold the type of error that occurred */
|
||||
#define RDB_LOAD_ERR_EMPTY_KEY 1 /* Error of empty key */
|
||||
#define RDB_LOAD_ERR_OTHER 2 /* Any other errors */
|
||||
#define RDB_LOAD_ERR_EMPTY_KEY 1 /* Error of empty key */
|
||||
#define RDB_LOAD_ERR_EXPIRED_HASH 2 /* Expired hash since all its fields are expired */
|
||||
#define RDB_LOAD_ERR_OTHER 3 /* Any other errors */
|
||||
|
||||
ssize_t rdbWriteRaw(rio *rdb, void *p, size_t len);
|
||||
int rdbSaveType(rio *rdb, unsigned char type);
|
||||
@@ -138,7 +143,7 @@ int rdbSaveToFile(const char *filename);
|
||||
int rdbSave(int req, char *filename, rdbSaveInfo *rsi, int rdbflags);
|
||||
ssize_t rdbSaveObject(rio *rdb, robj *o, robj *key, int dbid);
|
||||
size_t rdbSavedObjectLen(robj *o, robj *key, int dbid);
|
||||
robj *rdbLoadObject(int rdbtype, rio *rdb, sds key, int dbid, int *error);
|
||||
robj *rdbLoadObject(int rdbtype, rio *rdb, sds key, redisDb *db, int *error, uint64_t *minExpiredField);
|
||||
void backgroundSaveDoneHandler(int exitcode, int bysignal);
|
||||
int rdbSaveKeyValuePair(rio *rdb, robj *key, robj *val, long long expiretime,int dbid);
|
||||
ssize_t rdbSaveSingleModuleAux(rio *rdb, int when, moduleType *mt);
|
||||
|
||||
@@ -80,6 +80,8 @@ char *rdb_type_string[] = {
|
||||
"stream-v2",
|
||||
"set-listpack",
|
||||
"stream-v3",
|
||||
"hash-hashtable-md",
|
||||
"hash-listpack-md",
|
||||
};
|
||||
|
||||
/* Show a few stats collected into 'rdbstate' */
|
||||
@@ -173,7 +175,6 @@ void rdbCheckSetupSignals(void) {
|
||||
* otherwise the already open file 'fp' is checked. */
|
||||
int redis_check_rdb(char *rdbfilename, FILE *fp) {
|
||||
uint64_t dbid;
|
||||
int selected_dbid = -1;
|
||||
int type, rdbver;
|
||||
char buf[1024];
|
||||
long long expiretime, now = mstime();
|
||||
@@ -245,7 +246,6 @@ int redis_check_rdb(char *rdbfilename, FILE *fp) {
|
||||
if ((dbid = rdbLoadLen(&rdb,NULL)) == RDB_LENERR)
|
||||
goto eoferr;
|
||||
rdbCheckInfo("Selecting DB ID %llu", (unsigned long long)dbid);
|
||||
selected_dbid = dbid;
|
||||
continue; /* Read type again. */
|
||||
} else if (type == RDB_OPCODE_RESIZEDB) {
|
||||
/* RESIZEDB: Hint about the size of the keys in the currently
|
||||
@@ -331,7 +331,8 @@ int redis_check_rdb(char *rdbfilename, FILE *fp) {
|
||||
rdbstate.keys++;
|
||||
/* Read value */
|
||||
rdbstate.doing = RDB_CHECK_DOING_READ_OBJECT_VALUE;
|
||||
if ((val = rdbLoadObject(type,&rdb,key->ptr,selected_dbid,NULL)) == NULL) goto eoferr;
|
||||
if ((val = rdbLoadObject(type,&rdb,key->ptr,NULL,NULL,NULL)) == NULL)
|
||||
goto eoferr;
|
||||
/* Check if the key already expired. */
|
||||
if (expiretime != -1 && expiretime < now)
|
||||
rdbstate.already_expired++;
|
||||
|
||||
+27
-3
@@ -19,6 +19,8 @@
|
||||
#include "syscheck.h"
|
||||
#include "threads_mngr.h"
|
||||
#include "fmtargs.h"
|
||||
#include "mstr.h"
|
||||
#include "ebuckets.h"
|
||||
|
||||
#include <time.h>
|
||||
#include <signal.h>
|
||||
@@ -281,6 +283,18 @@ int dictSdsKeyCompare(dict *d, const void *key1,
|
||||
return memcmp(key1, key2, l1) == 0;
|
||||
}
|
||||
|
||||
int dictSdsMstrKeyCompare(dict *d, const void *sdsLookup, const void *mstrStored)
|
||||
{
|
||||
int l1,l2;
|
||||
UNUSED(d);
|
||||
|
||||
l1 = sdslen((sds)sdsLookup);
|
||||
l2 = hfieldlen((hfield)mstrStored);
|
||||
if (l1 != l2) return 0;
|
||||
return memcmp(sdsLookup, mstrStored, l1) == 0;
|
||||
}
|
||||
|
||||
|
||||
/* A case insensitive version used for the command lookup table and other
|
||||
* places where case insensitive non binary-safe comparison is needed. */
|
||||
int dictSdsKeyCaseCompare(dict *d, const void *key1,
|
||||
@@ -1945,6 +1959,8 @@ void createSharedObjects(void) {
|
||||
shared.persist = createStringObject("PERSIST",7);
|
||||
shared.set = createStringObject("SET",3);
|
||||
shared.eval = createStringObject("EVAL",4);
|
||||
shared.hpexpireat = createStringObject("HPEXPIREAT",10);
|
||||
shared.hdel = createStringObject("HDEL",4);
|
||||
|
||||
/* Shared command argument */
|
||||
shared.left = createStringObject("left",4);
|
||||
@@ -2504,6 +2520,7 @@ void resetServerStats(void) {
|
||||
server.stat_numcommands = 0;
|
||||
server.stat_numconnections = 0;
|
||||
server.stat_expiredkeys = 0;
|
||||
server.stat_expired_hash_fields = 0;
|
||||
server.stat_expired_stale_perc = 0;
|
||||
server.stat_expired_time_cap_reached_count = 0;
|
||||
server.stat_expire_cycle_time_used = 0;
|
||||
@@ -2652,6 +2669,7 @@ void initServer(void) {
|
||||
for (j = 0; j < server.dbnum; j++) {
|
||||
server.db[j].keys = kvstoreCreate(&dbDictType, slot_count_bits, flags);
|
||||
server.db[j].expires = kvstoreCreate(&dbExpiresDictType, slot_count_bits, flags);
|
||||
server.db[j].hexpires = ebCreate();
|
||||
server.db[j].expires_cursor = 0;
|
||||
server.db[j].blocking_keys = dictCreate(&keylistDictType);
|
||||
server.db[j].blocking_keys_unblock_on_nokey = dictCreate(&objectKeyPointerValueDictType);
|
||||
@@ -5854,6 +5872,7 @@ sds genRedisInfoString(dict *section_dict, int all_sections, int everything) {
|
||||
"sync_full:%lld\r\n", server.stat_sync_full,
|
||||
"sync_partial_ok:%lld\r\n", server.stat_sync_partial_ok,
|
||||
"sync_partial_err:%lld\r\n", server.stat_sync_partial_err,
|
||||
"expired_hash_fields:%lld\r\n", server.stat_expired_hash_fields,
|
||||
"expired_keys:%lld\r\n", server.stat_expiredkeys,
|
||||
"expired_stale_perc:%.2f\r\n", server.stat_expired_stale_perc*100,
|
||||
"expired_time_cap_reached_count:%lld\r\n", server.stat_expired_time_cap_reached_count,
|
||||
@@ -6092,14 +6111,16 @@ sds genRedisInfoString(dict *section_dict, int all_sections, int everything) {
|
||||
if (sections++) info = sdscat(info,"\r\n");
|
||||
info = sdscatprintf(info, "# Keyspace\r\n");
|
||||
for (j = 0; j < server.dbnum; j++) {
|
||||
long long keys, vkeys;
|
||||
long long keys, vkeys, hexpires;
|
||||
|
||||
keys = kvstoreSize(server.db[j].keys);
|
||||
vkeys = kvstoreSize(server.db[j].expires);
|
||||
hexpires = ebGetTotalItems(server.db[j].hexpires, &hashExpireBucketsType);
|
||||
|
||||
if (keys || vkeys) {
|
||||
info = sdscatprintf(info,
|
||||
"db%d:keys=%lld,expires=%lld,avg_ttl=%lld\r\n",
|
||||
j, keys, vkeys, server.db[j].avg_ttl);
|
||||
"db%d:keys=%lld,expires=%lld,avg_ttl=%lld,hashes_with_expiry_fields=%lld\r\n",
|
||||
j, keys, vkeys, server.db[j].avg_ttl, hexpires);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6871,9 +6892,11 @@ struct redisTest {
|
||||
{"crc64", crc64Test},
|
||||
{"zmalloc", zmalloc_test},
|
||||
{"sds", sdsTest},
|
||||
{"mstr", mstrTest},
|
||||
{"dict", dictTest},
|
||||
{"listpack", listpackTest},
|
||||
{"kvstore", kvstoreTest},
|
||||
{"ebuckets", ebucketsTest},
|
||||
};
|
||||
redisTestProc *getTestProcByName(const char *name) {
|
||||
int numtests = sizeof(redisTests)/sizeof(struct redisTest);
|
||||
@@ -6900,6 +6923,7 @@ int main(int argc, char **argv) {
|
||||
if (!strcasecmp(arg, "--accurate")) flags |= REDIS_TEST_ACCURATE;
|
||||
else if (!strcasecmp(arg, "--large-memory")) flags |= REDIS_TEST_LARGE_MEMORY;
|
||||
else if (!strcasecmp(arg, "--valgrind")) flags |= REDIS_TEST_VALGRIND;
|
||||
else if (!strcasecmp(arg, "--verbose")) flags |= REDIS_TEST_VERBOSE;
|
||||
}
|
||||
|
||||
if (!strcasecmp(argv[2], "all")) {
|
||||
|
||||
+103
-17
@@ -45,6 +45,8 @@ typedef long long ustime_t; /* microsecond time type. */
|
||||
|
||||
#include "ae.h" /* Event driven programming library */
|
||||
#include "sds.h" /* Dynamic safe strings */
|
||||
#include "mstr.h" /* Immutable strings with optional metadata attached */
|
||||
#include "ebuckets.h" /* expiry data structure */
|
||||
#include "dict.h" /* Hash tables */
|
||||
#include "kvstore.h" /* Slot-based hash table */
|
||||
#include "adlist.h" /* Linked lists */
|
||||
@@ -884,6 +886,7 @@ struct RedisModuleDigest {
|
||||
#define OBJ_ENCODING_QUICKLIST 9 /* Encoded as linked list of listpacks */
|
||||
#define OBJ_ENCODING_STREAM 10 /* Encoded as a radix tree of listpacks */
|
||||
#define OBJ_ENCODING_LISTPACK 11 /* Encoded as a listpack */
|
||||
#define OBJ_ENCODING_LISTPACK_EX 12 /* Encoded as listpack, extended with metadata */
|
||||
|
||||
#define LRU_BITS 24
|
||||
#define LRU_CLOCK_MAX ((1<<LRU_BITS)-1) /* Max value of obj->lru */
|
||||
@@ -960,6 +963,7 @@ typedef struct replBufBlock {
|
||||
typedef struct redisDb {
|
||||
kvstore *keys; /* The keyspace for this DB */
|
||||
kvstore *expires; /* Timeout of keys with a timeout set */
|
||||
ebuckets hexpires; /* Hash expiration DS. Single TTL per hash (of next min field to expire) */
|
||||
dict *blocking_keys; /* Keys with clients waiting for data (BLPOP)*/
|
||||
dict *blocking_keys_unblock_on_nokey; /* Keys with clients waiting for
|
||||
* data, and should be unblocked if key is deleted (XREADEDGROUP).
|
||||
@@ -1313,7 +1317,8 @@ struct sharedObjectsStruct {
|
||||
*unsubscribebulk, *psubscribebulk, *punsubscribebulk, *del, *unlink,
|
||||
*rpop, *lpop, *lpush, *rpoplpush, *lmove, *blmove, *zpopmin, *zpopmax,
|
||||
*emptyscan, *multi, *exec, *left, *right, *hset, *srem, *xgroup, *xclaim,
|
||||
*script, *replconf, *eval, *persist, *set, *pexpireat, *pexpire,
|
||||
*script, *replconf, *eval, *persist, *set, *pexpireat, *pexpire,
|
||||
*hdel, *hpexpireat,
|
||||
*time, *pxat, *absttl, *retrycount, *force, *justid, *entriesread,
|
||||
*lastid, *ping, *setid, *keepttl, *load, *createconsumer,
|
||||
*getack, *special_asterick, *special_equals, *default_username, *redacted,
|
||||
@@ -1646,6 +1651,7 @@ struct redisServer {
|
||||
long long stat_numcommands; /* Number of processed commands */
|
||||
long long stat_numconnections; /* Number of connections received */
|
||||
long long stat_expiredkeys; /* Number of expired keys */
|
||||
long long stat_expired_hash_fields; /* Number of expired hash-fields */
|
||||
double stat_expired_stale_perc; /* Percentage of keys probably expired */
|
||||
long long stat_expired_time_cap_reached_count; /* Early expire cycle stops.*/
|
||||
long long stat_expire_cycle_time_used; /* Cumulative microseconds used. */
|
||||
@@ -2433,7 +2439,8 @@ typedef struct {
|
||||
robj *subject;
|
||||
int encoding;
|
||||
|
||||
unsigned char *fptr, *vptr;
|
||||
unsigned char *fptr, *vptr, *tptr;
|
||||
uint64_t expire_time; /* Only used with OBJ_ENCODING_LISTPACK_EX */
|
||||
|
||||
dictIterator *di;
|
||||
dictEntry *de;
|
||||
@@ -2449,6 +2456,10 @@ typedef struct {
|
||||
#define IO_THREADS_OP_WRITE 2
|
||||
extern int io_threads_op;
|
||||
|
||||
/* Hash-field data type (of t_hash.c) */
|
||||
typedef mstr hfield;
|
||||
extern mstrKind mstrFieldKind;
|
||||
|
||||
/*-----------------------------------------------------------------------------
|
||||
* Extern declarations
|
||||
*----------------------------------------------------------------------------*/
|
||||
@@ -2463,6 +2474,8 @@ extern dictType zsetDictType;
|
||||
extern dictType dbDictType;
|
||||
extern double R_Zero, R_PosInf, R_NegInf, R_Nan;
|
||||
extern dictType hashDictType;
|
||||
extern dictType mstrHashDictType;
|
||||
extern dictType mstrHashDictTypeWithHFE;
|
||||
extern dictType stringSetDictType;
|
||||
extern dictType externalStringType;
|
||||
extern dictType sdsHashDictType;
|
||||
@@ -2474,6 +2487,9 @@ extern dictType sdsReplyDictType;
|
||||
extern dictType keylistDictType;
|
||||
extern dict *modules;
|
||||
|
||||
extern EbucketsType hashExpireBucketsType; /* global expires */
|
||||
extern EbucketsType hashFieldExpireBucketsType; /* local per hash */
|
||||
|
||||
/*-----------------------------------------------------------------------------
|
||||
* Functions prototypes
|
||||
*----------------------------------------------------------------------------*/
|
||||
@@ -2616,6 +2632,7 @@ void copyReplicaOutputBuffer(client *dst, client *src);
|
||||
void addListRangeReply(client *c, robj *o, long start, long end, int reverse);
|
||||
void deferredAfterErrorReply(client *c, list *errors);
|
||||
size_t sdsZmallocSize(sds s);
|
||||
size_t hfieldZmallocSize(hfield s);
|
||||
size_t getStringObjectSdsUsedMemory(robj *o);
|
||||
void freeClientReplyValue(void *o);
|
||||
void *dupClientReplyValue(void *o);
|
||||
@@ -3140,30 +3157,87 @@ void setTypeConvert(robj *subject, int enc);
|
||||
int setTypeConvertAndExpand(robj *setobj, int enc, unsigned long cap, int panic);
|
||||
robj *setTypeDup(robj *o);
|
||||
|
||||
/* Data structure for OBJ_ENCODING_LISTPACK_EX for hash. It contains listpack
|
||||
* and metadata fields for hash field expiration.*/
|
||||
typedef struct listpackEx {
|
||||
ExpireMeta meta; /* To be used in order to register the hash in the
|
||||
global ebuckets (i.e. db->hexpires) with next,
|
||||
minimum, hash-field to expire. */
|
||||
sds key; /* reference to the key, same one that stored in
|
||||
db->dict. Will be used from active-expiration flow
|
||||
for notification and deletion of the object, if
|
||||
needed. */
|
||||
void *lp; /* listpack that contains 'key-value-ttl' tuples which
|
||||
are ordered by ttl. */
|
||||
} listpackEx;
|
||||
|
||||
/* Each dict of hash object that has fields with time-Expiration will have the
|
||||
* following metadata attached to dict header */
|
||||
typedef struct dictExpireMetadata {
|
||||
ExpireMeta expireMeta; /* embedded ExpireMeta in dict.
|
||||
To be used in order to register the hash in the
|
||||
global ebuckets (i.e db->hexpires) with next,
|
||||
minimum, hash-field to expire */
|
||||
ebuckets hfe; /* DS of Hash Fields Expiration, associated to each hash */
|
||||
sds key; /* reference to the key, same one that stored in
|
||||
db->dict. Will be used from active-expiration flow
|
||||
for notification and deletion of the object, if
|
||||
needed. */
|
||||
} dictExpireMetadata;
|
||||
|
||||
/* Hash data type */
|
||||
#define HASH_SET_TAKE_FIELD (1<<0)
|
||||
#define HASH_SET_TAKE_VALUE (1<<1)
|
||||
#define HASH_SET_COPY 0
|
||||
|
||||
void hashTypeConvert(robj *o, int enc);
|
||||
void hashTypeTryConversion(robj *subject, robj **argv, int start, int end);
|
||||
int hashTypeExists(robj *o, sds key);
|
||||
int hashTypeDelete(robj *o, sds key);
|
||||
unsigned long hashTypeLength(const robj *o);
|
||||
void hashTypeConvert(robj *o, int enc, ebuckets *hexpires);
|
||||
void hashTypeTryConversion(redisDb *db, robj *subject, robj **argv, int start, int end);
|
||||
int hashTypeExists(redisDb *db, robj *o, sds key, int *isHashDeleted);
|
||||
int hashTypeDelete(robj *o, void *key, int isSdsField);
|
||||
unsigned long hashTypeLength(const robj *o, int subtractExpiredFields);
|
||||
hashTypeIterator *hashTypeInitIterator(robj *subject);
|
||||
void hashTypeReleaseIterator(hashTypeIterator *hi);
|
||||
int hashTypeNext(hashTypeIterator *hi);
|
||||
int hashTypeNext(hashTypeIterator *hi, int skipExpiredFields);
|
||||
void hashTypeCurrentFromListpack(hashTypeIterator *hi, int what,
|
||||
unsigned char **vstr,
|
||||
unsigned int *vlen,
|
||||
long long *vll);
|
||||
sds hashTypeCurrentFromHashTable(hashTypeIterator *hi, int what);
|
||||
void hashTypeCurrentObject(hashTypeIterator *hi, int what, unsigned char **vstr, unsigned int *vlen, long long *vll);
|
||||
long long *vll,
|
||||
uint64_t *expireTime);
|
||||
void hashTypeCurrentFromHashTable(hashTypeIterator *hi, int what, char **str,
|
||||
size_t *len, uint64_t *expireTime);
|
||||
void hashTypeCurrentObject(hashTypeIterator *hi, int what, unsigned char **vstr,
|
||||
unsigned int *vlen, long long *vll, uint64_t *expireTime);
|
||||
sds hashTypeCurrentObjectNewSds(hashTypeIterator *hi, int what);
|
||||
robj *hashTypeLookupWriteOrCreate(client *c, robj *key);
|
||||
robj *hashTypeGetValueObject(robj *o, sds field);
|
||||
int hashTypeSet(robj *o, sds field, sds value, int flags);
|
||||
robj *hashTypeDup(robj *o);
|
||||
hfield hashTypeCurrentObjectNewHfield(hashTypeIterator *hi);
|
||||
robj *hashTypeGetValueObject(redisDb *db, robj *o, sds field, int *isHashDeleted);
|
||||
int hashTypeSet(redisDb *db, robj *o, sds field, sds value, int flags);
|
||||
robj *hashTypeDup(robj *o, sds newkey, uint64_t *minHashExpire);
|
||||
uint64_t hashTypeRemoveFromExpires(ebuckets *hexpires, robj *o);
|
||||
void hashTypeAddToExpires(redisDb *db, sds key, robj *hashObj, uint64_t expireTime);
|
||||
void hashTypeFree(robj *o);
|
||||
int hashTypeIsExpired(const robj *o, uint64_t expireAt);
|
||||
uint64_t hashTypeGetMinExpire(robj *o);
|
||||
unsigned char *hashTypeListpackGetLp(robj *o);
|
||||
uint64_t hashTypeGetMinExpire(robj *o);
|
||||
void hashTypeUpdateKeyRef(robj *o, sds newkey);
|
||||
ebuckets *hashTypeGetDictMetaHFE(dict *d);
|
||||
uint64_t hashTypeGetMinExpire(robj *keyObj);
|
||||
uint64_t hashTypeGetNextTimeToExpire(robj *o);
|
||||
void initDictExpireMetadata(sds key, robj *o);
|
||||
struct listpackEx *listpackExCreate(void);
|
||||
void listpackExAddNew(robj *o, char *field, size_t flen,
|
||||
char *value, size_t vlen, uint64_t expireAt);
|
||||
|
||||
/* Hash-Field data type (of t_hash.c) */
|
||||
hfield hfieldNew(const void *field, size_t fieldlen, int withExpireMeta);
|
||||
hfield hfieldTryNew(const void *field, size_t fieldlen, int withExpireMeta);
|
||||
int hfieldIsExpireAttached(hfield field);
|
||||
int hfieldIsExpired(hfield field);
|
||||
uint64_t hfieldGetExpireTime(hfield field);
|
||||
static inline void hfieldFree(hfield field) { mstrFree(&mstrFieldKind, field); }
|
||||
static inline void *hfieldGetAllocPtr(hfield field) { return mstrGetAllocPtr(&mstrFieldKind, field); }
|
||||
static inline size_t hfieldlen(hfield field) { return mstrlen(field);}
|
||||
uint64_t hfieldGetExpireTime(hfield field);
|
||||
|
||||
/* Pub / Sub */
|
||||
int pubsubUnsubscribeAllChannels(client *c, int notify);
|
||||
@@ -3182,7 +3256,7 @@ dict *getClientPubSubChannels(client *c);
|
||||
dict *getClientPubSubShardChannels(client *c);
|
||||
|
||||
/* Keyspace events notification */
|
||||
void notifyKeyspaceEvent(int type, char *event, robj *key, int dbid);
|
||||
void notifyKeyspaceEvent(int type, const char *event, robj *key, int dbid);
|
||||
int keyspaceEventsStringToFlags(char *classes);
|
||||
sds keyspaceEventsFlagsToString(int flags);
|
||||
|
||||
@@ -3266,6 +3340,7 @@ int keyIsExpired(redisDb *db, robj *key);
|
||||
long long getExpire(redisDb *db, robj *key);
|
||||
void setExpire(client *c, redisDb *db, robj *key, long long when);
|
||||
int checkAlreadyExpired(long long when);
|
||||
int parseExtendedExpireArgumentsOrReply(client *c, int *flags);
|
||||
robj *lookupKeyRead(redisDb *db, robj *key);
|
||||
robj *lookupKeyWrite(redisDb *db, robj *key);
|
||||
robj *lookupKeyReadOrReply(client *c, robj *key, robj *reply);
|
||||
@@ -3284,7 +3359,7 @@ int objectSetLRUOrLFU(robj *val, long long lfu_freq, long long lru_idle,
|
||||
#define LOOKUP_NOEXPIRE (1<<4) /* Avoid deleting lazy expired keys. */
|
||||
#define LOOKUP_NOEFFECTS (LOOKUP_NONOTIFY | LOOKUP_NOSTATS | LOOKUP_NOTOUCH | LOOKUP_NOEXPIRE) /* Avoid any effects from fetching the key */
|
||||
|
||||
void dbAdd(redisDb *db, robj *key, robj *val);
|
||||
dictEntry *dbAdd(redisDb *db, robj *key, robj *val);
|
||||
int dbAddRDBLoad(redisDb *db, sds key, robj *val);
|
||||
void dbReplaceValue(redisDb *db, robj *key, robj *val);
|
||||
|
||||
@@ -3439,6 +3514,7 @@ void expireSlaveKeys(void);
|
||||
void rememberSlaveKeyWithExpire(redisDb *db, robj *key);
|
||||
void flushSlaveKeysWithExpireList(void);
|
||||
size_t getSlaveKeyWithExpireCount(void);
|
||||
uint64_t hashTypeDbActiveExpire(redisDb *db, uint32_t maxFieldsToExpire);
|
||||
|
||||
/* evict.c -- maxmemory handling and LRU eviction. */
|
||||
void evictionPoolAlloc(void);
|
||||
@@ -3456,6 +3532,7 @@ void startEvictionTimeProc(void);
|
||||
uint64_t dictSdsHash(const void *key);
|
||||
uint64_t dictSdsCaseHash(const void *key);
|
||||
int dictSdsKeyCompare(dict *d, const void *key1, const void *key2);
|
||||
int dictSdsMstrKeyCompare(dict *d, const void *sdsLookup, const void *mstrStored);
|
||||
int dictSdsKeyCaseCompare(dict *d, const void *key1, const void *key2);
|
||||
void dictSdsDestructor(dict *d, void *val);
|
||||
void dictListDestructor(dict *d, void *val);
|
||||
@@ -3611,6 +3688,15 @@ void strlenCommand(client *c);
|
||||
void zrankCommand(client *c);
|
||||
void zrevrankCommand(client *c);
|
||||
void hsetCommand(client *c);
|
||||
void hpexpireCommand(client *c);
|
||||
void hexpireCommand(client *c);
|
||||
void hpexpireatCommand(client *c);
|
||||
void hexpireatCommand(client *c);
|
||||
void httlCommand(client *c);
|
||||
void hpttlCommand(client *c);
|
||||
void hexpiretimeCommand(client *c);
|
||||
void hpexpiretimeCommand(client *c);
|
||||
void hpersistCommand(client *c);
|
||||
void hsetnxCommand(client *c);
|
||||
void hgetCommand(client *c);
|
||||
void hmgetCommand(client *c);
|
||||
|
||||
+6
-1
@@ -94,7 +94,12 @@ robj *lookupKeyByPattern(redisDb *db, robj *pattern, robj *subst) {
|
||||
|
||||
/* Retrieve value from hash by the field name. The returned object
|
||||
* is a new object with refcount already incremented. */
|
||||
o = hashTypeGetValueObject(o, fieldobj->ptr);
|
||||
int isHashDeleted;
|
||||
o = hashTypeGetValueObject(db, o, fieldobj->ptr, &isHashDeleted);
|
||||
|
||||
if (isHashDeleted)
|
||||
goto noobj;
|
||||
|
||||
} else {
|
||||
if (o->type != OBJ_STRING) goto noobj;
|
||||
|
||||
|
||||
+2289
-201
File diff suppressed because it is too large
Load Diff
+4
-4
@@ -432,7 +432,7 @@ robj *setTypePopRandom(robj *set) {
|
||||
if (set->encoding == OBJ_ENCODING_LISTPACK) {
|
||||
/* Find random and delete it without re-seeking the listpack. */
|
||||
unsigned int i = 0;
|
||||
unsigned char *p = lpNextRandom(set->ptr, lpFirst(set->ptr), &i, 1, 0);
|
||||
unsigned char *p = lpNextRandom(set->ptr, lpFirst(set->ptr), &i, 1, 1);
|
||||
unsigned int len = 0; /* initialize to silence warning */
|
||||
long long llele = 0; /* initialize to silence warning */
|
||||
char *str = (char *)lpGetValue(p, &len, &llele);
|
||||
@@ -815,7 +815,7 @@ void spopWithCountCommand(client *c) {
|
||||
unsigned int index = 0;
|
||||
unsigned char **ps = zmalloc(sizeof(char *) * count);
|
||||
for (unsigned long i = 0; i < count; i++) {
|
||||
p = lpNextRandom(lp, p, &index, count - i, 0);
|
||||
p = lpNextRandom(lp, p, &index, count - i, 1);
|
||||
unsigned int len;
|
||||
str = (char *)lpGetValue(p, &len, (long long *)&llele);
|
||||
|
||||
@@ -877,7 +877,7 @@ void spopWithCountCommand(client *c) {
|
||||
unsigned int index = 0;
|
||||
unsigned char **ps = zmalloc(sizeof(char *) * remaining);
|
||||
for (unsigned long i = 0; i < remaining; i++) {
|
||||
p = lpNextRandom(lp, p, &index, remaining - i, 0);
|
||||
p = lpNextRandom(lp, p, &index, remaining - i, 1);
|
||||
unsigned int len;
|
||||
str = (char *)lpGetValue(p, &len, (long long *)&llele);
|
||||
setTypeAddAux(newset, str, len, llele, 0);
|
||||
@@ -1103,7 +1103,7 @@ void srandmemberWithCountCommand(client *c) {
|
||||
unsigned int i = 0;
|
||||
addReplyArrayLen(c, count);
|
||||
while (count) {
|
||||
p = lpNextRandom(lp, p, &i, count--, 0);
|
||||
p = lpNextRandom(lp, p, &i, count--, 1);
|
||||
unsigned int len;
|
||||
str = (char *)lpGetValue(p, &len, (long long *)&llele);
|
||||
if (str == NULL) {
|
||||
|
||||
+3
-3
@@ -1754,7 +1754,7 @@ void zsetTypeRandomElement(robj *zsetobj, unsigned long zsetsize, listpackEntry
|
||||
*score = *(double*)dictGetVal(de);
|
||||
} else if (zsetobj->encoding == OBJ_ENCODING_LISTPACK) {
|
||||
listpackEntry val;
|
||||
lpRandomPair(zsetobj->ptr, zsetsize, key, &val);
|
||||
lpRandomPair(zsetobj->ptr, zsetsize, key, &val, 2);
|
||||
if (score) {
|
||||
if (val.sval) {
|
||||
*score = zzlStrtod(val.sval,val.slen);
|
||||
@@ -4263,7 +4263,7 @@ void zrandmemberWithCountCommand(client *c, long l, int withscores) {
|
||||
while (count) {
|
||||
sample_count = count > limit ? limit : count;
|
||||
count -= sample_count;
|
||||
lpRandomPairs(zsetobj->ptr, sample_count, keys, vals);
|
||||
lpRandomPairs(zsetobj->ptr, sample_count, keys, vals, 2);
|
||||
zrandmemberReplyWithListpack(c, sample_count, keys, vals);
|
||||
if (c->flags & CLIENT_CLOSE_ASAP)
|
||||
break;
|
||||
@@ -4317,7 +4317,7 @@ void zrandmemberWithCountCommand(client *c, long l, int withscores) {
|
||||
keys = zmalloc(sizeof(listpackEntry)*count);
|
||||
if (withscores)
|
||||
vals = zmalloc(sizeof(listpackEntry)*count);
|
||||
serverAssert(lpRandomPairsUnique(zsetobj->ptr, count, keys, vals) == count);
|
||||
serverAssert(lpRandomPairsUnique(zsetobj->ptr, count, keys, vals, 2) == count);
|
||||
zrandmemberReplyWithListpack(c, count, keys, vals);
|
||||
zfree(keys);
|
||||
zfree(vals);
|
||||
|
||||
@@ -21,6 +21,8 @@
|
||||
#define REDIS_TEST_ACCURATE (1<<0)
|
||||
#define REDIS_TEST_LARGE_MEMORY (1<<1)
|
||||
#define REDIS_TEST_VALGRIND (1<<2)
|
||||
#define REDIS_TEST_VERBOSE (1<<3)
|
||||
|
||||
|
||||
extern int __failed_tests;
|
||||
extern int __test_num;
|
||||
|
||||
@@ -19,12 +19,14 @@ proc generate_collections {suffix elements} {
|
||||
# add both string values and integers
|
||||
if {$j % 2 == 0} {set val $j} else {set val "_$j"}
|
||||
$rd hset hash$suffix $j $val
|
||||
$rd hset hashmd$suffix $j $val
|
||||
$rd hexpire hashmd$suffix [expr {int(rand() * 10000)}] FIELDS 1 $j
|
||||
$rd lpush list$suffix $val
|
||||
$rd zadd zset$suffix $j $val
|
||||
$rd sadd set$suffix $val
|
||||
$rd xadd stream$suffix * item 1 value $val
|
||||
}
|
||||
for {set j 0} {$j < $elements * 5} {incr j} {
|
||||
for {set j 0} {$j < $elements * 7} {incr j} {
|
||||
$rd read ; # Discard replies
|
||||
}
|
||||
$rd close
|
||||
|
||||
@@ -59,6 +59,62 @@ test {corrupt payload: valid zipped hash header, dup records} {
|
||||
}
|
||||
}
|
||||
|
||||
test {corrupt payload: hash listpackex with invalid string TTL} {
|
||||
start_server [list overrides [list loglevel verbose use-exit-on-panic yes crash-memcheck-enabled no] ] {
|
||||
r config set sanitize-dump-payload yes
|
||||
catch {
|
||||
r restore key 0 "\x17\x2d\x2d\x00\x00\x00\x09\x00\x81\x61\x02\x01\x01\xf4\xa6\x96\x18\xb8\x8f\x01\x00\x00\x09\x82\x66\x31\x03\x82\x76\x31\x03\x83\x66\x6f\x6f\x04\x82\x66\x32\x03\x82\x76\x32\x03\x00\x01\xff\x0c\x00\xde\x40\xe5\x37\x51\x1c\x12\x56" replace
|
||||
} err
|
||||
assert_match "*Bad data format*" $err
|
||||
r ping
|
||||
}
|
||||
}
|
||||
|
||||
test {corrupt payload: hash listpackex with TTL large than EB_EXPIRE_TIME_MAX} {
|
||||
start_server [list overrides [list loglevel verbose use-exit-on-panic yes crash-memcheck-enabled no] ] {
|
||||
r config set sanitize-dump-payload yes
|
||||
catch {
|
||||
r restore key 0 "\x17\x33\x33\x00\x00\x00\x09\x00\x00\x01\x00\x01\xf4\x01\xc5\x89\x95\x8f\x01\x00\x00\x09\x01\x01\x82\x5f\x31\x03\xf4\x29\x94\x97\x95\x8f\x01\x00\x00\x09\x02\x01\x02\x01\xf4\x01\x5e\xaf\x95\x8f\x01\x33\x00\x09\xff\x0c\x00\x7e\x4f\xf4\x33\xe9\xc5\x3e\x56" replace
|
||||
} err
|
||||
assert_match "*Bad data format*" $err
|
||||
r ping
|
||||
}
|
||||
}
|
||||
|
||||
test {corrupt payload: hash listpackex with unordered TTL fields} {
|
||||
start_server [list overrides [list loglevel verbose use-exit-on-panic yes crash-memcheck-enabled no] ] {
|
||||
r config set sanitize-dump-payload yes
|
||||
catch {
|
||||
r restore key 0 "\x17\xc3\x30\x35\x14\x35\x00\x00\x00\t\x00\x82\x66\x32\x03\x82\x76\x32\x03\xf4\x80\x73\x16\xd1\x8f\x01\x20\x12\x02\x82\x66\x31\x20\x11\x03\x31\x03\xf4\x7f\xe0\x01\x11\x00\x33\x20\x11\x04\x33\x03\x00\x01\xff\x0c\x00\xf6\x70\x29\x57\x11\x68\x9d\xe5" replace
|
||||
} err
|
||||
assert_match "*Bad data format*" $err
|
||||
r ping
|
||||
}
|
||||
}
|
||||
|
||||
test {corrupt payload: hash listpackex field without TTL should not be followed by field with TTL} {
|
||||
start_server [list overrides [list loglevel verbose use-exit-on-panic yes crash-memcheck-enabled no] ] {
|
||||
r config set sanitize-dump-payload yes
|
||||
catch {
|
||||
r restore key 0 "\x17\x2d\x2d\x00\x00\x00\x09\x00\x82\x66\x31\x03\x82\x76\x31\x03\x00\x01\x82\x66\x32\x03\x82\x76\x32\x03\xf4\xe0\x59\x7a\x96\x00\x00\x00\x00\x09\x82\x66\x33\x03\x82\x76\x33\x03\x00\x01\xff\x0c\x00\x42\x66\xd4\xbe\x17\xc3\x96\x72" replace
|
||||
} err
|
||||
assert_match "*Bad data format*" $err
|
||||
r ping
|
||||
}
|
||||
}
|
||||
|
||||
test {corrupt payload: hash hashtable with TTL large than EB_EXPIRE_TIME_MAX} {
|
||||
start_server [list overrides [list loglevel verbose use-exit-on-panic yes crash-memcheck-enabled no] ] {
|
||||
r config set hash-max-listpack-entries 0
|
||||
r config set sanitize-dump-payload yes
|
||||
catch {
|
||||
r restore key 0 "\x16\x02\x81\x00\x01\x00\x00\x00\x00\x00\x00\x02\x66\x31\x02\x76\x31\x81\x00\x01\x00\x00\x00\x00\x00\x00\x02\x66\x32\x02\x76\x32\x0c\x00\xb9\x3c\x65\x28\x40\x94\x58\x36" replace
|
||||
} err
|
||||
assert_match "*Bad data format*" $err
|
||||
r ping
|
||||
}
|
||||
}
|
||||
|
||||
test {corrupt payload: quicklist big ziplist prev len} {
|
||||
start_server [list overrides [list loglevel verbose use-exit-on-panic yes crash-memcheck-enabled no] ] {
|
||||
r config set sanitize-dump-payload no
|
||||
|
||||
@@ -179,6 +179,17 @@ start_server {} {
|
||||
$master set $j somevalue px 10
|
||||
}
|
||||
|
||||
##### hash-field-expiration
|
||||
# Hashes of type OBJ_ENCODING_LISTPACK_EX won't be discarded during
|
||||
# RDB load, even if they are expired.
|
||||
$master hset myhash1 f1 v1 f2 v2 f3 v3
|
||||
$master hpexpire myhash1 10 FIELDS 3 f1 f2 f3
|
||||
# Hashes of type RDB_TYPE_HASH_METADATA will be discarded during RDB load.
|
||||
$master config set hash-max-listpack-entries 0
|
||||
$master hset myhash2 f1 v1 f2 v2
|
||||
$master hpexpire myhash2 10 FIELDS 2 f1 f2
|
||||
$master config set hash-max-listpack-entries 1
|
||||
|
||||
after 20
|
||||
|
||||
wait_for_condition 500 100 {
|
||||
|
||||
@@ -416,4 +416,228 @@ start_server {} {
|
||||
} {OK}
|
||||
}
|
||||
|
||||
set server_path [tmpdir "server.partial-hfield-exp-test"]
|
||||
|
||||
# verifies writing and reading hash key with expiring and persistent fields
|
||||
start_server [list overrides [list "dir" $server_path]] {
|
||||
foreach {type lp_entries} {listpack 512 dict 0} {
|
||||
test "HFE - save and load expired fields, expired soon after, or long after ($type)" {
|
||||
r config set hash-max-listpack-entries $lp_entries
|
||||
|
||||
r FLUSHALL
|
||||
|
||||
r HMSET key a 1 b 2 c 3 d 4 e 5
|
||||
# expected to be expired long after restart
|
||||
r HEXPIREAT key 2524600800 FIELDS 1 a
|
||||
# expected long TTL value (6 bytes) is saved and loaded correctly
|
||||
r HPEXPIREAT key 188900976391764 FIELDS 1 b
|
||||
# expected to be already expired after restart
|
||||
r HPEXPIRE key 80 FIELDS 1 d
|
||||
# expected to be expired soon after restart
|
||||
r HPEXPIRE key 200 FIELDS 1 e
|
||||
|
||||
r save
|
||||
# sleep 101 ms to make sure d will expire after restart
|
||||
after 101
|
||||
restart_server 0 true false
|
||||
wait_done_loading r
|
||||
|
||||
assert_equal [lsort [r hgetall key]] "1 2 3 a b c"
|
||||
assert_equal [r hpexpiretime key FIELDS 3 a b c] {2524600800000 188900976391764 -1}
|
||||
assert_equal [s rdb_last_load_keys_loaded] 1
|
||||
|
||||
# wait until expired_hash_fields equals 2
|
||||
wait_for_condition 10 100 {
|
||||
[s expired_hash_fields] == 2
|
||||
} else {
|
||||
fail "Value of expired_hash_fields is not as expected"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
set server_path [tmpdir "server.all-hfield-exp-test"]
|
||||
|
||||
# verifies writing hash with several expired keys, and active-expiring it on load
|
||||
start_server [list overrides [list "dir" $server_path]] {
|
||||
foreach {type lp_entries} {listpack 512 dict 0} {
|
||||
test "HFE - save and load rdb all fields expired, ($type)" {
|
||||
r config set hash-max-listpack-entries $lp_entries
|
||||
|
||||
r FLUSHALL
|
||||
|
||||
r HMSET key a 1 b 2 c 3 d 4
|
||||
r HPEXPIRE key 100 FIELDS 4 a b c d
|
||||
|
||||
r save
|
||||
# sleep 101 ms to make sure all fields will expire after restart
|
||||
after 101
|
||||
|
||||
restart_server 0 true false
|
||||
wait_done_loading r
|
||||
|
||||
# it is expected that no field was expired on load and the key was
|
||||
# loaded, even though all its fields are actually expired.
|
||||
assert_equal [s rdb_last_load_keys_loaded] 1
|
||||
|
||||
assert_equal [r hgetall key] {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
set server_path [tmpdir "server.listpack-to-dict-test"]
|
||||
|
||||
test "save listpack, load dict" {
|
||||
start_server [list overrides [list "dir" $server_path enable-debug-command yes]] {
|
||||
r config set hash-max-listpack-entries 512
|
||||
|
||||
r FLUSHALL
|
||||
|
||||
r HMSET key a 1 b 2 c 3 d 4
|
||||
assert_match "*encoding:listpack*" [r debug object key]
|
||||
r HPEXPIRE key 100 FIELDS 1 d
|
||||
r save
|
||||
|
||||
# sleep 200 ms to make sure 'd' will expire after when reloading
|
||||
after 200
|
||||
|
||||
# change configuration and reload - result should be dict-encoded key
|
||||
r config set hash-max-listpack-entries 0
|
||||
r debug reload nosave
|
||||
|
||||
# first verify d was not expired during load (no expiry when loading
|
||||
# a hash that was saved listpack-encoded)
|
||||
assert_equal [s rdb_last_load_keys_loaded] 1
|
||||
|
||||
# d should be lazy expired in hgetall
|
||||
assert_equal [lsort [r hgetall key]] "1 2 3 a b c"
|
||||
assert_match "*encoding:hashtable*" [r debug object key]
|
||||
}
|
||||
}
|
||||
|
||||
set server_path [tmpdir "server.dict-to-listpack-test"]
|
||||
|
||||
test "save dict, load listpack" {
|
||||
start_server [list overrides [list "dir" $server_path enable-debug-command yes]] {
|
||||
r config set hash-max-listpack-entries 0
|
||||
|
||||
r FLUSHALL
|
||||
|
||||
r HMSET key a 1 b 2 c 3 d 4
|
||||
assert_match "*encoding:hashtable*" [r debug object key]
|
||||
r HPEXPIRE key 200 FIELDS 1 d
|
||||
r save
|
||||
|
||||
# sleep 201 ms to make sure 'd' will expire during reload
|
||||
after 201
|
||||
|
||||
# change configuration and reload - result should be LP-encoded key
|
||||
r config set hash-max-listpack-entries 512
|
||||
r debug reload nosave
|
||||
|
||||
# verify d was expired during load
|
||||
assert_equal [s rdb_last_load_keys_loaded] 1
|
||||
|
||||
assert_equal [lsort [r hgetall key]] "1 2 3 a b c"
|
||||
assert_match "*encoding:listpack*" [r debug object key]
|
||||
}
|
||||
}
|
||||
|
||||
set server_path [tmpdir "server.active-expiry-after-load"]
|
||||
|
||||
# verifies a field is correctly expired by active expiry AFTER loading from RDB
|
||||
foreach {type lp_entries} {listpack 512 dict 0} {
|
||||
start_server [list overrides [list "dir" $server_path enable-debug-command yes]] {
|
||||
test "active field expiry after load, ($type)" {
|
||||
r config set hash-max-listpack-entries $lp_entries
|
||||
|
||||
r FLUSHALL
|
||||
|
||||
r HMSET key a 1 b 2 c 3 d 4 e 5 f 6
|
||||
r HEXPIREAT key 2524600800 FIELDS 2 a b
|
||||
r HPEXPIRE key 200 FIELDS 2 c d
|
||||
|
||||
r save
|
||||
r debug reload nosave
|
||||
|
||||
# wait at most 2 secs to make sure 'c' and 'd' will active-expire
|
||||
wait_for_condition 20 100 {
|
||||
[s expired_hash_fields] == 2
|
||||
} else {
|
||||
fail "expired hash fields is [s expired_hash_fields] != 2"
|
||||
}
|
||||
|
||||
assert_equal [s rdb_last_load_keys_loaded] 1
|
||||
|
||||
# hgetall might lazy expire fields, so it's only called after the stat asserts
|
||||
assert_equal [lsort [r hgetall key]] "1 2 5 6 a b e f"
|
||||
assert_equal [r hexpiretime key FIELDS 6 a b c d e f] {2524600800 2524600800 -2 -2 -1 -1}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
set server_path [tmpdir "server.lazy-expiry-after-load"]
|
||||
|
||||
foreach {type lp_entries} {listpack 512 dict 0} {
|
||||
start_server [list overrides [list "dir" $server_path enable-debug-command yes]] {
|
||||
test "lazy field expiry after load, ($type)" {
|
||||
r config set hash-max-listpack-entries $lp_entries
|
||||
r debug set-active-expire 0
|
||||
|
||||
r FLUSHALL
|
||||
|
||||
r HMSET key a 1 b 2 c 3 d 4 e 5 f 6
|
||||
r HEXPIREAT key 2524600800 FIELDS 2 a b
|
||||
r HPEXPIRE key 200 FIELDS 2 c d
|
||||
|
||||
r save
|
||||
r debug reload nosave
|
||||
|
||||
# sleep 500 msec to make sure 'c' and 'd' will lazy-expire when calling hgetall
|
||||
after 500
|
||||
|
||||
assert_equal [s rdb_last_load_keys_loaded] 1
|
||||
assert_equal [s expired_hash_fields] 0
|
||||
|
||||
# hgetall will lazy expire fields, so it's only called after the stat asserts
|
||||
assert_equal [lsort [r hgetall key]] "1 2 5 6 a b e f"
|
||||
assert_equal [r hexpiretime key FIELDS 6 a b c d e f] {2524600800 2524600800 -2 -2 -1 -1}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
set server_path [tmpdir "server.unexpired-items-rax-list-boundary"]
|
||||
|
||||
foreach {type lp_entries} {listpack 512 dict 0} {
|
||||
start_server [list overrides [list "dir" $server_path enable-debug-command yes]] {
|
||||
test "load un-expired items below and above rax-list boundary, ($type)" {
|
||||
r config set hash-max-listpack-entries $lp_entries
|
||||
|
||||
r flushall
|
||||
|
||||
set hash_sizes {15 16 17 31 32 33}
|
||||
foreach h $hash_sizes {
|
||||
for {set i 1} {$i <= $h} {incr i} {
|
||||
r hset key$h f$i v$i
|
||||
r hexpireat key$h 2524600800 FIELDS 1 f$i
|
||||
}
|
||||
}
|
||||
|
||||
r save
|
||||
|
||||
restart_server 0 true false
|
||||
wait_done_loading r
|
||||
|
||||
set hash_sizes {15 16 17 31 32 33}
|
||||
foreach h $hash_sizes {
|
||||
for {set i 1} {$i <= $h} {incr i} {
|
||||
# random expiration time
|
||||
assert_equal [r hget key$h f$i] v$i
|
||||
assert_equal [r hexpiretime key$h FIELDS 1 f$i] 2524600800
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} ;# tags
|
||||
|
||||
+15
-2
@@ -293,6 +293,9 @@ proc findKeyWithType {r type} {
|
||||
|
||||
proc createComplexDataset {r ops {opt {}}} {
|
||||
set useexpire [expr {[lsearch -exact $opt useexpire] != -1}]
|
||||
# TODO: Remove usehexpire on next commit, when RDB will support replication
|
||||
set usehexpire [expr {[lsearch -exact $opt usehexpire] != -1}]
|
||||
|
||||
if {[lsearch -exact $opt usetag] != -1} {
|
||||
set tag "{t}"
|
||||
} else {
|
||||
@@ -386,6 +389,10 @@ proc createComplexDataset {r ops {opt {}}} {
|
||||
{hash} {
|
||||
randpath {{*}$r hset $k $f $v} \
|
||||
{{*}$r hdel $k $f}
|
||||
|
||||
if { [{*}$r hexists $k $f] && $usehexpire && rand() < 0.5} {
|
||||
{*}$r hexpire $k 1000 FIELDS 1 $f
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -438,8 +445,14 @@ proc csvdump r {
|
||||
hash {
|
||||
set fields [{*}$r hgetall $k]
|
||||
set newfields {}
|
||||
foreach {k v} $fields {
|
||||
lappend newfields [list $k $v]
|
||||
foreach {f v} $fields {
|
||||
set expirylist [{*}$r hexpiretime $k FIELDS 1 $f]
|
||||
if {$expirylist eq (-1)} {
|
||||
lappend newfields [list $f $v]
|
||||
} else {
|
||||
set e [lindex $expirylist 0]
|
||||
lappend newfields [list $f $e $v] # TODO: extract the actual ttl value from the list in $e
|
||||
}
|
||||
}
|
||||
set fields [lsort -index 0 $newfields]
|
||||
foreach kv $fields {
|
||||
|
||||
@@ -34,6 +34,7 @@ set ::all_tests {
|
||||
unit/type/set
|
||||
unit/type/zset
|
||||
unit/type/hash
|
||||
unit/type/hash-field-expire
|
||||
unit/type/stream
|
||||
unit/type/stream-cgroups
|
||||
unit/sort
|
||||
|
||||
@@ -503,6 +503,105 @@ run_solo {defrag} {
|
||||
$rd_pubsub close
|
||||
}
|
||||
|
||||
test "Active Defrag HFE: $type" {
|
||||
r flushdb
|
||||
r config resetstat
|
||||
r config set hz 100
|
||||
r config set activedefrag no
|
||||
# TODO: Lower the threshold after defraging the ebuckets.
|
||||
# Now just to ensure that the reference is updated correctly.
|
||||
r config set active-defrag-threshold-lower 12
|
||||
r config set active-defrag-cycle-min 65
|
||||
r config set active-defrag-cycle-max 75
|
||||
r config set active-defrag-ignore-bytes 1500kb
|
||||
r config set maxmemory 0
|
||||
r config set hash-max-listpack-value 512
|
||||
r config set hash-max-listpack-entries 10
|
||||
|
||||
# Populate memory with interleaving hash field of same size
|
||||
set n 3000
|
||||
set fields 16 ;# make all the fields in an eblist.
|
||||
set dummy_field "[string repeat x 400]"
|
||||
set rd [redis_deferring_client]
|
||||
for {set i 0} {$i < $n} {incr i} {
|
||||
for {set j 0} {$j < $fields} {incr j} {
|
||||
$rd hset h$i f$j $dummy_field
|
||||
$rd hexpire h$i 9999999 FIELDS 1 f$j
|
||||
$rd set "k$i$j" $dummy_field
|
||||
}
|
||||
}
|
||||
for {set j 0} {$j < [expr $n*$fields]} {incr j} {
|
||||
$rd read ; # Discard hset replies
|
||||
$rd read ; # Discard hexpire replies
|
||||
$rd read ; # Discard set replies
|
||||
}
|
||||
|
||||
# Coverage for listpackex.
|
||||
r hset h_lpex f0 $dummy_field
|
||||
r hexpire h_lpex 9999999 FIELDS 1 f0
|
||||
assert_encoding listpackex h_lpex
|
||||
|
||||
after 120 ;# serverCron only updates the info once in 100ms
|
||||
if {$::verbose} {
|
||||
puts "used [s allocator_allocated]"
|
||||
puts "rss [s allocator_active]"
|
||||
puts "frag [s allocator_frag_ratio]"
|
||||
puts "frag_bytes [s allocator_frag_bytes]"
|
||||
}
|
||||
assert_lessthan [s allocator_frag_ratio] 1.05
|
||||
|
||||
# Delete all the keys to create fragmentation
|
||||
for {set i 0} {$i < $n} {incr i} {
|
||||
for {set j 0} {$j < $fields} {incr j} {
|
||||
r del "k$i$j"
|
||||
}
|
||||
}
|
||||
$rd close
|
||||
after 120 ;# serverCron only updates the info once in 100ms
|
||||
if {$::verbose} {
|
||||
puts "used [s allocator_allocated]"
|
||||
puts "rss [s allocator_active]"
|
||||
puts "frag [s allocator_frag_ratio]"
|
||||
puts "frag_bytes [s allocator_frag_bytes]"
|
||||
}
|
||||
assert_morethan [s allocator_frag_ratio] 1.35
|
||||
|
||||
catch {r config set activedefrag yes} e
|
||||
if {[r config get activedefrag] eq "activedefrag yes"} {
|
||||
|
||||
# wait for the active defrag to start working (decision once a second)
|
||||
wait_for_condition 50 100 {
|
||||
[s total_active_defrag_time] ne 0
|
||||
} else {
|
||||
after 120 ;# serverCron only updates the info once in 100ms
|
||||
puts [r info memory]
|
||||
puts [r info stats]
|
||||
puts [r memory malloc-stats]
|
||||
fail "defrag not started."
|
||||
}
|
||||
|
||||
# wait for the active defrag to stop working
|
||||
wait_for_condition 500 100 {
|
||||
[s active_defrag_running] eq 0
|
||||
} else {
|
||||
after 120 ;# serverCron only updates the info once in 100ms
|
||||
puts [r info memory]
|
||||
puts [r memory malloc-stats]
|
||||
fail "defrag didn't stop."
|
||||
}
|
||||
|
||||
# test the fragmentation is lower
|
||||
after 120 ;# serverCron only updates the info once in 100ms
|
||||
if {$::verbose} {
|
||||
puts "used [s allocator_allocated]"
|
||||
puts "rss [s allocator_active]"
|
||||
puts "frag [s allocator_frag_ratio]"
|
||||
puts "frag_bytes [s allocator_frag_bytes]"
|
||||
}
|
||||
assert_lessthan_equal [s allocator_frag_ratio] 1.5
|
||||
}
|
||||
}
|
||||
|
||||
if {$type eq "standalone"} { ;# skip in cluster mode
|
||||
test "Active defrag big list: $type" {
|
||||
r flushdb
|
||||
|
||||
@@ -124,7 +124,8 @@ start_server {tags {"other"}} {
|
||||
if {$::accurate} {set numops 10000} else {set numops 1000}
|
||||
test {Check consistency of different data types after a reload} {
|
||||
r flushdb
|
||||
createComplexDataset r $numops usetag
|
||||
# TODO: integrate usehexpire following next commit that will support replication
|
||||
createComplexDataset r $numops {usetag usehexpire}
|
||||
if {$::ignoredigest} {
|
||||
set _ 1
|
||||
} else {
|
||||
|
||||
+13
-1
@@ -353,17 +353,29 @@ start_server {tags {"pubsub network"}} {
|
||||
$rd1 close
|
||||
}
|
||||
|
||||
test "Keyspace notifications: hash events test" {
|
||||
foreach {type max_lp_entries} {listpackex 512 hashtable 0} {
|
||||
test "Keyspace notifications: hash events test ($type)" {
|
||||
r config set hash-max-listpack-entries $max_lp_entries
|
||||
r config set notify-keyspace-events Kh
|
||||
r del myhash
|
||||
set rd1 [redis_deferring_client]
|
||||
assert_equal {1} [psubscribe $rd1 *]
|
||||
r hmset myhash yes 1 no 0
|
||||
r hincrby myhash yes 10
|
||||
r hexpire myhash 999999 FIELDS 1 yes
|
||||
r hexpireat myhash [expr {[clock seconds] + 999999}] NX FIELDS 1 no
|
||||
r hpexpire myhash 5 FIELDS 1 yes
|
||||
r hpersist myhash FIELDS 1 yes
|
||||
assert_encoding $type myhash
|
||||
assert_equal "pmessage * __keyspace@${db}__:myhash hset" [$rd1 read]
|
||||
assert_equal "pmessage * __keyspace@${db}__:myhash hincrby" [$rd1 read]
|
||||
assert_equal "pmessage * __keyspace@${db}__:myhash hexpire" [$rd1 read]
|
||||
assert_equal "pmessage * __keyspace@${db}__:myhash hexpire" [$rd1 read]
|
||||
assert_equal "pmessage * __keyspace@${db}__:myhash hexpire" [$rd1 read]
|
||||
assert_equal "pmessage * __keyspace@${db}__:myhash hpersist" [$rd1 read]
|
||||
$rd1 close
|
||||
}
|
||||
} ;# foreach
|
||||
|
||||
test "Keyspace notifications: stream events test" {
|
||||
r config set notify-keyspace-events Kt
|
||||
|
||||
@@ -277,6 +277,32 @@ proc test_scan {type} {
|
||||
set res [r hscan hash 0 count 1000 novalues]
|
||||
assert_equal [lsort $keys2] [lsort [lindex $res 1]]
|
||||
}
|
||||
|
||||
test "{$type} HSCAN with large value $enc" {
|
||||
r del hash
|
||||
|
||||
if {$enc eq {listpack}} {
|
||||
set count 60
|
||||
} else {
|
||||
set count 170
|
||||
}
|
||||
|
||||
set val1 [string repeat "1" $count]
|
||||
r hset hash $val1 $val1
|
||||
|
||||
set val2 [string repeat "2" $count]
|
||||
r hset hash $val2 $val2
|
||||
|
||||
set res [lsort [lindex [r hscan hash 0] 1]]
|
||||
assert_equal $val1 [lindex $res 0]
|
||||
assert_equal $val1 [lindex $res 1]
|
||||
assert_equal $val2 [lindex $res 2]
|
||||
assert_equal $val2 [lindex $res 3]
|
||||
|
||||
set res [lsort [lindex [r hscan hash 0 novalues] 1]]
|
||||
assert_equal $val1 [lindex $res 0]
|
||||
assert_equal $val2 [lindex $res 1]
|
||||
}
|
||||
}
|
||||
|
||||
foreach enc {listpack skiplist} {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user