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

Hash Field Expiration (#13303)

## Background

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

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

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

... after 10 seconds ...

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

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

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

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

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

This work was done by @moticless, @tezc, @ronen-kalish, @sundb, I just release it.
parents 2d1bb42c f0389f28
...@@ -94,6 +94,7 @@ int activeExpireCycleTryExpire(redisDb *db, dictEntry *de, long long now) { ...@@ -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_SLOW_TIME_PERC 25 /* Max % of CPU to use. */
#define ACTIVE_EXPIRE_CYCLE_ACCEPTABLE_STALE 10 /* % of stale keys after which #define ACTIVE_EXPIRE_CYCLE_ACCEPTABLE_STALE 10 /* % of stale keys after which
we do extra efforts. */ we do extra efforts. */
#define HFE_ACTIVE_EXPIRE_CYCLE_FIELDS 1000
/* Data used by the expire dict scan callback. */ /* Data used by the expire dict scan callback. */
typedef struct { typedef struct {
...@@ -134,6 +135,53 @@ static inline int isExpiryDictValidForSamplingCb(dict *d) { ...@@ -134,6 +135,53 @@ static inline int isExpiryDictValidForSamplingCb(dict *d) {
return C_OK; 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) { void activeExpireCycle(int type) {
/* Adjust the running parameters according to the configured expire /* Adjust the running parameters according to the configured expire
* effort. The default effort is 1, and the maximum configurable effort * effort. The default effort is 1, and the maximum configurable effort
...@@ -232,6 +280,11 @@ void activeExpireCycle(int type) { ...@@ -232,6 +280,11 @@ void activeExpireCycle(int type) {
* distribute the time evenly across DBs. */ * distribute the time evenly across DBs. */
current_db++; 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)) if (kvstoreSize(db->expires))
dbs_performed++; dbs_performed++;
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
#include "atomicvar.h" #include "atomicvar.h"
#include "functions.h" #include "functions.h"
#include "cluster.h" #include "cluster.h"
#include "ebuckets.h"
static redisAtomic size_t lazyfree_objects = 0; static redisAtomic size_t lazyfree_objects = 0;
static redisAtomic size_t lazyfreed_objects = 0; static redisAtomic size_t lazyfreed_objects = 0;
...@@ -22,7 +23,8 @@ void lazyfreeFreeObject(void *args[]) { ...@@ -22,7 +23,8 @@ void lazyfreeFreeObject(void *args[]) {
void lazyfreeFreeDatabase(void *args[]) { void lazyfreeFreeDatabase(void *args[]) {
kvstore *da1 = args[0]; kvstore *da1 = args[0];
kvstore *da2 = args[1]; kvstore *da2 = args[1];
ebuckets oldHfe = args[2];
ebDestroy(&oldHfe, &hashExpireBucketsType, NULL);
size_t numkeys = kvstoreSize(da1); size_t numkeys = kvstoreSize(da1);
kvstoreRelease(da1); kvstoreRelease(da1);
kvstoreRelease(da2); kvstoreRelease(da2);
...@@ -201,10 +203,12 @@ void emptyDbAsync(redisDb *db) { ...@@ -201,10 +203,12 @@ void emptyDbAsync(redisDb *db) {
flags |= KVSTORE_FREE_EMPTY_DICTS; flags |= KVSTORE_FREE_EMPTY_DICTS;
} }
kvstore *oldkeys = db->keys, *oldexpires = db->expires; kvstore *oldkeys = db->keys, *oldexpires = db->expires;
ebuckets oldHfe = db->hexpires;
db->keys = kvstoreCreate(&dbDictType, slot_count_bits, flags); db->keys = kvstoreCreate(&dbDictType, slot_count_bits, flags);
db->expires = kvstoreCreate(&dbExpiresDictType, slot_count_bits, flags); db->expires = kvstoreCreate(&dbExpiresDictType, slot_count_bits, flags);
db->hexpires = ebCreate();
atomicIncr(lazyfree_objects, kvstoreSize(oldkeys)); atomicIncr(lazyfree_objects, kvstoreSize(oldkeys));
bioCreateLazyFreeJob(lazyfreeFreeDatabase, 2, oldkeys, oldexpires); bioCreateLazyFreeJob(lazyfreeFreeDatabase, 3, oldkeys, oldexpires, oldHfe);
} }
/* Free the key tracking table. /* Free the key tracking table.
......
...@@ -245,41 +245,50 @@ unsigned char* lpShrinkToFit(unsigned char *lp) { ...@@ -245,41 +245,50 @@ unsigned char* lpShrinkToFit(unsigned char *lp) {
static inline void lpEncodeIntegerGetType(int64_t v, unsigned char *intenc, uint64_t *enclen) { static inline void lpEncodeIntegerGetType(int64_t v, unsigned char *intenc, uint64_t *enclen) {
if (v >= 0 && v <= 127) { if (v >= 0 && v <= 127) {
/* Single byte 0-127 integer. */ /* Single byte 0-127 integer. */
intenc[0] = v; if (intenc != NULL) intenc[0] = v;
*enclen = 1; if (enclen != NULL) *enclen = 1;
} else if (v >= -4096 && v <= 4095) { } else if (v >= -4096 && v <= 4095) {
/* 13 bit integer. */ /* 13 bit integer. */
if (v < 0) v = ((int64_t)1<<13)+v; if (v < 0) v = ((int64_t)1<<13)+v;
if (intenc != NULL) {
intenc[0] = (v>>8)|LP_ENCODING_13BIT_INT; intenc[0] = (v>>8)|LP_ENCODING_13BIT_INT;
intenc[1] = v&0xff; intenc[1] = v&0xff;
*enclen = 2; }
if (enclen != NULL) *enclen = 2;
} else if (v >= -32768 && v <= 32767) { } else if (v >= -32768 && v <= 32767) {
/* 16 bit integer. */ /* 16 bit integer. */
if (v < 0) v = ((int64_t)1<<16)+v; if (v < 0) v = ((int64_t)1<<16)+v;
if (intenc != NULL) {
intenc[0] = LP_ENCODING_16BIT_INT; intenc[0] = LP_ENCODING_16BIT_INT;
intenc[1] = v&0xff; intenc[1] = v&0xff;
intenc[2] = v>>8; intenc[2] = v>>8;
*enclen = 3; }
if (enclen != NULL) *enclen = 3;
} else if (v >= -8388608 && v <= 8388607) { } else if (v >= -8388608 && v <= 8388607) {
/* 24 bit integer. */ /* 24 bit integer. */
if (v < 0) v = ((int64_t)1<<24)+v; if (v < 0) v = ((int64_t)1<<24)+v;
if (intenc != NULL) {
intenc[0] = LP_ENCODING_24BIT_INT; intenc[0] = LP_ENCODING_24BIT_INT;
intenc[1] = v&0xff; intenc[1] = v&0xff;
intenc[2] = (v>>8)&0xff; intenc[2] = (v>>8)&0xff;
intenc[3] = v>>16; intenc[3] = v>>16;
*enclen = 4; }
if (enclen != NULL) *enclen = 4;
} else if (v >= -2147483648 && v <= 2147483647) { } else if (v >= -2147483648 && v <= 2147483647) {
/* 32 bit integer. */ /* 32 bit integer. */
if (v < 0) v = ((int64_t)1<<32)+v; if (v < 0) v = ((int64_t)1<<32)+v;
if (intenc != NULL) {
intenc[0] = LP_ENCODING_32BIT_INT; intenc[0] = LP_ENCODING_32BIT_INT;
intenc[1] = v&0xff; intenc[1] = v&0xff;
intenc[2] = (v>>8)&0xff; intenc[2] = (v>>8)&0xff;
intenc[3] = (v>>16)&0xff; intenc[3] = (v>>16)&0xff;
intenc[4] = v>>24; intenc[4] = v>>24;
*enclen = 5; }
if (enclen != NULL) *enclen = 5;
} else { } else {
/* 64 bit integer. */ /* 64 bit integer. */
uint64_t uv = v; uint64_t uv = v;
if (intenc != NULL) {
intenc[0] = LP_ENCODING_64BIT_INT; intenc[0] = LP_ENCODING_64BIT_INT;
intenc[1] = uv&0xff; intenc[1] = uv&0xff;
intenc[2] = (uv>>8)&0xff; intenc[2] = (uv>>8)&0xff;
...@@ -289,7 +298,8 @@ static inline void lpEncodeIntegerGetType(int64_t v, unsigned char *intenc, uint ...@@ -289,7 +298,8 @@ static inline void lpEncodeIntegerGetType(int64_t v, unsigned char *intenc, uint
intenc[6] = (uv>>40)&0xff; intenc[6] = (uv>>40)&0xff;
intenc[7] = (uv>>48)&0xff; intenc[7] = (uv>>48)&0xff;
intenc[8] = uv>>56; intenc[8] = uv>>56;
*enclen = 9; }
if (enclen != NULL) *enclen = 9;
} }
} }
...@@ -659,49 +669,46 @@ unsigned char *lpGetValue(unsigned char *p, unsigned int *slen, long long *lval) ...@@ -659,49 +669,46 @@ unsigned char *lpGetValue(unsigned char *p, unsigned int *slen, long long *lval)
return vstr; return vstr;
} }
/* Find pointer to the entry equal to the specified entry. Skip 'skip' entries /* This is just a wrapper to lpGet() that is able to get an integer from an entry directly.
* between every comparison. Returns NULL when the field could not be found. */ * Returns 1 and stores the integer in 'lval' if the entry is an integer.
unsigned char *lpFind(unsigned char *lp, unsigned char *p, unsigned char *s, * Returns 0 if the entry is a string. */
uint32_t slen, unsigned int skip) { 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; int skipcnt = 0;
unsigned char vencoding = 0;
unsigned char *value; unsigned char *value;
int64_t ll, vll; int64_t ll;
uint64_t entry_size = 123456789; /* initialized to avoid warning. */ uint64_t entry_size = 123456789; /* initialized to avoid warning. */
uint32_t lp_bytes = lpBytes(lp); uint32_t lp_bytes = lpBytes(lp);
assert(p); if (!p)
p = lpFirst(lp);
while (p) { while (p) {
if (skipcnt == 0) { if (skipcnt == 0) {
value = lpGetWithSize(p, &ll, NULL, &entry_size); value = lpGetWithSize(p, &ll, NULL, &entry_size);
if (value) { if (value) {
/* check the value doesn't reach outside the listpack before accessing it */ /* check the value doesn't reach outside the listpack before accessing it */
assert(p >= lp + LP_HDR_SIZE && p + entry_size < lp + lp_bytes); 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 (cmp(lp, p, user, value, ll) == 0)
* 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; return p;
}
}
/* Reset skip count */ /* Reset skip count */
skipcnt = skip; skipcnt = skip;
...@@ -727,6 +734,62 @@ unsigned char *lpFind(unsigned char *lp, unsigned char *p, unsigned char *s, ...@@ -727,6 +734,62 @@ unsigned char *lpFind(unsigned char *lp, unsigned char *p, unsigned char *s,
return NULL; 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 /* Insert, delete or replace the specified string element 'elestr' of length
* 'size' or integer element 'eleint' at the specified position 'p', with 'p' * 'size' or integer element 'eleint' at the specified position 'p', with 'p'
* being a listpack element pointer obtained with lpFirst(), lpLast(), lpNext(), * 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 ...@@ -904,6 +967,140 @@ unsigned char *lpInsert(unsigned char *lp, unsigned char *elestr, unsigned char
return lp; 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. */ /* 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 *lpInsertString(unsigned char *lp, unsigned char *s, uint32_t slen,
unsigned char *p, int where, unsigned char **newp) unsigned char *p, int where, unsigned char **newp)
...@@ -951,6 +1148,20 @@ unsigned char *lpAppendInteger(unsigned char *lp, long long lval) { ...@@ -951,6 +1148,20 @@ unsigned char *lpAppendInteger(unsigned char *lp, long long lval) {
return lpInsertInteger(lp, lval, eofptr, LP_BEFORE, NULL); 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 /* 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 * the current element. The function returns the new listpack as return
* value, and also updates the current cursor by updating '*p'. */ * value, and also updates the current cursor by updating '*p'. */
...@@ -1199,13 +1410,17 @@ size_t lpBytes(unsigned char *lp) { ...@@ -1199,13 +1410,17 @@ size_t lpBytes(unsigned char *lp) {
return lpGetTotalBytes(lp); return lpGetTotalBytes(lp);
} }
/* Returns the size of a listpack consisting of an integer repeated 'rep' times. */ /* Returns the size 'lval' will require when encoded, in bytes */
size_t lpEstimateBytesRepeatedInteger(long long lval, unsigned long rep) { size_t lpEntrySizeInteger(long long lval) {
uint64_t enclen; uint64_t enclen;
unsigned char intenc[LP_MAX_INT_ENCODING_LEN]; lpEncodeIntegerGetType(lval, NULL, &enclen);
lpEncodeIntegerGetType(lval, intenc, &enclen);
unsigned long backlen = lpEncodeBacklen(NULL, enclen); unsigned long backlen = lpEncodeBacklen(NULL, enclen);
return LP_HDR_SIZE + (enclen + backlen) * rep + 1; 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) {
return LP_HDR_SIZE + lpEntrySizeInteger(lval) * rep + 1;
} }
/* Seek the specified element and returns the pointer to the seeked element. /* 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 ...@@ -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. /* Randomly select a pair of key and value.
* total_count is a pre-computed length/2 of the listpack (to avoid calls to lpLength) * 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. * 'key' and 'val' are used to store the result key value pair.
* 'val' can be NULL if the value is not needed. */ * 'val' can be NULL if the value is not needed.
void lpRandomPair(unsigned char *lp, unsigned long total_count, listpackEntry *key, listpackEntry *val) { * '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; unsigned char *p;
assert(tuple_len >= 2);
/* Avoid div by zero on corrupt listpack */ /* Avoid div by zero on corrupt listpack */
assert(total_count); assert(total_count);
/* Generate even numbers, because listpack saved K-V pair */ int r = (rand() % total_count) * tuple_len;
int r = (rand() % total_count) * 2;
assert((p = lpSeek(lp, r))); assert((p = lpSeek(lp, r)));
key->sval = lpGetValue(p, &(key->slen), &(key->lval)); key->sval = lpGetValue(p, &(key->slen), &(key->lval));
...@@ -1466,26 +1686,31 @@ void lpRandomEntries(unsigned char *lp, unsigned int count, listpackEntry *entri ...@@ -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 /* 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 * 'vals' args. The order of the picked entries is random, and the selections
* are non-unique (repetitions are possible). * are non-unique (repetitions are possible).
* The 'vals' arg can be NULL in which case we skip these. */ * The 'vals' arg can be NULL in which case we skip these.
void lpRandomPairs(unsigned char *lp, unsigned int count, listpackEntry *keys, listpackEntry *vals) { * '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 char *p, *key, *value;
unsigned int klen = 0, vlen = 0; unsigned int klen = 0, vlen = 0;
long long klval = 0, vlval = 0; long long klval = 0, vlval = 0;
assert(tuple_len >= 2);
/* Notice: the index member must be first due to the use in uintCompare */ /* Notice: the index member must be first due to the use in uintCompare */
typedef struct { typedef struct {
unsigned int index; unsigned int index;
unsigned int order; unsigned int order;
} rand_pick; } rand_pick;
rand_pick *picks = lp_malloc(sizeof(rand_pick)*count); 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 */ /* Avoid div by zero on corrupt listpack */
assert(total_size); assert(total_size);
/* create a pool of random indexes (some may be duplicate). */ /* create a pool of random indexes (some may be duplicate). */
for (unsigned int i = 0; i < count; i++) { 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 */ /* keep track of the order we picked them */
picks[i].order = i; picks[i].order = i;
} }
...@@ -1507,9 +1732,12 @@ void lpRandomPairs(unsigned char *lp, unsigned int count, listpackEntry *keys, l ...@@ -1507,9 +1732,12 @@ void lpRandomPairs(unsigned char *lp, unsigned int count, listpackEntry *keys, l
lpSaveValue(value, vlen, vlval, &vals[storeorder]); lpSaveValue(value, vlen, vlval, &vals[storeorder]);
pickindex++; pickindex++;
} }
lpindex += 2; lpindex += tuple_len;
for (int i = 0; i < tuple_len - 1; i++) {
p = lpNext(lp, p); p = lpNext(lp, p);
} }
}
lp_free(picks); lp_free(picks);
} }
...@@ -1518,13 +1746,20 @@ void lpRandomPairs(unsigned char *lp, unsigned int count, listpackEntry *keys, l ...@@ -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 * 'vals' args. The selections are unique (no repetitions), and the order of
* the picked entries is NOT-random. * the picked entries is NOT-random.
* The 'vals' arg can be NULL in which case we skip these. * 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 * 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. */ * 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 char *p, *key;
unsigned int klen = 0; unsigned int klen = 0;
long long klval = 0; long long klval = 0;
unsigned int total_size = lpLength(lp)/2; unsigned int total_size = lpLength(lp)/tuple_len;
unsigned int index = 0; unsigned int index = 0;
if (count > total_size) if (count > total_size)
count = total_size; count = total_size;
...@@ -1532,7 +1767,7 @@ unsigned int lpRandomPairsUnique(unsigned char *lp, unsigned int count, listpack ...@@ -1532,7 +1767,7 @@ unsigned int lpRandomPairsUnique(unsigned char *lp, unsigned int count, listpack
p = lpFirst(lp); p = lpFirst(lp);
unsigned int picked = 0, remaining = count; unsigned int picked = 0, remaining = count;
while (picked < count && p) { 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); key = lpGetValue(p, &klen, &klval);
lpSaveValue(key, klen, klval, &keys[picked]); lpSaveValue(key, klen, klval, &keys[picked]);
assert((p = lpNext(lp, p))); assert((p = lpNext(lp, p)));
...@@ -1554,8 +1789,9 @@ unsigned int lpRandomPairsUnique(unsigned char *lp, unsigned int count, listpack ...@@ -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 * 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' * 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 * 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 * 'tuple_len' indicates entry count of a single logical item. e.g. This is
* useful if the listpack represents a key-value pair sequence. * 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 * 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 * 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 ...@@ -1565,7 +1801,7 @@ unsigned int lpRandomPairsUnique(unsigned char *lp, unsigned int count, listpack
* p = lpFirst(lp); * p = lpFirst(lp);
* i = 0; * i = 0;
* while (remaining > 0) { * while (remaining > 0) {
* p = lpNextRandom(lp, p, &i, remaining--, 0); * p = lpNextRandom(lp, p, &i, remaining--, 1);
* *
* // ... Do stuff with p ... * // ... Do stuff with p ...
* *
...@@ -1574,8 +1810,9 @@ unsigned int lpRandomPairsUnique(unsigned char *lp, unsigned int count, listpack ...@@ -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 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 /* 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 * 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 * 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 ...@@ -1583,15 +1820,14 @@ unsigned char *lpNextRandom(unsigned char *lp, unsigned char *p, unsigned int *i
unsigned int i = *index; unsigned int i = *index;
unsigned int total_size = lpLength(lp); unsigned int total_size = lpLength(lp);
while (i < total_size && p != NULL) { while (i < total_size && p != NULL) {
if (even_only && i % 2 != 0) { if (i % tuple_len != 0) {
p = lpNext(lp, p); p = lpNext(lp, p);
i++; i++;
continue; continue;
} }
/* Do we pick this element? */ /* Do we pick this element? */
unsigned int available = total_size - i; unsigned int available = (total_size - i) / tuple_len;
if (even_only) available /= 2;
double randomDouble = ((double)rand()) / RAND_MAX; double randomDouble = ((double)rand()) / RAND_MAX;
double threshold = ((double)remaining) / available; double threshold = ((double)remaining) / available;
if (randomDouble <= threshold) { if (randomDouble <= threshold) {
...@@ -1787,6 +2023,24 @@ static int lpValidation(unsigned char *p, unsigned int head_count, void *userdat ...@@ -1787,6 +2023,24 @@ static int lpValidation(unsigned char *p, unsigned int head_count, void *userdat
return ret; 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) { int listpackTest(int argc, char *argv[], int flags) {
UNUSED(argc); UNUSED(argc);
UNUSED(argv); UNUSED(argv);
...@@ -2031,6 +2285,111 @@ int listpackTest(int argc, char *argv[], int flags) { ...@@ -2031,6 +2285,111 @@ int listpackTest(int argc, char *argv[], int flags) {
zfree(lp); 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") { TEST("Batch delete") {
unsigned char *lp = createList(); /* char *mixlist[] = {"hello", "foo", "quux", "1024"} */ unsigned char *lp = createList(); /* char *mixlist[] = {"hello", "foo", "quux", "1024"} */
assert(lpLength(lp) == 4); /* Pre-condition */ assert(lpLength(lp) == 4); /* Pre-condition */
...@@ -2210,7 +2569,7 @@ int listpackTest(int argc, char *argv[], int flags) { ...@@ -2210,7 +2569,7 @@ int listpackTest(int argc, char *argv[], int flags) {
unsigned index = 0; unsigned index = 0;
while (remaining > 0) { while (remaining > 0) {
assert(p != NULL); assert(p != NULL);
p = lpNextRandom(lp, p, &index, remaining--, 0); p = lpNextRandom(lp, p, &index, remaining--, 1);
assert(p != NULL); assert(p != NULL);
assert(p != prev); assert(p != prev);
prev = p; prev = p;
...@@ -2226,7 +2585,7 @@ int listpackTest(int argc, char *argv[], int flags) { ...@@ -2226,7 +2585,7 @@ int listpackTest(int argc, char *argv[], int flags) {
unsigned i = 0; unsigned i = 0;
/* Pick from empty listpack returns NULL. */ /* 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. */ /* Add some elements and find their pointers within the listpack. */
lp = lpAppend(lp, (unsigned char *)"abc", 3); lp = lpAppend(lp, (unsigned char *)"abc", 3);
...@@ -2239,19 +2598,19 @@ int listpackTest(int argc, char *argv[], int flags) { ...@@ -2239,19 +2598,19 @@ int listpackTest(int argc, char *argv[], int flags) {
assert(lpNext(lp, p2) == NULL); assert(lpNext(lp, p2) == NULL);
/* Pick zero elements returns 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. */ /* Pick all returns all. */
i = 0; assert(lpNextRandom(lp, p0, &i, 3, 0) == p0 && i == 0); i = 0; assert(lpNextRandom(lp, p0, &i, 3, 1) == p0 && i == 0);
i = 1; assert(lpNextRandom(lp, p1, &i, 2, 0) == p1 && i == 1); i = 1; assert(lpNextRandom(lp, p1, &i, 2, 1) == p1 && i == 1);
i = 2; assert(lpNextRandom(lp, p2, &i, 1, 0) == p2 && i == 2); 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. */ /* 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. */ /* Pick all even elements returns p0 and p2. */
i = 0; assert(lpNextRandom(lp, p0, &i, 10, 1) == p0 && i == 0); i = 0; assert(lpNextRandom(lp, p0, &i, 10, 2) == p0 && i == 0);
i = 1; assert(lpNextRandom(lp, p1, &i, 10, 1) == p2 && i == 2); i = 1; assert(lpNextRandom(lp, p1, &i, 10, 2) == p2 && i == 2);
/* Don't crash even for bad index. */ /* Don't crash even for bad index. */
for (int j = 0; j < 100; j++) { for (int j = 0; j < 100; j++) {
...@@ -2264,7 +2623,7 @@ int listpackTest(int argc, char *argv[], int flags) { ...@@ -2264,7 +2623,7 @@ int listpackTest(int argc, char *argv[], int flags) {
} }
i = j % 7; i = j % 7;
unsigned int remaining = j % 5; 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); assert(p == p0 || p == p1 || p == p2 || p == NULL);
} }
lpFree(lp); lpFree(lp);
...@@ -2275,7 +2634,7 @@ int listpackTest(int argc, char *argv[], int flags) { ...@@ -2275,7 +2634,7 @@ int listpackTest(int argc, char *argv[], int flags) {
unsigned char *lp = lpNew(0); unsigned char *lp = lpNew(0);
lp = lpAppend(lp, (unsigned char*)"abc", 3); lp = lpAppend(lp, (unsigned char*)"abc", 3);
lp = lpAppend(lp, (unsigned char*)"123", 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(memcmp(key.sval, "abc", key.slen) == 0);
assert(val.lval == 123); assert(val.lval == 123);
lpFree(lp); lpFree(lp);
...@@ -2288,7 +2647,7 @@ int listpackTest(int argc, char *argv[], int flags) { ...@@ -2288,7 +2647,7 @@ int listpackTest(int argc, char *argv[], int flags) {
lp = lpAppend(lp, (unsigned char*)"123", 3); lp = lpAppend(lp, (unsigned char*)"123", 3);
lp = lpAppend(lp, (unsigned char*)"456", 3); lp = lpAppend(lp, (unsigned char*)"456", 3);
lp = lpAppend(lp, (unsigned char*)"def", 3); lp = lpAppend(lp, (unsigned char*)"def", 3);
lpRandomPair(lp, 2, &key, &val); lpRandomPair(lp, 2, &key, &val, 2);
if (key.sval) { if (key.sval) {
assert(!memcmp(key.sval, "abc", key.slen)); assert(!memcmp(key.sval, "abc", key.slen));
assert(key.slen == 3); assert(key.slen == 3);
...@@ -2301,6 +2660,42 @@ int listpackTest(int argc, char *argv[], int flags) { ...@@ -2301,6 +2660,42 @@ int listpackTest(int argc, char *argv[], int flags) {
lpFree(lp); 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") { TEST("Random pairs with one element") {
int count = 5; int count = 5;
unsigned char *lp = lpNew(0); unsigned char *lp = lpNew(0);
...@@ -2309,7 +2704,7 @@ int listpackTest(int argc, char *argv[], int flags) { ...@@ -2309,7 +2704,7 @@ int listpackTest(int argc, char *argv[], int flags) {
lp = lpAppend(lp, (unsigned char*)"abc", 3); lp = lpAppend(lp, (unsigned char*)"abc", 3);
lp = lpAppend(lp, (unsigned char*)"123", 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(memcmp(keys[4].sval, "abc", keys[4].slen) == 0);
assert(vals[4].lval == 123); assert(vals[4].lval == 123);
zfree(keys); zfree(keys);
...@@ -2327,7 +2722,7 @@ int listpackTest(int argc, char *argv[], int flags) { ...@@ -2327,7 +2722,7 @@ int listpackTest(int argc, char *argv[], int flags) {
lp = lpAppend(lp, (unsigned char*)"123", 3); lp = lpAppend(lp, (unsigned char*)"123", 3);
lp = lpAppend(lp, (unsigned char*)"456", 3); lp = lpAppend(lp, (unsigned char*)"456", 3);
lp = lpAppend(lp, (unsigned char*)"def", 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++) { for (int i = 0; i < count; i++) {
if (keys[i].sval) { if (keys[i].sval) {
assert(!memcmp(keys[i].sval, "abc", keys[i].slen)); assert(!memcmp(keys[i].sval, "abc", keys[i].slen));
...@@ -2344,6 +2739,47 @@ int listpackTest(int argc, char *argv[], int flags) { ...@@ -2344,6 +2739,47 @@ int listpackTest(int argc, char *argv[], int flags) {
lpFree(lp); 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") { TEST("Random pairs unique with one element") {
unsigned picked; unsigned picked;
int count = 5; int count = 5;
...@@ -2353,7 +2789,7 @@ int listpackTest(int argc, char *argv[], int flags) { ...@@ -2353,7 +2789,7 @@ int listpackTest(int argc, char *argv[], int flags) {
lp = lpAppend(lp, (unsigned char*)"abc", 3); lp = lpAppend(lp, (unsigned char*)"abc", 3);
lp = lpAppend(lp, (unsigned char*)"123", 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(picked == 1);
assert(memcmp(keys[0].sval, "abc", keys[0].slen) == 0); assert(memcmp(keys[0].sval, "abc", keys[0].slen) == 0);
assert(vals[0].lval == 123); assert(vals[0].lval == 123);
...@@ -2373,7 +2809,7 @@ int listpackTest(int argc, char *argv[], int flags) { ...@@ -2373,7 +2809,7 @@ int listpackTest(int argc, char *argv[], int flags) {
lp = lpAppend(lp, (unsigned char*)"123", 3); lp = lpAppend(lp, (unsigned char*)"123", 3);
lp = lpAppend(lp, (unsigned char*)"456", 3); lp = lpAppend(lp, (unsigned char*)"456", 3);
lp = lpAppend(lp, (unsigned char*)"def", 3); lp = lpAppend(lp, (unsigned char*)"def", 3);
picked = lpRandomPairsUnique(lp, count, keys, vals); picked = lpRandomPairsUnique(lp, count, keys, vals, 2);
assert(picked == 2); assert(picked == 2);
for (int i = 0; i < 2; i++) { for (int i = 0; i < 2; i++) {
if (keys[i].sval) { if (keys[i].sval) {
...@@ -2391,6 +2827,47 @@ int listpackTest(int argc, char *argv[], int flags) { ...@@ -2391,6 +2827,47 @@ int listpackTest(int argc, char *argv[], int flags) {
lpFree(lp); 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") { TEST("push various encodings") {
lp = lpNew(0); lp = lpNew(0);
...@@ -2449,6 +2926,21 @@ int listpackTest(int argc, char *argv[], int flags) { ...@@ -2449,6 +2926,21 @@ int listpackTest(int argc, char *argv[], int flags) {
lpFree(lp); 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") { TEST("Test lpValidateIntegrity") {
lp = createList(); lp = createList();
long count = 0; long count = 0;
...@@ -2471,6 +2963,26 @@ int listpackTest(int argc, char *argv[], int flags) { ...@@ -2471,6 +2963,26 @@ int listpackTest(int argc, char *argv[], int flags) {
lpFree(lp); 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") { TEST("Stress with random payloads of different encoding") {
unsigned long long start = usec(); unsigned long long start = usec();
int i,j,len,where; int i,j,len,where;
......
...@@ -49,18 +49,25 @@ unsigned char *lpReplaceInteger(unsigned char *lp, unsigned char **p, long long ...@@ -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 *lpDelete(unsigned char *lp, unsigned char *p, unsigned char **newp);
unsigned char *lpDeleteRangeWithEntry(unsigned char *lp, unsigned char **p, unsigned long num); 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 *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 *lpBatchDelete(unsigned char *lp, unsigned char **ps, unsigned long count);
unsigned char *lpMerge(unsigned char **first, unsigned char **second); unsigned char *lpMerge(unsigned char **first, unsigned char **second);
unsigned char *lpDup(unsigned char *lp); unsigned char *lpDup(unsigned char *lp);
unsigned long lpLength(unsigned char *lp); unsigned long lpLength(unsigned char *lp);
unsigned char *lpGet(unsigned char *p, int64_t *count, unsigned char *intbuf); unsigned char *lpGet(unsigned char *p, int64_t *count, unsigned char *intbuf);
unsigned char *lpGetValue(unsigned char *p, unsigned int *slen, long long *lval); 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); 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 *lpFirst(unsigned char *lp);
unsigned char *lpLast(unsigned char *lp); unsigned char *lpLast(unsigned char *lp);
unsigned char *lpNext(unsigned char *lp, unsigned char *p); unsigned char *lpNext(unsigned char *lp, unsigned char *p);
unsigned char *lpPrev(unsigned char *lp, unsigned char *p); unsigned char *lpPrev(unsigned char *lp, unsigned char *p);
size_t lpBytes(unsigned char *lp); size_t lpBytes(unsigned char *lp);
size_t lpEntrySizeInteger(long long lval);
size_t lpEstimateBytesRepeatedInteger(long long lval, unsigned long rep); size_t lpEstimateBytesRepeatedInteger(long long lval, unsigned long rep);
unsigned char *lpSeek(unsigned char *lp, long index); unsigned char *lpSeek(unsigned char *lp, long index);
typedef int (*listpackValidateEntryCB)(unsigned char *p, unsigned int head_count, void *userdata); 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, ...@@ -69,12 +76,15 @@ int lpValidateIntegrity(unsigned char *lp, size_t size, int deep,
unsigned char *lpValidateFirst(unsigned char *lp); unsigned char *lpValidateFirst(unsigned char *lp);
int lpValidateNext(unsigned char *lp, unsigned char **pp, size_t lpbytes); int lpValidateNext(unsigned char *lp, unsigned char **pp, size_t lpbytes);
unsigned int lpCompare(unsigned char *p, unsigned char *s, uint32_t slen); 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 lpRandomPair(unsigned char *lp, unsigned long total_count,
void lpRandomPairs(unsigned char *lp, unsigned int count, listpackEntry *keys, listpackEntry *vals); listpackEntry *key, listpackEntry *val, int tuple_len);
unsigned int lpRandomPairsUnique(unsigned char *lp, unsigned int count, listpackEntry *keys, listpackEntry *vals); 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); void lpRandomEntries(unsigned char *lp, unsigned int count, listpackEntry *entries);
unsigned char *lpNextRandom(unsigned char *lp, unsigned char *p, unsigned int *index, 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); int lpSafeToAdd(unsigned char* lp, size_t add);
void lpRepr(unsigned char *lp); void lpRepr(unsigned char *lp);
......
...@@ -745,7 +745,7 @@ int moduleDelKeyIfEmpty(RedisModuleKey *key) { ...@@ -745,7 +745,7 @@ int moduleDelKeyIfEmpty(RedisModuleKey *key) {
case OBJ_LIST: isempty = listTypeLength(o) == 0; break; case OBJ_LIST: isempty = listTypeLength(o) == 0; break;
case OBJ_SET: isempty = setTypeSize(o) == 0; break; case OBJ_SET: isempty = setTypeSize(o) == 0; break;
case OBJ_ZSET: isempty = zsetLength(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; case OBJ_STREAM: isempty = streamLength(o) == 0; break;
default: isempty = 0; default: isempty = 0;
} }
...@@ -4168,7 +4168,7 @@ size_t RM_ValueLength(RedisModuleKey *key) { ...@@ -4168,7 +4168,7 @@ size_t RM_ValueLength(RedisModuleKey *key) {
case OBJ_LIST: return listTypeLength(key->value); case OBJ_LIST: return listTypeLength(key->value);
case OBJ_SET: return setTypeSize(key->value); case OBJ_SET: return setTypeSize(key->value);
case OBJ_ZSET: return zsetLength(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); case OBJ_STREAM: return streamLength(key->value);
default: return 0; default: return 0;
} }
...@@ -5271,7 +5271,10 @@ int RM_HashSet(RedisModuleKey *key, int flags, ...) { ...@@ -5271,7 +5271,10 @@ int RM_HashSet(RedisModuleKey *key, int flags, ...) {
   
/* Handle XX and NX */ /* Handle XX and NX */
if (flags & (REDISMODULE_HASH_XX|REDISMODULE_HASH_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) || if (((flags & REDISMODULE_HASH_XX) && !exists) ||
((flags & REDISMODULE_HASH_NX) && exists)) ((flags & REDISMODULE_HASH_NX) && exists))
{ {
...@@ -5282,7 +5285,7 @@ int RM_HashSet(RedisModuleKey *key, int flags, ...) { ...@@ -5282,7 +5285,7 @@ int RM_HashSet(RedisModuleKey *key, int flags, ...) {
   
/* Handle deletion if value is REDISMODULE_HASH_DELETE. */ /* Handle deletion if value is REDISMODULE_HASH_DELETE. */
if (value == 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); if (flags & REDISMODULE_HASH_CFIELDS) decrRefCount(field);
continue; continue;
} }
...@@ -5295,8 +5298,8 @@ int RM_HashSet(RedisModuleKey *key, int flags, ...) { ...@@ -5295,8 +5298,8 @@ int RM_HashSet(RedisModuleKey *key, int flags, ...) {
low_flags |= HASH_SET_TAKE_FIELD; low_flags |= HASH_SET_TAKE_FIELD;
   
robj *argv[2] = {field,value}; robj *argv[2] = {field,value};
hashTypeTryConversion(key->value,argv,0,1); hashTypeTryConversion(key->db,key->value,argv,0,1);
int updated = hashTypeSet(key->value, field->ptr, value->ptr, low_flags); int updated = hashTypeSet(key->db, key->value, field->ptr, value->ptr, low_flags);
count += (flags & REDISMODULE_HASH_COUNT_ALL) ? 1 : updated; count += (flags & REDISMODULE_HASH_COUNT_ALL) ? 1 : updated;
   
/* If CFIELDS is active, SDS string ownership is now of hashTypeSet(), /* If CFIELDS is active, SDS string ownership is now of hashTypeSet(),
...@@ -5374,14 +5377,22 @@ int RM_HashGet(RedisModuleKey *key, int flags, ...) { ...@@ -5374,14 +5377,22 @@ int RM_HashGet(RedisModuleKey *key, int flags, ...) {
/* Query the hash for existence or value object. */ /* Query the hash for existence or value object. */
if (flags & REDISMODULE_HASH_EXISTS) { if (flags & REDISMODULE_HASH_EXISTS) {
existsptr = va_arg(ap,int*); existsptr = va_arg(ap,int*);
if (key->value) if (key->value) {
*existsptr = hashTypeExists(key->value,field->ptr); int isHashDeleted;
else *existsptr = hashTypeExists(key->db, key->value, field->ptr, &isHashDeleted);
/* hash-field-expiration is not exposed to modules */
serverAssert(isHashDeleted == 0);
} else {
*existsptr = 0; *existsptr = 0;
}
} else { } else {
int isHashDeleted;
valueptr = va_arg(ap,RedisModuleString**); valueptr = va_arg(ap,RedisModuleString**);
if (key->value) { 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) { if (*valueptr) {
robj *decoded = getDecodedObject(*valueptr); robj *decoded = getDecodedObject(*valueptr);
decrRefCount(*valueptr); decrRefCount(*valueptr);
...@@ -11071,18 +11082,22 @@ static void moduleScanKeyCallback(void *privdata, const dictEntry *de) { ...@@ -11071,18 +11082,22 @@ static void moduleScanKeyCallback(void *privdata, const dictEntry *de) {
ScanKeyCBData *data = privdata; ScanKeyCBData *data = privdata;
sds key = dictGetKey(de); sds key = dictGetKey(de);
robj *o = data->key->value; robj *o = data->key->value;
robj *field = createStringObject(key, sdslen(key)); robj *field = NULL;
robj *value = NULL; robj *value = NULL;
if (o->type == OBJ_SET) { if (o->type == OBJ_SET) {
value = NULL; value = NULL;
} else if (o->type == OBJ_HASH) { } else if (o->type == OBJ_HASH) {
sds val = dictGetVal(de); sds val = dictGetVal(de);
field = createStringObject(key, hfieldlen(key));
value = createStringObject(val, sdslen(val)); value = createStringObject(val, sdslen(val));
} else if (o->type == OBJ_ZSET) { } else if (o->type == OBJ_ZSET) {
double *val = (double*)dictGetVal(de); double *val = (double*)dictGetVal(de);
value = createStringObjectFromLongDouble(*val, 0); 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); data->fn(data->key, field, value, data->user_data);
decrRefCount(field); decrRefCount(field);
if (value) decrRefCount(value); if (value) decrRefCount(value);
......
/*
* 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
/*
* 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
...@@ -31,6 +31,14 @@ size_t sdsZmallocSize(sds s) { ...@@ -31,6 +31,14 @@ size_t sdsZmallocSize(sds s) {
return zmalloc_size(sh); 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 /* Return the amount of memory used by the sds string at object->ptr
* for a string object. This includes internal fragmentation. */ * for a string object. This includes internal fragmentation. */
size_t getStringObjectSdsUsedMemory(robj *o) { size_t getStringObjectSdsUsedMemory(robj *o) {
...@@ -3749,7 +3757,9 @@ void replaceClientCommandVector(client *c, int argc, robj **argv) { ...@@ -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. * 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 * 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 * 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) { void rewriteClientCommandArgument(client *c, int i, robj *newval) {
robj *oldval; robj *oldval;
retainOriginalCommandVector(c); retainOriginalCommandVector(c);
...@@ -3767,9 +3777,18 @@ void rewriteClientCommandArgument(client *c, int i, robj *newval) { ...@@ -3767,9 +3777,18 @@ void rewriteClientCommandArgument(client *c, int i, robj *newval) {
} }
oldval = c->argv[i]; oldval = c->argv[i];
if (oldval) c->argv_len_sum -= getStringObjectLen(oldval); if (oldval) c->argv_len_sum -= getStringObjectLen(oldval);
if (newval) c->argv_len_sum += getStringObjectLen(newval);
if (newval) {
c->argv[i] = newval; c->argv[i] = newval;
incrRefCount(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 (oldval) decrRefCount(oldval);
/* If this is the command name make sure to fix c->cmd. */ /* If this is the command name make sure to fix c->cmd. */
......
...@@ -80,7 +80,7 @@ sds keyspaceEventsFlagsToString(int flags) { ...@@ -80,7 +80,7 @@ sds keyspaceEventsFlagsToString(int flags) {
* 'event' is a C string representing the event name. * 'event' is a C string representing the event name.
* 'key' is a Redis object representing the key name. * 'key' is a Redis object representing the key name.
* 'dbid' is the database ID where the key lives. */ * '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; sds chan;
robj *chanobj, *eventobj; robj *chanobj, *eventobj;
int len = -1; int len = -1;
......
...@@ -333,17 +333,7 @@ void freeZsetObject(robj *o) { ...@@ -333,17 +333,7 @@ void freeZsetObject(robj *o) {
} }
void freeHashObject(robj *o) { void freeHashObject(robj *o) {
switch (o->encoding) { hashTypeFree(o);
case OBJ_ENCODING_HT:
dictRelease((dict*) o->ptr);
break;
case OBJ_ENCODING_LISTPACK:
lpFree(o->ptr);
break;
default:
serverPanic("Unknown hash encoding type");
break;
}
} }
void freeModuleObject(robj *o) { void freeModuleObject(robj *o) {
...@@ -502,6 +492,9 @@ void dismissHashObject(robj *o, size_t size_hint) { ...@@ -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*)); dismissMemory(d->ht_table[1], DICTHT_SIZE(d->ht_size_exp[1])*sizeof(dictEntry*));
} else if (o->encoding == OBJ_ENCODING_LISTPACK) { } else if (o->encoding == OBJ_ENCODING_LISTPACK) {
dismissMemory(o->ptr, lpBytes((unsigned char*)o->ptr)); 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 { } else {
serverPanic("Unknown hash encoding type"); serverPanic("Unknown hash encoding type");
} }
...@@ -939,6 +932,7 @@ char *strEncoding(int encoding) { ...@@ -939,6 +932,7 @@ char *strEncoding(int encoding) {
case OBJ_ENCODING_HT: return "hashtable"; case OBJ_ENCODING_HT: return "hashtable";
case OBJ_ENCODING_QUICKLIST: return "quicklist"; case OBJ_ENCODING_QUICKLIST: return "quicklist";
case OBJ_ENCODING_LISTPACK: return "listpack"; case OBJ_ENCODING_LISTPACK: return "listpack";
case OBJ_ENCODING_LISTPACK_EX: return "listpackex";
case OBJ_ENCODING_INTSET: return "intset"; case OBJ_ENCODING_INTSET: return "intset";
case OBJ_ENCODING_SKIPLIST: return "skiplist"; case OBJ_ENCODING_SKIPLIST: return "skiplist";
case OBJ_ENCODING_EMBSTR: return "embstr"; case OBJ_ENCODING_EMBSTR: return "embstr";
...@@ -979,7 +973,6 @@ size_t streamRadixTreeMemoryUsage(rax *rax) { ...@@ -979,7 +973,6 @@ size_t streamRadixTreeMemoryUsage(rax *rax) {
* are checked and averaged to estimate the total size. */ * are checked and averaged to estimate the total size. */
#define OBJ_COMPUTE_SIZE_DEF_SAMPLES 5 /* Default sample size. */ #define OBJ_COMPUTE_SIZE_DEF_SAMPLES 5 /* Default sample size. */
size_t objectComputeSize(robj *key, robj *o, size_t sample_size, int dbid) { size_t objectComputeSize(robj *key, robj *o, size_t sample_size, int dbid) {
sds ele, ele2;
dict *d; dict *d;
dictIterator *di; dictIterator *di;
struct dictEntry *de; struct dictEntry *de;
...@@ -1016,7 +1009,7 @@ size_t objectComputeSize(robj *key, robj *o, size_t sample_size, int dbid) { ...@@ -1016,7 +1009,7 @@ size_t objectComputeSize(robj *key, robj *o, size_t sample_size, int dbid) {
di = dictGetIterator(d); di = dictGetIterator(d);
asize = sizeof(*o)+sizeof(dict)+(sizeof(struct dictEntry*)*dictBuckets(d)); asize = sizeof(*o)+sizeof(dict)+(sizeof(struct dictEntry*)*dictBuckets(d));
while((de = dictNext(di)) != NULL && samples < sample_size) { while((de = dictNext(di)) != NULL && samples < sample_size) {
ele = dictGetKey(de); sds ele = dictGetKey(de);
elesize += dictEntryMemUsage() + sdsZmallocSize(ele); elesize += dictEntryMemUsage() + sdsZmallocSize(ele);
samples++; samples++;
} }
...@@ -1052,14 +1045,17 @@ size_t objectComputeSize(robj *key, robj *o, size_t sample_size, int dbid) { ...@@ -1052,14 +1045,17 @@ size_t objectComputeSize(robj *key, robj *o, size_t sample_size, int dbid) {
} else if (o->type == OBJ_HASH) { } else if (o->type == OBJ_HASH) {
if (o->encoding == OBJ_ENCODING_LISTPACK) { if (o->encoding == OBJ_ENCODING_LISTPACK) {
asize = sizeof(*o)+zmalloc_size(o->ptr); 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) { } else if (o->encoding == OBJ_ENCODING_HT) {
d = o->ptr; d = o->ptr;
di = dictGetIterator(d); di = dictGetIterator(d);
asize = sizeof(*o)+sizeof(dict)+(sizeof(struct dictEntry*)*dictBuckets(d)); asize = sizeof(*o)+sizeof(dict)+(sizeof(struct dictEntry*)*dictBuckets(d));
while((de = dictNext(di)) != NULL && samples < sample_size) { while((de = dictNext(di)) != NULL && samples < sample_size) {
ele = dictGetKey(de); hfield ele = dictGetKey(de);
ele2 = dictGetVal(de); sds ele2 = dictGetVal(de);
elesize += sdsZmallocSize(ele) + sdsZmallocSize(ele2); elesize += hfieldZmallocSize(ele) + sdsZmallocSize(ele2);
elesize += dictEntryMemUsage(); elesize += dictEntryMemUsage();
samples++; samples++;
} }
......
...@@ -173,11 +173,16 @@ raxNode *raxNewNode(size_t children, int datafield) { ...@@ -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 /* Allocate a new rax and return its pointer. On out of memory the function
* returns NULL. */ * returns NULL. */
rax *raxNew(void) { 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; if (rax == NULL) return NULL;
rax->numele = 0; rax->numele = 0;
rax->numnodes = 1; rax->numnodes = 1;
rax->head = raxNewNode(0,0); rax->head = raxNewNode(0, 0);
if (rax->head == NULL) { if (rax->head == NULL) {
rax_free(rax); rax_free(rax);
return NULL; return NULL;
...@@ -1210,6 +1215,25 @@ void raxRecursiveFree(rax *rax, raxNode *n, void (*free_callback)(void*)) { ...@@ -1210,6 +1215,25 @@ void raxRecursiveFree(rax *rax, raxNode *n, void (*free_callback)(void*)) {
rax->numnodes--; 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 a whole radix tree, calling the specified callback in order to
* free the auxiliary data. */ * free the auxiliary data. */
void raxFreeWithCallback(rax *rax, void (*free_callback)(void*)) { void raxFreeWithCallback(rax *rax, void (*free_callback)(void*)) {
...@@ -1218,6 +1242,15 @@ void raxFreeWithCallback(rax *rax, void (*free_callback)(void*)) { ...@@ -1218,6 +1242,15 @@ void raxFreeWithCallback(rax *rax, void (*free_callback)(void*)) {
rax_free(rax); 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. */ /* Free a whole radix tree. */
void raxFree(rax *rax) { void raxFree(rax *rax) {
raxFreeWithCallback(rax,NULL); raxFreeWithCallback(rax,NULL);
......
...@@ -113,6 +113,7 @@ typedef struct rax { ...@@ -113,6 +113,7 @@ typedef struct rax {
raxNode *head; raxNode *head;
uint64_t numele; uint64_t numele;
uint64_t numnodes; uint64_t numnodes;
void *metadata[];
} rax; } rax;
/* Stack data structure used by raxLowWalk() in order to, optionally, return /* Stack data structure used by raxLowWalk() in order to, optionally, return
...@@ -166,12 +167,16 @@ typedef struct raxIterator { ...@@ -166,12 +167,16 @@ typedef struct raxIterator {
/* Exported API. */ /* Exported API. */
rax *raxNew(void); rax *raxNew(void);
rax *raxNewWithMetadata(int metaSize);
int raxInsert(rax *rax, unsigned char *s, size_t len, void *data, void **old); 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 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 raxRemove(rax *rax, unsigned char *s, size_t len, void **old);
int raxFind(rax *rax, unsigned char *s, size_t len, void **value); int raxFind(rax *rax, unsigned char *s, size_t len, void **value);
void raxFree(rax *rax); void raxFree(rax *rax);
void raxFreeWithCallback(rax *rax, void (*free_callback)(void*)); 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); void raxStart(raxIterator *it, rax *rt);
int raxSeek(raxIterator *it, const char *op, unsigned char *ele, size_t len); int raxSeek(raxIterator *it, const char *op, unsigned char *ele, size_t len);
int raxNext(raxIterator *it); int raxNext(raxIterator *it);
......
...@@ -268,8 +268,9 @@ int rdbEncodeInteger(long long value, unsigned char *enc) { ...@@ -268,8 +268,9 @@ int rdbEncodeInteger(long long value, unsigned char *enc) {
* The returned value changes according to the flags, see * The returned value changes according to the flags, see
* rdbGenericLoadStringObject() for more info. */ * rdbGenericLoadStringObject() for more info. */
void *rdbLoadIntegerObject(rio *rdb, int enctype, int flags, size_t *lenptr) { void *rdbLoadIntegerObject(rio *rdb, int enctype, int flags, size_t *lenptr) {
int plain = flags & RDB_LOAD_PLAIN; int plainFlag = flags & RDB_LOAD_PLAIN;
int sds = flags & RDB_LOAD_SDS; int sdsFlag = flags & RDB_LOAD_SDS;
int hfldFlag = flags & (RDB_LOAD_HFLD|RDB_LOAD_HFLD_TTL);
int encode = flags & RDB_LOAD_ENC; int encode = flags & RDB_LOAD_ENC;
unsigned char enc[4]; unsigned char enc[4];
long long val; long long val;
...@@ -295,11 +296,17 @@ void *rdbLoadIntegerObject(rio *rdb, int enctype, int flags, size_t *lenptr) { ...@@ -295,11 +296,17 @@ void *rdbLoadIntegerObject(rio *rdb, int enctype, int flags, size_t *lenptr) {
rdbReportCorruptRDB("Unknown RDB integer encoding type %d",enctype); rdbReportCorruptRDB("Unknown RDB integer encoding type %d",enctype);
return NULL; /* Never reached. */ return NULL; /* Never reached. */
} }
if (plain || sds) { if (plainFlag || sdsFlag || hfldFlag) {
char buf[LONG_STR_SIZE], *p; char buf[LONG_STR_SIZE], *p;
int len = ll2string(buf,sizeof(buf),val); int len = ll2string(buf,sizeof(buf),val);
if (lenptr) *lenptr = len; 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); memcpy(p,buf,len);
return p; return p;
} else if (encode) { } else if (encode) {
...@@ -368,8 +375,11 @@ ssize_t rdbSaveLzfStringObject(rio *rdb, unsigned char *s, size_t len) { ...@@ -368,8 +375,11 @@ ssize_t rdbSaveLzfStringObject(rio *rdb, unsigned char *s, size_t len) {
* changes according to 'flags'. For more info check the * changes according to 'flags'. For more info check the
* rdbGenericLoadStringObject() function. */ * rdbGenericLoadStringObject() function. */
void *rdbLoadLzfStringObject(rio *rdb, int flags, size_t *lenptr) { void *rdbLoadLzfStringObject(rio *rdb, int flags, size_t *lenptr) {
int plain = flags & RDB_LOAD_PLAIN; int plainFlag = flags & RDB_LOAD_PLAIN;
int sds = flags & RDB_LOAD_SDS; 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; uint64_t len, clen;
unsigned char *c = NULL; unsigned char *c = NULL;
char *val = NULL; char *val = NULL;
...@@ -382,11 +392,14 @@ void *rdbLoadLzfStringObject(rio *rdb, int flags, size_t *lenptr) { ...@@ -382,11 +392,14 @@ void *rdbLoadLzfStringObject(rio *rdb, int flags, size_t *lenptr) {
} }
/* Allocate our target according to the uncompressed size. */ /* Allocate our target according to the uncompressed size. */
if (plain) { if (plainFlag) {
val = ztrymalloc(len); val = ztrymalloc(len);
} else { } else if (sdsFlag || robjFlag) {
val = sdstrynewlen(SDS_NOINIT,len); val = sdstrynewlen(SDS_NOINIT,len);
} else { /* hfldFlag */
val = hfieldTryNew(NULL, len, (flags&RDB_LOAD_HFLD) ? 0 : 1);
} }
if (!val) { if (!val) {
serverLog(isRestoreContext()? LL_VERBOSE: LL_WARNING, "rdbLoadLzfStringObject failed allocating %llu bytes", (unsigned long long)len); serverLog(isRestoreContext()? LL_VERBOSE: LL_WARNING, "rdbLoadLzfStringObject failed allocating %llu bytes", (unsigned long long)len);
goto err; goto err;
...@@ -402,17 +415,17 @@ void *rdbLoadLzfStringObject(rio *rdb, int flags, size_t *lenptr) { ...@@ -402,17 +415,17 @@ void *rdbLoadLzfStringObject(rio *rdb, int flags, size_t *lenptr) {
} }
zfree(c); zfree(c);
if (plain || sds) { return (robjFlag) ? createObject(OBJ_STRING,val) : (void *) val;
return val;
} else {
return createObject(OBJ_STRING,val);
}
err: err:
zfree(c); zfree(c);
if (plain) if (plainFlag) {
zfree(val); zfree(val);
else } else if (sdsFlag || robjFlag) {
sdsfree(val); sdsfree(val);
} else { /* hfldFlag*/
hfieldFree(val);
}
return NULL; return NULL;
} }
...@@ -491,12 +504,18 @@ ssize_t rdbSaveStringObject(rio *rdb, robj *obj) { ...@@ -491,12 +504,18 @@ ssize_t rdbSaveStringObject(rio *rdb, robj *obj) {
* RDB_LOAD_PLAIN: Return a plain string allocated with zmalloc() * RDB_LOAD_PLAIN: Return a plain string allocated with zmalloc()
* instead of a Redis object with an sds in it. * instead of a Redis object with an sds in it.
* RDB_LOAD_SDS: Return an SDS string instead of a Redis object. * 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. * On I/O error NULL is returned.
*/ */
void *rdbGenericLoadStringObject(rio *rdb, int flags, size_t *lenptr) { void *rdbGenericLoadStringObject(rio *rdb, int flags, size_t *lenptr) {
int plain = flags & RDB_LOAD_PLAIN; void *buf;
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 */
int isencoded; int isencoded;
unsigned long long len; unsigned long long len;
...@@ -517,22 +536,8 @@ void *rdbGenericLoadStringObject(rio *rdb, int flags, size_t *lenptr) { ...@@ -517,22 +536,8 @@ void *rdbGenericLoadStringObject(rio *rdb, int flags, size_t *lenptr) {
} }
} }
if (plain || sds) { /* return robj */
void *buf = plain ? ztrymalloc(len) : sdstrynewlen(SDS_NOINIT,len); if (robjFlag) {
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 {
robj *o = tryCreateStringObject(SDS_NOINIT,len); robj *o = tryCreateStringObject(SDS_NOINIT,len);
if (!o) { if (!o) {
serverLog(isRestoreContext()? LL_VERBOSE: LL_WARNING, "rdbGenericLoadStringObject failed allocating %llu bytes", len); 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) { ...@@ -544,6 +549,32 @@ void *rdbGenericLoadStringObject(rio *rdb, int flags, size_t *lenptr) {
} }
return o; 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) { robj *rdbLoadStringObject(rio *rdb) {
...@@ -665,9 +696,14 @@ int rdbSaveObjectType(rio *rdb, robj *o) { ...@@ -665,9 +696,14 @@ int rdbSaveObjectType(rio *rdb, robj *o) {
case OBJ_HASH: case OBJ_HASH:
if (o->encoding == OBJ_ENCODING_LISTPACK) if (o->encoding == OBJ_ENCODING_LISTPACK)
return rdbSaveType(rdb,RDB_TYPE_HASH_LISTPACK); return rdbSaveType(rdb,RDB_TYPE_HASH_LISTPACK);
else if (o->encoding == OBJ_ENCODING_HT) 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); return rdbSaveType(rdb,RDB_TYPE_HASH);
else else
return rdbSaveType(rdb,RDB_TYPE_HASH_METADATA);
} else
serverPanic("Unknown hash encoding"); serverPanic("Unknown hash encoding");
case OBJ_STREAM: case OBJ_STREAM:
return rdbSaveType(rdb,RDB_TYPE_STREAM_LISTPACKS_3); return rdbSaveType(rdb,RDB_TYPE_STREAM_LISTPACKS_3);
...@@ -908,32 +944,58 @@ ssize_t rdbSaveObject(rio *rdb, robj *o, robj *key, int dbid) { ...@@ -908,32 +944,58 @@ ssize_t rdbSaveObject(rio *rdb, robj *o, robj *key, int dbid) {
} }
} else if (o->type == OBJ_HASH) { } else if (o->type == OBJ_HASH) {
/* Save a hash value */ /* Save a hash value */
if (o->encoding == OBJ_ENCODING_LISTPACK) { if ((o->encoding == OBJ_ENCODING_LISTPACK) ||
size_t l = lpBytes((unsigned char*)o->ptr); (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; nwritten += n;
} else if (o->encoding == OBJ_ENCODING_HT) { } else if (o->encoding == OBJ_ENCODING_HT) {
dictIterator *di = dictGetIterator(o->ptr); dictIterator *di = dictGetIterator(o->ptr);
dictEntry *de; 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) { if ((n = rdbSaveLen(rdb,dictSize((dict*)o->ptr))) == -1) {
dictReleaseIterator(di); dictReleaseIterator(di);
return -1; return -1;
} }
nwritten += n; nwritten += n;
/* save all hash fields */
while((de = dictNext(di)) != NULL) { while((de = dictNext(di)) != NULL) {
sds field = dictGetKey(de); hfield field = dictGetKey(de);
sds value = dictGetVal(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, if ((n = rdbSaveRawString(rdb,(unsigned char*)field,
sdslen(field))) == -1) hfieldlen(field))) == -1)
{ {
dictReleaseIterator(di); dictReleaseIterator(di);
return -1; return -1;
} }
nwritten += n; nwritten += n;
/* save the value */
if ((n = rdbSaveRawString(rdb,(unsigned char*)value, if ((n = rdbSaveRawString(rdb,(unsigned char*)value,
sdslen(value))) == -1) sdslen(value))) == -1)
{ {
...@@ -1753,19 +1815,20 @@ static int _listZiplistEntryConvertAndValidate(unsigned char *p, unsigned int he ...@@ -1753,19 +1815,20 @@ static int _listZiplistEntryConvertAndValidate(unsigned char *p, unsigned int he
/* callback for to check the listpack doesn't have duplicate records */ /* callback for to check the listpack doesn't have duplicate records */
static int _lpEntryValidation(unsigned char *p, unsigned int head_count, void *userdata) { static int _lpEntryValidation(unsigned char *p, unsigned int head_count, void *userdata) {
struct { struct {
int pairs; int tuple_len;
long count; long count;
dict *fields; dict *fields;
long long last_expireat;
} *data = userdata; } *data = userdata;
if (data->fields == NULL) { if (data->fields == NULL) {
data->fields = dictCreate(&hashDictType); 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 /* 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 */ * 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; unsigned char *str;
int64_t slen; int64_t slen;
unsigned char buf[LP_INTBUF_SIZE]; unsigned char buf[LP_INTBUF_SIZE];
...@@ -1779,6 +1842,19 @@ static int _lpEntryValidation(unsigned char *p, unsigned int head_count, void *u ...@@ -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)++; (data->count)++;
return 1; return 1;
} }
...@@ -1786,23 +1862,25 @@ static int _lpEntryValidation(unsigned char *p, unsigned int head_count, void *u ...@@ -1786,23 +1862,25 @@ static int _lpEntryValidation(unsigned char *p, unsigned int head_count, void *u
/* Validate the integrity of the listpack structure. /* Validate the integrity of the listpack structure.
* when `deep` is 0, only the integrity of the header is validated. * 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 `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) * tuple_len indicates what is a logical entry tuple size.
* when `pairs` is 1, odd elements need to be unique (it's a key-value map) */ * Whether tuple is of size 1 (set), 2 (feild-value) or 3 (field-value[-ttl]),
int lpValidateIntegrityAndDups(unsigned char *lp, size_t size, int deep, int pairs) { * first element in the tuple must be unique */
int lpValidateIntegrityAndDups(unsigned char *lp, size_t size, int deep, int tuple_len) {
if (!deep) if (!deep)
return lpValidateIntegrity(lp, size, 0, NULL, NULL); return lpValidateIntegrity(lp, size, 0, NULL, NULL);
/* Keep track of the field names to locate duplicate ones */ /* Keep track of the field names to locate duplicate ones */
struct { struct {
int pairs; int tuple_len;
long count; long count;
dict *fields; /* Initialisation at the first callback. */ 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); int ret = lpValidateIntegrity(lp, size, 1, _lpEntryValidation, &data);
/* make sure we have an even number of records. */ /* the number of records should be a multiple of the tuple length */
if (pairs && data.count & 1) if (data.count % tuple_len != 0)
ret = 0; ret = 0;
if (data.fields) dictRelease(data.fields); if (data.fields) dictRelease(data.fields);
...@@ -1811,9 +1889,18 @@ int lpValidateIntegrityAndDups(unsigned char *lp, size_t size, int deep, int pai ...@@ -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. /* Load a Redis object of the specified type from the specified file.
* On success a newly allocated object is returned, otherwise NULL. * 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 */ * error - When the function returns NULL and if 'error' is not NULL, the
robj *rdbLoadObject(int rdbtype, rio *rdb, sds key, int dbid, int *error) { * 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; robj *o = NULL, *ele, *dec;
uint64_t len; uint64_t len;
unsigned int i; unsigned int i;
...@@ -1856,7 +1943,7 @@ robj *rdbLoadObject(int rdbtype, rio *rdb, sds key, int dbid, int *error) { ...@@ -1856,7 +1943,7 @@ robj *rdbLoadObject(int rdbtype, rio *rdb, sds key, int dbid, int *error) {
decrRefCount(ele); decrRefCount(ele);
} }
listTypeTryConversion(o,LIST_CONV_AUTO,NULL,NULL); listTypeTryConversion(o, LIST_CONV_AUTO, NULL, NULL);
} else if (rdbtype == RDB_TYPE_SET) { } else if (rdbtype == RDB_TYPE_SET) {
/* Read Set value */ /* Read Set value */
if ((len = rdbLoadLen(rdb,NULL)) == RDB_LENERR) return NULL; 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) { ...@@ -1869,7 +1956,7 @@ robj *rdbLoadObject(int rdbtype, rio *rdb, sds key, int dbid, int *error) {
o = createSetObject(); o = createSetObject();
/* It's faster to expand the dict to the right size asap in order /* It's faster to expand the dict to the right size asap in order
* to avoid rehashing */ * 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); rdbReportCorruptRDB("OOM in dictTryExpand %llu", (unsigned long long)len);
decrRefCount(o); decrRefCount(o);
return NULL; return NULL;
...@@ -1896,7 +1983,7 @@ robj *rdbLoadObject(int rdbtype, rio *rdb, sds key, int dbid, int *error) { ...@@ -1896,7 +1983,7 @@ robj *rdbLoadObject(int rdbtype, rio *rdb, sds key, int dbid, int *error) {
/* Fetch integer value from element. */ /* Fetch integer value from element. */
if (isSdsRepresentableAsLongLong(sdsele,&llval) == C_OK) { if (isSdsRepresentableAsLongLong(sdsele,&llval) == C_OK) {
uint8_t success; uint8_t success;
o->ptr = intsetAdd(o->ptr,llval,&success); o->ptr = intsetAdd(o->ptr, llval, &success);
if (!success) { if (!success) {
rdbReportCorruptRDB("Duplicate set members detected"); rdbReportCorruptRDB("Duplicate set members detected");
decrRefCount(o); decrRefCount(o);
...@@ -1946,7 +2033,7 @@ robj *rdbLoadObject(int rdbtype, rio *rdb, sds key, int dbid, int *error) { ...@@ -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 /* This will also be called when the set was just converted
* to a regular hash table encoded set. */ * to a regular hash table encoded set. */
if (o->encoding == OBJ_ENCODING_HT) { 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"); rdbReportCorruptRDB("Duplicate set members detected");
decrRefCount(o); decrRefCount(o);
sdsfree(sdsele); sdsfree(sdsele);
...@@ -2024,12 +2111,13 @@ robj *rdbLoadObject(int rdbtype, rio *rdb, sds key, int dbid, int *error) { ...@@ -2024,12 +2111,13 @@ robj *rdbLoadObject(int rdbtype, rio *rdb, sds key, int dbid, int *error) {
maxelelen <= server.zset_max_listpack_value && maxelelen <= server.zset_max_listpack_value &&
lpSafeToAdd(NULL, totelelen)) lpSafeToAdd(NULL, totelelen))
{ {
zsetConvert(o,OBJ_ENCODING_LISTPACK); zsetConvert(o, OBJ_ENCODING_LISTPACK);
} }
} else if (rdbtype == RDB_TYPE_HASH) { } else if (rdbtype == RDB_TYPE_HASH) {
uint64_t len; uint64_t len;
int ret; int ret;
sds field, value; sds value;
hfield field;
dict *dupSearchDict = NULL; dict *dupSearchDict = NULL;
len = rdbLoadLen(rdb, NULL); len = rdbLoadLen(rdb, NULL);
...@@ -2040,7 +2128,7 @@ robj *rdbLoadObject(int rdbtype, rio *rdb, sds key, int dbid, int *error) { ...@@ -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. */ /* Too many entries? Use a hash table right from the start. */
if (len > server.hash_max_listpack_entries) if (len > server.hash_max_listpack_entries)
hashTypeConvert(o, OBJ_ENCODING_HT); hashTypeConvert(o, OBJ_ENCODING_HT, NULL);
else if (deep_integrity_validation) { else if (deep_integrity_validation) {
/* In this mode, we need to guarantee that the server won't crash /* In this mode, we need to guarantee that the server won't crash
* later when the ziplist is converted to a dict. * 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) { ...@@ -2049,48 +2137,50 @@ robj *rdbLoadObject(int rdbtype, rio *rdb, sds key, int dbid, int *error) {
dupSearchDict = dictCreate(&hashDictType); dupSearchDict = dictCreate(&hashDictType);
} }
/* Load every field and value into the listpack */
/* Load every field and value into the ziplist */
while (o->encoding == OBJ_ENCODING_LISTPACK && len > 0) { while (o->encoding == OBJ_ENCODING_LISTPACK && len > 0) {
len--; len--;
/* Load raw strings */ /* Load raw strings */
if ((field = rdbGenericLoadStringObject(rdb,RDB_LOAD_SDS,NULL)) == NULL) { if ((field = rdbGenericLoadStringObject(rdb,RDB_LOAD_HFLD,NULL)) == NULL) {
decrRefCount(o); decrRefCount(o);
if (dupSearchDict) dictRelease(dupSearchDict); if (dupSearchDict) dictRelease(dupSearchDict);
return NULL; return NULL;
} }
if ((value = rdbGenericLoadStringObject(rdb,RDB_LOAD_SDS,NULL)) == NULL) { if ((value = rdbGenericLoadStringObject(rdb,RDB_LOAD_SDS,NULL)) == NULL) {
sdsfree(field); hfieldFree(field);
decrRefCount(o); decrRefCount(o);
if (dupSearchDict) dictRelease(dupSearchDict); if (dupSearchDict) dictRelease(dupSearchDict);
return NULL; return NULL;
} }
if (dupSearchDict) { if (dupSearchDict) {
sds field_dup = sdsdup(field); sds field_dup = sdsnewlen(field, hfieldlen(field));
if (dictAdd(dupSearchDict, field_dup, NULL) != DICT_OK) { if (dictAdd(dupSearchDict, field_dup, NULL) != DICT_OK) {
rdbReportCorruptRDB("Hash with dup elements"); rdbReportCorruptRDB("Hash with dup elements");
dictRelease(dupSearchDict); dictRelease(dupSearchDict);
decrRefCount(o); decrRefCount(o);
sdsfree(field_dup); sdsfree(field_dup);
sdsfree(field); hfieldFree(field);
sdsfree(value); sdsfree(value);
return NULL; return NULL;
} }
} }
/* Convert to hash table if size threshold is exceeded */ /* 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 || 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); ret = dictAdd((dict*)o->ptr, field, value);
dictUseStoredKeyApi((dict *)o->ptr, 0);
if (ret == DICT_ERR) { if (ret == DICT_ERR) {
rdbReportCorruptRDB("Duplicate hash fields detected"); rdbReportCorruptRDB("Duplicate hash fields detected");
if (dupSearchDict) dictRelease(dupSearchDict); if (dupSearchDict) dictRelease(dupSearchDict);
sdsfree(value); sdsfree(value);
sdsfree(field); hfieldFree(field);
decrRefCount(o); decrRefCount(o);
return NULL; return NULL;
} }
...@@ -2098,10 +2188,10 @@ robj *rdbLoadObject(int rdbtype, rio *rdb, sds key, int dbid, int *error) { ...@@ -2098,10 +2188,10 @@ robj *rdbLoadObject(int rdbtype, rio *rdb, sds key, int dbid, int *error) {
} }
/* Add pair to listpack */ /* 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)); o->ptr = lpAppend(o->ptr, (unsigned char*)value, sdslen(value));
sdsfree(field); hfieldFree(field);
sdsfree(value); sdsfree(value);
} }
...@@ -2113,7 +2203,7 @@ robj *rdbLoadObject(int rdbtype, rio *rdb, sds key, int dbid, int *error) { ...@@ -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 (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); rdbReportCorruptRDB("OOM in dictTryExpand %llu", (unsigned long long)len);
decrRefCount(o); decrRefCount(o);
return NULL; return NULL;
...@@ -2124,22 +2214,25 @@ robj *rdbLoadObject(int rdbtype, rio *rdb, sds key, int dbid, int *error) { ...@@ -2124,22 +2214,25 @@ robj *rdbLoadObject(int rdbtype, rio *rdb, sds key, int dbid, int *error) {
while (o->encoding == OBJ_ENCODING_HT && len > 0) { while (o->encoding == OBJ_ENCODING_HT && len > 0) {
len--; len--;
/* Load encoded strings */ /* Load encoded strings */
if ((field = rdbGenericLoadStringObject(rdb,RDB_LOAD_SDS,NULL)) == NULL) { if ((field = rdbGenericLoadStringObject(rdb,RDB_LOAD_HFLD,NULL)) == NULL) {
decrRefCount(o); decrRefCount(o);
return NULL; return NULL;
} }
if ((value = rdbGenericLoadStringObject(rdb,RDB_LOAD_SDS,NULL)) == NULL) { if ((value = rdbGenericLoadStringObject(rdb,RDB_LOAD_SDS,NULL)) == NULL) {
sdsfree(field); hfieldFree(field);
decrRefCount(o); decrRefCount(o);
return NULL; return NULL;
} }
/* Add pair to hash table */ /* 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) { if (ret == DICT_ERR) {
rdbReportCorruptRDB("Duplicate hash fields detected"); rdbReportCorruptRDB("Duplicate hash fields detected");
sdsfree(value); sdsfree(value);
sdsfree(field); hfieldFree(field);
decrRefCount(o); decrRefCount(o);
return NULL; return NULL;
} }
...@@ -2147,6 +2240,149 @@ robj *rdbLoadObject(int rdbtype, rio *rdb, sds key, int dbid, int *error) { ...@@ -2147,6 +2240,149 @@ robj *rdbLoadObject(int rdbtype, rio *rdb, sds key, int dbid, int *error) {
/* All pairs should be read by now */ /* All pairs should be read by now */
serverAssert(len == 0); 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) { } else if (rdbtype == RDB_TYPE_LIST_QUICKLIST || rdbtype == RDB_TYPE_LIST_QUICKLIST_2) {
if ((len = rdbLoadLen(rdb,NULL)) == RDB_LENERR) return NULL; if ((len = rdbLoadLen(rdb,NULL)) == RDB_LENERR) return NULL;
if (len == 0) goto emptykey; if (len == 0) goto emptykey;
...@@ -2221,7 +2457,7 @@ robj *rdbLoadObject(int rdbtype, rio *rdb, sds key, int dbid, int *error) { ...@@ -2221,7 +2457,7 @@ robj *rdbLoadObject(int rdbtype, rio *rdb, sds key, int dbid, int *error) {
goto emptykey; goto emptykey;
} }
listTypeTryConversion(o,LIST_CONV_AUTO,NULL,NULL); listTypeTryConversion(o, LIST_CONV_AUTO, NULL, NULL);
} else if (rdbtype == RDB_TYPE_HASH_ZIPMAP || } else if (rdbtype == RDB_TYPE_HASH_ZIPMAP ||
rdbtype == RDB_TYPE_LIST_ZIPLIST || rdbtype == RDB_TYPE_LIST_ZIPLIST ||
rdbtype == RDB_TYPE_SET_INTSET || rdbtype == RDB_TYPE_SET_INTSET ||
...@@ -2229,14 +2465,15 @@ robj *rdbLoadObject(int rdbtype, rio *rdb, sds key, int dbid, int *error) { ...@@ -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_ZIPLIST ||
rdbtype == RDB_TYPE_ZSET_LISTPACK || rdbtype == RDB_TYPE_ZSET_LISTPACK ||
rdbtype == RDB_TYPE_HASH_ZIPLIST || 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; size_t encoded_len;
unsigned char *encoded = unsigned char *encoded =
rdbGenericLoadStringObject(rdb,RDB_LOAD_PLAIN,&encoded_len); rdbGenericLoadStringObject(rdb,RDB_LOAD_PLAIN,&encoded_len);
if (encoded == NULL) return NULL; 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 /* Fix the object encoding, and make sure to convert the encoded
* data type into the base type if accordingly to the current * data type into the base type if accordingly to the current
...@@ -2292,10 +2529,10 @@ robj *rdbLoadObject(int rdbtype, rio *rdb, sds key, int dbid, int *error) { ...@@ -2292,10 +2529,10 @@ robj *rdbLoadObject(int rdbtype, rio *rdb, sds key, int dbid, int *error) {
o->type = OBJ_HASH; o->type = OBJ_HASH;
o->encoding = OBJ_ENCODING_LISTPACK; 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) maxlen > server.hash_max_listpack_value)
{ {
hashTypeConvert(o, OBJ_ENCODING_HT); hashTypeConvert(o, OBJ_ENCODING_HT, NULL);
} }
} }
break; break;
...@@ -2341,11 +2578,11 @@ robj *rdbLoadObject(int rdbtype, rio *rdb, sds key, int dbid, int *error) { ...@@ -2341,11 +2578,11 @@ robj *rdbLoadObject(int rdbtype, rio *rdb, sds key, int dbid, int *error) {
o->type = OBJ_SET; o->type = OBJ_SET;
o->encoding = OBJ_ENCODING_INTSET; o->encoding = OBJ_ENCODING_INTSET;
if (intsetLen(o->ptr) > server.set_max_intset_entries) if (intsetLen(o->ptr) > server.set_max_intset_entries)
setTypeConvert(o,OBJ_ENCODING_HT); setTypeConvert(o, OBJ_ENCODING_HT);
break; break;
case RDB_TYPE_SET_LISTPACK: case RDB_TYPE_SET_LISTPACK:
if (deep_integrity_validation) server.stat_dump_payload_sanitizations++; 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."); rdbReportCorruptRDB("Set listpack integrity check failed.");
zfree(encoded); zfree(encoded);
o->ptr = NULL; o->ptr = NULL;
...@@ -2386,14 +2623,14 @@ robj *rdbLoadObject(int rdbtype, rio *rdb, sds key, int dbid, int *error) { ...@@ -2386,14 +2623,14 @@ robj *rdbLoadObject(int rdbtype, rio *rdb, sds key, int dbid, int *error) {
} }
if (zsetLength(o) > server.zset_max_listpack_entries) if (zsetLength(o) > server.zset_max_listpack_entries)
zsetConvert(o,OBJ_ENCODING_SKIPLIST); zsetConvert(o, OBJ_ENCODING_SKIPLIST);
else else
o->ptr = lpShrinkToFit(o->ptr); o->ptr = lpShrinkToFit(o->ptr);
break; break;
} }
case RDB_TYPE_ZSET_LISTPACK: case RDB_TYPE_ZSET_LISTPACK:
if (deep_integrity_validation) server.stat_dump_payload_sanitizations++; 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."); rdbReportCorruptRDB("Zset listpack integrity check failed.");
zfree(encoded); zfree(encoded);
o->ptr = NULL; o->ptr = NULL;
...@@ -2408,7 +2645,7 @@ robj *rdbLoadObject(int rdbtype, rio *rdb, sds key, int dbid, int *error) { ...@@ -2408,7 +2645,7 @@ robj *rdbLoadObject(int rdbtype, rio *rdb, sds key, int dbid, int *error) {
} }
if (zsetLength(o) > server.zset_max_listpack_entries) if (zsetLength(o) > server.zset_max_listpack_entries)
zsetConvert(o,OBJ_ENCODING_SKIPLIST); zsetConvert(o, OBJ_ENCODING_SKIPLIST);
break; break;
case RDB_TYPE_HASH_ZIPLIST: case RDB_TYPE_HASH_ZIPLIST:
{ {
...@@ -2426,35 +2663,57 @@ robj *rdbLoadObject(int rdbtype, rio *rdb, sds key, int dbid, int *error) { ...@@ -2426,35 +2663,57 @@ robj *rdbLoadObject(int rdbtype, rio *rdb, sds key, int dbid, int *error) {
o->ptr = lp; o->ptr = lp;
o->type = OBJ_HASH; o->type = OBJ_HASH;
o->encoding = OBJ_ENCODING_LISTPACK; o->encoding = OBJ_ENCODING_LISTPACK;
if (hashTypeLength(o) == 0) { if (hashTypeLength(o, 0) == 0) {
decrRefCount(o); decrRefCount(o);
goto emptykey; goto emptykey;
} }
if (hashTypeLength(o) > server.hash_max_listpack_entries) if (hashTypeLength(o, 0) > server.hash_max_listpack_entries)
hashTypeConvert(o, OBJ_ENCODING_HT); hashTypeConvert(o, OBJ_ENCODING_HT, NULL);
else else
o->ptr = lpShrinkToFit(o->ptr); o->ptr = lpShrinkToFit(o->ptr);
break; break;
} }
case RDB_TYPE_HASH_LISTPACK: 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 (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."); rdbReportCorruptRDB("Hash listpack integrity check failed.");
zfree(encoded);
o->ptr = NULL;
decrRefCount(o); decrRefCount(o);
return NULL; return NULL;
} }
o->type = OBJ_HASH;
o->encoding = OBJ_ENCODING_LISTPACK; /* if listpack is empty, delete it */
if (hashTypeLength(o) == 0) { if (hashTypeLength(o, 0) == 0) {
decrRefCount(o); decrRefCount(o);
goto emptykey; goto emptykey;
} }
if (hashTypeLength(o) > server.hash_max_listpack_entries) /* for TTL listpack, find the minimum expiry */
hashTypeConvert(o, OBJ_ENCODING_HT); 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; break;
default: default:
/* totally unreachable */ /* totally unreachable */
...@@ -2794,7 +3053,13 @@ robj *rdbLoadObject(int rdbtype, rio *rdb, sds key, int dbid, int *error) { ...@@ -2794,7 +3053,13 @@ robj *rdbLoadObject(int rdbtype, rio *rdb, sds key, int dbid, int *error) {
RedisModuleIO io; RedisModuleIO io;
robj keyobj; robj keyobj;
initStaticStringObject(keyobj,key); 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 /* Call the rdb_load method of the module providing the 10 bit
* encoding version in the lower 10 bits of the module ID. */ * encoding version in the lower 10 bits of the module ID. */
void *ptr = mt->rdb_load(&io,moduleid&1023); void *ptr = mt->rdb_load(&io,moduleid&1023);
...@@ -2807,7 +3072,7 @@ robj *rdbLoadObject(int rdbtype, rio *rdb, sds key, int dbid, int *error) { ...@@ -2807,7 +3072,7 @@ robj *rdbLoadObject(int rdbtype, rio *rdb, sds key, int dbid, int *error) {
uint64_t eof = rdbLoadLen(rdb,NULL); uint64_t eof = rdbLoadLen(rdb,NULL);
if (eof == RDB_LENERR) { if (eof == RDB_LENERR) {
if (ptr) { 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); decrRefCount(o);
} }
return NULL; return NULL;
...@@ -2816,7 +3081,7 @@ robj *rdbLoadObject(int rdbtype, rio *rdb, sds key, int dbid, int *error) { ...@@ -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 " rdbReportCorruptRDB("The RDB file contains module data for the module '%s' that is not terminated by "
"the proper module value EOF marker", moduleTypeModuleName(mt)); "the proper module value EOF marker", moduleTypeModuleName(mt));
if (ptr) { 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); decrRefCount(o);
} }
return NULL; return NULL;
...@@ -2828,17 +3093,23 @@ robj *rdbLoadObject(int rdbtype, rio *rdb, sds key, int dbid, int *error) { ...@@ -2828,17 +3093,23 @@ robj *rdbLoadObject(int rdbtype, rio *rdb, sds key, int dbid, int *error) {
moduleTypeModuleName(mt)); moduleTypeModuleName(mt));
return NULL; return NULL;
} }
o = createModuleObject(mt,ptr); o = createModuleObject(mt, ptr);
} else { } else {
rdbReportReadError("Unknown RDB encoding type %d",rdbtype); rdbReportReadError("Unknown RDB encoding type %d",rdbtype);
return NULL; return NULL;
} }
if (minExpiredField) *minExpiredField = minExpField;
if (error) *error = 0; if (error) *error = 0;
return o; return o;
emptykey: emptykey:
if (error) *error = RDB_LOAD_ERR_EMPTY_KEY; if (error) *error = RDB_LOAD_ERR_EMPTY_KEY;
return NULL; 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 /* 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) { ...@@ -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 * 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). */ * will be loaded (in the future it might contains more such objects). */
int rdbLoadRioWithLoadingCtx(rio *rdb, int rdbflags, rdbSaveInfo *rsi, rdbLoadingCtx *rdb_loading_ctx) { int rdbLoadRioWithLoadingCtx(rio *rdb, int rdbflags, rdbSaveInfo *rsi, rdbLoadingCtx *rdb_loading_ctx) {
uint64_t minExpiredField = EB_EXPIRE_TIME_INVALID;
uint64_t dbid = 0; uint64_t dbid = 0;
int type, rdbver; int type, rdbver;
uint64_t db_size = 0, expires_size = 0; uint64_t db_size = 0, expires_size = 0;
...@@ -3249,7 +3521,7 @@ int rdbLoadRioWithLoadingCtx(rio *rdb, int rdbflags, rdbSaveInfo *rsi, rdbLoadin ...@@ -3249,7 +3521,7 @@ int rdbLoadRioWithLoadingCtx(rio *rdb, int rdbflags, rdbSaveInfo *rsi, rdbLoadin
if ((key = rdbGenericLoadStringObject(rdb,RDB_LOAD_SDS,NULL)) == NULL) if ((key = rdbGenericLoadStringObject(rdb,RDB_LOAD_SDS,NULL)) == NULL)
goto eoferr; goto eoferr;
/* Read value */ /* 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 /* 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 * an RDB file from disk, either at startup, or when an RDB was
...@@ -3268,6 +3540,9 @@ int rdbLoadRioWithLoadingCtx(rio *rdb, int rdbflags, rdbSaveInfo *rsi, rdbLoadin ...@@ -3268,6 +3540,9 @@ int rdbLoadRioWithLoadingCtx(rio *rdb, int rdbflags, rdbSaveInfo *rsi, rdbLoadin
if(empty_keys_skipped++ < 10) if(empty_keys_skipped++ < 10)
serverLog(LL_NOTICE, "rdbLoadObject skipping empty key: %s", key); serverLog(LL_NOTICE, "rdbLoadObject skipping empty key: %s", key);
sdsfree(key); sdsfree(key);
} else if (error == RDB_LOAD_ERR_EXPIRED_HASH) {
/* Valid flow. Continue. */
sdsfree(key);
} else { } else {
sdsfree(key); sdsfree(key);
goto eoferr; goto eoferr;
...@@ -3312,6 +3587,11 @@ int rdbLoadRioWithLoadingCtx(rio *rdb, int rdbflags, rdbSaveInfo *rsi, rdbLoadin ...@@ -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 */ /* Set the expire time if needed */
if (expiretime != -1) { if (expiretime != -1) {
setExpire(NULL,db,&keyobj,expiretime); setExpire(NULL,db,&keyobj,expiretime);
......
...@@ -73,10 +73,12 @@ ...@@ -73,10 +73,12 @@
#define RDB_TYPE_STREAM_LISTPACKS_2 19 #define RDB_TYPE_STREAM_LISTPACKS_2 19
#define RDB_TYPE_SET_LISTPACK 20 #define RDB_TYPE_SET_LISTPACK 20
#define RDB_TYPE_STREAM_LISTPACKS_3 21 #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[] */ /* NOTE: WHEN ADDING NEW RDB TYPE, UPDATE rdbIsObjectType(), and rdb_type_string[] */
/* Test if a type is an object type. */ /* 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). */ /* 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). */ #define RDB_OPCODE_SLOT_INFO 244 /* Individual slot info, such as slot id and size (cluster mode only). */
...@@ -105,6 +107,8 @@ ...@@ -105,6 +107,8 @@
#define RDB_LOAD_ENC (1<<0) #define RDB_LOAD_ENC (1<<0)
#define RDB_LOAD_PLAIN (1<<1) #define RDB_LOAD_PLAIN (1<<1)
#define RDB_LOAD_SDS (1<<2) #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 */ /* flags on the purpose of rdb save or load */
#define RDBFLAGS_NONE 0 /* No special RDB loading or saving. */ #define RDBFLAGS_NONE 0 /* No special RDB loading or saving. */
...@@ -117,7 +121,8 @@ ...@@ -117,7 +121,8 @@
/* When rdbLoadObject() returns NULL, the err flag is /* When rdbLoadObject() returns NULL, the err flag is
* set to hold the type of error that occurred */ * set to hold the type of error that occurred */
#define RDB_LOAD_ERR_EMPTY_KEY 1 /* Error of empty key */ #define RDB_LOAD_ERR_EMPTY_KEY 1 /* Error of empty key */
#define RDB_LOAD_ERR_OTHER 2 /* Any other errors */ #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); ssize_t rdbWriteRaw(rio *rdb, void *p, size_t len);
int rdbSaveType(rio *rdb, unsigned char type); int rdbSaveType(rio *rdb, unsigned char type);
...@@ -138,7 +143,7 @@ int rdbSaveToFile(const char *filename); ...@@ -138,7 +143,7 @@ int rdbSaveToFile(const char *filename);
int rdbSave(int req, char *filename, rdbSaveInfo *rsi, int rdbflags); int rdbSave(int req, char *filename, rdbSaveInfo *rsi, int rdbflags);
ssize_t rdbSaveObject(rio *rdb, robj *o, robj *key, int dbid); ssize_t rdbSaveObject(rio *rdb, robj *o, robj *key, int dbid);
size_t rdbSavedObjectLen(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); void backgroundSaveDoneHandler(int exitcode, int bysignal);
int rdbSaveKeyValuePair(rio *rdb, robj *key, robj *val, long long expiretime,int dbid); int rdbSaveKeyValuePair(rio *rdb, robj *key, robj *val, long long expiretime,int dbid);
ssize_t rdbSaveSingleModuleAux(rio *rdb, int when, moduleType *mt); ssize_t rdbSaveSingleModuleAux(rio *rdb, int when, moduleType *mt);
......
...@@ -80,6 +80,8 @@ char *rdb_type_string[] = { ...@@ -80,6 +80,8 @@ char *rdb_type_string[] = {
"stream-v2", "stream-v2",
"set-listpack", "set-listpack",
"stream-v3", "stream-v3",
"hash-hashtable-md",
"hash-listpack-md",
}; };
/* Show a few stats collected into 'rdbstate' */ /* Show a few stats collected into 'rdbstate' */
...@@ -173,7 +175,6 @@ void rdbCheckSetupSignals(void) { ...@@ -173,7 +175,6 @@ void rdbCheckSetupSignals(void) {
* otherwise the already open file 'fp' is checked. */ * otherwise the already open file 'fp' is checked. */
int redis_check_rdb(char *rdbfilename, FILE *fp) { int redis_check_rdb(char *rdbfilename, FILE *fp) {
uint64_t dbid; uint64_t dbid;
int selected_dbid = -1;
int type, rdbver; int type, rdbver;
char buf[1024]; char buf[1024];
long long expiretime, now = mstime(); long long expiretime, now = mstime();
...@@ -245,7 +246,6 @@ int redis_check_rdb(char *rdbfilename, FILE *fp) { ...@@ -245,7 +246,6 @@ int redis_check_rdb(char *rdbfilename, FILE *fp) {
if ((dbid = rdbLoadLen(&rdb,NULL)) == RDB_LENERR) if ((dbid = rdbLoadLen(&rdb,NULL)) == RDB_LENERR)
goto eoferr; goto eoferr;
rdbCheckInfo("Selecting DB ID %llu", (unsigned long long)dbid); rdbCheckInfo("Selecting DB ID %llu", (unsigned long long)dbid);
selected_dbid = dbid;
continue; /* Read type again. */ continue; /* Read type again. */
} else if (type == RDB_OPCODE_RESIZEDB) { } else if (type == RDB_OPCODE_RESIZEDB) {
/* RESIZEDB: Hint about the size of the keys in the currently /* RESIZEDB: Hint about the size of the keys in the currently
...@@ -331,7 +331,8 @@ int redis_check_rdb(char *rdbfilename, FILE *fp) { ...@@ -331,7 +331,8 @@ int redis_check_rdb(char *rdbfilename, FILE *fp) {
rdbstate.keys++; rdbstate.keys++;
/* Read value */ /* Read value */
rdbstate.doing = RDB_CHECK_DOING_READ_OBJECT_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. */ /* Check if the key already expired. */
if (expiretime != -1 && expiretime < now) if (expiretime != -1 && expiretime < now)
rdbstate.already_expired++; rdbstate.already_expired++;
......
...@@ -19,6 +19,8 @@ ...@@ -19,6 +19,8 @@
#include "syscheck.h" #include "syscheck.h"
#include "threads_mngr.h" #include "threads_mngr.h"
#include "fmtargs.h" #include "fmtargs.h"
#include "mstr.h"
#include "ebuckets.h"
#include <time.h> #include <time.h>
#include <signal.h> #include <signal.h>
...@@ -281,6 +283,18 @@ int dictSdsKeyCompare(dict *d, const void *key1, ...@@ -281,6 +283,18 @@ int dictSdsKeyCompare(dict *d, const void *key1,
return memcmp(key1, key2, l1) == 0; 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 /* A case insensitive version used for the command lookup table and other
* places where case insensitive non binary-safe comparison is needed. */ * places where case insensitive non binary-safe comparison is needed. */
int dictSdsKeyCaseCompare(dict *d, const void *key1, int dictSdsKeyCaseCompare(dict *d, const void *key1,
...@@ -1945,6 +1959,8 @@ void createSharedObjects(void) { ...@@ -1945,6 +1959,8 @@ void createSharedObjects(void) {
shared.persist = createStringObject("PERSIST",7); shared.persist = createStringObject("PERSIST",7);
shared.set = createStringObject("SET",3); shared.set = createStringObject("SET",3);
shared.eval = createStringObject("EVAL",4); shared.eval = createStringObject("EVAL",4);
shared.hpexpireat = createStringObject("HPEXPIREAT",10);
shared.hdel = createStringObject("HDEL",4);
/* Shared command argument */ /* Shared command argument */
shared.left = createStringObject("left",4); shared.left = createStringObject("left",4);
...@@ -2504,6 +2520,7 @@ void resetServerStats(void) { ...@@ -2504,6 +2520,7 @@ void resetServerStats(void) {
server.stat_numcommands = 0; server.stat_numcommands = 0;
server.stat_numconnections = 0; server.stat_numconnections = 0;
server.stat_expiredkeys = 0; server.stat_expiredkeys = 0;
server.stat_expired_hash_fields = 0;
server.stat_expired_stale_perc = 0; server.stat_expired_stale_perc = 0;
server.stat_expired_time_cap_reached_count = 0; server.stat_expired_time_cap_reached_count = 0;
server.stat_expire_cycle_time_used = 0; server.stat_expire_cycle_time_used = 0;
...@@ -2652,6 +2669,7 @@ void initServer(void) { ...@@ -2652,6 +2669,7 @@ void initServer(void) {
for (j = 0; j < server.dbnum; j++) { for (j = 0; j < server.dbnum; j++) {
server.db[j].keys = kvstoreCreate(&dbDictType, slot_count_bits, flags); server.db[j].keys = kvstoreCreate(&dbDictType, slot_count_bits, flags);
server.db[j].expires = kvstoreCreate(&dbExpiresDictType, 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].expires_cursor = 0;
server.db[j].blocking_keys = dictCreate(&keylistDictType); server.db[j].blocking_keys = dictCreate(&keylistDictType);
server.db[j].blocking_keys_unblock_on_nokey = dictCreate(&objectKeyPointerValueDictType); server.db[j].blocking_keys_unblock_on_nokey = dictCreate(&objectKeyPointerValueDictType);
...@@ -5854,6 +5872,7 @@ sds genRedisInfoString(dict *section_dict, int all_sections, int everything) { ...@@ -5854,6 +5872,7 @@ sds genRedisInfoString(dict *section_dict, int all_sections, int everything) {
"sync_full:%lld\r\n", server.stat_sync_full, "sync_full:%lld\r\n", server.stat_sync_full,
"sync_partial_ok:%lld\r\n", server.stat_sync_partial_ok, "sync_partial_ok:%lld\r\n", server.stat_sync_partial_ok,
"sync_partial_err:%lld\r\n", server.stat_sync_partial_err, "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_keys:%lld\r\n", server.stat_expiredkeys,
"expired_stale_perc:%.2f\r\n", server.stat_expired_stale_perc*100, "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, "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) { ...@@ -6092,14 +6111,16 @@ sds genRedisInfoString(dict *section_dict, int all_sections, int everything) {
if (sections++) info = sdscat(info,"\r\n"); if (sections++) info = sdscat(info,"\r\n");
info = sdscatprintf(info, "# Keyspace\r\n"); info = sdscatprintf(info, "# Keyspace\r\n");
for (j = 0; j < server.dbnum; j++) { for (j = 0; j < server.dbnum; j++) {
long long keys, vkeys; long long keys, vkeys, hexpires;
keys = kvstoreSize(server.db[j].keys); keys = kvstoreSize(server.db[j].keys);
vkeys = kvstoreSize(server.db[j].expires); vkeys = kvstoreSize(server.db[j].expires);
hexpires = ebGetTotalItems(server.db[j].hexpires, &hashExpireBucketsType);
if (keys || vkeys) { if (keys || vkeys) {
info = sdscatprintf(info, info = sdscatprintf(info,
"db%d:keys=%lld,expires=%lld,avg_ttl=%lld\r\n", "db%d:keys=%lld,expires=%lld,avg_ttl=%lld,hashes_with_expiry_fields=%lld\r\n",
j, keys, vkeys, server.db[j].avg_ttl); j, keys, vkeys, server.db[j].avg_ttl, hexpires);
} }
} }
} }
...@@ -6871,9 +6892,11 @@ struct redisTest { ...@@ -6871,9 +6892,11 @@ struct redisTest {
{"crc64", crc64Test}, {"crc64", crc64Test},
{"zmalloc", zmalloc_test}, {"zmalloc", zmalloc_test},
{"sds", sdsTest}, {"sds", sdsTest},
{"mstr", mstrTest},
{"dict", dictTest}, {"dict", dictTest},
{"listpack", listpackTest}, {"listpack", listpackTest},
{"kvstore", kvstoreTest}, {"kvstore", kvstoreTest},
{"ebuckets", ebucketsTest},
}; };
redisTestProc *getTestProcByName(const char *name) { redisTestProc *getTestProcByName(const char *name) {
int numtests = sizeof(redisTests)/sizeof(struct redisTest); int numtests = sizeof(redisTests)/sizeof(struct redisTest);
...@@ -6900,6 +6923,7 @@ int main(int argc, char **argv) { ...@@ -6900,6 +6923,7 @@ int main(int argc, char **argv) {
if (!strcasecmp(arg, "--accurate")) flags |= REDIS_TEST_ACCURATE; if (!strcasecmp(arg, "--accurate")) flags |= REDIS_TEST_ACCURATE;
else if (!strcasecmp(arg, "--large-memory")) flags |= REDIS_TEST_LARGE_MEMORY; else if (!strcasecmp(arg, "--large-memory")) flags |= REDIS_TEST_LARGE_MEMORY;
else if (!strcasecmp(arg, "--valgrind")) flags |= REDIS_TEST_VALGRIND; else if (!strcasecmp(arg, "--valgrind")) flags |= REDIS_TEST_VALGRIND;
else if (!strcasecmp(arg, "--verbose")) flags |= REDIS_TEST_VERBOSE;
} }
if (!strcasecmp(argv[2], "all")) { if (!strcasecmp(argv[2], "all")) {
......
...@@ -45,6 +45,8 @@ typedef long long ustime_t; /* microsecond time type. */ ...@@ -45,6 +45,8 @@ typedef long long ustime_t; /* microsecond time type. */
#include "ae.h" /* Event driven programming library */ #include "ae.h" /* Event driven programming library */
#include "sds.h" /* Dynamic safe strings */ #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 "dict.h" /* Hash tables */
#include "kvstore.h" /* Slot-based hash table */ #include "kvstore.h" /* Slot-based hash table */
#include "adlist.h" /* Linked lists */ #include "adlist.h" /* Linked lists */
...@@ -884,6 +886,7 @@ struct RedisModuleDigest { ...@@ -884,6 +886,7 @@ struct RedisModuleDigest {
#define OBJ_ENCODING_QUICKLIST 9 /* Encoded as linked list of listpacks */ #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_STREAM 10 /* Encoded as a radix tree of listpacks */
#define OBJ_ENCODING_LISTPACK 11 /* Encoded as a listpack */ #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_BITS 24
#define LRU_CLOCK_MAX ((1<<LRU_BITS)-1) /* Max value of obj->lru */ #define LRU_CLOCK_MAX ((1<<LRU_BITS)-1) /* Max value of obj->lru */
...@@ -960,6 +963,7 @@ typedef struct replBufBlock { ...@@ -960,6 +963,7 @@ typedef struct replBufBlock {
typedef struct redisDb { typedef struct redisDb {
kvstore *keys; /* The keyspace for this DB */ kvstore *keys; /* The keyspace for this DB */
kvstore *expires; /* Timeout of keys with a timeout set */ 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; /* Keys with clients waiting for data (BLPOP)*/
dict *blocking_keys_unblock_on_nokey; /* Keys with clients waiting for dict *blocking_keys_unblock_on_nokey; /* Keys with clients waiting for
* data, and should be unblocked if key is deleted (XREADEDGROUP). * data, and should be unblocked if key is deleted (XREADEDGROUP).
...@@ -1314,6 +1318,7 @@ struct sharedObjectsStruct { ...@@ -1314,6 +1318,7 @@ struct sharedObjectsStruct {
*rpop, *lpop, *lpush, *rpoplpush, *lmove, *blmove, *zpopmin, *zpopmax, *rpop, *lpop, *lpush, *rpoplpush, *lmove, *blmove, *zpopmin, *zpopmax,
*emptyscan, *multi, *exec, *left, *right, *hset, *srem, *xgroup, *xclaim, *emptyscan, *multi, *exec, *left, *right, *hset, *srem, *xgroup, *xclaim,
*script, *replconf, *eval, *persist, *set, *pexpireat, *pexpire, *script, *replconf, *eval, *persist, *set, *pexpireat, *pexpire,
*hdel, *hpexpireat,
*time, *pxat, *absttl, *retrycount, *force, *justid, *entriesread, *time, *pxat, *absttl, *retrycount, *force, *justid, *entriesread,
*lastid, *ping, *setid, *keepttl, *load, *createconsumer, *lastid, *ping, *setid, *keepttl, *load, *createconsumer,
*getack, *special_asterick, *special_equals, *default_username, *redacted, *getack, *special_asterick, *special_equals, *default_username, *redacted,
...@@ -1646,6 +1651,7 @@ struct redisServer { ...@@ -1646,6 +1651,7 @@ struct redisServer {
long long stat_numcommands; /* Number of processed commands */ long long stat_numcommands; /* Number of processed commands */
long long stat_numconnections; /* Number of connections received */ long long stat_numconnections; /* Number of connections received */
long long stat_expiredkeys; /* Number of expired keys */ 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 */ 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_expired_time_cap_reached_count; /* Early expire cycle stops.*/
long long stat_expire_cycle_time_used; /* Cumulative microseconds used. */ long long stat_expire_cycle_time_used; /* Cumulative microseconds used. */
...@@ -2433,7 +2439,8 @@ typedef struct { ...@@ -2433,7 +2439,8 @@ typedef struct {
robj *subject; robj *subject;
int encoding; int encoding;
unsigned char *fptr, *vptr; unsigned char *fptr, *vptr, *tptr;
uint64_t expire_time; /* Only used with OBJ_ENCODING_LISTPACK_EX */
dictIterator *di; dictIterator *di;
dictEntry *de; dictEntry *de;
...@@ -2449,6 +2456,10 @@ typedef struct { ...@@ -2449,6 +2456,10 @@ typedef struct {
#define IO_THREADS_OP_WRITE 2 #define IO_THREADS_OP_WRITE 2
extern int io_threads_op; extern int io_threads_op;
/* Hash-field data type (of t_hash.c) */
typedef mstr hfield;
extern mstrKind mstrFieldKind;
/*----------------------------------------------------------------------------- /*-----------------------------------------------------------------------------
* Extern declarations * Extern declarations
*----------------------------------------------------------------------------*/ *----------------------------------------------------------------------------*/
...@@ -2463,6 +2474,8 @@ extern dictType zsetDictType; ...@@ -2463,6 +2474,8 @@ extern dictType zsetDictType;
extern dictType dbDictType; extern dictType dbDictType;
extern double R_Zero, R_PosInf, R_NegInf, R_Nan; extern double R_Zero, R_PosInf, R_NegInf, R_Nan;
extern dictType hashDictType; extern dictType hashDictType;
extern dictType mstrHashDictType;
extern dictType mstrHashDictTypeWithHFE;
extern dictType stringSetDictType; extern dictType stringSetDictType;
extern dictType externalStringType; extern dictType externalStringType;
extern dictType sdsHashDictType; extern dictType sdsHashDictType;
...@@ -2474,6 +2487,9 @@ extern dictType sdsReplyDictType; ...@@ -2474,6 +2487,9 @@ extern dictType sdsReplyDictType;
extern dictType keylistDictType; extern dictType keylistDictType;
extern dict *modules; extern dict *modules;
extern EbucketsType hashExpireBucketsType; /* global expires */
extern EbucketsType hashFieldExpireBucketsType; /* local per hash */
/*----------------------------------------------------------------------------- /*-----------------------------------------------------------------------------
* Functions prototypes * Functions prototypes
*----------------------------------------------------------------------------*/ *----------------------------------------------------------------------------*/
...@@ -2616,6 +2632,7 @@ void copyReplicaOutputBuffer(client *dst, client *src); ...@@ -2616,6 +2632,7 @@ void copyReplicaOutputBuffer(client *dst, client *src);
void addListRangeReply(client *c, robj *o, long start, long end, int reverse); void addListRangeReply(client *c, robj *o, long start, long end, int reverse);
void deferredAfterErrorReply(client *c, list *errors); void deferredAfterErrorReply(client *c, list *errors);
size_t sdsZmallocSize(sds s); size_t sdsZmallocSize(sds s);
size_t hfieldZmallocSize(hfield s);
size_t getStringObjectSdsUsedMemory(robj *o); size_t getStringObjectSdsUsedMemory(robj *o);
void freeClientReplyValue(void *o); void freeClientReplyValue(void *o);
void *dupClientReplyValue(void *o); void *dupClientReplyValue(void *o);
...@@ -3140,30 +3157,87 @@ void setTypeConvert(robj *subject, int enc); ...@@ -3140,30 +3157,87 @@ void setTypeConvert(robj *subject, int enc);
int setTypeConvertAndExpand(robj *setobj, int enc, unsigned long cap, int panic); int setTypeConvertAndExpand(robj *setobj, int enc, unsigned long cap, int panic);
robj *setTypeDup(robj *o); 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 */ /* Hash data type */
#define HASH_SET_TAKE_FIELD (1<<0) #define HASH_SET_TAKE_FIELD (1<<0)
#define HASH_SET_TAKE_VALUE (1<<1) #define HASH_SET_TAKE_VALUE (1<<1)
#define HASH_SET_COPY 0 #define HASH_SET_COPY 0
void hashTypeConvert(robj *o, int enc); void hashTypeConvert(robj *o, int enc, ebuckets *hexpires);
void hashTypeTryConversion(robj *subject, robj **argv, int start, int end); void hashTypeTryConversion(redisDb *db, robj *subject, robj **argv, int start, int end);
int hashTypeExists(robj *o, sds key); int hashTypeExists(redisDb *db, robj *o, sds key, int *isHashDeleted);
int hashTypeDelete(robj *o, sds key); int hashTypeDelete(robj *o, void *key, int isSdsField);
unsigned long hashTypeLength(const robj *o); unsigned long hashTypeLength(const robj *o, int subtractExpiredFields);
hashTypeIterator *hashTypeInitIterator(robj *subject); hashTypeIterator *hashTypeInitIterator(robj *subject);
void hashTypeReleaseIterator(hashTypeIterator *hi); void hashTypeReleaseIterator(hashTypeIterator *hi);
int hashTypeNext(hashTypeIterator *hi); int hashTypeNext(hashTypeIterator *hi, int skipExpiredFields);
void hashTypeCurrentFromListpack(hashTypeIterator *hi, int what, void hashTypeCurrentFromListpack(hashTypeIterator *hi, int what,
unsigned char **vstr, unsigned char **vstr,
unsigned int *vlen, unsigned int *vlen,
long long *vll); long long *vll,
sds hashTypeCurrentFromHashTable(hashTypeIterator *hi, int what); uint64_t *expireTime);
void hashTypeCurrentObject(hashTypeIterator *hi, int what, unsigned char **vstr, unsigned int *vlen, long long *vll); 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); sds hashTypeCurrentObjectNewSds(hashTypeIterator *hi, int what);
robj *hashTypeLookupWriteOrCreate(client *c, robj *key); hfield hashTypeCurrentObjectNewHfield(hashTypeIterator *hi);
robj *hashTypeGetValueObject(robj *o, sds field); robj *hashTypeGetValueObject(redisDb *db, robj *o, sds field, int *isHashDeleted);
int hashTypeSet(robj *o, sds field, sds value, int flags); int hashTypeSet(redisDb *db, robj *o, sds field, sds value, int flags);
robj *hashTypeDup(robj *o); 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 */ /* Pub / Sub */
int pubsubUnsubscribeAllChannels(client *c, int notify); int pubsubUnsubscribeAllChannels(client *c, int notify);
...@@ -3182,7 +3256,7 @@ dict *getClientPubSubChannels(client *c); ...@@ -3182,7 +3256,7 @@ dict *getClientPubSubChannels(client *c);
dict *getClientPubSubShardChannels(client *c); dict *getClientPubSubShardChannels(client *c);
/* Keyspace events notification */ /* 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); int keyspaceEventsStringToFlags(char *classes);
sds keyspaceEventsFlagsToString(int flags); sds keyspaceEventsFlagsToString(int flags);
...@@ -3266,6 +3340,7 @@ int keyIsExpired(redisDb *db, robj *key); ...@@ -3266,6 +3340,7 @@ int keyIsExpired(redisDb *db, robj *key);
long long getExpire(redisDb *db, robj *key); long long getExpire(redisDb *db, robj *key);
void setExpire(client *c, redisDb *db, robj *key, long long when); void setExpire(client *c, redisDb *db, robj *key, long long when);
int checkAlreadyExpired(long long when); int checkAlreadyExpired(long long when);
int parseExtendedExpireArgumentsOrReply(client *c, int *flags);
robj *lookupKeyRead(redisDb *db, robj *key); robj *lookupKeyRead(redisDb *db, robj *key);
robj *lookupKeyWrite(redisDb *db, robj *key); robj *lookupKeyWrite(redisDb *db, robj *key);
robj *lookupKeyReadOrReply(client *c, robj *key, robj *reply); robj *lookupKeyReadOrReply(client *c, robj *key, robj *reply);
...@@ -3284,7 +3359,7 @@ int objectSetLRUOrLFU(robj *val, long long lfu_freq, long long lru_idle, ...@@ -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_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 */ #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); int dbAddRDBLoad(redisDb *db, sds key, robj *val);
void dbReplaceValue(redisDb *db, robj *key, robj *val); void dbReplaceValue(redisDb *db, robj *key, robj *val);
...@@ -3439,6 +3514,7 @@ void expireSlaveKeys(void); ...@@ -3439,6 +3514,7 @@ void expireSlaveKeys(void);
void rememberSlaveKeyWithExpire(redisDb *db, robj *key); void rememberSlaveKeyWithExpire(redisDb *db, robj *key);
void flushSlaveKeysWithExpireList(void); void flushSlaveKeysWithExpireList(void);
size_t getSlaveKeyWithExpireCount(void); size_t getSlaveKeyWithExpireCount(void);
uint64_t hashTypeDbActiveExpire(redisDb *db, uint32_t maxFieldsToExpire);
/* evict.c -- maxmemory handling and LRU eviction. */ /* evict.c -- maxmemory handling and LRU eviction. */
void evictionPoolAlloc(void); void evictionPoolAlloc(void);
...@@ -3456,6 +3532,7 @@ void startEvictionTimeProc(void); ...@@ -3456,6 +3532,7 @@ void startEvictionTimeProc(void);
uint64_t dictSdsHash(const void *key); uint64_t dictSdsHash(const void *key);
uint64_t dictSdsCaseHash(const void *key); uint64_t dictSdsCaseHash(const void *key);
int dictSdsKeyCompare(dict *d, const void *key1, const void *key2); 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); int dictSdsKeyCaseCompare(dict *d, const void *key1, const void *key2);
void dictSdsDestructor(dict *d, void *val); void dictSdsDestructor(dict *d, void *val);
void dictListDestructor(dict *d, void *val); void dictListDestructor(dict *d, void *val);
...@@ -3611,6 +3688,15 @@ void strlenCommand(client *c); ...@@ -3611,6 +3688,15 @@ void strlenCommand(client *c);
void zrankCommand(client *c); void zrankCommand(client *c);
void zrevrankCommand(client *c); void zrevrankCommand(client *c);
void hsetCommand(client *c); void hsetCommand(client *c);
void 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 hsetnxCommand(client *c);
void hgetCommand(client *c); void hgetCommand(client *c);
void hmgetCommand(client *c); void hmgetCommand(client *c);
......
...@@ -94,7 +94,12 @@ robj *lookupKeyByPattern(redisDb *db, robj *pattern, robj *subst) { ...@@ -94,7 +94,12 @@ robj *lookupKeyByPattern(redisDb *db, robj *pattern, robj *subst) {
/* Retrieve value from hash by the field name. The returned object /* Retrieve value from hash by the field name. The returned object
* is a new object with refcount already incremented. */ * 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 { } else {
if (o->type != OBJ_STRING) goto noobj; if (o->type != OBJ_STRING) goto noobj;
......
...@@ -7,8 +7,600 @@ ...@@ -7,8 +7,600 @@
*/ */
#include "server.h" #include "server.h"
#include "ebuckets.h"
#include <math.h> #include <math.h>
/* Threshold for HEXPIRE and HPERSIST to be considered whether it is worth to
* update the expiration time of the hash object in global HFE DS. */
#define HASH_NEW_EXPIRE_DIFF_THRESHOLD max(4000, 1<<EB_BUCKET_KEY_PRECISION)
/* Returned by hashTypeGetValue() */
typedef enum GetFieldRes {
/* common (Used by hashTypeGet* value family) */
GETF_OK = 0,
GETF_NOT_FOUND, /* The field was not found. */
/* used only by hashTypeGetValue() */
GETF_EXPIRED, /* Logically expired but not yet deleted. */
GETF_EXPIRED_HASH, /* Delete hash since retrieved field was expired and
* it was the last field in the hash. */
} GetFieldRes;
/* hash field expiration (HFE) funcs */
static ExpireAction onFieldExpire(eItem item, void *ctx);
static ExpireMeta* hfieldGetExpireMeta(const eItem field);
static ExpireMeta *hashGetExpireMeta(const eItem hash);
static void hexpireGenericCommand(client *c, const char *cmd, long long basetime, int unit);
static ExpireAction hashTypeActiveExpire(eItem hashObj, void *ctx);
static void hfieldPersist(robj *hashObj, hfield field);
static void propagateHashFieldDeletion(redisDb *db, sds key, char *field, size_t fieldLen);
/* hash dictType funcs */
static int dictHfieldKeyCompare(dict *d, const void *key1, const void *key2);
static uint64_t dictMstrHash(const void *key);
static void dictHfieldDestructor(dict *d, void *field);
static size_t hashDictWithExpireMetadataBytes(dict *d);
static void hashDictWithExpireOnRelease(dict *d);
static robj* hashTypeLookupWriteOrCreate(client *c, robj *key);
/*-----------------------------------------------------------------------------
* Define dictType of hash
*
* - Stores fields as mstr strings with optional metadata to attach TTL
* - Note that small hashes are represented with listpacks
* - Once expiration is set for a field, the dict instance and corresponding
* dictType are replaced with a dict containing metadata for Hash Field
* Expiration (HFE) and using dictType `mstrHashDictTypeWithHFE`
*----------------------------------------------------------------------------*/
dictType mstrHashDictType = {
dictSdsHash, /* lookup hash function */
NULL, /* key dup */
NULL, /* val dup */
dictSdsMstrKeyCompare, /* lookup key compare */
dictHfieldDestructor, /* key destructor */
dictSdsDestructor, /* val destructor */
.storedHashFunction = dictMstrHash, /* stored hash function */
.storedKeyCompare = dictHfieldKeyCompare, /* stored key compare */
};
/* Define alternative dictType of hash with hash-field expiration (HFE) support */
dictType mstrHashDictTypeWithHFE = {
dictSdsHash, /* lookup hash function */
NULL, /* key dup */
NULL, /* val dup */
dictSdsMstrKeyCompare, /* lookup key compare */
dictHfieldDestructor, /* key destructor */
dictSdsDestructor, /* val destructor */
.storedHashFunction = dictMstrHash, /* stored hash function */
.storedKeyCompare = dictHfieldKeyCompare, /* stored key compare */
.dictMetadataBytes = hashDictWithExpireMetadataBytes,
.onDictRelease = hashDictWithExpireOnRelease,
};
/*-----------------------------------------------------------------------------
* Hash Field Expiration (HFE) Feature
*
* Each hash instance maintains its own set of hash field expiration within its
* private ebuckets DS. In order to support HFE active expire cycle across hash
* instances, hashes with associated HFE will be also registered in a global
* ebuckets DS with expiration time value that reflects their next minimum
* time to expire. The global HFE Active expiration will be triggered from
* activeExpireCycle() function and will invoke "local" HFE Active expiration
* for each hash instance that has expired fields.
*
* hashExpireBucketsType - ebuckets-type to be used at the global space
* (db->hexpires) to register hashes that have one or more fields with time-Expiration.
* The hashes will be registered in with the expiration time of the earliest field
* in the hash.
*----------------------------------------------------------------------------*/
EbucketsType hashExpireBucketsType = {
.onDeleteItem = NULL,
.getExpireMeta = hashGetExpireMeta, /* get ExpireMeta attached to each hash */
.itemsAddrAreOdd = 0, /* Addresses of dict are even */
};
/* dictExpireMetadata - ebuckets-type for hash fields with time-Expiration. ebuckets
* instance Will be attached to each hash that has at least one field with expiry
* time. */
EbucketsType hashFieldExpireBucketsType = {
.onDeleteItem = NULL,
.getExpireMeta = hfieldGetExpireMeta, /* get ExpireMeta attached to each field */
.itemsAddrAreOdd = 1, /* Addresses of hfield (mstr) are odd!! */
};
/* ActiveExpireCtx passed to hashTypeActiveExpire() */
typedef struct ActiveExpireCtx {
uint32_t fieldsToExpireQuota;
redisDb *db;
} ActiveExpireCtx;
/* OnFieldExpireCtx passed to OnFieldExpire() */
typedef struct OnFieldExpireCtx {
robj *hashObj;
redisDb *db;
} OnFieldExpireCtx;
/* The implementation of hashes by dict was modified from storing fields as sds
* strings to store "mstr" (Immutable string with metadata) in order to be able to
* attach TTL (ExpireMeta) to the hash-field. This usage of mstr opens up the
* opportunity for future features to attach additional metadata by need to the
* fields.
*
* The following defines new hfield kind of mstr */
typedef enum HfieldMetaFlags {
HFIELD_META_EXPIRE = 0,
} HfieldMetaFlags;
mstrKind mstrFieldKind = {
.name = "hField",
/* Taking care that all metaSize[*] values are even ensures that all
* addresses of hfield instances will be odd. */
.metaSize[HFIELD_META_EXPIRE] = sizeof(ExpireMeta),
};
static_assert(sizeof(struct ExpireMeta ) % 2 == 0, "must be even!");
/* Used by hpersistCommand() */
typedef enum SetPersistRes {
HFE_PERSIST_NO_FIELD = -2, /* No such hash-field */
HFE_PERSIST_NO_TTL = -1, /* No TTL attached to the field */
HFE_PERSIST_OK = 1
} SetPersistRes;
static inline int isDictWithMetaHFE(dict *d) {
return d->type == &mstrHashDictTypeWithHFE;
}
/*-----------------------------------------------------------------------------
* setex* - Set field OR field's expiration
*
* Whereas setting plain fields is rather straightforward, setting expiration
* time to fields might be time-consuming and complex since each update of
* expiration time, not only updates `ebuckets` of corresponding hash, but also
* might update `ebuckets` of global HFE DS. It is required to opt sequence of
* field updates with expirartion for a given hash, such that only once done,
* the global HFE DS will get updated.
*
* To do so, follow the scheme:
* 1. Call hashTypeSetExInit() to initialize the HashTypeSetEx struct.
* 2. Call hashTypeSetEx() one time or more, for each field/expiration update.
* 3. Call hashTypeSetExDone() for notification and update of global HFE.
*
* If expiration is not required, then avoid this API and use instead hashTypeSet()
*----------------------------------------------------------------------------*/
/* Returned value of hashTypeSetEx() */
typedef enum SetExRes {
/* Common res from hashTypeSetEx() */
HSETEX_OK = 1, /* Expiration time set/updated as expected */
/* If provided HashTypeSetEx struct to hashTypeSetEx() */
HSETEX_NO_FIELD = -2, /* No such hash-field */
HSETEX_NO_CONDITION_MET = 0, /* Specified NX | XX | GT | LT condition not met */
HSETEX_DELETED = 2, /* Field deleted because the specified time is in the past */
/* If not provided HashTypeSetEx struct to hashTypeSetEx() (plain HSET) */
HSET_UPDATE = 4, /* Update of the field without expiration time */
} SetExRes;
/* Used by httlGenericCommand() */
typedef enum GetExpireTimeRes {
HFE_GET_NO_FIELD = -2, /* No such hash-field */
HFE_GET_NO_TTL = -1, /* No TTL attached to the field */
} GetExpireTimeRes;
/* on fail return HSETEX_NO_CONDITION_MET */
typedef enum FieldSetCond {
FIELD_CREATE_OR_OVRWRT = 0,
FIELD_DONT_CREATE = 1,
FIELD_DONT_CREATE2 = 2, /* on fail return HSETEX_NO_FIELD */
FIELD_DONT_OVRWRT = 3
} FieldSetCond;
typedef enum FieldGet { /* TBD */
FIELD_GET_NONE = 0,
FIELD_GET_NEW = 1,
FIELD_GET_OLD = 2
} FieldGet;
typedef enum ExpireSetCond {
HFE_NX = 1<<0,
HFE_XX = 1<<1,
HFE_GT = 1<<2,
HFE_LT = 1<<3
} ExpireSetCond;
typedef struct HashTypeSet {
sds value;
int flags;
} HashTypeSet;
/* Used by hashTypeSetEx() for setting fields or their expiry */
typedef struct HashTypeSetEx {
/*** config ***/
FieldSetCond fieldSetCond; /* [DCF | DOF] */
ExpireSetCond expireSetCond; /* [XX | NX | GT | LT] */
/*** metadata ***/
uint64_t minExpire; /* if uninit EB_EXPIRE_TIME_INVALID */
redisDb *db;
robj *key, *hashObj;
uint64_t minExpireFields; /* Trace updated fields and their previous/new
* minimum expiration time. If minimum recorded
* is above minExpire of the hash, then we don't
* have to update global HFE DS */
int fieldDeleted; /* Number of fields deleted */
int fieldUpdated; /* Number of fields updated */
/* Optionally provide client for notification */
client *c;
const char *cmd;
} HashTypeSetEx;
static SetExRes hashTypeSetExListpack(redisDb *db, robj *o, sds field, HashTypeSet *setParams,
uint64_t expireAt, HashTypeSetEx *exParams);
int hashTypeSetExInit(robj *key, robj *o, client *c, redisDb *db, const char *cmd,
FieldSetCond fieldSetCond, ExpireSetCond expireSetCond, HashTypeSetEx *ex);
SetExRes hashTypeSetEx(redisDb *db, robj *o, sds field, HashTypeSet *setKeyVal,
uint64_t expireAt, HashTypeSetEx *exInfo);
void hashTypeSetExDone(HashTypeSetEx *e);
/*-----------------------------------------------------------------------------
* Accessor functions for dictType of hash
*----------------------------------------------------------------------------*/
static int dictHfieldKeyCompare(dict *d, const void *key1, const void *key2)
{
int l1,l2;
UNUSED(d);
l1 = hfieldlen((hfield)key1);
l2 = hfieldlen((hfield)key2);
if (l1 != l2) return 0;
return memcmp(key1, key2, l1) == 0;
}
static uint64_t dictMstrHash(const void *key) {
return dictGenHashFunction((unsigned char*)key, mstrlen((char*)key));
}
static void dictHfieldDestructor(dict *d, void *field) {
/* If attached TTL to the field, then remove it from hash's private ebuckets. */
if (hfieldGetExpireTime(field) != EB_EXPIRE_TIME_INVALID) {
dictExpireMetadata *dictExpireMeta = (dictExpireMetadata *) dictMetadata(d);
ebRemove(&dictExpireMeta->hfe, &hashFieldExpireBucketsType, field);
}
hfieldFree(field);
/* Don't have to update global HFE DS. It's unnecessary. Implementing this
* would introduce significant complexity and overhead for an operation that
* isn't critical. In the worst case scenario, the hash will be efficiently
* updated later by an active-expire operation, or it will be removed by the
* hash's dbGenericDelete() function. */
}
static size_t hashDictWithExpireMetadataBytes(dict *d) {
UNUSED(d);
/* expireMeta of the hash, ref to ebuckets and pointer to hash's key */
return sizeof(dictExpireMetadata);
}
static void hashDictWithExpireOnRelease(dict *d) {
/* for sure allocated with metadata. Otherwise, this func won't be registered */
dictExpireMetadata *dictExpireMeta = (dictExpireMetadata *) dictMetadata(d);
ebDestroy(&dictExpireMeta->hfe, &hashFieldExpireBucketsType, NULL);
}
/*-----------------------------------------------------------------------------
* listpackEx functions
*----------------------------------------------------------------------------*/
/*
* If any of hash field expiration command is called on a listpack hash object
* for the first time, we convert it to OBJ_ENCODING_LISTPACK_EX encoding.
* We allocate "struct listpackEx" which holds listpack pointer and metadata to
* register key to the global DS. In the listpack, we append another TTL entry
* for each field-value pair. From now on, listpack will have triplets in it:
* field-value-ttl. If TTL is not set for a field, we store 'zero' as the TTL
* value. 'zero' is encoded as two bytes in the listpack. Memory overhead of a
* non-existing TTL will be two bytes per field.
*
* Fields in the listpack will be ordered by TTL. Field with the smallest expiry
* time will be the first item. Fields without TTL will be at the end of the
* listpack. This way, it is easier/faster to find expired items.
*/
#define HASH_LP_NO_TTL 0
struct listpackEx *listpackExCreate(void) {
listpackEx *lpt = zcalloc(sizeof(*lpt));
lpt->meta.trash = 1;
lpt->lp = NULL;
lpt->key = NULL;
return lpt;
}
static void listpackExFree(listpackEx *lpt) {
lpFree(lpt->lp);
zfree(lpt);
}
struct lpFingArgs {
uint64_t max_to_search; /* [in] Max number of tuples to search */
uint64_t expire_time; /* [in] Find the tuple that has a TTL larger than expire_time */
unsigned char *p; /* [out] First item of the tuple that has a TTL larger than expire_time */
int expired; /* [out] Number of tuples that have TTLs less than expire_time */
int index; /* Internally used */
unsigned char *fptr; /* Internally used, temp ptr */
};
/* Callback for lpFindCb(). Used to find number of expired fields as part of
* active expiry or when trying to find the position for the new field according
* to its expiry time.*/
static int cbFindInListpack(const unsigned char *lp, unsigned char *p,
void *user, unsigned char *s, long long slen)
{
(void) lp;
struct lpFingArgs *r = user;
r->index++;
if (r->max_to_search == 0)
return 0; /* Break the loop and return */
if (r->index % 3 == 1) {
r->fptr = p; /* First item of the tuple. */
} else if (r->index % 3 == 0) {
serverAssert(!s);
/* Third item of a tuple is expiry time */
if (slen == HASH_LP_NO_TTL || (uint64_t) slen >= r->expire_time) {
r->p = r->fptr;
return 0; /* Break the loop and return */
}
r->expired++;
r->max_to_search--;
}
return 1;
}
/* Returns number of expired fields. */
static uint64_t listpackExExpireDryRun(const robj *o) {
serverAssert(o->encoding == OBJ_ENCODING_LISTPACK_EX);
listpackEx *lpt = o->ptr;
struct lpFingArgs r = {
.max_to_search = UINT64_MAX,
.expire_time = commandTimeSnapshot(),
};
lpFindCb(lpt->lp, NULL, &r, cbFindInListpack, 0);
return r.expired;
}
/* Returns the expiration time of the item with the nearest expiration. */
static uint64_t listpackExGetMinExpire(robj *o) {
serverAssert(o->encoding == OBJ_ENCODING_LISTPACK_EX);
long long expireAt;
unsigned char *fptr;
listpackEx *lpt = o->ptr;
/* As fields are ordered by expire time, first field will have the smallest
* expiry time. Third element is the expiry time of the first field */
fptr = lpSeek(lpt->lp, 2);
if (fptr != NULL) {
serverAssert(lpGetIntegerValue(fptr, &expireAt));
/* Check if this is a non-volatile field. */
if (expireAt != HASH_LP_NO_TTL)
return expireAt;
}
return EB_EXPIRE_TIME_INVALID;
}
/* Walk over fields and delete the expired ones. */
void listpackExExpire(redisDb *db, robj *o, ExpireInfo *info) {
serverAssert(o->encoding == OBJ_ENCODING_LISTPACK_EX);
uint64_t expired = 0, min = EB_EXPIRE_TIME_INVALID;
unsigned char *ptr;
listpackEx *lpt = o->ptr;
ptr = lpFirst(lpt->lp);
while (ptr != NULL && (info->itemsExpired < info->maxToExpire)) {
long long val;
int64_t flen;
unsigned char intbuf[LP_INTBUF_SIZE], *fref;
fref = lpGet(ptr, &flen, intbuf);
ptr = lpNext(lpt->lp, ptr);
serverAssert(ptr);
ptr = lpNext(lpt->lp, ptr);
serverAssert(ptr && lpGetIntegerValue(ptr, &val));
/* Fields are ordered by expiry time. If we reached to a non-expired
* or a non-volatile field, we know rest is not yet expired. */
if (val == HASH_LP_NO_TTL || (uint64_t) val > info->now)
break;
propagateHashFieldDeletion(db, ((listpackEx *) o->ptr)->key, (char *)((fref) ? fref : intbuf), flen);
ptr = lpNext(lpt->lp, ptr);
info->itemsExpired++;
expired++;
}
if (expired)
lpt->lp = lpDeleteRange(lpt->lp, 0, expired * 3);
min = hashTypeGetNextTimeToExpire(o);
info->nextExpireTime = (min != EB_EXPIRE_TIME_INVALID) ? min : 0;
}
static void listpackExAddInternal(robj *o, listpackEntry ent[3]) {
listpackEx *lpt = o->ptr;
/* Shortcut, just append at the end if this is a non-volatile field. */
if (ent[2].lval == HASH_LP_NO_TTL) {
lpt->lp = lpBatchAppend(lpt->lp, ent, 3);
return;
}
struct lpFingArgs r = {
.max_to_search = UINT64_MAX,
.expire_time = ent[2].lval,
};
/* Check if there is a field with a larger TTL. */
lpFindCb(lpt->lp, NULL, &r, cbFindInListpack, 0);
/* If list is empty or there is no field with a larger TTL, result will be
* NULL. Otherwise, just insert before the found item.*/
if (r.p)
lpt->lp = lpBatchInsert(lpt->lp, r.p, LP_BEFORE, ent, 3, NULL);
else
lpt->lp = lpBatchAppend(lpt->lp, ent, 3);
}
/* Add new field ordered by expire time. */
void listpackExAddNew(robj *o, char *field, size_t flen,
char *value, size_t vlen, uint64_t expireAt) {
listpackEntry ent[3] = {
{.sval = (unsigned char*) field, .slen = flen},
{.sval = (unsigned char*) value, .slen = vlen},
{.lval = expireAt}
};
listpackExAddInternal(o, ent);
}
/* If expiry time is changed, this function will place field into the correct
* position. First, it deletes the field and re-inserts to the listpack ordered
* by expiry time. */
static void listpackExUpdateExpiry(robj *o, sds field,
unsigned char *fptr,
unsigned char *vptr,
uint64_t expire_at) {
unsigned int slen = 0;
long long val = 0;
unsigned char tmp[512] = {0};
unsigned char *valstr;
sds tmpval = NULL;
listpackEx *lpt = o->ptr;
/* Copy value */
valstr = lpGetValue(vptr, &slen, &val);
if (valstr) {
/* Normally, item length in the listpack is limited by
* 'hash-max-listpack-value' config. It is unlikely, but it might be
* larger than sizeof(tmp). */
if (slen > sizeof(tmp))
tmpval = sdsnewlen(valstr, slen);
else
memcpy(tmp, valstr, slen);
}
/* Delete field name, value and expiry time */
lpt->lp = lpDeleteRangeWithEntry(lpt->lp, &fptr, 3);
listpackEntry ent[3] = {{0}};
ent[0].sval = (unsigned char*) field;
ent[0].slen = sdslen(field);
if (valstr) {
ent[1].sval = tmpval ? (unsigned char *) tmpval : tmp;
ent[1].slen = slen;
} else {
ent[1].lval = val;
}
ent[2].lval = expire_at;
listpackExAddInternal(o, ent);
sdsfree(tmpval);
}
/* Update field expire time. */
SetExRes hashTypeSetExpiryListpack(HashTypeSetEx *ex, sds field,
unsigned char *fptr, unsigned char *vptr,
unsigned char *tptr, uint64_t expireAt)
{
long long expireTime;
uint64_t prevExpire = EB_EXPIRE_TIME_INVALID;
serverAssert(lpGetIntegerValue(tptr, &expireTime));
if (expireTime != HASH_LP_NO_TTL) {
prevExpire = (uint64_t) expireTime;
}
if (prevExpire == EB_EXPIRE_TIME_INVALID) {
/* For fields without expiry, LT condition is considered valid */
if (ex->expireSetCond & (HFE_XX | HFE_GT))
return HSETEX_NO_CONDITION_MET;
} else {
if (((ex->expireSetCond == HFE_GT) && (prevExpire >= expireAt)) ||
((ex->expireSetCond == HFE_LT) && (prevExpire <= expireAt)) ||
(ex->expireSetCond == HFE_NX) )
return HSETEX_NO_CONDITION_MET;
/* Track of minimum expiration time (only later update global HFE DS) */
if (ex->minExpireFields > prevExpire)
ex->minExpireFields = prevExpire;
}
/* if expiration time is in the past */
if (unlikely(checkAlreadyExpired(expireAt))) {
hashTypeDelete(ex->hashObj, field, 1);
ex->fieldDeleted++;
return HSETEX_DELETED;
}
if (ex->minExpireFields > expireAt)
ex->minExpireFields = expireAt;
listpackExUpdateExpiry(ex->hashObj, field, fptr, vptr, expireAt);
ex->fieldUpdated++;
return HSETEX_OK;
}
/* Returns 1 if expired */
int hashTypeIsExpired(const robj *o, uint64_t expireAt) {
if (o->encoding == OBJ_ENCODING_LISTPACK_EX) {
if (expireAt == HASH_LP_NO_TTL)
return 0;
} else if (o->encoding == OBJ_ENCODING_HT) {
if (expireAt == EB_EXPIRE_TIME_INVALID)
return 0;
} else {
serverPanic("Unknown encoding: %d", o->encoding);
}
return (mstime_t) expireAt < commandTimeSnapshot();
}
/* Returns listpack pointer of the object. */
unsigned char *hashTypeListpackGetLp(robj *o) {
if (o->encoding == OBJ_ENCODING_LISTPACK)
return o->ptr;
else if (o->encoding == OBJ_ENCODING_LISTPACK_EX)
return ((listpackEx*)o->ptr)->lp;
serverPanic("Unknown encoding: %d", o->encoding);
}
/*----------------------------------------------------------------------------- /*-----------------------------------------------------------------------------
* Hash type API * Hash type API
*----------------------------------------------------------------------------*/ *----------------------------------------------------------------------------*/
...@@ -16,18 +608,19 @@ ...@@ -16,18 +608,19 @@
/* Check the length of a number of objects to see if we need to convert a /* Check the length of a number of objects to see if we need to convert a
* listpack to a real hash. Note that we only check string encoded objects * listpack to a real hash. Note that we only check string encoded objects
* as their string length can be queried in constant time. */ * as their string length can be queried in constant time. */
void hashTypeTryConversion(robj *o, robj **argv, int start, int end) { void hashTypeTryConversion(redisDb *db, robj *o, robj **argv, int start, int end) {
int i; int i;
size_t sum = 0; size_t sum = 0;
if (o->encoding != OBJ_ENCODING_LISTPACK) return; if (o->encoding != OBJ_ENCODING_LISTPACK && o->encoding != OBJ_ENCODING_LISTPACK_EX)
return;
/* We guess that most of the values in the input are unique, so /* We guess that most of the values in the input are unique, so
* if there are enough arguments we create a pre-sized hash, which * if there are enough arguments we create a pre-sized hash, which
* might over allocate memory if there are duplicates. */ * might over allocate memory if there are duplicates. */
size_t new_fields = (end - start + 1) / 2; size_t new_fields = (end - start + 1) / 2;
if (new_fields > server.hash_max_listpack_entries) { if (new_fields > server.hash_max_listpack_entries) {
hashTypeConvert(o, OBJ_ENCODING_HT); hashTypeConvert(o, OBJ_ENCODING_HT, &db->hexpires);
dictExpand(o->ptr, new_fields); dictExpand(o->ptr, new_fields);
return; return;
} }
...@@ -37,26 +630,26 @@ void hashTypeTryConversion(robj *o, robj **argv, int start, int end) { ...@@ -37,26 +630,26 @@ void hashTypeTryConversion(robj *o, robj **argv, int start, int end) {
continue; continue;
size_t len = sdslen(argv[i]->ptr); size_t len = sdslen(argv[i]->ptr);
if (len > server.hash_max_listpack_value) { if (len > server.hash_max_listpack_value) {
hashTypeConvert(o, OBJ_ENCODING_HT); hashTypeConvert(o, OBJ_ENCODING_HT, &db->hexpires);
return; return;
} }
sum += len; sum += len;
} }
if (!lpSafeToAdd(o->ptr, sum)) if (!lpSafeToAdd(hashTypeListpackGetLp(o), sum))
hashTypeConvert(o, OBJ_ENCODING_HT); hashTypeConvert(o, OBJ_ENCODING_HT, &db->hexpires);
} }
/* Get the value from a listpack encoded hash, identified by field. /* Get the value from a listpack encoded hash, identified by field. */
* Returns -1 when the field cannot be found. */ GetFieldRes hashTypeGetFromListpack(robj *o, sds field,
int hashTypeGetFromListpack(robj *o, sds field,
unsigned char **vstr, unsigned char **vstr,
unsigned int *vlen, unsigned int *vlen,
long long *vll) long long *vll,
uint64_t *expiredAt)
{ {
*expiredAt = EB_EXPIRE_TIME_INVALID;
unsigned char *zl, *fptr = NULL, *vptr = NULL; unsigned char *zl, *fptr = NULL, *vptr = NULL;
serverAssert(o->encoding == OBJ_ENCODING_LISTPACK); if (o->encoding == OBJ_ENCODING_LISTPACK) {
zl = o->ptr; zl = o->ptr;
fptr = lpFirst(zl); fptr = lpFirst(zl);
if (fptr != NULL) { if (fptr != NULL) {
...@@ -67,130 +660,521 @@ int hashTypeGetFromListpack(robj *o, sds field, ...@@ -67,130 +660,521 @@ int hashTypeGetFromListpack(robj *o, sds field,
serverAssert(vptr != NULL); serverAssert(vptr != NULL);
} }
} }
} else if (o->encoding == OBJ_ENCODING_LISTPACK_EX) {
long long expire;
unsigned char *h;
listpackEx *lpt = o->ptr;
fptr = lpFirst(lpt->lp);
if (fptr != NULL) {
fptr = lpFind(lpt->lp, fptr, (unsigned char*)field, sdslen(field), 2);
if (fptr != NULL) {
vptr = lpNext(lpt->lp, fptr);
serverAssert(vptr != NULL);
h = lpNext(lpt->lp, vptr);
serverAssert(h && lpGetIntegerValue(h, &expire));
if (expire != HASH_LP_NO_TTL)
*expiredAt = expire;
}
}
} else {
serverPanic("Unknown hash encoding: %d", o->encoding);
}
if (vptr != NULL) { if (vptr != NULL) {
*vstr = lpGetValue(vptr, vlen, vll); *vstr = lpGetValue(vptr, vlen, vll);
return 0; return GETF_OK;
} }
return -1; return GETF_NOT_FOUND;
} }
/* Get the value from a hash table encoded hash, identified by field. /* Get the value from a hash table encoded hash, identified by field.
* Returns NULL when the field cannot be found, otherwise the SDS value * Returns NULL when the field cannot be found, otherwise the SDS value
* is returned. */ * is returned. */
sds hashTypeGetFromHashTable(robj *o, sds field) { GetFieldRes hashTypeGetFromHashTable(robj *o, sds field, sds *value, uint64_t *expiredAt) {
dictEntry *de; dictEntry *de;
*expiredAt = EB_EXPIRE_TIME_INVALID;
serverAssert(o->encoding == OBJ_ENCODING_HT); serverAssert(o->encoding == OBJ_ENCODING_HT);
de = dictFind(o->ptr, field); de = dictFind(o->ptr, field);
if (de == NULL) return NULL;
return dictGetVal(de); if (de == NULL)
return GETF_NOT_FOUND;
*expiredAt = hfieldGetExpireTime(dictGetKey(de));
*value = (sds) dictGetVal(de);
return GETF_OK;
} }
/* Higher level function of hashTypeGet*() that returns the hash value /* Higher level function of hashTypeGet*() that returns the hash value
* associated with the specified field. If the field is found C_OK * associated with the specified field.
* is returned, otherwise C_ERR. The returned object is returned by
* reference in either *vstr and *vlen if it's returned in string form,
* or stored in *vll if it's returned as a number.
* *
* If *vll is populated *vstr is set to NULL, so the caller * Returned:
* can always check the function return by checking the return value * - GetFieldRes: OK: Return Field's valid value
* for C_OK and checking if vll (or vstr) is NULL. */ * NOT_FOUND: Field was not found.
int hashTypeGetValue(robj *o, sds field, unsigned char **vstr, unsigned int *vlen, long long *vll) { * EXPIRED: Field is expired and Lazy deleted
if (o->encoding == OBJ_ENCODING_LISTPACK) { * EXPIRED_HASH: Returned only if the field is the last one in the
* hash and the hash is deleted.
* - vstr, vlen : if string, ref in either *vstr and *vlen if it's
* returned in string form,
* - vll : or stored in *vll if it's returned as a number.
* If *vll is populated *vstr is set to NULL, so the caller can
* always check the function return by checking the return value
* for GETF_OK and checking if vll (or vstr) is NULL.
*
*/
GetFieldRes hashTypeGetValue(redisDb *db, robj *o, sds field, unsigned char **vstr,
unsigned int *vlen, long long *vll) {
uint64_t expiredAt;
sds key;
GetFieldRes res;
if (o->encoding == OBJ_ENCODING_LISTPACK ||
o->encoding == OBJ_ENCODING_LISTPACK_EX) {
*vstr = NULL; *vstr = NULL;
if (hashTypeGetFromListpack(o, field, vstr, vlen, vll) == 0) res = hashTypeGetFromListpack(o, field, vstr, vlen, vll, &expiredAt);
return C_OK;
if (res == GETF_NOT_FOUND)
return GETF_NOT_FOUND;
} else if (o->encoding == OBJ_ENCODING_HT) { } else if (o->encoding == OBJ_ENCODING_HT) {
sds value; sds value = NULL;
if ((value = hashTypeGetFromHashTable(o, field)) != NULL) { res = hashTypeGetFromHashTable(o, field, &value, &expiredAt);
if (res == GETF_NOT_FOUND)
return GETF_NOT_FOUND;
*vstr = (unsigned char*) value; *vstr = (unsigned char*) value;
*vlen = sdslen(value); *vlen = sdslen(value);
return C_OK;
}
} else { } else {
serverPanic("Unknown hash encoding"); serverPanic("Unknown hash encoding");
} }
return C_ERR;
/* Don't expire anything while loading. It will be done later. */
if ( (server.loading) ||
(server.lazy_expire_disabled) ||
((server.masterhost) && (server.current_client && (server.current_client->flags & CLIENT_MASTER))) ||
(expiredAt >= (uint64_t) commandTimeSnapshot()) )
return GETF_OK;
/* Got expired. Extract attached key from LISTPACK_EX/HT */
if (o->encoding == OBJ_ENCODING_LISTPACK_EX)
key = ((listpackEx *) o->ptr)->key;
else
key = ((dictExpireMetadata *) dictMetadata((dict*)o->ptr))->key;
/* delete the field and propagate the deletion */
serverAssert(hashTypeDelete(o, field, 1) == 1);
propagateHashFieldDeletion(db, key, field, sdslen(field));
/* If the field is the last one in the hash, then the hash will be deleted */
if (hashTypeLength(o, 0) == 0) {
robj *keyObj = createStringObject(key, sdslen(key));
notifyKeyspaceEvent(NOTIFY_GENERIC, "del", keyObj, db->id);
dbDelete(db,keyObj);
decrRefCount(keyObj);
return GETF_EXPIRED_HASH;
}
return GETF_EXPIRED;
} }
/* Like hashTypeGetValue() but returns a Redis object, which is useful for /* Like hashTypeGetValue() but returns a Redis object, which is useful for
* interaction with the hash type outside t_hash.c. * interaction with the hash type outside t_hash.c.
* The function returns NULL if the field is not found in the hash. Otherwise * The function returns NULL if the field is not found in the hash. Otherwise
* a newly allocated string object with the value is returned. */ * a newly allocated string object with the value is returned.
robj *hashTypeGetValueObject(robj *o, sds field) { *
* isHashDeleted - If attempted to access expired field and it's the last field
* in the hash, then the hash will as well be deleted. In this case,
* isHashDeleted will be set to 1.
*/
robj *hashTypeGetValueObject(redisDb *db, robj *o, sds field, int *isHashDeleted) {
unsigned char *vstr; unsigned char *vstr;
unsigned int vlen; unsigned int vlen;
long long vll; long long vll;
if (hashTypeGetValue(o,field,&vstr,&vlen,&vll) == C_ERR) return NULL; *isHashDeleted = 0; /*default*/
GetFieldRes res = hashTypeGetValue(db,o,field,&vstr,&vlen,&vll);
if (res == GETF_OK) {
if (vstr) return createStringObject((char*)vstr,vlen); if (vstr) return createStringObject((char*)vstr,vlen);
else return createStringObjectFromLongLong(vll); else return createStringObjectFromLongLong(vll);
}
if (res == GETF_EXPIRED_HASH)
*isHashDeleted = 1;
/* GETF_EXPIRED_HASH, GETF_EXPIRED, GETF_NOT_FOUND */
return NULL;
} }
/* Higher level function using hashTypeGet*() to return the length of the /* Test if the specified field exists in the given hash. If the field is
* object associated with the requested field, or 0 if the field does not * expired (HFE), then it will be lazy deleted
* exist. */ *
size_t hashTypeGetValueLength(robj *o, sds field) { * Returns 1 if the field exists, and 0 when it doesn't.
size_t len = 0; *
* isHashDeleted - If attempted to access expired field and it is the last field
* in the hash, then the hash will as well be deleted. In this case,
* isHashDeleted will be set to 1.
*/
int hashTypeExists(redisDb *db, robj *o, sds field, int *isHashDeleted) {
unsigned char *vstr = NULL; unsigned char *vstr = NULL;
unsigned int vlen = UINT_MAX; unsigned int vlen = UINT_MAX;
long long vll = LLONG_MAX; long long vll = LLONG_MAX;
if (hashTypeGetValue(o, field, &vstr, &vlen, &vll) == C_OK) GetFieldRes res = hashTypeGetValue(db, o, field, &vstr, &vlen, &vll);
len = vstr ? vlen : sdigits10(vll); *isHashDeleted = (res == GETF_EXPIRED_HASH) ? 1 : 0;
return (res == GETF_OK) ? 1 : 0;
}
return len; /* Add a new field, overwrite the old with the new value if it already exists.
* Return 0 on insert and 1 on update.
*
* By default, the key and value SDS strings are copied if needed, so the
* caller retains ownership of the strings passed. However this behavior
* can be effected by passing appropriate flags (possibly bitwise OR-ed):
*
* HASH_SET_TAKE_FIELD -- The SDS field ownership passes to the function.
* HASH_SET_TAKE_VALUE -- The SDS value ownership passes to the function.
* HASH_SET_KEEP_FIELD -- keep original field along with TTL if already exists
*
* When the flags are used the caller does not need to release the passed
* SDS string(s). It's up to the function to use the string to create a new
* entry or to free the SDS string before returning to the caller.
*
* HASH_SET_COPY corresponds to no flags passed, and means the default
* semantics of copying the values if needed.
*
*/
#define HASH_SET_TAKE_FIELD (1<<0)
#define HASH_SET_TAKE_VALUE (1<<1)
#define HASH_SET_KEEP_FIELD (1<<2)
#define HASH_SET_COPY 0
int hashTypeSet(redisDb *db, robj *o, sds field, sds value, int flags) {
HashTypeSet set = {value, flags};
return (hashTypeSetEx(db, o, field, &set, 0, NULL) == HSET_UPDATE) ? 1 : 0;
} }
/* Test if the specified field exists in the given hash. Returns 1 if the field SetExRes hashTypeSetExpiry(HashTypeSetEx *ex, sds field, uint64_t expireAt, dictEntry **de) {
* exists, and 0 when it doesn't. */ dict *ht = ex->hashObj->ptr;
int hashTypeExists(robj *o, sds field) { dictEntry *newEntry = NULL, *existingEntry = NULL;
unsigned char *vstr = NULL;
unsigned int vlen = UINT_MAX; /* New field with expiration metadata */
long long vll = LLONG_MAX; hfield hfNew = hfieldNew(field, sdslen(field), 1 /*withExpireMeta*/);
if ((ex->fieldSetCond == FIELD_DONT_CREATE) || (ex->fieldSetCond == FIELD_DONT_CREATE2)) {
if ((existingEntry = dictFind(ht, field)) == NULL) {
hfieldFree(hfNew);
return (ex->fieldSetCond == FIELD_DONT_CREATE) ?
HSETEX_NO_CONDITION_MET : HSETEX_NO_FIELD;
}
} else {
dictUseStoredKeyApi(ht, 1);
newEntry = dictAddRaw(ht, hfNew, &existingEntry);
dictUseStoredKeyApi(ht, 0);
}
if (newEntry) {
*de = newEntry;
if (ex->expireSetCond & (HFE_XX | HFE_LT | HFE_GT)) {
dictDelete(ht, field);
return HSETEX_NO_CONDITION_MET;
}
} else { /* field exist */
*de = existingEntry;
if (ex->fieldSetCond == FIELD_DONT_OVRWRT) {
hfieldFree(hfNew);
return HSETEX_NO_CONDITION_MET;
}
hfield hfOld = dictGetKey(existingEntry);
/* If field doesn't have expiry metadata attached */
if (!hfieldIsExpireAttached(hfOld)) {
/* For fields without expiry, LT condition is considered valid */
if (ex->expireSetCond & (HFE_XX | HFE_GT)) {
hfieldFree(hfNew);
return HSETEX_NO_CONDITION_MET;
}
/* Delete old field. Below goanna dictSetKey(..,hfNew) */
hfieldFree(hfOld);
} else { /* field has ExpireMeta struct attached */
/* No need for hfNew (Just modify expire-time of existing field) */
hfieldFree(hfNew);
uint64_t prevExpire = hfieldGetExpireTime(hfOld);
/* If field has valid expiration time, then check GT|LT|NX */
if (prevExpire != EB_EXPIRE_TIME_INVALID) {
if (((ex->expireSetCond == HFE_GT) && (prevExpire >= expireAt)) ||
((ex->expireSetCond == HFE_LT) && (prevExpire <= expireAt)) ||
(ex->expireSetCond == HFE_NX) )
return HSETEX_NO_CONDITION_MET;
/* remove old expiry time from hash's private ebuckets */
dictExpireMetadata *dm = (dictExpireMetadata *) dictMetadata(ht);
ebRemove(&dm->hfe, &hashFieldExpireBucketsType, hfOld);
/* Track of minimum expiration time (only later update global HFE DS) */
if (ex->minExpireFields > prevExpire)
ex->minExpireFields = prevExpire;
} else {
/* field has invalid expiry. No need to ebRemove() */
/* Check XX|LT|GT */
if (ex->expireSetCond & (HFE_XX | HFE_GT))
return HSETEX_NO_CONDITION_MET;
}
/* Reuse hfOld as hfNew and rewrite its expiry with ebAdd() */
hfNew = hfOld;
}
dictSetKey(ht, existingEntry, hfNew);
}
/* if expiration time is in the past */
if (unlikely(checkAlreadyExpired(expireAt))) {
hashTypeDelete(ex->hashObj, field, 1);
ex->fieldDeleted++;
return HSETEX_DELETED;
}
if (ex->minExpireFields > expireAt)
ex->minExpireFields = expireAt;
dictExpireMetadata *dm = (dictExpireMetadata *) dictMetadata(ht);
ebAdd(&dm->hfe, &hashFieldExpireBucketsType, hfNew, expireAt);
ex->fieldUpdated++;
return HSETEX_OK;
}
/*
* Set fields OR field's expiration (See also `setex*` comment above)
*
* Take care to call first hashTypeSetExInit() and then call this function.
* Finally, call hashTypeSetExDone() to notify and update global HFE DS.
*
* NOTE: this functions is also called during RDB load to set dict-encoded
* fields with and without expiration.
*/
SetExRes hashTypeSetEx(redisDb *db, robj *o, sds field, HashTypeSet *setKeyVal,
uint64_t expireAt, HashTypeSetEx *exInfo)
{
SetExRes res = HSETEX_OK;
int isSetKeyValue = (setKeyVal) ? 1 : 0;
int isSetExpire = (exInfo) ? 1 : 0;
int flags = (setKeyVal) ? setKeyVal->flags : 0;
/* Check if the field is too long for listpack, and convert before adding the item.
* This is needed for HINCRBY* case since in other commands this is handled early by
* hashTypeTryConversion, so this check will be a NOP. */
if (o->encoding == OBJ_ENCODING_LISTPACK ||
o->encoding == OBJ_ENCODING_LISTPACK_EX)
{
if ( (isSetKeyValue) &&
(sdslen(field) > server.hash_max_listpack_value ||
sdslen(setKeyVal->value) > server.hash_max_listpack_value) )
{
hashTypeConvert(o, OBJ_ENCODING_HT, &db->hexpires);
} else {
res = hashTypeSetExListpack(db, o, field, setKeyVal, expireAt, exInfo);
goto SetExDone; /*done*/
}
}
if (o->encoding != OBJ_ENCODING_HT)
serverPanic("Unknown hash encoding");
/*** now deal with HT ***/
hfield newField;
dict *ht = o->ptr;
dictEntry *de;
/* If needed to set the field along with expiry */
if (isSetExpire) {
res = hashTypeSetExpiry(exInfo, field, expireAt, &de);
if (res != HSETEX_OK) goto SetExDone;
} else {
dictEntry *existing;
/* Cannot leverage HASH_SET_TAKE_FIELD since hfield is not of type sds */
newField = hfieldNew(field, sdslen(field), 0);
/* stored key is different than lookup key */
dictUseStoredKeyApi(ht, 1);
de = dictAddRaw(ht, newField, &existing);
dictUseStoredKeyApi(ht, 0);
/* If field already exists, then update "field". "Value" will be set afterward */
if (de == NULL) {
if (flags & HASH_SET_KEEP_FIELD) {
/* Not keep old field along with TTL */
hfieldFree(newField);
} else {
/* If attached TTL to the old field, then remove it from hash's private ebuckets */
hfield oldField = dictGetKey(existing);
hfieldPersist(o, oldField);
hfieldFree(oldField);
dictSetKey(ht, existing, newField);
}
sdsfree(dictGetVal(existing));
res = HSET_UPDATE;
de = existing;
}
}
/* If need to set value */
if (isSetKeyValue) {
if (flags & HASH_SET_TAKE_VALUE) {
dictSetVal(ht, de, setKeyVal->value);
flags &= ~HASH_SET_TAKE_VALUE;
} else {
dictSetVal(ht, de, sdsdup(setKeyVal->value));
}
}
SetExDone:
/* Free SDS strings we did not referenced elsewhere if the flags
* want this function to be responsible. */
if (flags & HASH_SET_TAKE_FIELD && field) sdsfree(field);
if (flags & HASH_SET_TAKE_VALUE && setKeyVal->value) sdsfree(setKeyVal->value);
return res;
}
void initDictExpireMetadata(sds key, robj *o) {
dict *ht = o->ptr;
dictExpireMetadata *m = (dictExpireMetadata *) dictMetadata(ht);
m->key = key;
m->hfe = ebCreate(); /* Allocate HFE DS */
m->expireMeta.trash = 1; /* mark as trash (as long it wasn't ebAdd()) */
}
/*
* Init HashTypeSetEx struct before calling hashTypeSetEx()
*
* Don't have to provide client and "cmd". If provided, then notification once
* done by function hashTypeSetExDone().
*/
int hashTypeSetExInit(robj *key, robj *o, client *c, redisDb *db, const char *cmd, FieldSetCond fieldSetCond,
ExpireSetCond expireSetCond, HashTypeSetEx *ex)
{
dict *ht = o->ptr;
ex->fieldSetCond = fieldSetCond;
ex->expireSetCond = expireSetCond;
ex->minExpire = EB_EXPIRE_TIME_INVALID;
ex->c = c;
ex->cmd = cmd;
ex->db = db;
ex->key = key;
ex->hashObj = o;
ex->fieldDeleted = 0;
ex->fieldUpdated = 0;
ex->minExpireFields = EB_EXPIRE_TIME_INVALID;
/* Take care that HASH support expiration */
if (ex->hashObj->encoding == OBJ_ENCODING_LISTPACK) {
hashTypeConvert(ex->hashObj, OBJ_ENCODING_LISTPACK_EX, &c->db->hexpires);
listpackEx *lpt = ex->hashObj->ptr;
dictEntry *de = dbFind(c->db, key->ptr);
serverAssert(de != NULL);
lpt->key = dictGetKey(de);
} else if (ex->hashObj->encoding == OBJ_ENCODING_HT) {
/* Take care dict has HFE metadata */
if (!isDictWithMetaHFE(ht)) {
/* Realloc (only header of dict) with metadata for hash-field expiration */
dictTypeAddMeta(&ht, &mstrHashDictTypeWithHFE);
dictExpireMetadata *m = (dictExpireMetadata *) dictMetadata(ht);
ex->hashObj->ptr = ht;
/* Find the key in the keyspace. Need to keep reference to the key for
* notifications or even removal of the hash */
dictEntry *de = dbFind(db, key->ptr);
serverAssert(de != NULL);
/* Fillup dict HFE metadata */
m->key = dictGetKey(de); /* reference key in keyspace */
m->hfe = ebCreate(); /* Allocate HFE DS */
m->expireMeta.trash = 1; /* mark as trash (as long it wasn't ebAdd()) */
}
}
ex->minExpire = hashTypeGetMinExpire(ex->hashObj);
return C_OK;
}
/*
* After calling hashTypeSetEx() for setting fields or their expiry, call this
* function to notify and update global HFE DS.
*/
void hashTypeSetExDone(HashTypeSetEx *ex) {
/* Notify keyspace event, update dirty count and update global HFE DS */
if (ex->fieldDeleted + ex->fieldUpdated > 0) {
if (ex->c) {
server.dirty += ex->fieldDeleted + ex->fieldUpdated;
signalModifiedKey(ex->c, ex->db, ex->key);
notifyKeyspaceEvent(NOTIFY_HASH, "hexpire", ex->key, ex->db->id);
}
if (ex->fieldDeleted && hashTypeLength(ex->hashObj, 0) == 0) {
dbDelete(ex->db,ex->key);
if (ex->c) notifyKeyspaceEvent(NOTIFY_GENERIC,"del",ex->key, ex->db->id);
} else {
/* If minimum HFE of the hash is smaller than expiration time of the
* specified fields in the command as well as it is smaller or equal
* than expiration time provided in the command, then the minimum
* HFE of the hash won't change following this command. */
if ((ex->minExpire < ex->minExpireFields))
return;
return hashTypeGetValue(o, field, &vstr, &vlen, &vll) == C_OK; /* retrieve new expired time. It might have changed. */
} uint64_t newMinExpire = hashTypeGetNextTimeToExpire(ex->hashObj);
/* Add a new field, overwrite the old with the new value if it already exists. /* Calculate the diff between old minExpire and newMinExpire. If it is
* Return 0 on insert and 1 on update. * only few seconds, then don't have to update global HFE DS. At the worst
* * case fields of hash will be active-expired up to few seconds later.
* By default, the key and value SDS strings are copied if needed, so the
* caller retains ownership of the strings passed. However this behavior
* can be effected by passing appropriate flags (possibly bitwise OR-ed):
*
* HASH_SET_TAKE_FIELD -- The SDS field ownership passes to the function.
* HASH_SET_TAKE_VALUE -- The SDS value ownership passes to the function.
*
* When the flags are used the caller does not need to release the passed
* SDS string(s). It's up to the function to use the string to create a new
* entry or to free the SDS string before returning to the caller.
*
* HASH_SET_COPY corresponds to no flags passed, and means the default
* semantics of copying the values if needed.
* *
* In any case, active-expire operation will know to update global
* HFE DS more efficiently than here for a single item.
*/ */
#define HASH_SET_TAKE_FIELD (1<<0) uint64_t diff = (ex->minExpire > newMinExpire) ?
#define HASH_SET_TAKE_VALUE (1<<1) (ex->minExpire - newMinExpire) : (newMinExpire - ex->minExpire);
#define HASH_SET_COPY 0 if (diff < HASH_NEW_EXPIRE_DIFF_THRESHOLD) return;
int hashTypeSet(robj *o, sds field, sds value, int flags) {
int update = 0;
/* Check if the field is too long for listpack, and convert before adding the item. if (ex->minExpire != EB_EXPIRE_TIME_INVALID)
ebRemove(&ex->db->hexpires, &hashExpireBucketsType, ex->hashObj);
if (newMinExpire != EB_EXPIRE_TIME_INVALID)
ebAdd(&ex->db->hexpires, &hashExpireBucketsType, ex->hashObj, newMinExpire);
}
}
}
/* Check if the field is too long for listpack, and convert before adding the item.
* This is needed for HINCRBY* case since in other commands this is handled early by * This is needed for HINCRBY* case since in other commands this is handled early by
* hashTypeTryConversion, so this check will be a NOP. */ * hashTypeTryConversion, so this check will be a NOP. */
if (o->encoding == OBJ_ENCODING_LISTPACK) { static SetExRes hashTypeSetExListpack(redisDb *db, robj *o, sds field, HashTypeSet *setParams,
if (sdslen(field) > server.hash_max_listpack_value || sdslen(value) > server.hash_max_listpack_value) uint64_t expireAt, HashTypeSetEx *exParams)
hashTypeConvert(o, OBJ_ENCODING_HT); {
} int res = HSETEX_OK;
unsigned char *fptr = NULL, *vptr = NULL, *tptr = NULL;
if (o->encoding == OBJ_ENCODING_LISTPACK) { if (o->encoding == OBJ_ENCODING_LISTPACK) {
unsigned char *zl, *fptr, *vptr; /* If reached here, then no need to set expiration. Otherwise, as precond
* listpack is converted to listpackex by hashTypeSetExInit() */
zl = o->ptr; unsigned char *zl = o->ptr;
fptr = lpFirst(zl); fptr = lpFirst(zl);
if (fptr != NULL) { if (fptr != NULL) {
fptr = lpFind(zl, fptr, (unsigned char*)field, sdslen(field), 1); fptr = lpFind(zl, fptr, (unsigned char*)field, sdslen(field), 1);
...@@ -198,61 +1182,85 @@ int hashTypeSet(robj *o, sds field, sds value, int flags) { ...@@ -198,61 +1182,85 @@ int hashTypeSet(robj *o, sds field, sds value, int flags) {
/* Grab pointer to the value (fptr points to the field) */ /* Grab pointer to the value (fptr points to the field) */
vptr = lpNext(zl, fptr); vptr = lpNext(zl, fptr);
serverAssert(vptr != NULL); serverAssert(vptr != NULL);
update = 1; res = HSET_UPDATE;
/* Replace value */ /* Replace value */
zl = lpReplace(zl, &vptr, (unsigned char*)value, sdslen(value)); zl = lpReplace(zl, &vptr, (unsigned char *) setParams->value, sdslen(setParams->value));
} }
} }
if (!update) { if (res != HSET_UPDATE) {
/* Push new field/value pair onto the tail of the listpack */ /* Push new field/value pair onto the tail of the listpack */
zl = lpAppend(zl, (unsigned char*)field, sdslen(field)); zl = lpAppend(zl, (unsigned char*)field, sdslen(field));
zl = lpAppend(zl, (unsigned char*)value, sdslen(value)); zl = lpAppend(zl, (unsigned char*)setParams->value, sdslen(setParams->value));
} }
o->ptr = zl; o->ptr = zl;
goto out;
} else if (o->encoding == OBJ_ENCODING_LISTPACK_EX) {
listpackEx *lpt = o->ptr;
long long expireTime = HASH_LP_NO_TTL;
/* Check if the listpack needs to be converted to a hash table */ fptr = lpFirst(lpt->lp);
if (hashTypeLength(o) > server.hash_max_listpack_entries) if (fptr != NULL) {
hashTypeConvert(o, OBJ_ENCODING_HT); fptr = lpFind(lpt->lp, fptr, (unsigned char*)field, sdslen(field), 2);
} else if (o->encoding == OBJ_ENCODING_HT) { if (fptr != NULL) {
dict *ht = o->ptr; /* Grab pointer to the value (fptr points to the field) */
dictEntry *de, *existing; vptr = lpNext(lpt->lp, fptr);
sds v; serverAssert(vptr != NULL);
if (flags & HASH_SET_TAKE_VALUE) {
v = value; if (setParams) {
value = NULL; /* Replace value */
} else { lpt->lp = lpReplace(lpt->lp, &vptr,
v = sdsdup(value); (unsigned char *) setParams->value,
sdslen(setParams->value));
fptr = lpPrev(lpt->lp, vptr);
serverAssert(fptr != NULL);
res = HSET_UPDATE;
}
tptr = lpNext(lpt->lp, vptr);
serverAssert(tptr && lpGetIntegerValue(tptr, &expireTime));
/* Keep, update or clear TTL */
if (setParams && setParams->flags & HASH_SET_KEEP_FIELD) {
/* keep old field along with TTL */
} else if (exParams) {
res = hashTypeSetExpiryListpack(exParams, field, fptr, vptr, tptr,
expireAt);
if (res != HSETEX_OK)
goto out;
} else if (res == HSET_UPDATE && expireTime != HASH_LP_NO_TTL) {
/* Clear TTL */
listpackExUpdateExpiry(o, field, fptr, vptr, HASH_LP_NO_TTL);
} }
de = dictAddRaw(ht, field, &existing);
if (de) {
dictSetVal(ht, de, v);
if (flags & HASH_SET_TAKE_FIELD) {
field = NULL;
} else {
dictSetKey(ht, de, sdsdup(field));
} }
} else {
sdsfree(dictGetVal(existing));
dictSetVal(ht, existing, v);
update = 1;
} }
if (!fptr) {
if (setParams) {
listpackExAddNew(o, field, sdslen(field),
setParams->value, sdslen(setParams->value),
exParams ? expireAt : HASH_LP_NO_TTL);
} else { } else {
serverPanic("Unknown hash encoding"); res = HSETEX_NO_FIELD;
}
}
} }
out:
/* Check if the listpack needs to be converted to a hash table */
if (hashTypeLength(o, 0) > server.hash_max_listpack_entries)
hashTypeConvert(o, OBJ_ENCODING_HT, &db->hexpires);
/* Free SDS strings we did not referenced elsewhere if the flags return res;
* want this function to be responsible. */
if (flags & HASH_SET_TAKE_FIELD && field) sdsfree(field);
if (flags & HASH_SET_TAKE_VALUE && value) sdsfree(value);
return update;
} }
/* Delete an element from a hash. /* Delete an element from a hash.
* Return 1 on deleted and 0 on not found. */ *
int hashTypeDelete(robj *o, sds field) { * Return 1 on deleted and 0 on not found.
* isSdsField - 1 if the field is sds, 0 if it is hfield */
int hashTypeDelete(robj *o, void *field, int isSdsField) {
int deleted = 0; int deleted = 0;
int fieldLen = (isSdsField) ? sdslen((sds)field) : hfieldlen((hfield)field);
if (o->encoding == OBJ_ENCODING_LISTPACK) { if (o->encoding == OBJ_ENCODING_LISTPACK) {
unsigned char *zl, *fptr; unsigned char *zl, *fptr;
...@@ -260,7 +1268,7 @@ int hashTypeDelete(robj *o, sds field) { ...@@ -260,7 +1268,7 @@ int hashTypeDelete(robj *o, sds field) {
zl = o->ptr; zl = o->ptr;
fptr = lpFirst(zl); fptr = lpFirst(zl);
if (fptr != NULL) { if (fptr != NULL) {
fptr = lpFind(zl, fptr, (unsigned char*)field, sdslen(field), 1); fptr = lpFind(zl, fptr, (unsigned char*)field, fieldLen, 1);
if (fptr != NULL) { if (fptr != NULL) {
/* Delete both of the key and the value. */ /* Delete both of the key and the value. */
zl = lpDeleteRangeWithEntry(zl,&fptr,2); zl = lpDeleteRangeWithEntry(zl,&fptr,2);
...@@ -268,10 +1276,26 @@ int hashTypeDelete(robj *o, sds field) { ...@@ -268,10 +1276,26 @@ int hashTypeDelete(robj *o, sds field) {
deleted = 1; deleted = 1;
} }
} }
} else if (o->encoding == OBJ_ENCODING_LISTPACK_EX) {
unsigned char *fptr;
listpackEx *lpt = o->ptr;
fptr = lpFirst(lpt->lp);
if (fptr != NULL) {
fptr = lpFind(lpt->lp, fptr, (unsigned char*)field, fieldLen, 2);
if (fptr != NULL) {
/* Delete field, value and ttl */
lpt->lp = lpDeleteRangeWithEntry(lpt->lp, &fptr, 3);
deleted = 1;
}
}
} else if (o->encoding == OBJ_ENCODING_HT) { } else if (o->encoding == OBJ_ENCODING_HT) {
/* dictDelete() will call dictHfieldDestructor() */
dictUseStoredKeyApi((dict*)o->ptr, isSdsField ? 0 : 1);
if (dictDelete((dict*)o->ptr, field) == C_OK) { if (dictDelete((dict*)o->ptr, field) == C_OK) {
deleted = 1; deleted = 1;
} }
dictUseStoredKeyApi((dict*)o->ptr, 0);
} else { } else {
serverPanic("Unknown hash encoding"); serverPanic("Unknown hash encoding");
...@@ -279,14 +1303,33 @@ int hashTypeDelete(robj *o, sds field) { ...@@ -279,14 +1303,33 @@ int hashTypeDelete(robj *o, sds field) {
return deleted; return deleted;
} }
/* Return the number of elements in a hash. */ /* Return the number of elements in a hash.
unsigned long hashTypeLength(const robj *o) { *
* Note, subtractExpiredFields=1 might be pricy in case there are many HFEs
*/
unsigned long hashTypeLength(const robj *o, int subtractExpiredFields) {
unsigned long length = ULONG_MAX; unsigned long length = ULONG_MAX;
if (o->encoding == OBJ_ENCODING_LISTPACK) { if (o->encoding == OBJ_ENCODING_LISTPACK) {
length = lpLength(o->ptr) / 2; length = lpLength(o->ptr) / 2;
} else if (o->encoding == OBJ_ENCODING_LISTPACK_EX) {
listpackEx *lpt = o->ptr;
length = lpLength(lpt->lp) / 3;
if (subtractExpiredFields && lpt->meta.trash == 0)
length -= listpackExExpireDryRun(o);
} else if (o->encoding == OBJ_ENCODING_HT) { } else if (o->encoding == OBJ_ENCODING_HT) {
length = dictSize((const dict*)o->ptr); uint64_t expiredItems = 0;
dict *d = (dict*)o->ptr;
if (subtractExpiredFields && isDictWithMetaHFE(d)) {
dictExpireMetadata *meta = (dictExpireMetadata *) dictMetadata(d);
/* If dict registered in global HFE DS */
if (meta->expireMeta.trash == 0)
expiredItems = ebExpireDryRun(meta->hfe,
&hashFieldExpireBucketsType,
commandTimeSnapshot());
}
length = dictSize(d) - expiredItems;
} else { } else {
serverPanic("Unknown hash encoding"); serverPanic("Unknown hash encoding");
} }
...@@ -298,9 +1341,13 @@ hashTypeIterator *hashTypeInitIterator(robj *subject) { ...@@ -298,9 +1341,13 @@ hashTypeIterator *hashTypeInitIterator(robj *subject) {
hi->subject = subject; hi->subject = subject;
hi->encoding = subject->encoding; hi->encoding = subject->encoding;
if (hi->encoding == OBJ_ENCODING_LISTPACK) { if (hi->encoding == OBJ_ENCODING_LISTPACK ||
hi->encoding == OBJ_ENCODING_LISTPACK_EX)
{
hi->fptr = NULL; hi->fptr = NULL;
hi->vptr = NULL; hi->vptr = NULL;
hi->tptr = NULL;
hi->expire_time = EB_EXPIRE_TIME_INVALID;
} else if (hi->encoding == OBJ_ENCODING_HT) { } else if (hi->encoding == OBJ_ENCODING_HT) {
hi->di = dictGetIterator(subject->ptr); hi->di = dictGetIterator(subject->ptr);
} else { } else {
...@@ -317,7 +1364,8 @@ void hashTypeReleaseIterator(hashTypeIterator *hi) { ...@@ -317,7 +1364,8 @@ void hashTypeReleaseIterator(hashTypeIterator *hi) {
/* Move to the next entry in the hash. Return C_OK when the next entry /* Move to the next entry in the hash. Return C_OK when the next entry
* could be found and C_ERR when the iterator reaches the end. */ * could be found and C_ERR when the iterator reaches the end. */
int hashTypeNext(hashTypeIterator *hi) { int hashTypeNext(hashTypeIterator *hi, int skipExpiredFields) {
hi->expire_time = EB_EXPIRE_TIME_INVALID;
if (hi->encoding == OBJ_ENCODING_LISTPACK) { if (hi->encoding == OBJ_ENCODING_LISTPACK) {
unsigned char *zl; unsigned char *zl;
unsigned char *fptr, *vptr; unsigned char *fptr, *vptr;
...@@ -344,8 +1392,56 @@ int hashTypeNext(hashTypeIterator *hi) { ...@@ -344,8 +1392,56 @@ int hashTypeNext(hashTypeIterator *hi) {
/* fptr, vptr now point to the first or next pair */ /* fptr, vptr now point to the first or next pair */
hi->fptr = fptr; hi->fptr = fptr;
hi->vptr = vptr; hi->vptr = vptr;
} else if (hi->encoding == OBJ_ENCODING_LISTPACK_EX) {
long long expire_time;
unsigned char *zl = hashTypeListpackGetLp(hi->subject);
unsigned char *fptr, *vptr, *tptr;
fptr = hi->fptr;
vptr = hi->vptr;
tptr = hi->tptr;
if (fptr == NULL) {
/* Initialize cursor */
serverAssert(vptr == NULL);
fptr = lpFirst(zl);
} else {
/* Advance cursor */
serverAssert(tptr != NULL);
fptr = lpNext(zl, tptr);
}
if (fptr == NULL) return C_ERR;
while (fptr != NULL) {
/* Grab pointer to the value (fptr points to the field) */
vptr = lpNext(zl, fptr);
serverAssert(vptr != NULL);
tptr = lpNext(zl, vptr);
serverAssert(tptr && lpGetIntegerValue(tptr, &expire_time));
if (!skipExpiredFields || !hashTypeIsExpired(hi->subject, expire_time))
break;
fptr = lpNext(zl, tptr);
}
if (fptr == NULL) return C_ERR;
/* fptr, vptr now point to the first or next pair */
hi->fptr = fptr;
hi->vptr = vptr;
hi->tptr = tptr;
hi->expire_time = (expire_time != HASH_LP_NO_TTL) ? (uint64_t) expire_time : EB_EXPIRE_TIME_INVALID;
} else if (hi->encoding == OBJ_ENCODING_HT) { } else if (hi->encoding == OBJ_ENCODING_HT) {
if ((hi->de = dictNext(hi->di)) == NULL) return C_ERR;
while ((hi->de = dictNext(hi->di)) != NULL) {
hi->expire_time = hfieldGetExpireTime(dictGetKey(hi->de));
/* this condition still valid if expire_time equals EB_EXPIRE_TIME_INVALID */
if (skipExpiredFields && ((mstime_t)hi->expire_time < commandTimeSnapshot()))
continue;
return C_OK;
}
return C_ERR;
} else { } else {
serverPanic("Unknown hash encoding"); serverPanic("Unknown hash encoding");
} }
...@@ -357,28 +1453,45 @@ int hashTypeNext(hashTypeIterator *hi) { ...@@ -357,28 +1453,45 @@ int hashTypeNext(hashTypeIterator *hi) {
void hashTypeCurrentFromListpack(hashTypeIterator *hi, int what, void hashTypeCurrentFromListpack(hashTypeIterator *hi, int what,
unsigned char **vstr, unsigned char **vstr,
unsigned int *vlen, unsigned int *vlen,
long long *vll) long long *vll,
uint64_t *expireTime)
{ {
serverAssert(hi->encoding == OBJ_ENCODING_LISTPACK); serverAssert(hi->encoding == OBJ_ENCODING_LISTPACK ||
hi->encoding == OBJ_ENCODING_LISTPACK_EX);
if (what & OBJ_HASH_KEY) { if (what & OBJ_HASH_KEY) {
*vstr = lpGetValue(hi->fptr, vlen, vll); *vstr = lpGetValue(hi->fptr, vlen, vll);
} else { } else {
*vstr = lpGetValue(hi->vptr, vlen, vll); *vstr = lpGetValue(hi->vptr, vlen, vll);
} }
if (expireTime)
*expireTime = hi->expire_time;
} }
/* Get the field or value at iterator cursor, for an iterator on a hash value /* Get the field or value at iterator cursor, for an iterator on a hash value
* encoded as a hash table. Prototype is similar to * encoded as a hash table. Prototype is similar to
* `hashTypeGetFromHashTable`. */ * `hashTypeGetFromHashTable`.
sds hashTypeCurrentFromHashTable(hashTypeIterator *hi, int what) { *
* expireTime - If parameter is not null, then the function will return the expire
* time of the field. If expiry not set, return EB_EXPIRE_TIME_INVALID
*/
void hashTypeCurrentFromHashTable(hashTypeIterator *hi, int what, char **str, size_t *len, uint64_t *expireTime) {
serverAssert(hi->encoding == OBJ_ENCODING_HT); serverAssert(hi->encoding == OBJ_ENCODING_HT);
hfield key = NULL;
if (what & OBJ_HASH_KEY) { if (what & OBJ_HASH_KEY) {
return dictGetKey(hi->de); key = dictGetKey(hi->de);
*str = key;
*len = hfieldlen(key);
} else { } else {
return dictGetVal(hi->de); sds val = dictGetVal(hi->de);
*str = val;
*len = sdslen(val);
} }
if (expireTime)
*expireTime = hi->expire_time;
} }
/* Higher level function of hashTypeCurrent*() that returns the hash value /* Higher level function of hashTypeCurrent*() that returns the hash value
...@@ -391,14 +1504,24 @@ sds hashTypeCurrentFromHashTable(hashTypeIterator *hi, int what) { ...@@ -391,14 +1504,24 @@ sds hashTypeCurrentFromHashTable(hashTypeIterator *hi, int what) {
* If *vll is populated *vstr is set to NULL, so the caller * If *vll is populated *vstr is set to NULL, so the caller
* can always check the function return by checking the return value * can always check the function return by checking the return value
* type checking if vstr == NULL. */ * type checking if vstr == NULL. */
void hashTypeCurrentObject(hashTypeIterator *hi, int what, unsigned char **vstr, unsigned int *vlen, long long *vll) { void hashTypeCurrentObject(hashTypeIterator *hi,
if (hi->encoding == OBJ_ENCODING_LISTPACK) { int what,
unsigned char **vstr,
unsigned int *vlen,
long long *vll,
uint64_t *expireTime)
{
if (hi->encoding == OBJ_ENCODING_LISTPACK ||
hi->encoding == OBJ_ENCODING_LISTPACK_EX)
{
*vstr = NULL; *vstr = NULL;
hashTypeCurrentFromListpack(hi, what, vstr, vlen, vll); hashTypeCurrentFromListpack(hi, what, vstr, vlen, vll, expireTime);
} else if (hi->encoding == OBJ_ENCODING_HT) { } else if (hi->encoding == OBJ_ENCODING_HT) {
sds ele = hashTypeCurrentFromHashTable(hi, what); char *ele;
size_t eleLen;
hashTypeCurrentFromHashTable(hi, what, &ele, &eleLen, expireTime);
*vstr = (unsigned char*) ele; *vstr = (unsigned char*) ele;
*vlen = sdslen(ele); *vlen = eleLen;
} else { } else {
serverPanic("Unknown hash encoding"); serverPanic("Unknown hash encoding");
} }
...@@ -411,12 +1534,32 @@ sds hashTypeCurrentObjectNewSds(hashTypeIterator *hi, int what) { ...@@ -411,12 +1534,32 @@ sds hashTypeCurrentObjectNewSds(hashTypeIterator *hi, int what) {
unsigned int vlen; unsigned int vlen;
long long vll; long long vll;
hashTypeCurrentObject(hi,what,&vstr,&vlen,&vll); hashTypeCurrentObject(hi,what,&vstr,&vlen,&vll, NULL);
if (vstr) return sdsnewlen(vstr,vlen); if (vstr) return sdsnewlen(vstr,vlen);
return sdsfromlonglong(vll); return sdsfromlonglong(vll);
} }
robj *hashTypeLookupWriteOrCreate(client *c, robj *key) { /* Return the key at the current iterator position as a new hfield string. */
hfield hashTypeCurrentObjectNewHfield(hashTypeIterator *hi) {
char buf[LONG_STR_SIZE];
unsigned char *vstr;
unsigned int vlen;
long long vll;
uint64_t expireTime;
hfield hf;
hashTypeCurrentObject(hi,OBJ_HASH_KEY,&vstr,&vlen,&vll, &expireTime);
if (!vstr) {
vlen = ll2string(buf, sizeof(buf), vll);
vstr = (unsigned char *) buf;
}
hf = hfieldNew(vstr,vlen, expireTime != EB_EXPIRE_TIME_INVALID);
return hf;
}
static robj *hashTypeLookupWriteOrCreate(client *c, robj *key) {
robj *o = lookupKeyWrite(c->db,key); robj *o = lookupKeyWrite(c->db,key);
if (checkType(c,o,OBJ_HASH)) return NULL; if (checkType(c,o,OBJ_HASH)) return NULL;
...@@ -434,25 +1577,43 @@ void hashTypeConvertListpack(robj *o, int enc) { ...@@ -434,25 +1577,43 @@ void hashTypeConvertListpack(robj *o, int enc) {
if (enc == OBJ_ENCODING_LISTPACK) { if (enc == OBJ_ENCODING_LISTPACK) {
/* Nothing to do... */ /* Nothing to do... */
} else if (enc == OBJ_ENCODING_LISTPACK_EX) {
unsigned char *p;
/* Append HASH_LP_NO_TTL to each field name - value pair. */
p = lpFirst(o->ptr);
while (p != NULL) {
p = lpNext(o->ptr, p);
serverAssert(p);
o->ptr = lpInsertInteger(o->ptr, HASH_LP_NO_TTL, p, LP_AFTER, &p);
p = lpNext(o->ptr, p);
}
listpackEx *lpt = listpackExCreate();
lpt->lp = o->ptr;
o->encoding = OBJ_ENCODING_LISTPACK_EX;
o->ptr = lpt;
} else if (enc == OBJ_ENCODING_HT) { } else if (enc == OBJ_ENCODING_HT) {
hashTypeIterator *hi; hashTypeIterator *hi;
dict *dict; dict *dict;
int ret; int ret;
hi = hashTypeInitIterator(o); hi = hashTypeInitIterator(o);
dict = dictCreate(&hashDictType); dict = dictCreate(&mstrHashDictType);
/* Presize the dict to avoid rehashing */ /* Presize the dict to avoid rehashing */
dictExpand(dict,hashTypeLength(o)); dictExpand(dict,hashTypeLength(o, 0));
while (hashTypeNext(hi) != C_ERR) { while (hashTypeNext(hi, 0) != C_ERR) {
sds key, value;
key = hashTypeCurrentObjectNewSds(hi,OBJ_HASH_KEY); hfield key = hashTypeCurrentObjectNewHfield(hi);
value = hashTypeCurrentObjectNewSds(hi,OBJ_HASH_VALUE); sds value = hashTypeCurrentObjectNewSds(hi,OBJ_HASH_VALUE);
dictUseStoredKeyApi(dict, 1);
ret = dictAdd(dict, key, value); ret = dictAdd(dict, key, value);
dictUseStoredKeyApi(dict, 0);
if (ret != DICT_OK) { if (ret != DICT_OK) {
sdsfree(key); sdsfree(value); /* Needed for gcc ASAN */ hfieldFree(key); sdsfree(value); /* Needed for gcc ASAN */
hashTypeReleaseIterator(hi); /* Needed for gcc ASAN */ hashTypeReleaseIterator(hi); /* Needed for gcc ASAN */
serverLogHexDump(LL_WARNING,"listpack with dup elements dump", serverLogHexDump(LL_WARNING,"listpack with dup elements dump",
o->ptr,lpBytes(o->ptr)); o->ptr,lpBytes(o->ptr));
...@@ -468,9 +1629,69 @@ void hashTypeConvertListpack(robj *o, int enc) { ...@@ -468,9 +1629,69 @@ void hashTypeConvertListpack(robj *o, int enc) {
} }
} }
void hashTypeConvert(robj *o, int enc) { void hashTypeConvertListpackEx(robj *o, int enc, ebuckets *hexpires) {
serverAssert(o->encoding == OBJ_ENCODING_LISTPACK_EX);
if (enc == OBJ_ENCODING_LISTPACK_EX) {
return;
} else if (enc == OBJ_ENCODING_HT) {
int ret;
hashTypeIterator *hi;
dict *dict;
dictExpireMetadata *dictExpireMeta;
listpackEx *lpt = o->ptr;
uint64_t minExpire = hashTypeGetMinExpire(o);
if (hexpires && lpt->meta.trash != 1)
ebRemove(hexpires, &hashExpireBucketsType, o);
dict = dictCreate(&mstrHashDictTypeWithHFE);
dictExpand(dict,hashTypeLength(o, 0));
dictExpireMeta = (dictExpireMetadata *) dictMetadata(dict);
/* Fillup dict HFE metadata */
dictExpireMeta->key = lpt->key; /* reference key in keyspace */
dictExpireMeta->hfe = ebCreate(); /* Allocate HFE DS */
dictExpireMeta->expireMeta.trash = 1; /* mark as trash (as long it wasn't ebAdd()) */
hi = hashTypeInitIterator(o);
while (hashTypeNext(hi, 0) != C_ERR) {
hfield key = hashTypeCurrentObjectNewHfield(hi);
sds value = hashTypeCurrentObjectNewSds(hi,OBJ_HASH_VALUE);
dictUseStoredKeyApi(dict, 1);
ret = dictAdd(dict, key, value);
dictUseStoredKeyApi(dict, 0);
if (ret != DICT_OK) {
hfieldFree(key); sdsfree(value); /* Needed for gcc ASAN */
hashTypeReleaseIterator(hi); /* Needed for gcc ASAN */
serverLogHexDump(LL_WARNING,"listpack with dup elements dump",
lpt->lp,lpBytes(lpt->lp));
serverPanic("Listpack corruption detected");
}
if (hi->expire_time != EB_EXPIRE_TIME_INVALID)
ebAdd(&dictExpireMeta->hfe, &hashFieldExpireBucketsType, key, hi->expire_time);
}
hashTypeReleaseIterator(hi);
listpackExFree(lpt);
o->encoding = OBJ_ENCODING_HT;
o->ptr = dict;
if (hexpires && minExpire != EB_EXPIRE_TIME_INVALID)
ebAdd(hexpires, &hashExpireBucketsType, o, minExpire);
} else {
serverPanic("Unknown hash encoding: %d", enc);
}
}
/* NOTE: hexpires can be NULL (Won't register in global HFE DS) */
void hashTypeConvert(robj *o, int enc, ebuckets *hexpires) {
if (o->encoding == OBJ_ENCODING_LISTPACK) { if (o->encoding == OBJ_ENCODING_LISTPACK) {
hashTypeConvertListpack(o, enc); hashTypeConvertListpack(o, enc);
} else if (o->encoding == OBJ_ENCODING_LISTPACK_EX) {
hashTypeConvertListpackEx(o, enc, hexpires);
} else if (o->encoding == OBJ_ENCODING_HT) { } else if (o->encoding == OBJ_ENCODING_HT) {
serverPanic("Not implemented"); serverPanic("Not implemented");
} else { } else {
...@@ -483,7 +1704,7 @@ void hashTypeConvert(robj *o, int enc) { ...@@ -483,7 +1704,7 @@ void hashTypeConvert(robj *o, int enc) {
* has the same encoding as the original one. * has the same encoding as the original one.
* *
* The resulting object always has refcount set to 1 */ * The resulting object always has refcount set to 1 */
robj *hashTypeDup(robj *o) { robj *hashTypeDup(robj *o, sds newkey, uint64_t *minHashExpire) {
robj *hobj; robj *hobj;
hashTypeIterator *hi; hashTypeIterator *hi;
...@@ -496,87 +1717,386 @@ robj *hashTypeDup(robj *o) { ...@@ -496,87 +1717,386 @@ robj *hashTypeDup(robj *o) {
memcpy(new_zl, zl, sz); memcpy(new_zl, zl, sz);
hobj = createObject(OBJ_HASH, new_zl); hobj = createObject(OBJ_HASH, new_zl);
hobj->encoding = OBJ_ENCODING_LISTPACK; hobj->encoding = OBJ_ENCODING_LISTPACK;
} else if(o->encoding == OBJ_ENCODING_HT){ } else if(o->encoding == OBJ_ENCODING_LISTPACK_EX) {
dict *d = dictCreate(&hashDictType); listpackEx *lpt = o->ptr;
if (lpt->meta.trash == 0)
*minHashExpire = ebGetMetaExpTime(&lpt->meta);
listpackEx *dup = listpackExCreate();
dup->key = newkey;
size_t sz = lpBytes(lpt->lp);
dup->lp = lpNew(sz);
memcpy(dup->lp, lpt->lp, sz);
hobj = createObject(OBJ_HASH, dup);
hobj->encoding = OBJ_ENCODING_LISTPACK_EX;
} else if(o->encoding == OBJ_ENCODING_HT) {
dictExpireMetadata *dictExpireMetaSrc, *dictExpireMetaDst = NULL;
dict *d;
/* If dict doesn't have HFE metadata, then create a new dict without it */
if (!isDictWithMetaHFE(o->ptr)) {
d = dictCreate(&mstrHashDictType);
} else {
/* Create a new dict with HFE metadata */
d = dictCreate(&mstrHashDictTypeWithHFE);
dictExpireMetaSrc = (dictExpireMetadata *) dictMetadata((dict *) o->ptr);
dictExpireMetaDst = (dictExpireMetadata *) dictMetadata(d);
dictExpireMetaDst->key = newkey; /* reference key in keyspace */
dictExpireMetaDst->hfe = ebCreate(); /* Allocate HFE DS */
dictExpireMetaDst->expireMeta.trash = 1; /* mark as trash (as long it wasn't ebAdd()) */
/* Extract the minimum expire time of the source hash (Will be used by caller
* to register the new hash in the global ebuckets, i.e db->hexpires) */
if (dictExpireMetaSrc->expireMeta.trash == 0)
*minHashExpire = ebGetMetaExpTime(&dictExpireMetaSrc->expireMeta);
}
dictExpand(d, dictSize((const dict*)o->ptr)); dictExpand(d, dictSize((const dict*)o->ptr));
hi = hashTypeInitIterator(o); hi = hashTypeInitIterator(o);
while (hashTypeNext(hi) != C_ERR) { while (hashTypeNext(hi, 0) != C_ERR) {
sds field, value; uint64_t expireTime;
sds newfield, newvalue; sds newfield, newvalue;
/* Extract a field-value pair from an original hash object.*/ /* Extract a field-value pair from an original hash object.*/
field = hashTypeCurrentFromHashTable(hi, OBJ_HASH_KEY); char *field, *value;
value = hashTypeCurrentFromHashTable(hi, OBJ_HASH_VALUE); size_t fieldLen, valueLen;
newfield = sdsdup(field); hashTypeCurrentFromHashTable(hi, OBJ_HASH_KEY, &field, &fieldLen, &expireTime);
newvalue = sdsdup(value); if (expireTime == EB_EXPIRE_TIME_INVALID) {
newfield = hfieldNew(field, fieldLen, 0);
} else {
newfield = hfieldNew(field, fieldLen, 1);
ebAdd(&dictExpireMetaDst->hfe, &hashFieldExpireBucketsType, newfield, expireTime);
}
hashTypeCurrentFromHashTable(hi, OBJ_HASH_VALUE, &value, &valueLen, NULL);
newvalue = sdsnewlen(value, valueLen);
/* Add a field-value pair to a new hash object. */
dictUseStoredKeyApi(d, 1);
dictAdd(d,newfield,newvalue);
dictUseStoredKeyApi(d, 0);
}
hashTypeReleaseIterator(hi);
hobj = createObject(OBJ_HASH, d);
hobj->encoding = OBJ_ENCODING_HT;
} else {
serverPanic("Unknown hash encoding");
}
return hobj;
}
/* Create a new sds string from the listpack entry. */
sds hashSdsFromListpackEntry(listpackEntry *e) {
return e->sval ? sdsnewlen(e->sval, e->slen) : sdsfromlonglong(e->lval);
}
/* Reply with bulk string from the listpack entry. */
void hashReplyFromListpackEntry(client *c, listpackEntry *e) {
if (e->sval)
addReplyBulkCBuffer(c, e->sval, e->slen);
else
addReplyBulkLongLong(c, e->lval);
}
/* Return random element from a non empty hash.
* 'key' and 'val' will be set to hold the element.
* The memory in them is not to be freed or modified by the caller.
* 'val' can be NULL in which case it's not extracted. */
void hashTypeRandomElement(robj *hashobj, unsigned long hashsize, listpackEntry *key, listpackEntry *val) {
if (hashobj->encoding == OBJ_ENCODING_HT) {
dictEntry *de = dictGetFairRandomKey(hashobj->ptr);
hfield field = dictGetKey(de);
key->sval = (unsigned char*)field;
key->slen = hfieldlen(field);
if (val) {
sds s = dictGetVal(de);
val->sval = (unsigned char*)s;
val->slen = sdslen(s);
}
} else if (hashobj->encoding == OBJ_ENCODING_LISTPACK) {
lpRandomPair(hashobj->ptr, hashsize, key, val, 2);
} else if (hashobj->encoding == OBJ_ENCODING_LISTPACK_EX) {
lpRandomPair(hashTypeListpackGetLp(hashobj), hashsize, key, val, 3);
} else {
serverPanic("Unknown hash encoding");
}
}
/*
* Active expiration of fields in hash
*
* Called by hashTypeDbActiveExpire() for each hash registered in the HFE DB
* (db->hexpires) with an expiration-time less than or equal current time.
*
* This callback performs the following actions for each hash:
* - Delete expired fields as by calling ebExpire(hash)
* - If afterward there are future fields to expire, it will update the hash in
* HFE DB with the next hash-field minimum expiration time by returning
* ACT_UPDATE_EXP_ITEM.
* - If the hash has no more fields to expire, it is removed from the HFE DB
* by returning ACT_REMOVE_EXP_ITEM.
* - If hash has no more fields afterward, it will remove the hash from keyspace.
*/
static ExpireAction hashTypeActiveExpire(eItem _hashObj, void *ctx) {
robj *hashObj = (robj *) _hashObj;
ActiveExpireCtx *activeExpireCtx = (ActiveExpireCtx *) ctx;
sds keystr = NULL;
ExpireInfo info = {0};
/* If no more quota left for this callback, stop */
if (activeExpireCtx->fieldsToExpireQuota == 0)
return ACT_STOP_ACTIVE_EXP;
if (hashObj->encoding == OBJ_ENCODING_LISTPACK_EX) {
info = (ExpireInfo){
.maxToExpire = activeExpireCtx->fieldsToExpireQuota,
.now = commandTimeSnapshot(),
.itemsExpired = 0};
listpackExExpire(activeExpireCtx->db, hashObj, &info);
server.stat_expired_hash_fields += info.itemsExpired;
keystr = ((listpackEx*)hashObj->ptr)->key;
} else {
serverAssert(hashObj->encoding == OBJ_ENCODING_HT);
dict *d = hashObj->ptr;
dictExpireMetadata *dictExpireMeta = (dictExpireMetadata *) dictMetadata(d);
OnFieldExpireCtx onFieldExpireCtx = {
.hashObj = hashObj,
.db = activeExpireCtx->db
};
info = (ExpireInfo){
.maxToExpire = activeExpireCtx->fieldsToExpireQuota,
.onExpireItem = onFieldExpire,
.ctx = &onFieldExpireCtx,
.now = commandTimeSnapshot()
};
ebExpire(&dictExpireMeta->hfe, &hashFieldExpireBucketsType, &info);
keystr = dictExpireMeta->key;
}
/* Update quota left */
activeExpireCtx->fieldsToExpireQuota -= info.itemsExpired;
/* If hash has no more fields to expire, remove it from HFE DB */
if (info.nextExpireTime == 0) {
if (hashTypeLength(hashObj, 0) == 0) {
robj *key = createStringObject(keystr, sdslen(keystr));
dbDelete(activeExpireCtx->db, key);
notifyKeyspaceEvent(NOTIFY_GENERIC,"del",key, activeExpireCtx->db->id);
server.dirty++;
signalModifiedKey(NULL, &server.db[0], key);
decrRefCount(key);
}
return ACT_REMOVE_EXP_ITEM;
} else {
/* Hash has more fields to expire. Keep hash to pending items that will
* be added back to global HFE DS at the end of ebExpire() */
ExpireMeta *expireMeta = hashGetExpireMeta(hashObj);
ebSetMetaExpTime(expireMeta, info.nextExpireTime);
return ACT_UPDATE_EXP_ITEM;
}
}
/* Return the next/minimum expiry time of the hash-field. This is useful if a
* field with the minimum expiry is deleted, and you want to get the next
* minimum expiry. Otherwise, consider using hashTypeGetMinExpire() which will
* be faster. If there is no field with expiry, returns EB_EXPIRE_TIME_INVALID */
uint64_t hashTypeGetNextTimeToExpire(robj *o) {
if (o->encoding == OBJ_ENCODING_LISTPACK) {
return EB_EXPIRE_TIME_INVALID;
} else if (o->encoding == OBJ_ENCODING_LISTPACK_EX) {
return listpackExGetMinExpire(o);
} else {
serverAssert(o->encoding == OBJ_ENCODING_HT);
dict *d = o->ptr;
if (!isDictWithMetaHFE(d))
return EB_EXPIRE_TIME_INVALID;
dictExpireMetadata *expireMeta = (dictExpireMetadata *) dictMetadata(d);
return ebGetNextTimeToExpire(expireMeta->hfe, &hashFieldExpireBucketsType);
}
}
/* Return the next/minimum expiry time of the hash-field.
* If not found, return EB_EXPIRE_TIME_INVALID */
uint64_t hashTypeGetMinExpire(robj *o) {
ExpireMeta *expireMeta = NULL;
if (o->encoding == OBJ_ENCODING_LISTPACK) {
return EB_EXPIRE_TIME_INVALID;
} else if (o->encoding == OBJ_ENCODING_LISTPACK_EX) {
listpackEx *lpt = o->ptr;
expireMeta = &lpt->meta;
} else {
serverAssert(o->encoding == OBJ_ENCODING_HT);
dict *d = o->ptr;
if (!isDictWithMetaHFE(d))
return EB_EXPIRE_TIME_INVALID;
expireMeta = &((dictExpireMetadata *) dictMetadata(d))->expireMeta;
}
/* Keep aside next hash-field expiry before updating HFE DS. Verify it is not trash */
if (expireMeta->trash == 1)
return EB_EXPIRE_TIME_INVALID;
return ebGetMetaExpTime(expireMeta);
}
uint64_t hashTypeRemoveFromExpires(ebuckets *hexpires, robj *o) {
if (o->encoding == OBJ_ENCODING_LISTPACK) {
return EB_EXPIRE_TIME_INVALID;
} else if (o->encoding == OBJ_ENCODING_HT) {
/* If dict doesn't holds HFE metadata */
if (!isDictWithMetaHFE(o->ptr))
return EB_EXPIRE_TIME_INVALID;
}
uint64_t expireTime = ebGetExpireTime(&hashExpireBucketsType, o);
/* If registered in global HFE DS then remove it (not trash) */
if (expireTime != EB_EXPIRE_TIME_INVALID)
ebRemove(hexpires, &hashExpireBucketsType, o);
return expireTime;
}
/* Add hash to global HFE DS and update key for notifications.
*
* key - must be the same key instance that is persisted in db->dict
* expireTime - expiration in msec.
* If eq. 0 then the hash will be added to the global HFE DS with
* the minimum expiration time that is already written in advance
* to attached metadata (which considered as trash as long as it is
* not attached to global HFE DS).
*
* Precondition: It is a hash of type listpackex or HT with HFE metadata.
*/
void hashTypeAddToExpires(redisDb *db, sds key, robj *hashObj, uint64_t expireTime) {
if (expireTime > EB_EXPIRE_TIME_MAX)
return;
/* Add a field-value pair to a new hash object. */ if (hashObj->encoding == OBJ_ENCODING_LISTPACK_EX) {
dictAdd(d,newfield,newvalue); listpackEx *lpt = hashObj->ptr;
lpt->key = key;
expireTime = (expireTime) ? expireTime : ebGetMetaExpTime(&lpt->meta);
ebAdd(&db->hexpires, &hashExpireBucketsType, hashObj, expireTime);
} else if (hashObj->encoding == OBJ_ENCODING_HT) {
dict *d = hashObj->ptr;
if (isDictWithMetaHFE(d)) {
dictExpireMetadata *meta = (dictExpireMetadata *) dictMetadata(d);
expireTime = (expireTime) ? expireTime : ebGetMetaExpTime(&meta->expireMeta);
meta->key = key;
ebAdd(&db->hexpires, &hashExpireBucketsType, hashObj, expireTime);
} }
hashTypeReleaseIterator(hi);
hobj = createObject(OBJ_HASH, d);
hobj->encoding = OBJ_ENCODING_HT;
} else {
serverPanic("Unknown hash encoding");
} }
return hobj;
} }
/* Create a new sds string from the listpack entry. */ /* DB active expire and update hashes with time-expiration on fields.
sds hashSdsFromListpackEntry(listpackEntry *e) { *
return e->sval ? sdsnewlen(e->sval, e->slen) : sdsfromlonglong(e->lval); * The callback function hashTypeActiveExpire() is invoked for each hash registered
* in the HFE DB (db->expires) with an expiration-time less than or equal to the
* current time. This callback performs the following actions for each hash:
* - If the hash has one or more fields to expire, it will delete those fields.
* - If there are more fields to expire, it will update the hash with the next
* expiration time in HFE DB.
* - If the hash has no more fields to expire, it is removed from the HFE DB.
* - If the hash has no more fields, it is removed from the main DB.
*
* Returns number of fields active-expired.
*/
uint64_t hashTypeDbActiveExpire(redisDb *db, uint32_t maxFieldsToExpire) {
ActiveExpireCtx ctx = { .db = db, .fieldsToExpireQuota = maxFieldsToExpire };
ExpireInfo info = {
.maxToExpire = UINT64_MAX, /* Only maxFieldsToExpire play a role */
.onExpireItem = hashTypeActiveExpire,
.ctx = &ctx,
.now = commandTimeSnapshot(),
.itemsExpired = 0};
ebExpire(&db->hexpires, &hashExpireBucketsType, &info);
/* Return number of fields active-expired */
return maxFieldsToExpire - ctx.fieldsToExpireQuota;
} }
/* Reply with bulk string from the listpack entry. */ void hashTypeFree(robj *o) {
void hashReplyFromListpackEntry(client *c, listpackEntry *e) { switch (o->encoding) {
if (e->sval) case OBJ_ENCODING_HT:
addReplyBulkCBuffer(c, e->sval, e->slen); /* Verify hash is not registered in global HFE ds */
else if (isDictWithMetaHFE((dict*)o->ptr)) {
addReplyBulkLongLong(c, e->lval); dictExpireMetadata *m = (dictExpireMetadata *)dictMetadata((dict*)o->ptr);
serverAssert(m->expireMeta.trash == 1);
}
dictRelease((dict*) o->ptr);
break;
case OBJ_ENCODING_LISTPACK:
lpFree(o->ptr);
break;
case OBJ_ENCODING_LISTPACK_EX:
/* Verify hash is not registered in global HFE ds */
serverAssert(((listpackEx *) o->ptr)->meta.trash == 1);
listpackExFree(o->ptr);
break;
default:
serverPanic("Unknown hash encoding type");
break;
}
} }
/* Return random element from a non empty hash. /* Attempts to update the reference to the new key. Now it's only used in defrag. */
* 'key' and 'val' will be set to hold the element. void hashTypeUpdateKeyRef(robj *o, sds newkey) {
* The memory in them is not to be freed or modified by the caller. if (o->encoding == OBJ_ENCODING_LISTPACK_EX) {
* 'val' can be NULL in which case it's not extracted. */ listpackEx *lpt = o->ptr;
void hashTypeRandomElement(robj *hashobj, unsigned long hashsize, listpackEntry *key, listpackEntry *val) { lpt->key = newkey;
if (hashobj->encoding == OBJ_ENCODING_HT) { } else if (o->encoding == OBJ_ENCODING_HT && isDictWithMetaHFE(o->ptr)) {
dictEntry *de = dictGetFairRandomKey(hashobj->ptr); dictExpireMetadata *dictExpireMeta = (dictExpireMetadata *)dictMetadata((dict*)o->ptr);
sds s = dictGetKey(de); dictExpireMeta->key = newkey;
key->sval = (unsigned char*)s;
key->slen = sdslen(s);
if (val) {
sds s = dictGetVal(de);
val->sval = (unsigned char*)s;
val->slen = sdslen(s);
}
} else if (hashobj->encoding == OBJ_ENCODING_LISTPACK) {
lpRandomPair(hashobj->ptr, hashsize, key, val);
} else { } else {
serverPanic("Unknown hash encoding"); /* Nothing to do. */
} }
} }
ebuckets *hashTypeGetDictMetaHFE(dict *d) {
dictExpireMetadata *dictExpireMeta = (dictExpireMetadata *) dictMetadata(d);
return &dictExpireMeta->hfe;
}
/*----------------------------------------------------------------------------- /*-----------------------------------------------------------------------------
* Hash type commands * Hash type commands
*----------------------------------------------------------------------------*/ *----------------------------------------------------------------------------*/
void hsetnxCommand(client *c) { void hsetnxCommand(client *c) {
int isHashDeleted;
robj *o; robj *o;
if ((o = hashTypeLookupWriteOrCreate(c,c->argv[1])) == NULL) return; if ((o = hashTypeLookupWriteOrCreate(c,c->argv[1])) == NULL) return;
if (hashTypeExists(o, c->argv[2]->ptr)) { if (hashTypeExists(c->db, o, c->argv[2]->ptr, &isHashDeleted)) {
addReply(c, shared.czero); addReply(c, shared.czero);
} else { return;
hashTypeTryConversion(o,c->argv,2,3); }
hashTypeSet(o,c->argv[2]->ptr,c->argv[3]->ptr,HASH_SET_COPY);
/* Field expired and in turn hash deleted. Create new one! */
if (isHashDeleted) {
o = createHashObject();
dbAdd(c->db,c->argv[1],o);
}
hashTypeTryConversion(c->db, o,c->argv,2,3);
hashTypeSet(c->db, o,c->argv[2]->ptr,c->argv[3]->ptr,HASH_SET_COPY);
addReply(c, shared.cone); addReply(c, shared.cone);
signalModifiedKey(c,c->db,c->argv[1]); signalModifiedKey(c,c->db,c->argv[1]);
notifyKeyspaceEvent(NOTIFY_HASH,"hset",c->argv[1],c->db->id); notifyKeyspaceEvent(NOTIFY_HASH,"hset",c->argv[1],c->db->id);
server.dirty++; server.dirty++;
}
} }
void hsetCommand(client *c) { void hsetCommand(client *c) {
...@@ -589,10 +2109,10 @@ void hsetCommand(client *c) { ...@@ -589,10 +2109,10 @@ void hsetCommand(client *c) {
} }
if ((o = hashTypeLookupWriteOrCreate(c,c->argv[1])) == NULL) return; if ((o = hashTypeLookupWriteOrCreate(c,c->argv[1])) == NULL) return;
hashTypeTryConversion(o,c->argv,2,c->argc-1); hashTypeTryConversion(c->db,o,c->argv,2,c->argc-1);
for (i = 2; i < c->argc; i += 2) for (i = 2; i < c->argc; i += 2)
created += !hashTypeSet(o,c->argv[i]->ptr,c->argv[i+1]->ptr,HASH_SET_COPY); created += !hashTypeSet(c->db, o,c->argv[i]->ptr,c->argv[i+1]->ptr,HASH_SET_COPY);
/* HMSET (deprecated) and HSET return value is different. */ /* HMSET (deprecated) and HSET return value is different. */
char *cmdname = c->argv[0]->ptr; char *cmdname = c->argv[0]->ptr;
...@@ -617,14 +2137,21 @@ void hincrbyCommand(client *c) { ...@@ -617,14 +2137,21 @@ void hincrbyCommand(client *c) {
if (getLongLongFromObjectOrReply(c,c->argv[3],&incr,NULL) != C_OK) return; if (getLongLongFromObjectOrReply(c,c->argv[3],&incr,NULL) != C_OK) return;
if ((o = hashTypeLookupWriteOrCreate(c,c->argv[1])) == NULL) return; if ((o = hashTypeLookupWriteOrCreate(c,c->argv[1])) == NULL) return;
if (hashTypeGetValue(o,c->argv[2]->ptr,&vstr,&vlen,&value) == C_OK) {
GetFieldRes res = hashTypeGetValue(c->db,o,c->argv[2]->ptr,&vstr,&vlen,&value);
if (res == GETF_OK) {
if (vstr) { if (vstr) {
if (string2ll((char*)vstr,vlen,&value) == 0) { if (string2ll((char*)vstr,vlen,&value) == 0) {
addReplyError(c,"hash value is not an integer"); addReplyError(c,"hash value is not an integer");
return; return;
} }
} /* Else hashTypeGetValue() already stored it into &value */ } /* Else hashTypeGetValue() already stored it into &value */
} else if ((res == GETF_NOT_FOUND) || (res == GETF_EXPIRED)) {
value = 0;
} else { } else {
/* Field expired and in turn hash deleted. Create new one! */
o = createHashObject();
dbAdd(c->db,c->argv[1],o);
value = 0; value = 0;
} }
...@@ -636,7 +2163,7 @@ void hincrbyCommand(client *c) { ...@@ -636,7 +2163,7 @@ void hincrbyCommand(client *c) {
} }
value += incr; value += incr;
new = sdsfromlonglong(value); new = sdsfromlonglong(value);
hashTypeSet(o,c->argv[2]->ptr,new,HASH_SET_TAKE_VALUE); hashTypeSet(c->db, o,c->argv[2]->ptr,new,HASH_SET_TAKE_VALUE | HASH_SET_KEEP_FIELD);
addReplyLongLong(c,value); addReplyLongLong(c,value);
signalModifiedKey(c,c->db,c->argv[1]); signalModifiedKey(c,c->db,c->argv[1]);
notifyKeyspaceEvent(NOTIFY_HASH,"hincrby",c->argv[1],c->db->id); notifyKeyspaceEvent(NOTIFY_HASH,"hincrby",c->argv[1],c->db->id);
...@@ -657,7 +2184,8 @@ void hincrbyfloatCommand(client *c) { ...@@ -657,7 +2184,8 @@ void hincrbyfloatCommand(client *c) {
return; return;
} }
if ((o = hashTypeLookupWriteOrCreate(c,c->argv[1])) == NULL) return; if ((o = hashTypeLookupWriteOrCreate(c,c->argv[1])) == NULL) return;
if (hashTypeGetValue(o,c->argv[2]->ptr,&vstr,&vlen,&ll) == C_OK) { GetFieldRes res = hashTypeGetValue(c->db, o,c->argv[2]->ptr,&vstr,&vlen,&ll);
if (res == GETF_OK) {
if (vstr) { if (vstr) {
if (string2ld((char*)vstr,vlen,&value) == 0) { if (string2ld((char*)vstr,vlen,&value) == 0) {
addReplyError(c,"hash value is not a float"); addReplyError(c,"hash value is not a float");
...@@ -666,7 +2194,12 @@ void hincrbyfloatCommand(client *c) { ...@@ -666,7 +2194,12 @@ void hincrbyfloatCommand(client *c) {
} else { } else {
value = (long double)ll; value = (long double)ll;
} }
} else if ((res == GETF_NOT_FOUND) || (res == GETF_EXPIRED)) {
value = 0;
} else { } else {
/* Field expired and in turn hash deleted. Create new one! */
o = createHashObject();
dbAdd(c->db,c->argv[1],o);
value = 0; value = 0;
} }
...@@ -679,7 +2212,7 @@ void hincrbyfloatCommand(client *c) { ...@@ -679,7 +2212,7 @@ void hincrbyfloatCommand(client *c) {
char buf[MAX_LONG_DOUBLE_CHARS]; char buf[MAX_LONG_DOUBLE_CHARS];
int len = ld2string(buf,sizeof(buf),value,LD_STR_HUMAN); int len = ld2string(buf,sizeof(buf),value,LD_STR_HUMAN);
new = sdsnewlen(buf,len); new = sdsnewlen(buf,len);
hashTypeSet(o,c->argv[2]->ptr,new,HASH_SET_TAKE_VALUE); hashTypeSet(c->db, o,c->argv[2]->ptr,new,HASH_SET_TAKE_VALUE | HASH_SET_KEEP_FIELD);
addReplyBulkCBuffer(c,buf,len); addReplyBulkCBuffer(c,buf,len);
signalModifiedKey(c,c->db,c->argv[1]); signalModifiedKey(c,c->db,c->argv[1]);
notifyKeyspaceEvent(NOTIFY_HASH,"hincrbyfloat",c->argv[1],c->db->id); notifyKeyspaceEvent(NOTIFY_HASH,"hincrbyfloat",c->argv[1],c->db->id);
...@@ -695,17 +2228,18 @@ void hincrbyfloatCommand(client *c) { ...@@ -695,17 +2228,18 @@ void hincrbyfloatCommand(client *c) {
decrRefCount(newobj); decrRefCount(newobj);
} }
static void addHashFieldToReply(client *c, robj *o, sds field) { static GetFieldRes addHashFieldToReply(client *c, robj *o, sds field) {
if (o == NULL) { if (o == NULL) {
addReplyNull(c); addReplyNull(c);
return; return GETF_NOT_FOUND;
} }
unsigned char *vstr = NULL; unsigned char *vstr = NULL;
unsigned int vlen = UINT_MAX; unsigned int vlen = UINT_MAX;
long long vll = LLONG_MAX; long long vll = LLONG_MAX;
if (hashTypeGetValue(o, field, &vstr, &vlen, &vll) == C_OK) { GetFieldRes res = hashTypeGetValue(c->db, o, field, &vstr, &vlen, &vll);
if (res == GETF_OK) {
if (vstr) { if (vstr) {
addReplyBulkCBuffer(c, vstr, vlen); addReplyBulkCBuffer(c, vstr, vlen);
} else { } else {
...@@ -714,6 +2248,7 @@ static void addHashFieldToReply(client *c, robj *o, sds field) { ...@@ -714,6 +2248,7 @@ static void addHashFieldToReply(client *c, robj *o, sds field) {
} else { } else {
addReplyNull(c); addReplyNull(c);
} }
return res;
} }
void hgetCommand(client *c) { void hgetCommand(client *c) {
...@@ -726,6 +2261,7 @@ void hgetCommand(client *c) { ...@@ -726,6 +2261,7 @@ void hgetCommand(client *c) {
} }
void hmgetCommand(client *c) { void hmgetCommand(client *c) {
GetFieldRes res = GETF_OK;
robj *o; robj *o;
int i; int i;
...@@ -735,8 +2271,17 @@ void hmgetCommand(client *c) { ...@@ -735,8 +2271,17 @@ void hmgetCommand(client *c) {
if (checkType(c,o,OBJ_HASH)) return; if (checkType(c,o,OBJ_HASH)) return;
addReplyArrayLen(c, c->argc-2); addReplyArrayLen(c, c->argc-2);
for (i = 2; i < c->argc; i++) { for (i = 2; i < c->argc ; i++) {
addHashFieldToReply(c, o, c->argv[i]->ptr);
res = addHashFieldToReply(c, o, c->argv[i]->ptr);
/* If hash got lazy expired since all fields are expired (o is invalid),
* then fill the rest with trivial nulls and return */
if (res == GETF_EXPIRED_HASH) {
while (++i < c->argc)
addReplyNull(c);
return;
}
} }
} }
...@@ -748,9 +2293,9 @@ void hdelCommand(client *c) { ...@@ -748,9 +2293,9 @@ void hdelCommand(client *c) {
checkType(c,o,OBJ_HASH)) return; checkType(c,o,OBJ_HASH)) return;
for (j = 2; j < c->argc; j++) { for (j = 2; j < c->argc; j++) {
if (hashTypeDelete(o,c->argv[j]->ptr)) { if (hashTypeDelete(o,c->argv[j]->ptr,1)) {
deleted++; deleted++;
if (hashTypeLength(o) == 0) { if (hashTypeLength(o, 0) == 0) {
dbDelete(c->db,c->argv[1]); dbDelete(c->db,c->argv[1]);
keyremoved = 1; keyremoved = 1;
break; break;
...@@ -774,31 +2319,47 @@ void hlenCommand(client *c) { ...@@ -774,31 +2319,47 @@ void hlenCommand(client *c) {
if ((o = lookupKeyReadOrReply(c,c->argv[1],shared.czero)) == NULL || if ((o = lookupKeyReadOrReply(c,c->argv[1],shared.czero)) == NULL ||
checkType(c,o,OBJ_HASH)) return; checkType(c,o,OBJ_HASH)) return;
addReplyLongLong(c,hashTypeLength(o)); addReplyLongLong(c,hashTypeLength(o, 0));
} }
void hstrlenCommand(client *c) { void hstrlenCommand(client *c) {
robj *o; robj *o;
unsigned char *vstr = NULL;
unsigned int vlen = UINT_MAX;
long long vll = LLONG_MAX;
if ((o = lookupKeyReadOrReply(c,c->argv[1],shared.czero)) == NULL || if ((o = lookupKeyReadOrReply(c,c->argv[1],shared.czero)) == NULL ||
checkType(c,o,OBJ_HASH)) return; checkType(c,o,OBJ_HASH)) return;
addReplyLongLong(c,hashTypeGetValueLength(o,c->argv[2]->ptr));
GetFieldRes res = hashTypeGetValue(c->db, o, c->argv[2]->ptr, &vstr, &vlen, &vll);
if (res == GETF_NOT_FOUND || res == GETF_EXPIRED || res == GETF_EXPIRED_HASH) {
addReply(c, shared.czero);
return;
}
size_t len = vstr ? vlen : sdigits10(vll);
addReplyLongLong(c,len);
} }
static void addHashIteratorCursorToReply(client *c, hashTypeIterator *hi, int what) { static void addHashIteratorCursorToReply(client *c, hashTypeIterator *hi, int what) {
if (hi->encoding == OBJ_ENCODING_LISTPACK) { if (hi->encoding == OBJ_ENCODING_LISTPACK ||
hi->encoding == OBJ_ENCODING_LISTPACK_EX)
{
unsigned char *vstr = NULL; unsigned char *vstr = NULL;
unsigned int vlen = UINT_MAX; unsigned int vlen = UINT_MAX;
long long vll = LLONG_MAX; long long vll = LLONG_MAX;
hashTypeCurrentFromListpack(hi, what, &vstr, &vlen, &vll); hashTypeCurrentFromListpack(hi, what, &vstr, &vlen, &vll, NULL);
if (vstr) if (vstr)
addReplyBulkCBuffer(c, vstr, vlen); addReplyBulkCBuffer(c, vstr, vlen);
else else
addReplyBulkLongLong(c, vll); addReplyBulkLongLong(c, vll);
} else if (hi->encoding == OBJ_ENCODING_HT) { } else if (hi->encoding == OBJ_ENCODING_HT) {
sds value = hashTypeCurrentFromHashTable(hi, what); char *value;
addReplyBulkCBuffer(c, value, sdslen(value)); size_t len;
hashTypeCurrentFromHashTable(hi, what, &value, &len, NULL);
addReplyBulkCBuffer(c, value, len);
} else { } else {
serverPanic("Unknown hash encoding"); serverPanic("Unknown hash encoding");
} }
...@@ -816,7 +2377,7 @@ void genericHgetallCommand(client *c, int flags) { ...@@ -816,7 +2377,7 @@ void genericHgetallCommand(client *c, int flags) {
/* We return a map if the user requested keys and values, like in the /* We return a map if the user requested keys and values, like in the
* HGETALL case. Otherwise to use a flat array makes more sense. */ * HGETALL case. Otherwise to use a flat array makes more sense. */
length = hashTypeLength(o); length = hashTypeLength(o, 1 /*subtractExpiredFields*/);
if (flags & OBJ_HASH_KEY && flags & OBJ_HASH_VALUE) { if (flags & OBJ_HASH_KEY && flags & OBJ_HASH_VALUE) {
addReplyMapLen(c, length); addReplyMapLen(c, length);
} else { } else {
...@@ -824,7 +2385,12 @@ void genericHgetallCommand(client *c, int flags) { ...@@ -824,7 +2385,12 @@ void genericHgetallCommand(client *c, int flags) {
} }
hi = hashTypeInitIterator(o); hi = hashTypeInitIterator(o);
while (hashTypeNext(hi) != C_ERR) {
/* Skip expired fields if the hash has an expire time set at global HFE DS. We could
* set it to constant 1, but then it will make another lookup for each field expiration */
int skipExpiredFields = (EB_EXPIRE_TIME_INVALID == hashTypeGetMinExpire(o)) ? 0 : 1;
while (hashTypeNext(hi, skipExpiredFields) != C_ERR) {
if (flags & OBJ_HASH_KEY) { if (flags & OBJ_HASH_KEY) {
addHashIteratorCursorToReply(c, hi, OBJ_HASH_KEY); addHashIteratorCursorToReply(c, hi, OBJ_HASH_KEY);
count++; count++;
...@@ -856,10 +2422,11 @@ void hgetallCommand(client *c) { ...@@ -856,10 +2422,11 @@ void hgetallCommand(client *c) {
void hexistsCommand(client *c) { void hexistsCommand(client *c) {
robj *o; robj *o;
int isHashDeleted;
if ((o = lookupKeyReadOrReply(c,c->argv[1],shared.czero)) == NULL || if ((o = lookupKeyReadOrReply(c,c->argv[1],shared.czero)) == NULL ||
checkType(c,o,OBJ_HASH)) return; checkType(c,o,OBJ_HASH)) return;
addReply(c, hashTypeExists(o,c->argv[2]->ptr) ? shared.cone : shared.czero); addReply(c,hashTypeExists(c->db,o,c->argv[2]->ptr,&isHashDeleted) ? shared.cone : shared.czero);
} }
void hscanCommand(client *c) { void hscanCommand(client *c) {
...@@ -869,6 +2436,7 @@ void hscanCommand(client *c) { ...@@ -869,6 +2436,7 @@ void hscanCommand(client *c) {
if (parseScanCursorOrReply(c,c->argv[2],&cursor) == C_ERR) return; if (parseScanCursorOrReply(c,c->argv[2],&cursor) == C_ERR) return;
if ((o = lookupKeyReadOrReply(c,c->argv[1],shared.emptyscan)) == NULL || if ((o = lookupKeyReadOrReply(c,c->argv[1],shared.emptyscan)) == NULL ||
checkType(c,o,OBJ_HASH)) return; checkType(c,o,OBJ_HASH)) return;
scanGenericCommand(c,o,cursor); scanGenericCommand(c,o,cursor);
} }
...@@ -906,7 +2474,8 @@ void hrandfieldWithCountCommand(client *c, long l, int withvalues) { ...@@ -906,7 +2474,8 @@ void hrandfieldWithCountCommand(client *c, long l, int withvalues) {
if ((hash = lookupKeyReadOrReply(c,c->argv[1],shared.emptyarray)) if ((hash = lookupKeyReadOrReply(c,c->argv[1],shared.emptyarray))
== NULL || checkType(c,hash,OBJ_HASH)) return; == NULL || checkType(c,hash,OBJ_HASH)) return;
size = hashTypeLength(hash); /* TODO: Active-expire */
size = hashTypeLength(hash, 0);
if(l >= 0) { if(l >= 0) {
count = (unsigned long) l; count = (unsigned long) l;
...@@ -932,22 +2501,25 @@ void hrandfieldWithCountCommand(client *c, long l, int withvalues) { ...@@ -932,22 +2501,25 @@ void hrandfieldWithCountCommand(client *c, long l, int withvalues) {
else else
addReplyArrayLen(c, count); addReplyArrayLen(c, count);
if (hash->encoding == OBJ_ENCODING_HT) { if (hash->encoding == OBJ_ENCODING_HT) {
sds key, value;
while (count--) { while (count--) {
dictEntry *de = dictGetFairRandomKey(hash->ptr); dictEntry *de = dictGetFairRandomKey(hash->ptr);
key = dictGetKey(de); hfield field = dictGetKey(de);
value = dictGetVal(de); sds value = dictGetVal(de);
if (withvalues && c->resp > 2) if (withvalues && c->resp > 2)
addReplyArrayLen(c,2); addReplyArrayLen(c,2);
addReplyBulkCBuffer(c, key, sdslen(key)); addReplyBulkCBuffer(c, field, hfieldlen(field));
if (withvalues) if (withvalues)
addReplyBulkCBuffer(c, value, sdslen(value)); addReplyBulkCBuffer(c, value, sdslen(value));
if (c->flags & CLIENT_CLOSE_ASAP) if (c->flags & CLIENT_CLOSE_ASAP)
break; break;
} }
} else if (hash->encoding == OBJ_ENCODING_LISTPACK) { } else if (hash->encoding == OBJ_ENCODING_LISTPACK ||
hash->encoding == OBJ_ENCODING_LISTPACK_EX)
{
listpackEntry *keys, *vals = NULL; listpackEntry *keys, *vals = NULL;
unsigned long limit, sample_count; unsigned long limit, sample_count;
unsigned char *lp = hashTypeListpackGetLp(hash);
int tuple_len = hash->encoding == OBJ_ENCODING_LISTPACK ? 2 : 3;
limit = count > HRANDFIELD_RANDOM_SAMPLE_LIMIT ? HRANDFIELD_RANDOM_SAMPLE_LIMIT : count; limit = count > HRANDFIELD_RANDOM_SAMPLE_LIMIT ? HRANDFIELD_RANDOM_SAMPLE_LIMIT : count;
keys = zmalloc(sizeof(listpackEntry)*limit); keys = zmalloc(sizeof(listpackEntry)*limit);
...@@ -956,7 +2528,7 @@ void hrandfieldWithCountCommand(client *c, long l, int withvalues) { ...@@ -956,7 +2528,7 @@ void hrandfieldWithCountCommand(client *c, long l, int withvalues) {
while (count) { while (count) {
sample_count = count > limit ? limit : count; sample_count = count > limit ? limit : count;
count -= sample_count; count -= sample_count;
lpRandomPairs(hash->ptr, sample_count, keys, vals); lpRandomPairs(lp, sample_count, keys, vals, tuple_len);
hrandfieldReplyWithListpack(c, sample_count, keys, vals); hrandfieldReplyWithListpack(c, sample_count, keys, vals);
if (c->flags & CLIENT_CLOSE_ASAP) if (c->flags & CLIENT_CLOSE_ASAP)
break; break;
...@@ -979,7 +2551,7 @@ void hrandfieldWithCountCommand(client *c, long l, int withvalues) { ...@@ -979,7 +2551,7 @@ void hrandfieldWithCountCommand(client *c, long l, int withvalues) {
* elements inside the hash: simply return the whole hash. */ * elements inside the hash: simply return the whole hash. */
if(count >= size) { if(count >= size) {
hashTypeIterator *hi = hashTypeInitIterator(hash); hashTypeIterator *hi = hashTypeInitIterator(hash);
while (hashTypeNext(hi) != C_ERR) { while (hashTypeNext(hi, 0) != C_ERR) {
if (withvalues && c->resp > 2) if (withvalues && c->resp > 2)
addReplyArrayLen(c,2); addReplyArrayLen(c,2);
addHashIteratorCursorToReply(c, hi, OBJ_HASH_KEY); addHashIteratorCursorToReply(c, hi, OBJ_HASH_KEY);
...@@ -998,12 +2570,16 @@ void hrandfieldWithCountCommand(client *c, long l, int withvalues) { ...@@ -998,12 +2570,16 @@ void hrandfieldWithCountCommand(client *c, long l, int withvalues) {
* *
* And it is inefficient to repeatedly pick one random element from a * And it is inefficient to repeatedly pick one random element from a
* listpack in CASE 4. So we use this instead. */ * listpack in CASE 4. So we use this instead. */
if (hash->encoding == OBJ_ENCODING_LISTPACK) { if (hash->encoding == OBJ_ENCODING_LISTPACK ||
hash->encoding == OBJ_ENCODING_LISTPACK_EX)
{
unsigned char *lp = hashTypeListpackGetLp(hash);
int tuple_len = hash->encoding == OBJ_ENCODING_LISTPACK ? 2 : 3;
listpackEntry *keys, *vals = NULL; listpackEntry *keys, *vals = NULL;
keys = zmalloc(sizeof(listpackEntry)*count); keys = zmalloc(sizeof(listpackEntry)*count);
if (withvalues) if (withvalues)
vals = zmalloc(sizeof(listpackEntry)*count); vals = zmalloc(sizeof(listpackEntry)*count);
serverAssert(lpRandomPairsUnique(hash->ptr, count, keys, vals) == count); serverAssert(lpRandomPairsUnique(lp, count, keys, vals, tuple_len) == count);
hrandfieldReplyWithListpack(c, count, keys, vals); hrandfieldReplyWithListpack(c, count, keys, vals);
zfree(keys); zfree(keys);
zfree(vals); zfree(vals);
...@@ -1021,12 +2597,12 @@ void hrandfieldWithCountCommand(client *c, long l, int withvalues) { ...@@ -1021,12 +2597,12 @@ void hrandfieldWithCountCommand(client *c, long l, int withvalues) {
* used into CASE 4 is highly inefficient. */ * used into CASE 4 is highly inefficient. */
if (count*HRANDFIELD_SUB_STRATEGY_MUL > size) { if (count*HRANDFIELD_SUB_STRATEGY_MUL > size) {
/* Hashtable encoding (generic implementation) */ /* Hashtable encoding (generic implementation) */
dict *d = dictCreate(&sdsReplyDictType); dict *d = dictCreate(&sdsReplyDictType); /* without metadata! */
dictExpand(d, size); dictExpand(d, size);
hashTypeIterator *hi = hashTypeInitIterator(hash); hashTypeIterator *hi = hashTypeInitIterator(hash);
/* Add all the elements into the temporary dictionary. */ /* Add all the elements into the temporary dictionary. */
while ((hashTypeNext(hi)) != C_ERR) { while ((hashTypeNext(hi, 0)) != C_ERR) {
int ret = DICT_ERR; int ret = DICT_ERR;
sds key, value = NULL; sds key, value = NULL;
...@@ -1044,7 +2620,9 @@ void hrandfieldWithCountCommand(client *c, long l, int withvalues) { ...@@ -1044,7 +2620,9 @@ void hrandfieldWithCountCommand(client *c, long l, int withvalues) {
while (size > count) { while (size > count) {
dictEntry *de; dictEntry *de;
de = dictGetFairRandomKey(d); de = dictGetFairRandomKey(d);
dictUseStoredKeyApi(d, 1);
dictUnlink(d,dictGetKey(de)); dictUnlink(d,dictGetKey(de));
dictUseStoredKeyApi(d, 0);
sdsfree(dictGetKey(de)); sdsfree(dictGetKey(de));
sdsfree(dictGetVal(de)); sdsfree(dictGetVal(de));
dictFreeUnlinkedEntry(d,de); dictFreeUnlinkedEntry(d,de);
...@@ -1134,6 +2712,516 @@ void hrandfieldCommand(client *c) { ...@@ -1134,6 +2712,516 @@ void hrandfieldCommand(client *c) {
return; return;
} }
hashTypeRandomElement(hash,hashTypeLength(hash),&ele,NULL); hashTypeRandomElement(hash,hashTypeLength(hash, 0),&ele,NULL);
hashReplyFromListpackEntry(c, &ele); hashReplyFromListpackEntry(c, &ele);
} }
/*-----------------------------------------------------------------------------
* Hash Field with optional expiry (based on mstr)
*----------------------------------------------------------------------------*/
static hfield _hfieldNew(const void *field, size_t fieldlen, int withExpireMeta,
int trymalloc)
{
if (!withExpireMeta)
return mstrNew(field, fieldlen, trymalloc);
hfield hf = mstrNewWithMeta(&mstrFieldKind, field, fieldlen,
(mstrFlags) 1 << HFIELD_META_EXPIRE, trymalloc);
if (!hf) return NULL;
ExpireMeta *expireMeta = mstrMetaRef(hf, &mstrFieldKind, HFIELD_META_EXPIRE);
/* as long as it is not inside ebuckets, it is considered trash */
expireMeta->trash = 1;
return hf;
}
/* if expireAt is 0, then expireAt is ignored and no metadata is attached */
hfield hfieldNew(const void *field, size_t fieldlen, int withExpireMeta) {
return _hfieldNew(field, fieldlen, withExpireMeta, 0);
}
hfield hfieldTryNew(const void *field, size_t fieldlen, int withExpireMeta) {
return _hfieldNew(field, fieldlen, withExpireMeta, 1);
}
int hfieldIsExpireAttached(hfield field) {
return mstrIsMetaAttached(field) && mstrGetFlag(field, (int) HFIELD_META_EXPIRE);
}
static ExpireMeta* hfieldGetExpireMeta(const eItem field) {
/* extract the expireMeta from the field of type mstr */
return mstrMetaRef(field, &mstrFieldKind, (int) HFIELD_META_EXPIRE);
}
/* returned value is unix time in milliseconds */
uint64_t hfieldGetExpireTime(hfield field) {
if (!hfieldIsExpireAttached(field))
return EB_EXPIRE_TIME_INVALID;
ExpireMeta *expireMeta = mstrMetaRef(field, &mstrFieldKind, (int) HFIELD_META_EXPIRE);
if (expireMeta->trash)
return EB_EXPIRE_TIME_INVALID;
return ebGetMetaExpTime(expireMeta);
}
/* Remove TTL from the field. Assumed ExpireMeta is attached and has valid value */
static void hfieldPersist(robj *hashObj, hfield field) {
uint64_t fieldExpireTime = hfieldGetExpireTime(field);
if (fieldExpireTime == EB_EXPIRE_TIME_INVALID)
return;
/* if field is set with expire, then dict must has HFE metadata attached */
dict *d = hashObj->ptr;
dictExpireMetadata *dictExpireMeta = (dictExpireMetadata *)dictMetadata(d);
/* If field has valid expiry then dict must have valid metadata as well */
serverAssert(dictExpireMeta->expireMeta.trash == 0);
/* Remove field from private HFE DS */
ebRemove(&dictExpireMeta->hfe, &hashFieldExpireBucketsType, field);
/* Don't have to update global HFE DS. It's unnecessary. Implementing this
* would introduce significant complexity and overhead for an operation that
* isn't critical. In the worst case scenario, the hash will be efficiently
* updated later by an active-expire operation, or it will be removed by the
* hash's dbGenericDelete() function. */
}
int hfieldIsExpired(hfield field) {
/* Condition remains valid even if hfieldGetExpireTime() returns EB_EXPIRE_TIME_INVALID,
* as the constant is equivalent to (EB_EXPIRE_TIME_MAX + 1). */
return ( (mstime_t)hfieldGetExpireTime(field) < commandTimeSnapshot());
}
/*-----------------------------------------------------------------------------
* Hash Field Expiration (HFE)
*----------------------------------------------------------------------------*/
/* Can be called either by active-expire cron job or query from the client */
static void propagateHashFieldDeletion(redisDb *db, sds key, char *field, size_t fieldLen) {
robj *argv[] = {
shared.hdel,
createStringObject((char*) key, sdslen(key)),
createStringObject(field, fieldLen)
};
enterExecutionUnit(1, 0);
int prev_replication_allowed = server.replication_allowed;
server.replication_allowed = 1;
alsoPropagate(db->id,argv, 3, PROPAGATE_AOF|PROPAGATE_REPL);
server.replication_allowed = prev_replication_allowed;
exitExecutionUnit();
/* Propagate the HDEL command */
postExecutionUnitOperations();
decrRefCount(argv[1]);
decrRefCount(argv[2]);
}
/* Called during active expiration of hash-fields. Propagate to replica & Delete. */
static ExpireAction onFieldExpire(eItem item, void *ctx) {
OnFieldExpireCtx *expCtx = ctx;
hfield hf = item;
dict *d = expCtx->hashObj->ptr;
dictExpireMetadata *dictExpireMeta = (dictExpireMetadata *) dictMetadata(d);
propagateHashFieldDeletion(expCtx->db, dictExpireMeta->key, hf, hfieldlen(hf));
serverAssert(hashTypeDelete(expCtx->hashObj, hf, 0) == 1);
server.stat_expired_hash_fields++;
return ACT_REMOVE_EXP_ITEM;
}
/* Retrieve the ExpireMeta associated with the hash.
* The caller is responsible for ensuring that it is indeed attached. */
static ExpireMeta *hashGetExpireMeta(const eItem hash) {
robj *hashObj = (robj *)hash;
if (hashObj->encoding == OBJ_ENCODING_LISTPACK_EX) {
listpackEx *lpt = hashObj->ptr;
return &lpt->meta;
} else if (hashObj->encoding == OBJ_ENCODING_HT) {
dict *d = hashObj->ptr;
dictExpireMetadata *dictExpireMeta = (dictExpireMetadata *) dictMetadata(d);
return &dictExpireMeta->expireMeta;
} else {
serverPanic("Unknown encoding: %d", hashObj->encoding);
}
}
/* HTTL key <FIELDS count field [field ...]> */
static void httlGenericCommand(client *c, const char *cmd, long long basetime, int unit) {
UNUSED(cmd);
robj *hashObj;
long numFields = 0, numFieldsAt = 3;
/* Read the hash object */
if ((hashObj = lookupKeyReadOrReply(c, c->argv[1], shared.emptyarray)) == NULL ||
checkType(c, hashObj, OBJ_HASH)) return;
if (strcasecmp(c->argv[numFieldsAt-1]->ptr, "FIELDS")) {
addReplyError(c, "Mandatory argument FIELDS is missing or not at the right position");
return;
}
/* Read number of fields */
if (getRangeLongFromObjectOrReply(c, c->argv[numFieldsAt], 1, LONG_MAX,
&numFields, "Number of fields must be a positive integer") != C_OK)
return;
/* Verify `numFields` is consistent with number of arguments */
if (numFields > (c->argc - numFieldsAt - 1)) {
addReplyError(c, "Parameter `numFileds` is more than number of arguments");
return;
}
if (hashObj->encoding == OBJ_ENCODING_LISTPACK) {
void *lp = hashObj->ptr;
addReplyArrayLen(c, numFields);
for (int i = 0 ; i < numFields ; i++) {
sds field = c->argv[numFieldsAt+1+i]->ptr;
void *fptr = lpFirst(lp);
if (fptr != NULL)
fptr = lpFind(lp, fptr, (unsigned char *) field, sdslen(field), 1);
if (!fptr)
addReplyLongLong(c, HFE_GET_NO_FIELD);
else
addReplyLongLong(c, HFE_GET_NO_TTL);
}
return;
} else if (hashObj->encoding == OBJ_ENCODING_LISTPACK_EX) {
listpackEx *lpt = hashObj->ptr;
addReplyArrayLen(c, numFields);
for (int i = 0 ; i < numFields ; i++) {
long long expire;
sds field = c->argv[numFieldsAt+1+i]->ptr;
void *fptr = lpFirst(lpt->lp);
if (fptr != NULL)
fptr = lpFind(lpt->lp, fptr, (unsigned char *) field, sdslen(field), 2);
if (!fptr) {
addReplyLongLong(c, HFE_GET_NO_FIELD);
continue;
}
fptr = lpNext(lpt->lp, fptr);
serverAssert(fptr);
fptr = lpNext(lpt->lp, fptr);
serverAssert(fptr && lpGetIntegerValue(fptr, &expire));
if (expire == HASH_LP_NO_TTL) {
addReplyLongLong(c, HFE_GET_NO_TTL);
continue;
}
if (expire <= commandTimeSnapshot()) {
addReplyLongLong(c, HFE_GET_NO_FIELD);
continue;
}
if (unit == UNIT_SECONDS)
addReplyLongLong(c, (expire + 999 - basetime) / 1000);
else
addReplyLongLong(c, (expire - basetime));
}
return;
} else if (hashObj->encoding == OBJ_ENCODING_HT) {
dict *d = hashObj->ptr;
addReplyArrayLen(c, numFields);
for (int i = 0 ; i < numFields ; i++) {
sds field = c->argv[numFieldsAt+1+i]->ptr;
dictEntry *de = dictFind(d, field);
if (de == NULL) {
addReplyLongLong(c, HFE_GET_NO_FIELD);
continue;
}
hfield hf = dictGetKey(de);
uint64_t expire = hfieldGetExpireTime(hf);
if (expire == EB_EXPIRE_TIME_INVALID) {
addReplyLongLong(c, HFE_GET_NO_TTL); /* no ttl */
continue;
}
if ( (long long) expire < commandTimeSnapshot()) {
addReplyLongLong(c, HFE_GET_NO_FIELD);
continue;
}
if (unit == UNIT_SECONDS)
addReplyLongLong(c, (expire + 999 - basetime) / 1000);
else
addReplyLongLong(c, (expire - basetime));
}
return;
} else {
serverPanic("Unknown encoding: %d", hashObj->encoding);
}
}
/* This is the generic command implementation for HEXPIRE, HPEXPIRE, HEXPIREAT
* and HPEXPIREAT. Because the command second argument may be relative or absolute
* the "basetime" argument is used to signal what the base time is (either 0
* for *AT variants of the command, or the current time for relative expires).
*
* unit is either UNIT_SECONDS or UNIT_MILLISECONDS, and is only used for
* the argv[2] parameter. The basetime is always specified in milliseconds.
*
* Additional flags are supported and parsed via parseExtendedExpireArguments */
static void hexpireGenericCommand(client *c, const char *cmd, long long basetime, int unit) {
long numFields = 0, numFieldsAt = 4;
long long expire; /* unix time in msec */
int expireSetCond = 0;
robj *hashObj, *keyArg = c->argv[1], *expireArg = c->argv[2];
/* Read the hash object */
if ((hashObj = lookupKeyWriteOrReply(c, keyArg, shared.emptyarray)) == NULL ||
checkType(c, hashObj, OBJ_HASH)) return;
/* Read the expiry time from command */
if (getLongLongFromObjectOrReply(c, expireArg, &expire, NULL) != C_OK)
return;
/* Check expire overflow */
if (expire > (long long) EB_EXPIRE_TIME_MAX) {
addReplyErrorExpireTime(c);
return;
}
if (unit == UNIT_SECONDS) {
if (expire > (long long) EB_EXPIRE_TIME_MAX / 1000) {
addReplyErrorExpireTime(c);
return;
}
expire *= 1000;
} else {
if (expire > (long long) EB_EXPIRE_TIME_MAX) {
addReplyErrorExpireTime(c);
return;
}
}
if (expire > (long long) EB_EXPIRE_TIME_MAX - basetime) {
addReplyErrorExpireTime(c);
return;
}
expire += basetime;
/* Read optional expireSetCond [NX|XX|GT|LT] */
char *optArg = c->argv[3]->ptr;
if (!strcasecmp(optArg, "nx")) {
expireSetCond = HFE_NX; ++numFieldsAt;
} else if (!strcasecmp(optArg, "xx")) {
expireSetCond = HFE_XX; ++numFieldsAt;
} else if (!strcasecmp(optArg, "gt")) {
expireSetCond = HFE_GT; ++numFieldsAt;
} else if (!strcasecmp(optArg, "lt")) {
expireSetCond = HFE_LT; ++numFieldsAt;
}
if (strcasecmp(c->argv[numFieldsAt-1]->ptr, "FIELDS")) {
addReplyError(c, "Mandatory argument FIELDS is missing or not at the right position");
return;
}
/* Read number of fields */
if (getRangeLongFromObjectOrReply(c, c->argv[numFieldsAt], 1, LONG_MAX,
&numFields, "Parameter `numFields` should be greater than 0") != C_OK)
return;
/* Verify `numFields` is consistent with number of arguments */
if (numFields > (c->argc - numFieldsAt - 1)) {
addReplyError(c, "Parameter `numFileds` is more than number of arguments");
return;
}
HashTypeSetEx exCtx;
hashTypeSetExInit(keyArg, hashObj, c, c->db, cmd,
FIELD_DONT_CREATE2,
expireSetCond,
&exCtx);
addReplyArrayLen(c, numFields);
for (int i = 0 ; i < numFields ; i++) {
sds field = c->argv[numFieldsAt+i+1]->ptr;
SetExRes res = hashTypeSetEx(c->db, hashObj, field, NULL, expire, &exCtx);
addReplyLongLong(c,res);
}
hashTypeSetExDone(&exCtx);
/* rewrite command for the replica sake */
/* Propagate as HPEXPIREAT millisecond-timestamp. Rewrite only if not already */
if (c->cmd->proc != hpexpireatCommand) {
rewriteClientCommandArgument(c,0,shared.hpexpireat);
}
/* rewrite expiration time to unix time in msec */
if (basetime != 0 || unit == UNIT_SECONDS) {
robj *expireObj = createStringObjectFromLongLong(expire);
rewriteClientCommandArgument(c, 2, expireObj);
decrRefCount(expireObj);
}
}
/* HPEXPIRE key milliseconds [ NX | XX | GT | LT] numfields <field [field ...]> */
void hpexpireCommand(client *c) {
hexpireGenericCommand(c,"hpexpire", commandTimeSnapshot(),UNIT_MILLISECONDS);
}
/* HEXPIRE key seconds [NX | XX | GT | LT] numfields <field [field ...]> */
void hexpireCommand(client *c) {
hexpireGenericCommand(c,"hexpire", commandTimeSnapshot(),UNIT_SECONDS);
}
/* HEXPIREAT key unix-time-seconds [NX | XX | GT | LT] numfields <field [field ...]> */
void hexpireatCommand(client *c) {
hexpireGenericCommand(c,"hexpireat", 0,UNIT_SECONDS);
}
/* HPEXPIREAT key unix-time-milliseconds [NX | XX | GT | LT] numfields <field [field ...]> */
void hpexpireatCommand(client *c) {
hexpireGenericCommand(c,"hpexpireat", 0,UNIT_MILLISECONDS);
}
/* for each specified field: get the remaining time to live in seconds*/
/* HTTL key numfields <field [field ...]> */
void httlCommand(client *c) {
httlGenericCommand(c, "httl", commandTimeSnapshot(), UNIT_SECONDS);
}
/* HPTTL key numfields <field [field ...]> */
void hpttlCommand(client *c) {
httlGenericCommand(c, "hpttl", commandTimeSnapshot(), UNIT_MILLISECONDS);
}
/* HEXPIRETIME key numFields <field [field ...]> */
void hexpiretimeCommand(client *c) {
httlGenericCommand(c, "hexpiretime", 0, UNIT_SECONDS);
}
/* HPEXPIRETIME key numFields <field [field ...]> */
void hpexpiretimeCommand(client *c) {
httlGenericCommand(c, "hexpiretime", 0, UNIT_MILLISECONDS);
}
/* HPERSIST key <FIELDS count field [field ...]> */
void hpersistCommand(client *c) {
robj *hashObj;
long numFields = 0, numFieldsAt = 3;
int changed = 0; /* Used to determine whether to send a notification. */
/* Read the hash object */
if ((hashObj = lookupKeyReadOrReply(c, c->argv[1], shared.emptyarray)) == NULL ||
checkType(c, hashObj, OBJ_HASH)) return;
if (strcasecmp(c->argv[numFieldsAt-1]->ptr, "FIELDS")) {
addReplyError(c, "Mandatory argument FIELDS is missing or not at the right position");
return;
}
/* Read number of fields */
if (getRangeLongFromObjectOrReply(c, c->argv[numFieldsAt], 1, LONG_MAX,
&numFields, "Number of fields must be a positive integer") != C_OK)
return;
/* Verify `numFields` is consistent with number of arguments */
if (numFields > (c->argc - numFieldsAt - 1)) {
addReplyError(c, "Parameter `numFileds` is more than number of arguments");
return;
}
if (hashObj->encoding == OBJ_ENCODING_LISTPACK) {
addReplyArrayLen(c, numFields);
for (int i = 0 ; i < numFields ; i++) {
sds field = c->argv[numFieldsAt + 1 + i]->ptr;
unsigned char *fptr, *zl = hashObj->ptr;
fptr = lpFirst(zl);
if (fptr != NULL)
fptr = lpFind(zl, fptr, (unsigned char *) field, sdslen(field), 1);
if (!fptr)
addReplyLongLong(c, HFE_PERSIST_NO_FIELD);
else
addReplyLongLong(c, HFE_PERSIST_NO_TTL);
}
return;
} else if (hashObj->encoding == OBJ_ENCODING_LISTPACK_EX) {
long long prevExpire;
unsigned char *fptr, *vptr, *tptr;
listpackEx *lpt = hashObj->ptr;
addReplyArrayLen(c, numFields);
for (int i = 0 ; i < numFields ; i++) {
sds field = c->argv[numFieldsAt + 1 + i]->ptr;
fptr = lpFirst(lpt->lp);
if (fptr != NULL)
fptr = lpFind(lpt->lp, fptr, (unsigned char*)field, sdslen(field), 2);
if (!fptr) {
addReplyLongLong(c, HFE_PERSIST_NO_FIELD);
continue;
}
vptr = lpNext(lpt->lp, fptr);
serverAssert(vptr);
tptr = lpNext(lpt->lp, vptr);
serverAssert(tptr && lpGetIntegerValue(tptr, &prevExpire));
if (prevExpire == HASH_LP_NO_TTL) {
addReplyLongLong(c, HFE_PERSIST_NO_TTL);
continue;
}
if (prevExpire < commandTimeSnapshot()) {
addReplyLongLong(c, HFE_PERSIST_NO_FIELD);
continue;
}
listpackExUpdateExpiry(hashObj, field, fptr, vptr, HASH_LP_NO_TTL);
addReplyLongLong(c, HFE_PERSIST_OK);
changed = 1;
}
} else if (hashObj->encoding == OBJ_ENCODING_HT) {
dict *d = hashObj->ptr;
addReplyArrayLen(c, numFields);
for (int i = 0 ; i < numFields ; i++) {
sds field = c->argv[numFieldsAt + 1 + i]->ptr;
dictEntry *de = dictFind(d, field);
if (de == NULL) {
addReplyLongLong(c, HFE_PERSIST_NO_FIELD);
continue;
}
hfield hf = dictGetKey(de);
uint64_t expire = hfieldGetExpireTime(hf);
if (expire == EB_EXPIRE_TIME_INVALID) {
addReplyLongLong(c, HFE_PERSIST_NO_TTL);
continue;
}
/* Already expired. Pretend there is no such field */
if ( (long long) expire < commandTimeSnapshot()) {
addReplyLongLong(c, HFE_PERSIST_NO_FIELD);
continue;
}
hfieldPersist(hashObj, hf);
addReplyLongLong(c, HFE_PERSIST_OK);
changed = 1;
}
} else {
serverPanic("Unknown encoding: %d", hashObj->encoding);
}
/* Generates a hpersist event if the expiry time associated with any field
* has been successfully deleted. */
if (changed) notifyKeyspaceEvent(NOTIFY_HASH,"hpersist",c->argv[1],c->db->id);
}
...@@ -432,7 +432,7 @@ robj *setTypePopRandom(robj *set) { ...@@ -432,7 +432,7 @@ robj *setTypePopRandom(robj *set) {
if (set->encoding == OBJ_ENCODING_LISTPACK) { if (set->encoding == OBJ_ENCODING_LISTPACK) {
/* Find random and delete it without re-seeking the listpack. */ /* Find random and delete it without re-seeking the listpack. */
unsigned int i = 0; 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 */ unsigned int len = 0; /* initialize to silence warning */
long long llele = 0; /* initialize to silence warning */ long long llele = 0; /* initialize to silence warning */
char *str = (char *)lpGetValue(p, &len, &llele); char *str = (char *)lpGetValue(p, &len, &llele);
...@@ -815,7 +815,7 @@ void spopWithCountCommand(client *c) { ...@@ -815,7 +815,7 @@ void spopWithCountCommand(client *c) {
unsigned int index = 0; unsigned int index = 0;
unsigned char **ps = zmalloc(sizeof(char *) * count); unsigned char **ps = zmalloc(sizeof(char *) * count);
for (unsigned long i = 0; i < count; i++) { 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; unsigned int len;
str = (char *)lpGetValue(p, &len, (long long *)&llele); str = (char *)lpGetValue(p, &len, (long long *)&llele);
...@@ -877,7 +877,7 @@ void spopWithCountCommand(client *c) { ...@@ -877,7 +877,7 @@ void spopWithCountCommand(client *c) {
unsigned int index = 0; unsigned int index = 0;
unsigned char **ps = zmalloc(sizeof(char *) * remaining); unsigned char **ps = zmalloc(sizeof(char *) * remaining);
for (unsigned long i = 0; i < remaining; i++) { 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; unsigned int len;
str = (char *)lpGetValue(p, &len, (long long *)&llele); str = (char *)lpGetValue(p, &len, (long long *)&llele);
setTypeAddAux(newset, str, len, llele, 0); setTypeAddAux(newset, str, len, llele, 0);
...@@ -1103,7 +1103,7 @@ void srandmemberWithCountCommand(client *c) { ...@@ -1103,7 +1103,7 @@ void srandmemberWithCountCommand(client *c) {
unsigned int i = 0; unsigned int i = 0;
addReplyArrayLen(c, count); addReplyArrayLen(c, count);
while (count) { while (count) {
p = lpNextRandom(lp, p, &i, count--, 0); p = lpNextRandom(lp, p, &i, count--, 1);
unsigned int len; unsigned int len;
str = (char *)lpGetValue(p, &len, (long long *)&llele); str = (char *)lpGetValue(p, &len, (long long *)&llele);
if (str == NULL) { if (str == NULL) {
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment