summaryrefslogtreecommitdiff
path: root/examples/redis-unstable/src/t_hash.c
diff options
context:
space:
mode:
authorMitja Felicijan <mitja.felicijan@gmail.com>2026-01-21 22:52:54 +0100
committerMitja Felicijan <mitja.felicijan@gmail.com>2026-01-21 22:52:54 +0100
commitdcacc00e3750300617ba6e16eb346713f91a783a (patch)
tree38e2d4fb5ed9d119711d4295c6eda4b014af73fd /examples/redis-unstable/src/t_hash.c
parent58dac10aeb8f5a041c46bddbeaf4c7966a99b998 (diff)
downloadcrep-dcacc00e3750300617ba6e16eb346713f91a783a.tar.gz
Remove testing data
Diffstat (limited to 'examples/redis-unstable/src/t_hash.c')
-rw-r--r--examples/redis-unstable/src/t_hash.c4068
1 files changed, 0 insertions, 4068 deletions
diff --git a/examples/redis-unstable/src/t_hash.c b/examples/redis-unstable/src/t_hash.c
deleted file mode 100644
index 48fc1e7..0000000
--- a/examples/redis-unstable/src/t_hash.c
+++ /dev/null
@@ -1,4068 +0,0 @@
-/*
- * Copyright (c) 2009-Present, Redis Ltd.
- * All rights reserved.
- *
- * Licensed under your choice of (a) the Redis Source Available License 2.0
- * (RSALv2); or (b) the Server Side Public License v1 (SSPLv1); or (c) the
- * GNU Affero General Public License v3 (AGPLv3).
- */
-
-#include "server.h"
-#include "redisassert.h"
-#include "ebuckets.h"
-#include "entry.h"
-#include "cluster_asm.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)
-
-/* Reserve 2 bits out of hash-field expiration time for possible future lightweight
- * indexing/categorizing of fields. It can be achieved by hacking HFE as follows:
- *
- * HPEXPIREAT key [ 2^47 + USER_INDEX ] FIELDS numfields field [field …]
- *
- * Redis will also need to expose kind of HEXPIRESCAN and HEXPIRECOUNT for this
- * idea. Yet to be better defined.
- *
- * HFE_MAX_ABS_TIME_MSEC constraint must be enforced only at API level. Internally,
- * the expiration time can be up to EB_EXPIRE_TIME_MAX for future readiness.
- */
-#define HFE_MAX_ABS_TIME_MSEC (EB_EXPIRE_TIME_MAX >> 2)
-
-typedef enum GetFieldRes {
- /* common (Used by hashTypeGet* value family) */
- GETF_OK = 0, /* The field was found. */
- GETF_NOT_FOUND, /* The field was not found. */
- GETF_EXPIRED, /* Logically expired (Might be lazy deleted or not) */
- GETF_EXPIRED_HASH, /* Delete hash since retrieved field was expired and
- * it was the last field in the hash. */
-} GetFieldRes;
-
-typedef listpackEntry CommonEntry; /* extend usage beyond lp */
-
-/* hash field expiration (HFE) funcs */
-static ExpireAction onFieldExpire(eItem item, void *ctx);
-static ExpireMeta* hentryGetExpireMeta(const eItem field);
-static void hexpireGenericCommand(client *c, long long basetime, int unit);
-static void hfieldPersist(robj *hashObj, Entry *entry);
-static void propagateHashFieldDeletion(redisDb *db, sds key, char *field, size_t fieldLen);
-
-/* hash dictType funcs */
-static void dictEntryDestructor(dict *d, void *entry);
-static size_t hashDictMetadataBytes(dict *d);
-static size_t hashDictWithExpireMetadataBytes(dict *d);
-static void hashDictWithExpireOnRelease(dict *d);
-static kvobj* hashTypeLookupWriteOrCreate(client *c, robj *key);
-
-/*-----------------------------------------------------------------------------
- * Define dictType of hash
- *
- * - Stores fields as entry objects (field-value pairs) with optional expiration
- * - 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 `entryHashDictTypeWithHFE`
- * - Dict uses no_value=1 since entry contains both field and value
- *----------------------------------------------------------------------------*/
-dictType entryHashDictType = {
- dictSdsHash, /* lookup hash function */
- NULL, /* key dup */
- NULL, /* val dup */
- dictSdsKeyCompare, /* lookup key compare */
- dictEntryDestructor, /* key destructor */
- NULL, /* val destructor (value is in entry) */
- .dictMetadataBytes = hashDictMetadataBytes,
- .no_value = 1, /* entry contains both field and value */
- .keys_are_odd = 1, /* entry pointers (SDS) are always odd */
-};
-
-/* Define alternative dictType of hash with hash-field expiration (HFE) support */
-dictType entryHashDictTypeWithHFE = {
- dictSdsHash, /* lookup hash function */
- NULL, /* key dup */
- NULL, /* val dup */
- dictSdsKeyCompare, /* lookup key compare */
- dictEntryDestructor, /* key destructor */
- NULL, /* val destructor (value is in entry) */
- .dictMetadataBytes = hashDictWithExpireMetadataBytes,
- .onDictRelease = hashDictWithExpireOnRelease,
- .no_value = 1, /* entry contains both field and value */
- .keys_are_odd = 1, /* entry pointers (SDS) are always odd */
-};
-
-/*-----------------------------------------------------------------------------
- * 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 (db->subexpires). The global HFE Active expiration will be
- * triggered from activeExpireCycle() function and in turn will invoke "local"
- * HFE Active sub-expiration for each hash instance that has expired fields.
- *----------------------------------------------------------------------------*/
-EbucketsType subexpiresBucketsType = {
- .onDeleteItem = NULL,
- .getExpireMeta = hashGetExpireMeta, /* get ExpireMeta attached to each hash */
- .itemsAddrAreOdd = 0, /* Addresses of dict are even */
-};
-
-/* htExpireMetadata - 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 = hentryGetExpireMeta, /* get ExpireMeta attached to each field */
- .itemsAddrAreOdd = 1, /* Addresses of hfield (entry/sds) are odd!! */
-};
-
-/* 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 "entry" objects (field-value pairs with optional expiration).
- * The entry structure unifies field and value into a single allocation, with
- * optional expiration metadata. This is simpler than the previous mstr approach
- * and provides better memory locality.
- */
-
-/* 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 == &entryHashDictTypeWithHFE;
-}
-
-/*-----------------------------------------------------------------------------
- * setex* - Set field's expiration
- *
- * 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.
- *----------------------------------------------------------------------------*/
-
-/* Returned value of hashTypeSetEx() */
-typedef enum SetExRes {
- HSETEX_OK = 1, /* Expiration time set/updated as expected */
- 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 */
-} 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;
-
-typedef enum ExpireSetCond {
- HFE_NX = 1<<0,
- HFE_XX = 1<<1,
- HFE_GT = 1<<2,
- HFE_LT = 1<<3
-} ExpireSetCond;
-
-/* Used by hashTypeSetEx() for setting fields or their expiry */
-typedef struct HashTypeSetEx {
-
- /*** config ***/
- 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 */
-
- /* Optionally provide client for notification */
- client *c;
- const char *cmd;
-} HashTypeSetEx;
-
-int hashTypeSetExInit(robj *key, kvobj *kvo, client *c, redisDb *db,
- ExpireSetCond expireSetCond, HashTypeSetEx *ex);
-
-SetExRes hashTypeSetEx(robj *o, sds field, uint64_t expireAt, HashTypeSetEx *exInfo);
-
-void hashTypeSetExDone(HashTypeSetEx *e);
-
-/*-----------------------------------------------------------------------------
- * Accessor functions for dictType of hash
- *----------------------------------------------------------------------------*/
-
-static void dictEntryDestructor(dict *d, void *entry) {
- size_t usable;
- size_t *alloc_size = htGetMetadataSize(d);
-
- /* If attached TTL to the field, then remove it from hash's private ebuckets. */
- if (entryGetExpiry(entry) != EB_EXPIRE_TIME_INVALID) {
- htMetadataEx *dictExpireMeta = htGetMetadataEx(d);
- ebRemove(&dictExpireMeta->hfe, &hashFieldExpireBucketsType, entry);
- }
-
- entryFree(entry, &usable);
- *alloc_size -= usable;
-
- /* 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 hashDictMetadataBytes(dict *d) {
- UNUSED(d);
- return sizeof(size_t);
-}
-
-static size_t hashDictWithExpireMetadataBytes(dict *d) {
- UNUSED(d);
- /* expireMeta of the hash, ref to ebuckets and pointer to hash's key */
- return sizeof(htMetadataEx);
-}
-
-static void hashDictWithExpireOnRelease(dict *d) {
- /* for sure allocated with metadata. Otherwise, this func won't be registered */
- htMetadataEx *dictExpireMeta = htGetMetadataEx(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 expiry
- * metadata. In the listpack string, 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;
- 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, kvobj *kv, ExpireInfo *info) {
- serverAssert(kv->encoding == OBJ_ENCODING_LISTPACK_EX);
- uint64_t expired = 0, min = EB_EXPIRE_TIME_INVALID;
- unsigned char *ptr;
- listpackEx *lpt = kv->ptr;
-
- ptr = lpFirst(lpt->lp);
-
- sds key = kvobjGetKey(kv);
-
- 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, key, (char *)((fref) ? fref : intbuf), flen);
- server.stat_expired_subkeys++;
-
- ptr = lpNext(lpt->lp, ptr);
-
- info->itemsExpired++;
- expired++;
- }
-
- if (expired) {
- size_t oldsize = 0;
- if (server.memory_tracking_per_slot)
- oldsize = lpBytes(lpt->lp);
- lpt->lp = lpDeleteRange(lpt->lp, 0, expired * 3);
- if (server.memory_tracking_per_slot)
- updateSlotAllocSize(db, getKeySlot(key), oldsize, lpBytes(lpt->lp));
-
- /* update keysizes */
- unsigned long l = lpLength(lpt->lp) / 3;
- updateKeysizesHist(db, getKeySlot(key), OBJ_HASH, l + expired, l);
- }
-
- min = hashTypeGetMinExpire(kv, 1 /*accurate*/);
- info->nextExpireTime = min;
-}
-
-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;
- }
-
- /* Special value of EXPIRE_TIME_INVALID indicates field should be persisted.*/
- if (expireAt == EB_EXPIRE_TIME_INVALID) {
- /* Return error if already there is no ttl. */
- if (prevExpire == EB_EXPIRE_TIME_INVALID)
- return HSETEX_NO_CONDITION_MET;
- listpackExUpdateExpiry(ex->hashObj, field, fptr, vptr, HASH_LP_NO_TTL);
- return HSETEX_OK;
- }
-
- if (prevExpire == EB_EXPIRE_TIME_INVALID) {
- /* For fields without expiry, LT condition is considered valid */
- if (ex->expireSetCond & (HFE_XX | HFE_GT))
- 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 expired, then delete the field and propagate the deletion.
- * If replica, continue like the field is valid */
- if (unlikely(checkAlreadyExpired(expireAt))) {
- propagateHashFieldDeletion(ex->db, ex->key->ptr, field, sdslen(field));
- hashTypeDelete(ex->hashObj, field);
- server.stat_expired_subkeys++;
- return HSETEX_DELETED;
- }
-
- if (ex->minExpireFields > expireAt)
- ex->minExpireFields = expireAt;
-
- listpackExUpdateExpiry(ex->hashObj, field, fptr, vptr, expireAt);
- return HSETEX_OK;
-}
-
-/* Returns 1 if expired */
-int hashTypeIsExpired(const robj *o, uint64_t expireAt) {
- if (server.allow_access_expired) return 0;
-
- 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
- *----------------------------------------------------------------------------*/
-
-/* Check the length of a number of objects to see if we need to convert a
- * listpack to a real hash. Note that we only check string encoded objects
- * as their string length can be queried in constant time. */
-void hashTypeTryConversion(redisDb *db, kvobj *o, robj **argv, int start, int end) {
- int i;
- size_t sum = 0;
-
- if (o->encoding != OBJ_ENCODING_LISTPACK && o->encoding != OBJ_ENCODING_LISTPACK_EX)
- return;
-
- /* We guess that most of the values in the input are unique, so
- * if there are enough arguments we create a pre-sized hash, which
- * might over allocate memory if there are duplicates. */
- size_t new_fields = (end - start + 1) / 2;
- if (new_fields > server.hash_max_listpack_entries) {
- hashTypeConvert(db, o, OBJ_ENCODING_HT);
- dictExpand(o->ptr, new_fields);
- return;
- }
-
- for (i = start; i <= end; i++) {
- if (!sdsEncodedObject(argv[i]))
- continue;
- size_t len = sdslen(argv[i]->ptr);
- if (len > server.hash_max_listpack_value) {
- hashTypeConvert(db, o, OBJ_ENCODING_HT);
- return;
- }
- sum += len;
- }
- if (!lpSafeToAdd(hashTypeListpackGetLp(o), sum)) {
- hashTypeConvert(db, o, OBJ_ENCODING_HT);
- }
-}
-
-/* Get the value from a listpack encoded hash, identified by field. */
-GetFieldRes hashTypeGetFromListpack(robj *o, sds field,
- unsigned char **vstr,
- unsigned int *vlen,
- long long *vll,
- uint64_t *expiredAt)
-{
- *expiredAt = EB_EXPIRE_TIME_INVALID;
- unsigned char *zl, *fptr = NULL, *vptr = NULL;
-
- if (o->encoding == OBJ_ENCODING_LISTPACK) {
- zl = o->ptr;
- fptr = lpFirst(zl);
- if (fptr != NULL) {
- fptr = lpFind(zl, fptr, (unsigned char*)field, sdslen(field), 1);
- if (fptr != NULL) {
- /* Grab pointer to the value (fptr points to the field) */
- vptr = lpNext(zl, fptr);
- serverAssert(vptr != NULL);
- }
- }
- } else if (o->encoding == OBJ_ENCODING_LISTPACK_EX) {
- long long expire;
- unsigned char *h;
- listpackEx *lpt = o->ptr;
-
- fptr = lpFirst(lpt->lp);
- if (fptr != NULL) {
- fptr = lpFind(lpt->lp, fptr, (unsigned char*)field, sdslen(field), 2);
- if (fptr != NULL) {
- vptr = lpNext(lpt->lp, fptr);
- serverAssert(vptr != NULL);
-
- h = lpNext(lpt->lp, vptr);
- serverAssert(h && lpGetIntegerValue(h, &expire));
- if (expire != HASH_LP_NO_TTL)
- *expiredAt = expire;
- }
- }
- } else {
- serverPanic("Unknown hash encoding: %d", o->encoding);
- }
-
- if (vptr != NULL) {
- *vstr = lpGetValue(vptr, vlen, vll);
- return GETF_OK;
- }
-
- return GETF_NOT_FOUND;
-}
-
-/* Get the value from a hash table encoded hash, identified by field.
- * Returns NULL when the field cannot be found, otherwise the SDS value
- * is returned. */
-GetFieldRes hashTypeGetFromHashTable(robj *o, sds field, sds *value, uint64_t *expiredAt) {
- dictEntry *de;
-
- *expiredAt = EB_EXPIRE_TIME_INVALID;
-
- serverAssert(o->encoding == OBJ_ENCODING_HT);
-
- de = dictFind(o->ptr, field);
-
- if (de == NULL)
- return GETF_NOT_FOUND;
-
- Entry *entry = dictGetKey(de);
- *expiredAt = entryGetExpiry(entry);
- *value = entryGetValue(entry);
- return GETF_OK;
-}
-
-/* Higher level function of hashTypeGet*() that returns the hash value
- * associated with the specified field.
- * Arguments:
- * hfeFlags - Lookup for HFE_LAZY_* flags
- *
- * Returned:
- * GetFieldRes - Result of get operation
- * 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.
- * expiredAt - if the field has an expiration time, it will be set to the expiration
- * time of the field. Otherwise, will be set to EB_EXPIRE_TIME_INVALID.
- */
-GetFieldRes hashTypeGetValue(redisDb *db, kvobj *o, sds field, unsigned char **vstr,
- unsigned int *vlen, long long *vll,
- int hfeFlags, uint64_t *expiredAt)
-{
- sds key = kvobjGetKey(o);
- GetFieldRes res;
- uint64_t dummy;
- size_t oldsize = 0;
- if (expiredAt == NULL) expiredAt = &dummy;
- if (o->encoding == OBJ_ENCODING_LISTPACK ||
- o->encoding == OBJ_ENCODING_LISTPACK_EX) {
- *vstr = NULL;
- res = hashTypeGetFromListpack(o, field, vstr, vlen, vll, expiredAt);
-
- if (res == GETF_NOT_FOUND)
- return GETF_NOT_FOUND;
-
- } else if (o->encoding == OBJ_ENCODING_HT) {
- sds value = NULL;
- if (server.memory_tracking_per_slot && !(hfeFlags & HFE_LAZY_NO_UPDATE_ALLOCSIZES))
- oldsize = hashTypeAllocSize(o);
- res = hashTypeGetFromHashTable(o, field, &value, expiredAt);
- if (server.memory_tracking_per_slot && !(hfeFlags & HFE_LAZY_NO_UPDATE_ALLOCSIZES))
- updateSlotAllocSize(db, getKeySlot(key), oldsize, hashTypeAllocSize(o));
-
- if (res == GETF_NOT_FOUND)
- return GETF_NOT_FOUND;
-
- *vstr = (unsigned char*) value;
- *vlen = sdslen(value);
- } else {
- serverPanic("Unknown hash encoding");
- }
-
- if ((server.allow_access_expired) ||
- (*expiredAt >= (uint64_t) commandTimeSnapshot()) ||
- (hfeFlags & HFE_LAZY_ACCESS_EXPIRED))
- return GETF_OK;
-
- if (server.masterhost || server.cluster_enabled) {
- /* If CLIENT_MASTER, assume valid as long as it didn't get delete.
- *
- * In cluster mode, we also assume valid if we are importing data
- * from the source, to avoid deleting fields that are still in use.
- * We create a fake master client for data import, which can be
- * identified using the CLIENT_MASTER flag. */
- if (server.current_client && (server.current_client->flags & CLIENT_MASTER))
- return GETF_OK;
-
- /* For replica, if user client, then act as if expired, but don't delete! */
- if (server.masterhost) return GETF_EXPIRED;
- }
-
- if ((server.loading) ||
- (hfeFlags & HFE_LAZY_AVOID_FIELD_DEL) ||
- (isPausedActionsWithUpdate(PAUSE_ACTION_EXPIRE)))
- return GETF_EXPIRED;
-
- /* delete the field and propagate the deletion */
- if (server.memory_tracking_per_slot && !(hfeFlags & HFE_LAZY_NO_UPDATE_ALLOCSIZES))
- oldsize = hashTypeAllocSize(o);
- serverAssert(hashTypeDelete(o, field) == 1);
- if (server.memory_tracking_per_slot && !(hfeFlags & HFE_LAZY_NO_UPDATE_ALLOCSIZES))
- updateSlotAllocSize(db, getKeySlot(key), oldsize, hashTypeAllocSize(o));
- propagateHashFieldDeletion(db, key, field, sdslen(field));
- server.stat_expired_subkeys++;
-
- if (!(hfeFlags & HFE_LAZY_NO_UPDATE_KEYSIZES)) {
- uint64_t l = hashTypeLength(o, 0);
- updateKeysizesHist(db, getKeySlot(key), OBJ_HASH, l+1, l);
- }
-
- /* If the field is the last one in the hash, then the hash will be deleted */
- res = GETF_EXPIRED;
- robj *keyObj = createStringObject(key, sdslen(key));
- if (!(hfeFlags & HFE_LAZY_NO_NOTIFICATION))
- notifyKeyspaceEvent(NOTIFY_HASH, "hexpired", keyObj, db->id);
- if ((hashTypeLength(o, 0) == 0) && (!(hfeFlags & HFE_LAZY_AVOID_HASH_DEL))) {
- if (!(hfeFlags & HFE_LAZY_NO_NOTIFICATION))
- notifyKeyspaceEvent(NOTIFY_GENERIC, "del", keyObj, db->id);
- dbDelete(db,keyObj);
- o = NULL;
- res = GETF_EXPIRED_HASH;
- }
- keyModified(NULL, db, keyObj, o, !(hfeFlags & HFE_LAZY_NO_SIGNAL));
- decrRefCount(keyObj);
- return res;
-}
-
-/* Like hashTypeGetValue() but returns a Redis object, which is useful for
- * interaction with the hash type outside t_hash.c.
- * The function returns NULL if the field is not found in the hash. Otherwise
- * a newly allocated string object with the value is returned.
- *
- * hfeFlags - Lookup HFE_LAZY_* flags
- * 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.
- * val - If the field is found, then val will be set to the value object.
- * expireTime - If the field exists (`GETF_OK`) then expireTime will be set to
- * the expiration time of the field. Otherwise, it will be set to 0.
- *
- * Returns 1 if the field exists, and 0 when it doesn't.
- */
-int hashTypeGetValueObject(redisDb *db, kvobj *o, sds field, int hfeFlags,
- robj **val, uint64_t *expireTime, int *isHashDeleted) {
- unsigned char *vstr;
- unsigned int vlen;
- long long vll;
-
- if (isHashDeleted) *isHashDeleted = 0;
- if (val) *val = NULL;
- GetFieldRes res = hashTypeGetValue(db,o,field,&vstr,&vlen,&vll,
- hfeFlags, expireTime);
-
- if (res == GETF_OK) {
- /* expireTime set to 0 if the field has no expiration time */
- if (expireTime && (*expireTime == EB_EXPIRE_TIME_INVALID))
- *expireTime = 0;
-
- /* If expected to return the value, then create a new object */
- if (val) {
- if (vstr) *val = createStringObject((char *) vstr, vlen);
- else *val = createStringObjectFromLongLong(vll);
- }
- return 1;
- }
-
- if ((res == GETF_EXPIRED_HASH) && (isHashDeleted))
- *isHashDeleted = 1;
-
- /* GETF_EXPIRED_HASH, GETF_EXPIRED, GETF_NOT_FOUND */
- return 0;
-}
-
-/* Test if the specified field exists in the given hash. If the field is
- * expired (HFE), then it will be lazy deleted unless HFE_LAZY_AVOID_FIELD_DEL
- * hfeFlags is set.
- *
- * hfeFlags - Lookup HFE_LAZY_* flags
- * 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.
- *
- * Returns 1 if the field exists, and 0 when it doesn't.
- */
-int hashTypeExists(redisDb *db, kvobj *o, sds field, int hfeFlags, int *isHashDeleted) {
- unsigned char *vstr = NULL;
- unsigned int vlen = UINT_MAX;
- long long vll = LLONG_MAX;
-
- GetFieldRes res = hashTypeGetValue(db, o, field, &vstr, &vlen, &vll,
- hfeFlags, NULL);
- if (isHashDeleted)
- *isHashDeleted = (res == GETF_EXPIRED_HASH) ? 1 : 0;
- return (res == GETF_OK) ? 1 : 0;
-}
-
-/* Add a new field, overwrite the old with the new value if it already exists.
- * 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_TTL -- keep original TTL if field 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_TTL (1<<2)
-
-static_assert(HASH_SET_TAKE_VALUE == ENTRY_TAKE_VALUE, "ENTRY_TAKE_VALUE must match HASH_SET_TAKE_VALUE");
-
-int hashTypeSet(redisDb *db, kvobj *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.
- * 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 (sdslen(field) > server.hash_max_listpack_value || sdslen(value) > server.hash_max_listpack_value)
- hashTypeConvert(db, o, OBJ_ENCODING_HT);
- }
-
- if (o->encoding == OBJ_ENCODING_LISTPACK) {
- unsigned char *zl, *fptr, *vptr;
-
- zl = o->ptr;
- fptr = lpFirst(zl);
- if (fptr != NULL) {
- fptr = lpFind(zl, fptr, (unsigned char*)field, sdslen(field), 1);
- if (fptr != NULL) {
- /* Grab pointer to the value (fptr points to the field) */
- vptr = lpNext(zl, fptr);
- serverAssert(vptr != NULL);
-
- /* Replace value */
- zl = lpReplace(zl, &vptr, (unsigned char*)value, sdslen(value));
- update = 1;
- }
- }
-
- if (!update) {
- listpackEntry entries[2] = {
- {.sval = (unsigned char*) field, .slen = sdslen(field)},
- {.sval = (unsigned char*) value, .slen = sdslen(value)},
- };
-
- /* Push new field/value pair onto the tail of the listpack */
- zl = lpBatchAppend(zl, entries, 2);
- }
- o->ptr = zl;
-
- /* Check if the listpack needs to be converted to a hash table */
- if (hashTypeLength(o, 0) > server.hash_max_listpack_entries)
- hashTypeConvert(db, o, OBJ_ENCODING_HT);
- } else if (o->encoding == OBJ_ENCODING_LISTPACK_EX) {
- unsigned char *fptr = NULL, *vptr = NULL, *tptr = NULL;
- listpackEx *lpt = o->ptr;
- long long expireTime = HASH_LP_NO_TTL;
-
- fptr = lpFirst(lpt->lp);
- if (fptr != NULL) {
- fptr = lpFind(lpt->lp, fptr, (unsigned char*)field, sdslen(field), 2);
- if (fptr != NULL) {
- /* Grab pointer to the value (fptr points to the field) */
- vptr = lpNext(lpt->lp, fptr);
- serverAssert(vptr != NULL);
-
- /* Replace value */
- lpt->lp = lpReplace(lpt->lp, &vptr, (unsigned char *) value, sdslen(value));
- update = 1;
-
- fptr = lpPrev(lpt->lp, vptr);
- serverAssert(fptr != NULL);
-
- tptr = lpNext(lpt->lp, vptr);
- serverAssert(tptr && lpGetIntegerValue(tptr, &expireTime));
-
- if (flags & HASH_SET_KEEP_TTL) {
- /* keep old field along with TTL */
- } else if (expireTime != HASH_LP_NO_TTL) {
- /* re-insert field and override TTL */
- listpackExUpdateExpiry(o, field, fptr, vptr, HASH_LP_NO_TTL);
- }
- }
- }
-
- if (!update)
- listpackExAddNew(o, field, sdslen(field), value, sdslen(value),
- HASH_LP_NO_TTL);
-
- /* Check if the listpack needs to be converted to a hash table */
- if (hashTypeLength(o, 0) > server.hash_max_listpack_entries)
- hashTypeConvert(db, o, OBJ_ENCODING_HT);
-
- } else if (o->encoding == OBJ_ENCODING_HT) {
- dict *ht = o->ptr;
- /* check if field already exists */
- dictEntryLink bucket, link = dictFindLink(ht, field, &bucket);
- size_t *alloc_size = htGetMetadataSize(ht);
-
- /* take ownership of value if requested */
- uint32_t newEntryFlags = flags & HASH_SET_TAKE_VALUE;
- flags &= ~HASH_SET_TAKE_VALUE;
-
- if (link == NULL) {
- /* Create entry and transfer value ownership if possible */
- size_t usable;
- Entry *newEntry = entryCreate(field, value, newEntryFlags, &usable);
-
- dictSetKeyAtLink(ht, newEntry, &bucket, 1);
- *alloc_size += usable;
- } else {
- /* Existing field - update value in entry */
- Entry *oldEntry = dictGetKey(*link);
-
- /* Check if old entry has expiration before potentially freeing it */
- uint64_t oldExpireAt = entryGetExpiry(oldEntry);
- uint64_t newExpireAt = EB_EXPIRE_TIME_INVALID;
-
- /* If attached TTL to the old field, then remove it from hash's
- * private ebuckets. We do this before updating the value because
- * the entry might be reallocated and freed. */
- if (oldExpireAt != EB_EXPIRE_TIME_INVALID) {
- hfieldPersist(o, oldEntry);
- if (flags & HASH_SET_KEEP_TTL) {
- newExpireAt = oldExpireAt;
- newEntryFlags |= ENTRY_HAS_EXPIRY;
- }
- }
-
- ssize_t usableDiff;
- Entry *newEntry = entryUpdate(oldEntry, value, newEntryFlags, &usableDiff);
-
- /* If entry was reallocated, update the dict key */
- if (newEntry != oldEntry) {
- /* entryUpdate already freed the old entry if needed */
- /* Update the dict to point to the new entry using dictSetKeyAtLink (no_value=1) */
- dictSetKeyAtLink(ht, newEntry, &link, 0);
- }
-
- /* If keeping TTL, add the (potentially new) entry back to ebuckets */
- if (newExpireAt != EB_EXPIRE_TIME_INVALID) {
- dict *d = o->ptr;
- htMetadataEx *dictExpireMeta = htGetMetadataEx(d);
- serverAssert(dictExpireMeta->expireMeta.trash == 0);
- ebAdd(&dictExpireMeta->hfe, &hashFieldExpireBucketsType, newEntry, newExpireAt);
- }
-
- *alloc_size += usableDiff;
- update = 1;
- }
- } else {
- serverPanic("Unknown hash encoding");
- }
-
- /* Free SDS strings we did not referenced elsewhere if the flags
- * want this function to be responsible. */
- if (flags & HASH_SET_TAKE_FIELD && field) sdsfree(field);
- if (flags & HASH_SET_TAKE_VALUE && value) sdsfree(value);
- return update;
-}
-
-SetExRes hashTypeSetExpiryHT(HashTypeSetEx *exInfo, sds field, uint64_t expireAt) {
- dict *ht = exInfo->hashObj->ptr;
- dictEntryLink link = NULL;
- Entry *entryNew = NULL;
-
- link = dictFindLink(ht, field, NULL);
- if (link == NULL)
- return HSETEX_NO_FIELD;
-
- dictEntry *existingEntry = *link;
- Entry *oldEntry = dictGetKey(existingEntry);
- /* Special value of EXPIRE_TIME_INVALID indicates field should be persisted.*/
- if (expireAt == EB_EXPIRE_TIME_INVALID) {
- /* Return error if already there is no ttl. */
- if (entryGetExpiry(oldEntry) == EB_EXPIRE_TIME_INVALID)
- return HSETEX_NO_CONDITION_MET;
-
- hfieldPersist(exInfo->hashObj, oldEntry);
- return HSETEX_OK;
- }
-
- /* If field doesn't have expiry metadata attached */
- if (!entryHasExpiry(oldEntry)) {
- size_t *alloc_size = htGetMetadataSize(ht);
-
- /* For fields without expiry, LT condition is considered valid */
- if (exInfo->expireSetCond & (HFE_XX | HFE_GT))
- return HSETEX_NO_CONDITION_MET;
-
- ssize_t usableDiff;
- entryNew = entryUpdate(oldEntry, NULL, ENTRY_HAS_EXPIRY, &usableDiff);
- *alloc_size += usableDiff;
- } else { /* field has ExpireMeta struct attached */
- uint64_t prevExpire = entryGetExpiry(oldEntry);
-
- /* If field has valid expiration time, then check GT|LT|NX */
- if (prevExpire != EB_EXPIRE_TIME_INVALID) {
- if (((exInfo->expireSetCond == HFE_GT) && (prevExpire >= expireAt)) ||
- ((exInfo->expireSetCond == HFE_LT) && (prevExpire <= expireAt)) ||
- (exInfo->expireSetCond == HFE_NX) )
- return HSETEX_NO_CONDITION_MET;
-
- /* If expiry time is the same, then nothing to do */
- if (prevExpire == expireAt)
- return HSETEX_OK;
-
- /* remove old expiry time from hash's private ebuckets */
- htMetadataEx *dm = htGetMetadataEx(ht);
- ebRemove(&dm->hfe, &hashFieldExpireBucketsType, oldEntry);
-
- /* Track of minimum expiration time (only later update global HFE DS) */
- if (exInfo->minExpireFields > prevExpire)
- exInfo->minExpireFields = prevExpire;
-
- } else {
- /* field has invalid expiry. No need to ebRemove() */
-
- /* Check XX|LT|GT */
- if (exInfo->expireSetCond & (HFE_XX | HFE_GT))
- return HSETEX_NO_CONDITION_MET;
- }
-
- /* Reuse hfOld as hfNew and rewrite its expiry with ebAdd() */
- entryNew = oldEntry;
- }
-
- dictSetKeyAtLink(ht, entryNew, &link, 0); /* newItem=0 for updating existing entry */
-
-
- /* If expired, then delete the field and propagate the deletion.
- * If replica, continue like the field is valid */
- if (unlikely(checkAlreadyExpired(expireAt))) {
- /* replicas should not initiate deletion of fields */
- propagateHashFieldDeletion(exInfo->db, exInfo->key->ptr, field, sdslen(field));
- hashTypeDelete(exInfo->hashObj, field);
- server.stat_expired_subkeys++;
- return HSETEX_DELETED;
- }
-
- if (exInfo->minExpireFields > expireAt)
- exInfo->minExpireFields = expireAt;
-
- htMetadataEx *dm = htGetMetadataEx(ht);
- ebAdd(&dm->hfe, &hashFieldExpireBucketsType, entryNew, expireAt);
- return HSETEX_OK;
-}
-
-/*
- * Set field expiration
- *
- * Take care to call first hashTypeSetExInit() and then call this function.
- * Finally, call hashTypeSetExDone() to notify and update global HFE DS.
- *
- * Special value of EB_EXPIRE_TIME_INVALID for 'expireAt' argument will persist
- * the field.
- */
-SetExRes hashTypeSetEx(robj *o, sds field, uint64_t expireAt, HashTypeSetEx *exInfo) {
- if (o->encoding == OBJ_ENCODING_LISTPACK_EX) {
- unsigned char *fptr = NULL, *vptr = NULL, *tptr = NULL;
- listpackEx *lpt = o->ptr;
-
- fptr = lpFirst(lpt->lp);
- if (fptr)
- fptr = lpFind(lpt->lp, fptr, (unsigned char*)field, sdslen(field), 2);
-
- if (!fptr)
- return HSETEX_NO_FIELD;
-
- /* Grab pointer to the value (fptr points to the field) */
- vptr = lpNext(lpt->lp, fptr);
- serverAssert(vptr != NULL);
-
- tptr = lpNext(lpt->lp, vptr);
- serverAssert(tptr);
-
- /* update TTL */
- return hashTypeSetExpiryListpack(exInfo, field, fptr, vptr, tptr, expireAt);
- } else if (o->encoding == OBJ_ENCODING_HT) {
- /* If needed to set the field along with expiry */
- return hashTypeSetExpiryHT(exInfo, field, expireAt);
- } else {
- serverPanic("Unknown hash encoding");
- }
-
- return HSETEX_OK; /* never reach here */
-}
-
-void initDictExpireMetadata(robj *o) {
- dict *ht = o->ptr;
-
- htMetadataEx *m = htGetMetadataEx(ht);
- 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() */
-int hashTypeSetExInit(robj *key, kvobj *o, client *c, redisDb *db,
- ExpireSetCond expireSetCond, HashTypeSetEx *ex)
-{
- dict *ht = o->ptr;
- ex->expireSetCond = expireSetCond;
- ex->minExpire = EB_EXPIRE_TIME_INVALID;
- ex->c = c;
- ex->db = db;
- ex->key = key;
- ex->hashObj = o;
- ex->minExpireFields = EB_EXPIRE_TIME_INVALID;
-
- /* Take care that HASH support expiration */
- if (o->encoding == OBJ_ENCODING_LISTPACK) {
- hashTypeConvert(c->db, o, OBJ_ENCODING_LISTPACK_EX);
- } else if (o->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, &entryHashDictTypeWithHFE);
- htMetadataEx *m = htGetMetadataEx(ht);
- o->ptr = ht;
-
- /* Find the key in the keyspace. Need to keep reference to the key for
- * notifications or even removal of the hash */
-
- /* Fillup dict HFE metadata */
- m->hfe = ebCreate(); /* Allocate HFE DS */
- m->expireMeta.trash = 1; /* mark as trash (as long it wasn't ebAdd()) */
- }
- }
-
- /* Read minExpire from attached ExpireMeta to the hash */
- ex->minExpire = hashTypeGetMinExpire(o, 0);
- return C_OK;
-}
-
-/*
- * After calling hashTypeSetEx() for setting fields or their expiry, call this
- * function to update global HFE DS.
- */
-void hashTypeSetExDone(HashTypeSetEx *ex) {
-
- if (hashTypeLength(ex->hashObj, 0) == 0)
- return;
-
- /* If minimum HFE of the hash is smaller than expiration time of the
- * specified fields in the command as well as it is smaller or equal
- * than expiration time provided in the command, then the minimum
- * HFE of the hash won't change following this command. */
- if ((ex->minExpire < ex->minExpireFields))
- return;
-
- /* Retrieve new expired time. It might have changed. */
- uint64_t newMinExpire = hashTypeGetMinExpire(ex->hashObj, 1 /*accurate*/);
-
- /* Calculate the diff between old minExpire and newMinExpire. If it is
- * only few seconds, then don't have to update global HFE DS. At the worst
- * case fields of hash will be active-expired up to few seconds later.
- *
- * In any case, active-expire operation will know to update global
- * HFE DS more efficiently than here for a single item.
- */
- uint64_t diff = (ex->minExpire > newMinExpire) ?
- (ex->minExpire - newMinExpire) : (newMinExpire - ex->minExpire);
- if (diff < HASH_NEW_EXPIRE_DIFF_THRESHOLD) return;
-
- int slot = getKeySlot(ex->key->ptr);
- if (ex->minExpire != EB_EXPIRE_TIME_INVALID) {
- if (newMinExpire != EB_EXPIRE_TIME_INVALID)
- estoreUpdate(ex->db->subexpires, slot, ex->hashObj, newMinExpire);
- else
- estoreRemove(ex->db->subexpires, slot, ex->hashObj);
- } else {
- if (newMinExpire != EB_EXPIRE_TIME_INVALID)
- estoreAdd(ex->db->subexpires, slot, ex->hashObj, newMinExpire);
- }
-}
-
-/* Delete an element from a hash.
- *
- * Return 1 on deleted and 0 on not found.
- * field - sds field name to delete */
-int hashTypeDelete(robj *o, void *field) {
- int deleted = 0;
- int fieldLen = sdslen((sds)field);
-
- if (o->encoding == OBJ_ENCODING_LISTPACK) {
- unsigned char *zl, *fptr;
-
- zl = o->ptr;
- fptr = lpFirst(zl);
- if (fptr != NULL) {
- fptr = lpFind(zl, fptr, (unsigned char*)field, fieldLen, 1);
- if (fptr != NULL) {
- /* Delete both of the key and the value. */
- zl = lpDeleteRangeWithEntry(zl,&fptr,2);
- o->ptr = zl;
- deleted = 1;
- }
- }
- } else if (o->encoding == OBJ_ENCODING_LISTPACK_EX) {
- unsigned char *fptr;
- listpackEx *lpt = o->ptr;
-
- fptr = lpFirst(lpt->lp);
- if (fptr != NULL) {
- fptr = lpFind(lpt->lp, fptr, (unsigned char*)field, fieldLen, 2);
- if (fptr != NULL) {
- /* Delete field, value and ttl */
- lpt->lp = lpDeleteRangeWithEntry(lpt->lp, &fptr, 3);
- deleted = 1;
- }
- }
- } else if (o->encoding == OBJ_ENCODING_HT) {
- /* dictDelete() will call dictEntryDestructor() */
- if (dictDelete((dict*)o->ptr, field) == C_OK) {
- deleted = 1;
- }
- } else {
- serverPanic("Unknown hash encoding");
- }
- return deleted;
-}
-
-/* Return the number of elements in a hash.
- *
- * Note, subtractExpiredFields=1 might be pricy in case there are many HFEs
- */
-unsigned long hashTypeLength(const robj *o, int subtractExpiredFields) {
- unsigned long length = ULONG_MAX;
- /* If expired field access is allowed, don't subtract expired fields from the count. */
- if (server.allow_access_expired)
- subtractExpiredFields = 0;
-
- if (o->encoding == OBJ_ENCODING_LISTPACK) {
- length = lpLength(o->ptr) / 2;
- } else if (o->encoding == OBJ_ENCODING_LISTPACK_EX) {
- listpackEx *lpt = o->ptr;
- length = lpLength(lpt->lp) / 3;
-
- if (subtractExpiredFields && lpt->meta.trash == 0)
- length -= listpackExExpireDryRun(o);
- } else if (o->encoding == OBJ_ENCODING_HT) {
- uint64_t expiredItems = 0;
- dict *d = (dict*)o->ptr;
- if (subtractExpiredFields && isDictWithMetaHFE(d)) {
- htMetadataEx *meta = htGetMetadataEx(d);
- /* If dict registered in global HFE DS */
- if (meta->expireMeta.trash == 0)
- expiredItems = ebExpireDryRun(meta->hfe,
- &hashFieldExpireBucketsType,
- commandTimeSnapshot());
- }
- length = dictSize(d) - expiredItems;
- } else {
- serverPanic("Unknown hash encoding");
- }
- return length;
-}
-
-size_t hashTypeAllocSize(const robj *o) {
- serverAssertWithInfo(NULL,o,o->type == OBJ_HASH);
- size_t size = 0;
- if (o->encoding == OBJ_ENCODING_LISTPACK) {
- size = lpBytes(o->ptr);
- } else if (o->encoding == OBJ_ENCODING_LISTPACK_EX) {
- listpackEx *lpt = o->ptr;
- size = sizeof(listpackEx) + lpBytes(lpt->lp);
- } else if (o->encoding == OBJ_ENCODING_HT) {
- dict *d = o->ptr;
- size += sizeof(dict) + dictMemUsage(d) + *htGetMetadataSize(d);
- } else {
- serverPanic("Unknown hash encoding");
- }
- return size;
-}
-
-void hashTypeInitIterator(hashTypeIterator *hi, robj *subject) {
- hi->subject = subject;
- hi->encoding = subject->encoding;
-
- if (hi->encoding == OBJ_ENCODING_LISTPACK ||
- hi->encoding == OBJ_ENCODING_LISTPACK_EX)
- {
- hi->fptr = NULL;
- hi->vptr = NULL;
- hi->tptr = NULL;
- hi->expire_time = EB_EXPIRE_TIME_INVALID;
- } else if (hi->encoding == OBJ_ENCODING_HT) {
- dictInitIterator(&hi->di, subject->ptr);
- } else {
- serverPanic("Unknown hash encoding");
- }
-}
-
-void hashTypeResetIterator(hashTypeIterator *hi) {
- if (hi->encoding == OBJ_ENCODING_HT)
- dictResetIterator(&hi->di);
-}
-
-/* Move to the next entry in the hash. Return C_OK when the next entry
- * could be found and C_ERR when the iterator reaches the end. */
-int hashTypeNext(hashTypeIterator *hi, int skipExpiredFields) {
- /* If expired field access is allowed, don't skip expired fields during iteration */
- if (server.allow_access_expired)
- skipExpiredFields = 0;
-
- hi->expire_time = EB_EXPIRE_TIME_INVALID;
- if (hi->encoding == OBJ_ENCODING_LISTPACK) {
- unsigned char *zl;
- unsigned char *fptr, *vptr;
-
- zl = hi->subject->ptr;
- fptr = hi->fptr;
- vptr = hi->vptr;
-
- if (fptr == NULL) {
- /* Initialize cursor */
- serverAssert(vptr == NULL);
- fptr = lpFirst(zl);
- } else {
- /* Advance cursor */
- serverAssert(vptr != NULL);
- fptr = lpNext(zl, vptr);
- }
- if (fptr == NULL) return C_ERR;
-
- /* Grab pointer to the value (fptr points to the field) */
- vptr = lpNext(zl, fptr);
- serverAssert(vptr != NULL);
-
- /* fptr, vptr now point to the first or next pair */
- hi->fptr = fptr;
- hi->vptr = vptr;
- } else if (hi->encoding == OBJ_ENCODING_LISTPACK_EX) {
- long long expire_time;
- unsigned char *zl = hashTypeListpackGetLp(hi->subject);
- unsigned char *fptr, *vptr, *tptr;
-
- fptr = hi->fptr;
- vptr = hi->vptr;
- tptr = hi->tptr;
-
- if (fptr == NULL) {
- /* Initialize cursor */
- serverAssert(vptr == NULL);
- fptr = lpFirst(zl);
- } else {
- /* Advance cursor */
- serverAssert(tptr != NULL);
- fptr = lpNext(zl, tptr);
- }
- if (fptr == NULL) return C_ERR;
-
- while (fptr != NULL) {
- /* Grab pointer to the value (fptr points to the field) */
- vptr = lpNext(zl, fptr);
- serverAssert(vptr != NULL);
-
- tptr = lpNext(zl, vptr);
- serverAssert(tptr && lpGetIntegerValue(tptr, &expire_time));
-
- if (!skipExpiredFields || !hashTypeIsExpired(hi->subject, expire_time))
- break;
-
- fptr = lpNext(zl, tptr);
- }
- if (fptr == NULL) return C_ERR;
-
- /* fptr, vptr now point to the first or next pair */
- hi->fptr = fptr;
- hi->vptr = vptr;
- hi->tptr = tptr;
- hi->expire_time = (expire_time != HASH_LP_NO_TTL) ? (uint64_t) expire_time : EB_EXPIRE_TIME_INVALID;
- } else if (hi->encoding == OBJ_ENCODING_HT) {
-
- while ((hi->de = dictNext(&hi->di)) != NULL) {
- Entry *e = dictGetKey(hi->de);
- hi->expire_time = entryGetExpiry(e);
- /* this condition still valid if expire_time equals EB_EXPIRE_TIME_INVALID */
- if (skipExpiredFields && ((mstime_t)hi->expire_time < commandTimeSnapshot()))
- continue;
- return C_OK;
- }
- return C_ERR;
- } else {
- serverPanic("Unknown hash encoding");
- }
- return C_OK;
-}
-
-/* Get the field or value at iterator cursor, for an iterator on a hash value
- * encoded as a listpack. Prototype is similar to `hashTypeGetFromListpack`. */
-void hashTypeCurrentFromListpack(hashTypeIterator *hi, int what,
- unsigned char **vstr,
- unsigned int *vlen,
- long long *vll,
- uint64_t *expireTime)
-{
- serverAssert(hi->encoding == OBJ_ENCODING_LISTPACK ||
- hi->encoding == OBJ_ENCODING_LISTPACK_EX);
-
- if (what & OBJ_HASH_KEY) {
- *vstr = lpGetValue(hi->fptr, vlen, vll);
- } else {
- *vstr = lpGetValue(hi->vptr, vlen, vll);
- }
-
- if (expireTime)
- *expireTime = hi->expire_time;
-}
-
-/* Get the field or value at iterator cursor, for an iterator on a hash value
- * encoded as a hash table. Prototype is similar to
- * `hashTypeGetFromHashTable`.
- *
- * 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);
- Entry *e = dictGetKey(hi->de);
-
- if (what & OBJ_HASH_KEY) {
- sds field = entryGetField(e);
- *str = field;
- *len = sdslen(field);
- } else {
- sds val = entryGetValue(e);
- *str = val;
- *len = sdslen(val);
- }
-
- if (expireTime)
- *expireTime = hi->expire_time;
-}
-
-/* Higher level function of hashTypeCurrent*() that returns the hash value
- * at current iterator position.
- *
- * The returned element 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
- * can always check the function return by checking the return value
- * type checking if vstr == NULL. */
-void hashTypeCurrentObject(hashTypeIterator *hi,
- int what,
- unsigned char **vstr,
- unsigned int *vlen,
- long long *vll,
- uint64_t *expireTime)
-{
- if (hi->encoding == OBJ_ENCODING_LISTPACK ||
- hi->encoding == OBJ_ENCODING_LISTPACK_EX)
- {
- *vstr = NULL;
- hashTypeCurrentFromListpack(hi, what, vstr, vlen, vll, expireTime);
- } else if (hi->encoding == OBJ_ENCODING_HT) {
- char *ele;
- size_t eleLen;
- hashTypeCurrentFromHashTable(hi, what, &ele, &eleLen, expireTime);
- *vstr = (unsigned char*) ele;
- *vlen = eleLen;
- } else {
- serverPanic("Unknown hash encoding");
- }
-}
-
-/* Return the key or value at the current iterator position as a new
- * SDS string. */
-sds hashTypeCurrentObjectNewSds(hashTypeIterator *hi, int what) {
- unsigned char *vstr;
- unsigned int vlen;
- long long vll;
-
- hashTypeCurrentObject(hi,what,&vstr,&vlen,&vll, NULL);
- if (vstr) return sdsnewlen(vstr,vlen);
- return sdsfromlonglong(vll);
-}
-
-/* Return the key at the current iterator position as a new entry. */
-Entry *hashTypeCurrentObjectNewEntry(hashTypeIterator *hi, size_t *usable) {
- char fieldBuf[LONG_STR_SIZE], valueBuf[LONG_STR_SIZE];
- unsigned char *fieldStr, *valueStr;
- unsigned int fieldLen, valueLen;
- long long fieldLl, valueLl;
- Entry *entry;
-
- /* Get field */
- hashTypeCurrentObject(hi, OBJ_HASH_KEY, &fieldStr, &fieldLen, &fieldLl, NULL);
- if (!fieldStr) {
- fieldLen = ll2string(fieldBuf, sizeof(fieldBuf), fieldLl);
- fieldStr = (unsigned char *) fieldBuf;
- }
- sds field = sdsnewlen(fieldStr, fieldLen);
-
- /* Get value */
- hashTypeCurrentObject(hi, OBJ_HASH_VALUE, &valueStr, &valueLen, &valueLl, NULL);
- if (!valueStr) {
- valueLen = ll2string(valueBuf, sizeof(valueBuf), valueLl);
- valueStr = (unsigned char *) valueBuf;
- }
- sds value = sdsnewlen(valueStr, valueLen);
- int hasExpiry = (hi->expire_time != EB_EXPIRE_TIME_INVALID);
-
- /* Create entry with field and value, using iterator's expire_time */
- uint32_t entryFlags = ENTRY_TAKE_VALUE | ((hasExpiry) ? ENTRY_HAS_EXPIRY : 0);
- entry = entryCreate(field, value, entryFlags, usable);
- sdsfree(field); /* entryCreate() doesn't take ownership of field */
-
- return entry;
-}
-
-static kvobj *hashTypeLookupWriteOrCreate(client *c, robj *key) {
- dictEntryLink link;
- kvobj *kv = lookupKeyWriteWithLink(c->db, key, &link);
- if (checkType(c, kv, OBJ_HASH)) return NULL;
-
- if (kv == NULL) {
- robj *o = createHashObject();
- kv = dbAddByLink(c->db, key, &o, &link);
- }
- return kv;
-}
-
-
-void hashTypeConvertListpack(robj *o, int enc) {
- serverAssert(o->encoding == OBJ_ENCODING_LISTPACK);
-
- if (enc == OBJ_ENCODING_LISTPACK) {
- /* Nothing to do... */
-
- } else if (enc == OBJ_ENCODING_LISTPACK_EX) {
- unsigned char *p;
-
- /* Append HASH_LP_NO_TTL to each field name - value pair. */
- p = lpFirst(o->ptr);
- while (p != NULL) {
- p = lpNext(o->ptr, p);
- serverAssert(p);
-
- o->ptr = lpInsertInteger(o->ptr, HASH_LP_NO_TTL, p, LP_AFTER, &p);
- p = lpNext(o->ptr, p);
- }
-
- listpackEx *lpt = listpackExCreate();
- lpt->lp = o->ptr;
- o->encoding = OBJ_ENCODING_LISTPACK_EX;
- o->ptr = lpt;
- } else if (enc == OBJ_ENCODING_HT) {
- hashTypeIterator hi;
- dict *dict;
- int ret;
-
- hashTypeInitIterator(&hi, o);
- dict = dictCreate(&entryHashDictType);
-
- /* Presize the dict to avoid rehashing */
- dictExpand(dict,hashTypeLength(o, 0));
-
- size_t usable, *alloc_size = htGetMetadataSize(dict);
- while (hashTypeNext(&hi, 0) != C_ERR) {
- Entry *entry = hashTypeCurrentObjectNewEntry(&hi, &usable);
- ret = dictAdd(dict, entry, NULL);
- if (ret != DICT_OK) {
- entryFree(entry, NULL); /* Needed for gcc ASAN */
- hashTypeResetIterator(&hi); /* Needed for gcc ASAN */
- serverLogHexDump(LL_WARNING,"listpack with dup elements dump",
- o->ptr,lpBytes(o->ptr));
- serverPanic("Listpack corruption detected");
- }
- *alloc_size += usable;
- }
- hashTypeResetIterator(&hi);
- zfree(o->ptr);
- o->encoding = OBJ_ENCODING_HT;
- o->ptr = dict;
- } else {
- serverPanic("Unknown hash encoding");
- }
-}
-
-/* db can be NULL to avoid registration in subexpires */
-void hashTypeConvertListpackEx(redisDb *db, robj *o, int enc) {
- serverAssert(o->encoding == OBJ_ENCODING_LISTPACK_EX);
-
- if (enc == OBJ_ENCODING_LISTPACK_EX) {
- return;
- } else if (enc == OBJ_ENCODING_HT) {
- uint64_t minExpire = EB_EXPIRE_TIME_INVALID;
- int ret, slot = -1;
- hashTypeIterator hi;
- dict *dict;
- htMetadataEx *dictExpireMeta;
- listpackEx *lpt = o->ptr;
-
- if (db && lpt->meta.trash != 1) {
- minExpire = hashTypeGetMinExpire(o, 0);
- slot = getKeySlot(kvobjGetKey(o));
- estoreRemove(db->subexpires, slot, o);
- }
-
- dict = dictCreate(&entryHashDictTypeWithHFE);
- dictExpand(dict,hashTypeLength(o, 0));
- dictExpireMeta = htGetMetadataEx(dict);
-
- /* Fillup dict HFE metadata */
- dictExpireMeta->hfe = ebCreate(); /* Allocate HFE DS */
- dictExpireMeta->expireMeta.trash = 1; /* mark as trash (as long it wasn't ebAdd()) */
-
- hashTypeInitIterator(&hi, o);
-
- size_t usable, *alloc_size = &dictExpireMeta->alloc_size;
- while (hashTypeNext(&hi, 0) != C_ERR) {
- /* Create entry with both field and value */
- Entry *entry = hashTypeCurrentObjectNewEntry(&hi, &usable);
- ret = dictAdd(dict, entry, NULL);
- if (ret != DICT_OK) {
- entryFree(entry, NULL); /* Needed for gcc ASAN */
- hashTypeResetIterator(&hi); /* Needed for gcc ASAN */
- serverLogHexDump(LL_WARNING,"listpack with dup elements dump",
- lpt->lp,lpBytes(lpt->lp));
- serverPanic("Listpack corruption detected");
- }
- *alloc_size += usable;
-
- if (hi.expire_time != EB_EXPIRE_TIME_INVALID)
- ebAdd(&dictExpireMeta->hfe, &hashFieldExpireBucketsType, entry, hi.expire_time);
- }
- hashTypeResetIterator(&hi);
- listpackExFree(lpt);
-
- o->encoding = OBJ_ENCODING_HT;
- o->ptr = dict;
-
- if (minExpire != EB_EXPIRE_TIME_INVALID)
- estoreAdd(db->subexpires, slot, o, minExpire);
- } else {
- serverPanic("Unknown hash encoding: %d", enc);
- }
-}
-
-/* NOTE: db can be NULL (Won't register in global HFE DS) */
-void hashTypeConvert(redisDb *db, robj *o, int enc) {
- if (o->encoding == OBJ_ENCODING_LISTPACK) {
- hashTypeConvertListpack(o, enc);
- } else if (o->encoding == OBJ_ENCODING_LISTPACK_EX) {
- hashTypeConvertListpackEx(db, o, enc);
- } else if (o->encoding == OBJ_ENCODING_HT) {
- serverPanic("Not implemented");
- } else {
- serverPanic("Unknown hash encoding");
- }
-}
-
-/* This is a helper function for the COPY command.
- * Duplicate a hash object, with the guarantee that the returned object
- * has the same encoding as the original one.
- *
- * The resulting object always has refcount set to 1 */
-robj *hashTypeDup(kvobj *o, uint64_t *minHashExpire) {
- robj *hobj;
- hashTypeIterator hi;
-
- serverAssert(o->type == OBJ_HASH);
-
- if(o->encoding == OBJ_ENCODING_LISTPACK) {
- unsigned char *zl = o->ptr;
- size_t sz = lpBytes(zl);
- unsigned char *new_zl = zmalloc(sz);
- memcpy(new_zl, zl, sz);
- hobj = createObject(OBJ_HASH, new_zl);
- hobj->encoding = OBJ_ENCODING_LISTPACK;
- } else if(o->encoding == OBJ_ENCODING_LISTPACK_EX) {
- listpackEx *lpt = o->ptr;
-
- if (lpt->meta.trash == 0)
- *minHashExpire = ebGetMetaExpTime(&lpt->meta);
-
- listpackEx *dup = listpackExCreate();
-
- 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) {
- htMetadataEx *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(&entryHashDictType);
- } else {
- /* Create a new dict with HFE metadata */
- d = dictCreate(&entryHashDictTypeWithHFE);
- dictExpireMetaSrc = htGetMetadataEx((dict *) o->ptr);
- dictExpireMetaDst = htGetMetadataEx(d);
- 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 subexpires DB) */
- if (dictExpireMetaSrc->expireMeta.trash == 0)
- *minHashExpire = ebGetMetaExpTime(&dictExpireMetaSrc->expireMeta);
- }
- dictExpand(d, dictSize((const dict*)o->ptr));
-
- size_t usable, *alloc_size = htGetMetadataSize(d);
- hashTypeInitIterator(&hi, o);
- while (hashTypeNext(&hi, 0) != C_ERR) {
- Entry *newEntry;
- uint64_t expireTime;
- /* Extract a field-value pair from an original hash object.*/
- char *field, *value;
- size_t fieldLen, valueLen;
- hashTypeCurrentFromHashTable(&hi, OBJ_HASH_KEY, &field, &fieldLen, &expireTime);
- hashTypeCurrentFromHashTable(&hi, OBJ_HASH_VALUE, &value, &valueLen, NULL);
-
- /* Create new entry with field and value */
- sds newFieldSds = sdsnewlen(field, fieldLen);
- sds newValueSds = sdsnewlen(value, valueLen);
- /* Create new entry with field and value, optional expiry. */
- if (expireTime == EB_EXPIRE_TIME_INVALID) {
- newEntry = entryCreate(newFieldSds, newValueSds,
- ENTRY_TAKE_VALUE, &usable);
- } else {
- newEntry = entryCreate(newFieldSds, newValueSds,
- ENTRY_TAKE_VALUE | ENTRY_HAS_EXPIRY, &usable);
- ebAdd(&dictExpireMetaDst->hfe, &hashFieldExpireBucketsType, newEntry, expireTime);
- }
- sdsfree(newFieldSds); /* (Only value ownership transferred to entry) */
-
- /* Add entry to new hash object. */
- dictAdd(d, newEntry, NULL); /* no_value=1, so value is NULL */
- *alloc_size += usable;
- }
- hashTypeResetIterator(&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, CommonEntry *key, CommonEntry *val) {
- if (hashobj->encoding == OBJ_ENCODING_HT) {
- dictEntry *de = dictGetFairRandomKey(hashobj->ptr);
- Entry *entry = dictGetKey(de);
- sds field = entryGetField(entry);
- key->sval = (unsigned char*) field;
- key->slen = sdslen(field);
- if (val) {
- sds s = entryGetValue(entry);
- val->sval = (unsigned char*)s;
- val->slen = sdslen(s);
- }
- } else if (hashobj->encoding == OBJ_ENCODING_LISTPACK) {
- lpRandomPair(hashobj->ptr, hashsize, (listpackEntry *) key, (listpackEntry *) val, 2);
- } else if (hashobj->encoding == OBJ_ENCODING_LISTPACK_EX) {
- lpRandomPair(hashTypeListpackGetLp(hashobj), hashsize, (listpackEntry *) key,
- (listpackEntry *) val, 3);
- } else {
- serverPanic("Unknown hash encoding");
- }
-}
-
-/* Delete all expired fields from the hash and delete the hash if left empty.
- *
- * updateSubexpires - If the hash should be updated in the subexpires DB with new
- * expiration time in case expired fields were deleted.
- *
- * Return next Expire time of the hash
- * - 0 if hash got deleted
- * - EB_EXPIRE_TIME_INVALID if no more fields to expire
- */
-uint64_t hashTypeActiveExpire(redisDb *db, kvobj *o, uint32_t *quota, int updateSubexpires) {
- uint64_t noExpireLeftRes = EB_EXPIRE_TIME_INVALID;
- ExpireInfo info = {0};
-
- if (o->encoding == OBJ_ENCODING_LISTPACK_EX) {
- info = (ExpireInfo) {
- .maxToExpire = *quota,
- .now = commandTimeSnapshot(),
- .itemsExpired = 0};
-
- listpackExExpire(db, o, &info);
- } else {
- serverAssert(o->encoding == OBJ_ENCODING_HT);
-
- dict *d = o->ptr;
- htMetadataEx *dictExpireMeta = htGetMetadataEx(d);
-
- OnFieldExpireCtx onFieldExpireCtx = { .hashObj = o, .db = db };
-
- info = (ExpireInfo){
- .maxToExpire = *quota,
- .onExpireItem = onFieldExpire,
- .ctx = &onFieldExpireCtx,
- .now = commandTimeSnapshot()
- };
-
- ebExpire(&dictExpireMeta->hfe, &hashFieldExpireBucketsType, &info);
- }
-
- /* Update quota left */
- *quota -= info.itemsExpired;
-
- /* In some cases, a field might have been deleted without updating the global DS.
- * As a result, active-expire might not expire any fields, in such cases,
- * we don't need to send notifications or perform other operations for this key. */
- if (info.itemsExpired) {
- sds keystr = kvobjGetKey(o);
- robj *key = createStringObject(keystr, sdslen(keystr));
- notifyKeyspaceEvent(NOTIFY_HASH, "hexpired", key, db->id);
- int slot;
- int deleted = 0;
-
- if (updateSubexpires) {
- slot = getKeySlot(keystr);
- estoreRemove(db->subexpires, slot, o);
- }
-
- if (hashTypeLength(o, 0) == 0) {
- notifyKeyspaceEvent(NOTIFY_GENERIC, "del", key, db->id);
- dbDelete(db, key);
- noExpireLeftRes = 0;
- deleted = 1;
- } else {
- if ((updateSubexpires) && (info.nextExpireTime != EB_EXPIRE_TIME_INVALID))
- estoreAdd(db->subexpires, slot, o, info.nextExpireTime);
- }
-
- keyModified(NULL, db, key, deleted ? NULL : o, 1);
- decrRefCount(key);
- }
-
- /* return 0 if hash got deleted, EB_EXPIRE_TIME_INVALID if no more fields
- * with expiration. Else return next expiration time */
- return (info.nextExpireTime == EB_EXPIRE_TIME_INVALID) ? noExpireLeftRes : info.nextExpireTime;
-}
-
-/* Delete all expired fields in hash if needed (Currently used only by HRANDFIELD)
- *
- * NOTICE: If we call this function in other places, we should consider the slot
- * migration scenario, where we don't want to delete expired fields. See also
- * expireIfNeeded().
- *
- * Return 1 if the entire hash was deleted, 0 otherwise.
- * This function might be pricy in case there are many expired fields.
- */
-static int hashTypeExpireIfNeeded(redisDb *db, kvobj *o) {
- uint64_t nextExpireTime;
- uint64_t minExpire = hashTypeGetMinExpire(o, 1 /*accurate*/);
-
- /* Nothing to expire */
- if ((mstime_t) minExpire >= commandTimeSnapshot())
- return 0;
-
- /* Follow expireIfNeeded() conditions of when not lazy-expire */
- if ( (server.loading) ||
- (server.allow_access_expired) ||
- (server.masterhost) || /* master-client or user-client, don't delete */
- (isPausedActionsWithUpdate(PAUSE_ACTION_EXPIRE)))
- return 0;
-
- /* Take care to expire all the fields */
- uint32_t quota = UINT32_MAX;
- nextExpireTime = hashTypeActiveExpire(db, o, &quota, 1);
- /* return 1 if the entire hash was deleted */
- return nextExpireTime == 0;
-}
-
-/* Return the next/minimum expiry time of the hash-field.
- * accurate=1 - Return the exact time by looking into the object DS.
- * accurate=0 - Return the minimum expiration time maintained in expireMeta
- * (Verify it is not trash before using it) which might not be
- * accurate due to optimization reasons.
- *
- * If not found, return EB_EXPIRE_TIME_INVALID
- */
-uint64_t hashTypeGetMinExpire(robj *o, int accurate) {
- ExpireMeta *expireMeta = NULL;
-
- if (!accurate) {
- 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 = &htGetMetadataEx(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);
- }
-
- /* accurate == 1 */
-
- 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;
-
- htMetadataEx *expireMeta = htGetMetadataEx(d);
- return ebGetNextTimeToExpire(expireMeta->hfe, &hashFieldExpireBucketsType);
- }
-}
-
-int hashTypeIsFieldsWithExpire(robj *o) {
- if (o->encoding == OBJ_ENCODING_LISTPACK) {
- return 0;
- } else if (o->encoding == OBJ_ENCODING_LISTPACK_EX) {
- return EB_EXPIRE_TIME_INVALID != listpackExGetMinExpire(o);
- } else { /* o->encoding == OBJ_ENCODING_HT */
- dict *d = o->ptr;
- /* If dict doesn't holds HFE metadata */
- if (!isDictWithMetaHFE(d))
- return 0;
- htMetadataEx *meta = htGetMetadataEx(d);
- return ebGetTotalItems(meta->hfe, &hashFieldExpireBucketsType) != 0;
- }
-}
-
-void hashTypeFree(robj *o) {
- switch (o->encoding) {
- case OBJ_ENCODING_HT:
- /* Verify hash is not registered in global HFE ds */
- if (isDictWithMetaHFE((dict*)o->ptr)) {
- htMetadataEx *m = htGetMetadataEx((dict*)o->ptr);
- serverAssert(m->expireMeta.trash == 1);
- }
-#ifdef DEBUG_ASSERTIONS
- dictEmpty(o->ptr, NULL);
- debugServerAssert(*htGetMetadataSize(o->ptr) == 0);
-#endif
- 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;
- }
-}
-
-ebuckets *hashTypeGetDictMetaHFE(dict *d) {
- htMetadataEx *dictExpireMeta = htGetMetadataEx(d);
- return &dictExpireMeta->hfe;
-}
-
-/*-----------------------------------------------------------------------------
- * Hash type commands
- *----------------------------------------------------------------------------*/
-
-void hsetnxCommand(client *c) {
- unsigned long hlen;
- int isHashDeleted;
- size_t oldsize = 0;
- robj *kv = hashTypeLookupWriteOrCreate(c,c->argv[1]);
- if (kv == NULL) return;
-
- if (hashTypeExists(c->db, kv, c->argv[2]->ptr, HFE_LAZY_EXPIRE, &isHashDeleted)) {
- addReply(c, shared.czero);
- return;
- }
-
- /* Field expired and in turn hash deleted. Create new one! */
- if (isHashDeleted) {
- robj *o = createHashObject();
- kv = dbAdd(c->db,c->argv[1],&o);
- }
-
- if (server.memory_tracking_per_slot)
- oldsize = hashTypeAllocSize(kv);
- hashTypeTryConversion(c->db, kv, c->argv, 2, 3);
- hashTypeSet(c->db, kv, c->argv[2]->ptr, c->argv[3]->ptr, HASH_SET_COPY);
- addReply(c, shared.cone);
- keyModified(c,c->db,c->argv[1], kv, 1);
- notifyKeyspaceEvent(NOTIFY_HASH,"hset",c->argv[1],c->db->id);
- hlen = hashTypeLength(kv, 0);
- updateKeysizesHist(c->db, getKeySlot(c->argv[1]->ptr), OBJ_HASH, hlen - 1, hlen);
- if (server.memory_tracking_per_slot)
- updateSlotAllocSize(c->db, getKeySlot(c->argv[1]->ptr), oldsize, hashTypeAllocSize(kv));
- server.dirty++;
-}
-
-void hsetCommand(client *c) {
- int i, created = 0;
- size_t oldsize = 0;
- kvobj *kv;
-
- if ((c->argc % 2) == 1) {
- addReplyErrorArity(c);
- return;
- }
-
- if ((kv = hashTypeLookupWriteOrCreate(c,c->argv[1])) == NULL) return;
-
- if (server.memory_tracking_per_slot)
- oldsize = hashTypeAllocSize(kv);
- hashTypeTryConversion(c->db, kv, c->argv, 2, c->argc-1);
-
- for (i = 2; i < c->argc; i += 2)
- created += !hashTypeSet(c->db, kv, c->argv[i]->ptr, c->argv[i+1]->ptr, HASH_SET_COPY);
-
- /* HMSET (deprecated) and HSET return value is different. */
- char *cmdname = c->argv[0]->ptr;
- if (cmdname[1] == 's' || cmdname[1] == 'S') {
- /* HSET */
- addReplyLongLong(c, created);
- } else {
- /* HMSET */
- addReply(c, shared.ok);
- }
- keyModified(c,c->db,c->argv[1],kv,1);
- unsigned long l = hashTypeLength(kv, 0);
- updateKeysizesHist(c->db, getKeySlot(c->argv[1]->ptr), OBJ_HASH, l - created, l);
- if (server.memory_tracking_per_slot)
- updateSlotAllocSize(c->db, getKeySlot(c->argv[1]->ptr), oldsize, hashTypeAllocSize(kv));
- notifyKeyspaceEvent(NOTIFY_HASH,"hset",c->argv[1],c->db->id);
- server.dirty += (c->argc - 2)/2;
-}
-
-/* Parse expire time from argument and do boundary checks. */
-static int parseExpireTime(client *c, robj *o, int unit, long long basetime,
- long long *expire)
-{
- long long val;
-
- /* Read the expiry time from command */
- if (getLongLongFromObjectOrReply(c, o, &val, NULL) != C_OK)
- return C_ERR;
-
- if (val < 0) {
- addReplyError(c,"invalid expire time, must be >= 0");
- return C_ERR;
- }
-
- if (unit == UNIT_SECONDS) {
- if (val > (long long) HFE_MAX_ABS_TIME_MSEC / 1000) {
- addReplyErrorExpireTime(c);
- return C_ERR;
- }
- val *= 1000;
- }
-
- if (val > (long long) HFE_MAX_ABS_TIME_MSEC - basetime) {
- addReplyErrorExpireTime(c);
- return C_ERR;
- }
- val += basetime;
- *expire = val;
- return C_OK;
-}
-
-/* Flags that are used as part of HGETEX and HSETEX commands. */
-#define HFE_EX (1<<0) /* Expiration time in seconds */
-#define HFE_PX (1<<1) /* Expiration time in milliseconds */
-#define HFE_EXAT (1<<2) /* Expiration time in unix seconds */
-#define HFE_PXAT (1<<3) /* Expiration time in unix milliseconds */
-#define HFE_PERSIST (1<<4) /* Persist fields */
-#define HFE_KEEPTTL (1<<5) /* Do not discard field ttl on set op */
-#define HFE_FXX (1<<6) /* Set fields if all the fields already exist */
-#define HFE_FNX (1<<7) /* Set fields if none of the fields exist */
-
-/* Command types for unified hash argument parser */
-#define HASH_CMD_HGETEX 0
-#define HASH_CMD_HSETEX 1
-
-/* Parse hash field expiration command arguments for both HGETEX and HSETEX.
- * HGETEX <key> [EX seconds|PX milliseconds|EXAT unix-time-seconds|PXAT unix-time-milliseconds|PERSIST]
- * FIELDS <numfields> field [field ...]
- * HSETEX <key> [EX seconds|PX milliseconds|EXAT unix-time-seconds|PXAT unix-time-milliseconds|KEEPTTL]
- * [FXX|FNX] FIELDS <numfields> field value [field value ...]
- */
-static int parseHashFieldExpireArgs(client *c, int *flags,
- long long *expire_time, int *expire_time_pos,
- int *first_field_pos, int *field_count,
- int command_type) {
- *flags = 0;
- *first_field_pos = -1;
- *field_count = -1;
- *expire_time_pos = -1;
-
- for (int i = 2; i < c->argc; i++) {
- if (!strcasecmp(c->argv[i]->ptr, "fields")) {
- int args_per_field = (command_type == HASH_CMD_HSETEX) ? 2 : 1;
- long val;
- /* Ensure we have at least the numfields argument */
- if (i + 1 >= c->argc) {
- addReplyErrorArity(c);
- return C_ERR;
- }
-
- if (getRangeLongFromObjectOrReply(c, c->argv[i + 1], 1, INT_MAX, &val,
- "invalid number of fields") != C_OK)
- return C_ERR;
-
- *first_field_pos = i + 2;
- *field_count = (int) val;
-
- /* Validate field count based on command type */
- long long required_args = *first_field_pos + ((long long)*field_count * args_per_field);
- if (required_args > c->argc) {
- addReplyError(c, "wrong number of arguments");
- return C_ERR;
- }
-
- /* Skip over numfields and all field-value pairs
- * Set i to the last position of the FIELDS block, loop will increment past it */
- i = *first_field_pos + (*field_count * args_per_field) - 1;
- continue;
- } else if (!strcasecmp(c->argv[i]->ptr, "EX")) {
- if (*flags & (HFE_EX | HFE_EXAT | HFE_PX | HFE_PXAT | HFE_KEEPTTL | HFE_PERSIST))
- goto err_expiration;
-
- if (i >= c->argc - 1)
- goto err_missing_expire;
-
- *flags |= HFE_EX;
- i++;
- if (parseExpireTime(c, c->argv[i], UNIT_SECONDS,
- commandTimeSnapshot(), expire_time) != C_OK)
- return C_ERR;
-
- *expire_time_pos = i;
- } else if (!strcasecmp(c->argv[i]->ptr, "PX")) {
- if (*flags & (HFE_EX | HFE_EXAT | HFE_PX | HFE_PXAT | HFE_KEEPTTL | HFE_PERSIST))
- goto err_expiration;
-
- if (i >= c->argc - 1)
- goto err_missing_expire;
-
- *flags |= HFE_PX;
- i++;
- if (parseExpireTime(c, c->argv[i], UNIT_MILLISECONDS,
- commandTimeSnapshot(), expire_time) != C_OK)
- return C_ERR;
-
- *expire_time_pos = i;
- } else if (!strcasecmp(c->argv[i]->ptr, "EXAT")) {
- if (*flags & (HFE_EX | HFE_EXAT | HFE_PX | HFE_PXAT | HFE_KEEPTTL | HFE_PERSIST))
- goto err_expiration;
-
- if (i >= c->argc - 1)
- goto err_missing_expire;
-
- *flags |= HFE_EXAT;
- i++;
- if (parseExpireTime(c, c->argv[i], UNIT_SECONDS, 0, expire_time) != C_OK)
- return C_ERR;
-
- *expire_time_pos = i;
- } else if (!strcasecmp(c->argv[i]->ptr, "PXAT")) {
- if (*flags & (HFE_EX | HFE_EXAT | HFE_PX | HFE_PXAT | HFE_KEEPTTL | HFE_PERSIST))
- goto err_expiration;
-
- if (i >= c->argc - 1)
- goto err_missing_expire;
-
- *flags |= HFE_PXAT;
- i++;
- if (parseExpireTime(c, c->argv[i], UNIT_MILLISECONDS, 0,
- expire_time) != C_OK)
- return C_ERR;
-
- *expire_time_pos = i;
- } else if (!strcasecmp(c->argv[i]->ptr, "PERSIST")) {
- if (*flags & (HFE_EX | HFE_EXAT | HFE_PX | HFE_PXAT | HFE_PERSIST))
- goto err_expiration;
- *flags |= HFE_PERSIST;
- } else if (!strcasecmp(c->argv[i]->ptr, "KEEPTTL")) {
- if (*flags & (HFE_EX | HFE_EXAT | HFE_PX | HFE_PXAT | HFE_KEEPTTL))
- goto err_expiration;
- *flags |= HFE_KEEPTTL;
- } else if (!strcasecmp(c->argv[i]->ptr, "FXX")) {
- if (*flags & (HFE_FXX | HFE_FNX))
- goto err_condition;
- *flags |= HFE_FXX;
- } else if (!strcasecmp(c->argv[i]->ptr, "FNX")) {
- if (*flags & (HFE_FXX | HFE_FNX))
- goto err_condition;
- *flags |= HFE_FNX;
- } else {
- addReplyErrorFormat(c, "unknown argument: %s", (char*) c->argv[i]->ptr);
- return C_ERR;
- }
- }
-
- /* Validate command-specific argument compatibility */
- if ((command_type == HASH_CMD_HGETEX && (*flags & (HFE_KEEPTTL | HFE_FXX | HFE_FNX))) ||
- (command_type == HASH_CMD_HSETEX && (*flags & HFE_PERSIST))) {
- addReplyError(c, "unknown argument");
- return C_ERR;
- }
-
- return C_OK;
-
-err_missing_expire:
- addReplyError(c, "missing expire time");
- return C_ERR;
-err_condition:
- addReplyError(c, "Only one of FXX or FNX arguments can be specified");
- return C_ERR;
-err_expiration:
- if (command_type == HASH_CMD_HSETEX) {
- addReplyError(c, "Only one of EX, PX, EXAT, PXAT or KEEPTTL arguments can be specified");
- } else {
- addReplyError(c, "Only one of EX, PX, EXAT, PXAT or PERSIST arguments can be specified");
- }
- return C_ERR;
-}
-
-/* Set the value of one or more fields of a given hash key, and optionally set
- * their expiration.
- *
- * HSETEX key
- * [FNX | FXX]
- * [EX seconds | PX milliseconds | EXAT unix-time-seconds | PXAT unix-time-milliseconds | KEEPTTL]
- * FIELDS <numfields> field value [field value...]
- *
- * Reply:
- * Integer reply: 0 if no fields were set (due to FXX/FNX args)
- * Integer reply: 1 if all the fields were set
- */
-void hsetexCommand(client *c) {
- int flags = 0, first_field_pos = 0, field_count = 0, expire_time_pos = -1;
- int updated = 0, deleted = 0, set_expiry;
- int expired = 0, fields_set = 0;
- long long expire_time = EB_EXPIRE_TIME_INVALID;
- int64_t oldlen, newlen;
- HashTypeSetEx setex;
- dictEntryLink link;
- size_t oldsize = 0;
-
- if (parseHashFieldExpireArgs(c, &flags, &expire_time, &expire_time_pos,
- &first_field_pos, &field_count, HASH_CMD_HSETEX) != C_OK)
- return;
-
- kvobj *o = lookupKeyWriteWithLink(c->db, c->argv[1], &link);
- if (checkType(c, o, OBJ_HASH))
- return;
-
- if (!o) {
- if (flags & HFE_FXX) {
- addReplyLongLong(c, 0);
- return;
- }
- o = createHashObject();
- dbAddByLink(c->db, c->argv[1], &o, &link);
- }
- oldlen = (int64_t) hashTypeLength(o, 0);
- if (server.memory_tracking_per_slot)
- oldsize = hashTypeAllocSize(o);
-
- if (flags & (HFE_FXX | HFE_FNX)) {
- int found = 0;
- for (int i = 0; i < field_count; i++) {
- sds field = c->argv[first_field_pos + (i * 2)]->ptr;
- unsigned char *vstr = NULL;
- unsigned int vlen = UINT_MAX;
- long long vll = LLONG_MAX;
- const int opt = HFE_LAZY_NO_NOTIFICATION |
- HFE_LAZY_NO_SIGNAL |
- HFE_LAZY_AVOID_HASH_DEL |
- HFE_LAZY_NO_UPDATE_KEYSIZES |
- HFE_LAZY_NO_UPDATE_ALLOCSIZES;
-
- GetFieldRes res = hashTypeGetValue(c->db, o, field, &vstr, &vlen, &vll, opt, NULL);
- int exists = (res == GETF_OK);
- expired += (res == GETF_EXPIRED);
- found += exists;
-
- /* Check for early exit if the condition is already invalid. */
- if (((flags & HFE_FXX) && !exists) ||
- ((flags & HFE_FNX) && exists))
- break;
- }
-
- int all_exists = (found == field_count);
- int non_exists = (found == 0);
-
- if (((flags & HFE_FNX) && !non_exists) ||
- ((flags & HFE_FXX) && !all_exists))
- {
- addReplyLongLong(c, 0);
- goto out;
- }
- }
- hashTypeTryConversion(c->db, o,c->argv, first_field_pos, c->argc - 1);
-
- /* Check if we will set the expiration time. */
- set_expiry = flags & (HFE_EX | HFE_PX | HFE_EXAT | HFE_PXAT);
- if (set_expiry)
- hashTypeSetExInit(c->argv[1], o, c, c->db, 0, &setex);
-
- for (int i = 0; i < field_count; i++) {
- sds field = c->argv[first_field_pos + (i * 2)]->ptr;
- sds value = c->argv[first_field_pos + (i * 2) + 1]->ptr;
-
- int opt = HASH_SET_COPY;
- /* If we are going to set the expiration time later, no need to discard
- * it as part of set operation now. */
- if (flags & (HFE_EX | HFE_PX | HFE_EXAT | HFE_PXAT | HFE_KEEPTTL))
- opt |= HASH_SET_KEEP_TTL;
-
- hashTypeSet(c->db, o, field, value, opt);
- fields_set = 1;
- /* Update the expiration time. */
- if (set_expiry) {
- int ret = hashTypeSetEx(o, field, expire_time, &setex);
- updated += (ret == HSETEX_OK);
- deleted += (ret == HSETEX_DELETED);
- }
- }
-
- if (set_expiry)
- hashTypeSetExDone(&setex);
-
- server.dirty += field_count;
-
- if (deleted) {
- /* If fields are deleted due to timestamp is being in the past, hdel's
- * are already propagated. No need to propagate the command itself. */
- preventCommandPropagation(c);
- } else if (set_expiry && !(flags & HFE_PXAT)) {
- /* Propagate as 'HSETEX <key> PXAT ..' if there is EX/EXAT/PX flag*/
-
- /* Replace EX/EXAT/PX with PXAT */
- rewriteClientCommandArgument(c, expire_time_pos - 1, shared.pxat);
- /* Replace timestamp with unix timestamp milliseconds. */
- robj *expire = createStringObjectFromLongLong(expire_time);
- rewriteClientCommandArgument(c, expire_time_pos, expire);
- decrRefCount(expire);
- }
-
- addReplyLongLong(c, 1);
-
-out:
- if (server.memory_tracking_per_slot)
- updateSlotAllocSize(c->db, getKeySlot(c->argv[1]->ptr), oldsize, hashTypeAllocSize(o));
- /* Emit keyspace notifications based on field expiry, mutation, or key deletion */
- if (fields_set || expired) {
- keyModified(c, c->db, c->argv[1], o, 1);
- if (expired)
- notifyKeyspaceEvent(NOTIFY_HASH, "hexpired", c->argv[1], c->db->id);
- if (fields_set) {
- notifyKeyspaceEvent(NOTIFY_HASH, "hset", c->argv[1], c->db->id);
- if (deleted || updated)
- notifyKeyspaceEvent(NOTIFY_HASH, deleted ? "hdel" : "hexpire", c->argv[1], c->db->id);
- }
- }
- /* Key may become empty due to lazy expiry in hashTypeExists()
- * or the new expiration time is in the past.*/
- newlen = (int64_t) hashTypeLength(o, 0);
- if (newlen == 0) {
- newlen = -1;
- /* Del key but don't update KEYSIZES. else it will decr wrong bin in histogram */
- dbDeleteSkipKeysizesUpdate(c->db, c->argv[1]);
- notifyKeyspaceEvent(NOTIFY_GENERIC, "del", c->argv[1], c->db->id);
- }
- if (oldlen != newlen)
- updateKeysizesHist(c->db, getKeySlot(c->argv[1]->ptr), OBJ_HASH,
- oldlen, newlen);
-}
-
-void hincrbyCommand(client *c) {
- long long value, incr, oldvalue;
- kvobj *o;
- sds new;
- unsigned char *vstr;
- unsigned int vlen;
- size_t oldsize = 0;
-
- if (getLongLongFromObjectOrReply(c,c->argv[3],&incr,NULL) != C_OK) return;
- if ((o = hashTypeLookupWriteOrCreate(c,c->argv[1])) == NULL) return;
-
- GetFieldRes res = hashTypeGetValue(c->db,o,c->argv[2]->ptr,&vstr,&vlen,&value,
- HFE_LAZY_EXPIRE, NULL);
- if (res == GETF_OK) {
- if (vstr) {
- if (string2ll((char*)vstr,vlen,&value) == 0) {
- addReplyError(c,"hash value is not an integer");
- return;
- }
- } /* Else hashTypeGetValue() already stored it into &value */
- } else if ((res == GETF_NOT_FOUND) || (res == GETF_EXPIRED)) {
- value = 0;
- unsigned long l = hashTypeLength(o, 0);
- updateKeysizesHist(c->db, getKeySlot(c->argv[1]->ptr), OBJ_HASH, l, l + 1);
- } else {
- /* Field expired and in turn hash deleted. Create new one! */
- o = createHashObject();
- dbAdd(c->db,c->argv[1],&o);
- value = 0;
- updateKeysizesHist(c->db, getKeySlot(c->argv[1]->ptr), OBJ_HASH, 0, 1);
- }
-
- oldvalue = value;
- if ((incr < 0 && oldvalue < 0 && incr < (LLONG_MIN-oldvalue)) ||
- (incr > 0 && oldvalue > 0 && incr > (LLONG_MAX-oldvalue))) {
- addReplyError(c,"increment or decrement would overflow");
- return;
- }
- value += incr;
- new = sdsfromlonglong(value);
- if (server.memory_tracking_per_slot)
- oldsize = hashTypeAllocSize(o);
- hashTypeSet(c->db, o,c->argv[2]->ptr,new,HASH_SET_TAKE_VALUE | HASH_SET_KEEP_TTL);
- if (server.memory_tracking_per_slot)
- updateSlotAllocSize(c->db, getKeySlot(c->argv[1]->ptr), oldsize, hashTypeAllocSize(o));
- addReplyLongLong(c,value);
- keyModified(c,c->db,c->argv[1], o, 1);
- notifyKeyspaceEvent(NOTIFY_HASH,"hincrby",c->argv[1],c->db->id);
- server.dirty++;
-}
-
-void hincrbyfloatCommand(client *c) {
- long double value, incr;
- long long ll;
- kvobj *o;
- sds new;
- unsigned char *vstr;
- unsigned int vlen;
- size_t oldsize = 0;
-
- if (getLongDoubleFromObjectOrReply(c,c->argv[3],&incr,NULL) != C_OK) return;
- if (isnan(incr) || isinf(incr)) {
- addReplyError(c,"value is NaN or Infinity");
- return;
- }
- if ((o = hashTypeLookupWriteOrCreate(c,c->argv[1])) == NULL) return;
- GetFieldRes res = hashTypeGetValue(c->db, o,c->argv[2]->ptr,&vstr,&vlen,&ll,
- HFE_LAZY_EXPIRE, NULL);
- if (res == GETF_OK) {
- if (vstr) {
- if (string2ld((char*)vstr,vlen,&value) == 0) {
- addReplyError(c,"hash value is not a float");
- return;
- }
- } else {
- value = (long double)ll;
- }
- } else if ((res == GETF_NOT_FOUND) || (res == GETF_EXPIRED)) {
- value = 0;
- unsigned long l = hashTypeLength(o, 0);
- updateKeysizesHist(c->db, getKeySlot(c->argv[1]->ptr), OBJ_HASH, l, l + 1);
- } else {
- /* Field expired and in turn hash deleted. Create new one! */
- o = createHashObject();
- dbAdd(c->db, c->argv[1], &o);
- value = 0;
- updateKeysizesHist(c->db, getKeySlot(c->argv[1]->ptr), OBJ_HASH, 0, 1);
- }
-
- value += incr;
- if (isnan(value) || isinf(value)) {
- addReplyError(c,"increment would produce NaN or Infinity");
- return;
- }
-
- char buf[MAX_LONG_DOUBLE_CHARS];
- int len = ld2string(buf,sizeof(buf),value,LD_STR_HUMAN);
- new = sdsnewlen(buf,len);
- if (server.memory_tracking_per_slot)
- oldsize = hashTypeAllocSize(o);
- hashTypeSet(c->db, o,c->argv[2]->ptr,new,HASH_SET_TAKE_VALUE | HASH_SET_KEEP_TTL);
- if (server.memory_tracking_per_slot)
- updateSlotAllocSize(c->db, getKeySlot(c->argv[1]->ptr), oldsize, hashTypeAllocSize(o));
- addReplyBulkCBuffer(c,buf,len);
- keyModified(c,c->db,c->argv[1],o,1);
- notifyKeyspaceEvent(NOTIFY_HASH,"hincrbyfloat",c->argv[1],c->db->id);
- server.dirty++;
-
- /* Always replicate HINCRBYFLOAT as an HSETEX command with the final value
- * in order to make sure that differences in float precision or formatting
- * will not create differences in replicas or after an AOF restart.
- * The KEEPTTL flag is used to make sure the field TTL is preserved. */
- robj *newobj;
- newobj = createRawStringObject(buf,len);
- rewriteClientCommandVector(c, 7, shared.hsetex, c->argv[1], shared.keepttl,
- shared.fields, shared.integers[1], c->argv[2], newobj);
- decrRefCount(newobj);
-}
-
-static GetFieldRes addHashFieldToReply(client *c, kvobj *o, sds field, int hfeFlags) {
- if (o == NULL) {
- addReplyNull(c);
- return GETF_NOT_FOUND;
- }
-
- unsigned char *vstr = NULL;
- unsigned int vlen = UINT_MAX;
- long long vll = LLONG_MAX;
-
- GetFieldRes res = hashTypeGetValue(c->db, o, field, &vstr, &vlen, &vll, hfeFlags, NULL);
- if (res == GETF_OK) {
- if (vstr) {
- addReplyBulkCBuffer(c, vstr, vlen);
- } else {
- addReplyBulkLongLong(c, vll);
- }
- } else {
- addReplyNull(c);
- }
- return res;
-}
-
-void hgetCommand(client *c) {
- kvobj *o;
-
- if ((o = lookupKeyReadOrReply(c,c->argv[1],shared.null[c->resp])) == NULL ||
- checkType(c,o,OBJ_HASH)) return;
-
- addHashFieldToReply(c, o, c->argv[2]->ptr, HFE_LAZY_EXPIRE);
-}
-
-void hmgetCommand(client *c) {
- GetFieldRes res = GETF_OK;
- int i;
- int expired = 0, deleted = 0;
-
- /* Don't abort when the key cannot be found. Non-existing keys are empty
- * hashes, where HMGET should respond with a series of null bulks. */
- kvobj *o = lookupKeyRead(c->db, c->argv[1]);
- if (checkType(c,o,OBJ_HASH)) return;
-
- addReplyArrayLen(c, c->argc-2);
- for (i = 2; i < c->argc ; i++) {
- if (!deleted) {
- res = addHashFieldToReply(c, o, c->argv[i]->ptr, HFE_LAZY_NO_NOTIFICATION);
- expired += (res == GETF_EXPIRED);
- deleted += (res == GETF_EXPIRED_HASH);
- } else {
- /* If hash got lazy expired since all fields are expired (o is invalid),
- * then fill the rest with trivial nulls and return. */
- addReplyNull(c);
- }
- }
-
- if (expired) {
- notifyKeyspaceEvent(NOTIFY_HASH, "hexpired", c->argv[1], c->db->id);
- if (deleted)
- notifyKeyspaceEvent(NOTIFY_GENERIC, "del", c->argv[1], c->db->id);
- }
-}
-
-/* Get and delete the value of one or more fields of a given hash key.
- * HGETDEL <key> FIELDS <numfields> field1 field2 ...
- * Reply: list of the value associated with each field or nil if the field
- * doesn’t exist.
- */
-void hgetdelCommand(client *c) {
- int res = 0, hfe = 0, deleted = 0, expired = 0;
- int64_t oldlen = -1; /* not exists as long as it is not set */
- long num_fields = 0;
- size_t oldsize = 0;
-
- kvobj *o = lookupKeyWrite(c->db, c->argv[1]);
- if (checkType(c, o, OBJ_HASH))
- return;
-
- if (strcasecmp(c->argv[2]->ptr, "FIELDS") != 0) {
- addReplyError(c, "Mandatory argument FIELDS is missing or not at the right position");
- return;
- }
-
- /* Read number of fields */
- if (getRangeLongFromObjectOrReply(c, c->argv[3], 1, LONG_MAX, &num_fields,
- "Number of fields must be a positive integer") != C_OK)
- return;
-
- /* Verify `numFields` is consistent with number of arguments */
- if (num_fields != c->argc - 4) {
- addReplyError(c, "The `numfields` parameter must match the number of arguments");
- return;
- }
-
- /* Hash field expiration is optimized to avoid frequent update global HFE DS
- * for each field deletion. Eventually active-expiration will run and update
- * or remove the hash from global HFE DS gracefully. Nevertheless, statistic
- * "subexpiry" might reflect wrong number of hashes with HFE to the user if
- * it is the last field with expiration. The following logic checks if this
- * is the last field with expiration and removes it from global HFE DS. */
- if (o) {
- hfe = hashTypeIsFieldsWithExpire(o);
- oldlen = hashTypeLength(o, 0);
- if (server.memory_tracking_per_slot)
- oldsize = hashTypeAllocSize(o);
- }
-
- addReplyArrayLen(c, num_fields);
- for (int i = 4; i < c->argc; i++) {
- const int flags = HFE_LAZY_NO_NOTIFICATION |
- HFE_LAZY_NO_SIGNAL |
- HFE_LAZY_AVOID_HASH_DEL |
- HFE_LAZY_NO_UPDATE_KEYSIZES |
- HFE_LAZY_NO_UPDATE_ALLOCSIZES;
- res = addHashFieldToReply(c, o, c->argv[i]->ptr, flags);
- expired += (res == GETF_EXPIRED);
- /* Try to delete only if it's found and not expired lazily. */
- if (res == GETF_OK) {
- deleted++;
- serverAssert(hashTypeDelete(o, c->argv[i]->ptr) == 1);
- }
- }
-
- /* Return if no modification has been made. */
- if (expired == 0 && deleted == 0)
- return;
-
- if (server.memory_tracking_per_slot)
- updateSlotAllocSize(c->db, getKeySlot(c->argv[1]->ptr), oldsize, hashTypeAllocSize(o));
- keyModified(c, c->db, c->argv[1], o, 1);
-
- if (expired)
- notifyKeyspaceEvent(NOTIFY_HASH, "hexpired", c->argv[1], c->db->id);
- if (deleted) {
- notifyKeyspaceEvent(NOTIFY_HASH, "hdel", c->argv[1], c->db->id);
- server.dirty += deleted;
-
- /* Propagate as HDEL command.
- * Orig: HGETDEL <key> FIELDS <numfields> field1 field2 ...
- * Repl: HDEL <key> field1 field2 ... */
- rewriteClientCommandArgument(c, 0, shared.hdel);
- rewriteClientCommandArgument(c, 2, NULL); /* Delete FIELDS arg */
- rewriteClientCommandArgument(c, 2, NULL); /* Delete <numfields> arg */
- }
-
- /* Key may have become empty because of deleting fields or lazy expire. */
- int64_t newlen = (int64_t) hashTypeLength(o, 0);
- if (newlen == 0) {
- newlen = -1;
- /* Del key but don't update KEYSIZES. else it will decr wrong bin in histogram */
- dbDeleteSkipKeysizesUpdate(c->db, c->argv[1]);
- notifyKeyspaceEvent(NOTIFY_GENERIC, "del", c->argv[1], c->db->id);
- } else {
- if (hfe && (hashTypeIsFieldsWithExpire(o) == 0)) { /*is it last HFE*/
- estoreRemove(c->db->subexpires, getKeySlot(kvobjGetKey(o)), o);
- }
- }
-
- if (oldlen != newlen)
- updateKeysizesHist(c->db, getKeySlot(c->argv[1]->ptr), OBJ_HASH,
- oldlen, newlen);
-}
-
-/* Get the value of one or more fields of a given hash key and optionally set
- * their expiration.
- *
- * HGETEX <key>
- * [EX seconds | PX milliseconds | EXAT unix-time-seconds | PXAT unix-time-milliseconds | PERSIST]
- * FIELDS <numfields> field1 field2 ...
- *
- * Reply: list of the value associated with each field or nil if the field
- * doesn’t exist.
- */
-void hgetexCommand(client *c) {
- int expired = 0, deleted = 0, updated = 0;
- int parse_flags = 0, expire_time_pos = -1, first_field_pos = -1, num_fields = -1;
- long long expire_time = 0;
- int64_t oldlen = 0, newlen = -1;
- HashTypeSetEx setex;
- size_t oldsize = 0;
-
- kvobj *o = lookupKeyWrite(c->db, c->argv[1]);
- if (checkType(c, o, OBJ_HASH))
- return;
-
- /* Parse arguments using flexible parser */
- if (parseHashFieldExpireArgs(c, &parse_flags, &expire_time, &expire_time_pos, &first_field_pos, &num_fields, HASH_CMD_HGETEX) != C_OK)
- return;
-
- /* Non-existing keys and empty hashes are the same thing. Reply null if the
- * key does not exist.*/
- if (!o) {
- addReplyArrayLen(c, num_fields);
- for (int i = 0; i < num_fields; i++)
- addReplyNull(c);
- return;
- }
-
- if (server.memory_tracking_per_slot)
- oldsize = hashTypeAllocSize(o);
- oldlen = hashTypeLength(o, 0);
- if (parse_flags)
- hashTypeSetExInit(c->argv[1], o, c, c->db, 0, &setex);
-
- addReplyArrayLen(c, num_fields);
- for (int i = first_field_pos; i < first_field_pos + num_fields; i++) {
- const int flags = HFE_LAZY_NO_NOTIFICATION |
- HFE_LAZY_NO_SIGNAL |
- HFE_LAZY_AVOID_HASH_DEL |
- HFE_LAZY_NO_UPDATE_KEYSIZES |
- HFE_LAZY_NO_UPDATE_ALLOCSIZES;
- sds field = c->argv[i]->ptr;
- int res = addHashFieldToReply(c, o, c->argv[i]->ptr, flags);
- expired += (res == GETF_EXPIRED);
-
- /* Set expiration only if the field exists and not expired lazily. */
- if (res == GETF_OK && parse_flags) {
- if (parse_flags & HFE_PERSIST)
- expire_time = EB_EXPIRE_TIME_INVALID;
-
- res = hashTypeSetEx(o, field, expire_time, &setex);
- deleted += (res == HSETEX_DELETED);
- updated += (res == HSETEX_OK);
- }
- }
-
- if (parse_flags)
- hashTypeSetExDone(&setex);
-
- if (server.memory_tracking_per_slot)
- updateSlotAllocSize(c->db, getKeySlot(c->argv[1]->ptr), oldsize, hashTypeAllocSize(o));
-
- /* Exit early if no modification has been made. */
- if (expired == 0 && deleted == 0 && updated == 0)
- return;
-
- server.dirty += deleted + updated;
- keyModified(c, c->db, c->argv[1], o, 1);
-
- /* This command will never be propagated as it is. It will be propagated as
- * HDELs when fields are lazily expired or deleted, if the new timestamp is
- * in the past. HDEL's will be emitted as part of addHashFieldToReply()
- * or hashTypeSetEx() in this case.
- *
- * If PERSIST flags is used, it will be propagated as HPERSIST command.
- * IF EX/EXAT/PX/PXAT flags are used, it will be replicated as HPEXPRITEAT.
- */
- if (expired)
- notifyKeyspaceEvent(NOTIFY_HASH, "hexpired", c->argv[1], c->db->id);
- if (updated) {
- /* Build canonical command for propagation */
- int canonical_argc;
- robj **canonical_argv;
- int idx = 0;
-
- if (parse_flags & HFE_PERSIST) {
- notifyKeyspaceEvent(NOTIFY_HASH, "hpersist", c->argv[1], c->db->id);
- /* Build canonical HPERSIST command: HPERSIST key FIELDS numfields field1 field2 ... */
- canonical_argc = 4 + num_fields;
- canonical_argv = zmalloc(sizeof(robj*) * canonical_argc);
- canonical_argv[idx++] = shared.hpersist;
- incrRefCount(shared.hpersist);
- canonical_argv[idx++] = c->argv[1]; /* key */
- incrRefCount(c->argv[1]);
- } else {
- notifyKeyspaceEvent(NOTIFY_HASH, "hexpire", c->argv[1], c->db->id);
- /* Build canonical HPEXPIREAT command: HPEXPIREAT key timestamp FIELDS numfields field1 field2 ... */
- canonical_argc = 5 + num_fields;
- canonical_argv = zmalloc(sizeof(robj*) * canonical_argc);
- canonical_argv[idx++] = shared.hpexpireat;
- incrRefCount(shared.hpexpireat);
- canonical_argv[idx++] = c->argv[1]; /* key */
- incrRefCount(c->argv[1]);
- canonical_argv[idx++] = createStringObjectFromLongLong(expire_time); /* timestamp */
- }
-
- canonical_argv[idx++] = shared.fields;
- incrRefCount(shared.fields);
- canonical_argv[idx++] = createStringObjectFromLongLong(num_fields);
- for (int i = 0; i < num_fields; i++) {
- canonical_argv[idx++] = c->argv[first_field_pos + i];
- incrRefCount(c->argv[first_field_pos + i]);
- }
-
- replaceClientCommandVector(c, canonical_argc, canonical_argv);
- } else if (deleted) {
- /* If we are here, fields are deleted because new timestamp was in the
- * past. HDELs are already propagated as part of hashTypeSetEx(). */
- notifyKeyspaceEvent(NOTIFY_HASH, "hdel", c->argv[1], c->db->id);
- preventCommandPropagation(c);
- }
-
- /* Key may become empty due to lazy expiry in addHashFieldToReply()
- * or the new expiration time is in the past.*/
- newlen = hashTypeLength(o, 0);
-
- updateKeysizesHist(c->db, getKeySlot(c->argv[1]->ptr), OBJ_HASH, oldlen, newlen);
- if (newlen == 0) {
- dbDelete(c->db, c->argv[1]);
- notifyKeyspaceEvent(NOTIFY_GENERIC, "del", c->argv[1], c->db->id);
- }
-}
-
-void hdelCommand(client *c) {
- kvobj *o;
- int j, deleted = 0, keyremoved = 0;
- size_t oldsize = 0;
-
- if ((o = lookupKeyWriteOrReply(c,c->argv[1],shared.czero)) == NULL ||
- checkType(c,o,OBJ_HASH)) return;
-
- int64_t oldLen = (int64_t) hashTypeLength(o, 0);
- if (server.memory_tracking_per_slot)
- oldsize = hashTypeAllocSize(o);
-
- /* Hash field expiration is optimized to avoid frequent update global HFE DS for
- * each field deletion. Eventually active-expiration will run and update or remove
- * the hash from global HFE DS gracefully. Nevertheless, statistic "subexpiry"
- * might reflect wrong number of hashes with HFE to the user if it is the last
- * field with expiration. The following logic checks if this is indeed the last
- * field with expiration and removes it from global HFE DS. */
- int isHFE = hashTypeIsFieldsWithExpire(o);
-
- for (j = 2; j < c->argc; j++) {
- if (hashTypeDelete(o,c->argv[j]->ptr)) {
- deleted++;
- if (hashTypeLength(o, 0) == 0) {
- if (server.memory_tracking_per_slot)
- updateSlotAllocSize(c->db, getKeySlot(c->argv[1]->ptr), oldsize, hashTypeAllocSize(o));
- /* del key but don't update KEYSIZES. Else it will decr wrong bin in histogram */
- dbDeleteSkipKeysizesUpdate(c->db, c->argv[1]);
- keyremoved = 1;
- break;
- }
- }
- }
- if (server.memory_tracking_per_slot && !keyremoved)
- updateSlotAllocSize(c->db, getKeySlot(c->argv[1]->ptr), oldsize, hashTypeAllocSize(o));
- if (deleted) {
- int64_t newLen = -1; /* The value -1 indicates that the key is deleted. */
- keyModified(c, c->db, c->argv[1], keyremoved ? NULL : o, 1);
- notifyKeyspaceEvent(NOTIFY_HASH,"hdel",c->argv[1],c->db->id);
- if (keyremoved) {
- notifyKeyspaceEvent(NOTIFY_GENERIC, "del", c->argv[1], c->db->id);
- } else {
- if (isHFE && (hashTypeIsFieldsWithExpire(o) == 0)) /* is it last HFE */
- estoreRemove(c->db->subexpires, getKeySlot(c->argv[1]->ptr), o);
- newLen = oldLen - deleted;
- }
- updateKeysizesHist(c->db, getKeySlot(c->argv[1]->ptr), OBJ_HASH, oldLen, newLen);
- server.dirty += deleted;
- }
- addReplyLongLong(c,deleted);
-}
-
-void hlenCommand(client *c) {
- kvobj *o;
-
- if ((o = lookupKeyReadOrReply(c,c->argv[1],shared.czero)) == NULL ||
- checkType(c,o,OBJ_HASH)) return;
-
- addReplyLongLong(c,hashTypeLength(o, 0));
-}
-
-void hstrlenCommand(client *c) {
- kvobj *o;
- unsigned char *vstr = NULL;
- unsigned int vlen = UINT_MAX;
- long long vll = LLONG_MAX;
-
- if ((o = lookupKeyReadOrReply(c,c->argv[1],shared.czero)) == NULL ||
- checkType(c,o,OBJ_HASH)) return;
-
- GetFieldRes res = hashTypeGetValue(c->db, o, c->argv[2]->ptr, &vstr,
- &vlen, &vll, HFE_LAZY_EXPIRE, NULL);
-
- if (res == GETF_NOT_FOUND || res == GETF_EXPIRED || res == GETF_EXPIRED_HASH) {
- addReply(c, shared.czero);
- return;
- }
-
- size_t len = vstr ? vlen : sdigits10(vll);
- addReplyLongLong(c,len);
-}
-
-static void addHashIteratorCursorToReply(client *c, hashTypeIterator *hi, int what) {
- if (hi->encoding == OBJ_ENCODING_LISTPACK ||
- hi->encoding == OBJ_ENCODING_LISTPACK_EX)
- {
- unsigned char *vstr = NULL;
- unsigned int vlen = UINT_MAX;
- long long vll = LLONG_MAX;
-
- hashTypeCurrentFromListpack(hi, what, &vstr, &vlen, &vll, NULL);
- if (vstr)
- addReplyBulkCBuffer(c, vstr, vlen);
- else
- addReplyBulkLongLong(c, vll);
- } else if (hi->encoding == OBJ_ENCODING_HT) {
- char *value;
- size_t len;
- hashTypeCurrentFromHashTable(hi, what, &value, &len, NULL);
- addReplyBulkCBuffer(c, value, len);
- } else {
- serverPanic("Unknown hash encoding");
- }
-}
-
-void genericHgetallCommand(client *c, int flags) {
- kvobj *o;
- hashTypeIterator hi;
- int length, count = 0;
- size_t oldsize = 0;
-
- robj *emptyResp = (flags & OBJ_HASH_KEY && flags & OBJ_HASH_VALUE) ?
- shared.emptymap[c->resp] : shared.emptyarray;
- if ((o = lookupKeyReadOrReply(c,c->argv[1],emptyResp))
- == NULL || checkType(c,o,OBJ_HASH)) return;
-
- /* 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. */
- if ((length = hashTypeLength(o, 1 /*subtractExpiredFields*/)) == 0) {
- addReply(c, emptyResp);
- return;
- }
-
- if (flags & OBJ_HASH_KEY && flags & OBJ_HASH_VALUE) {
- addReplyMapLen(c, length);
- } else {
- addReplyArrayLen(c, length);
- }
-
- if (server.memory_tracking_per_slot)
- oldsize = hashTypeAllocSize(o);
- hashTypeInitIterator(&hi, o);
-
- while (hashTypeNext(&hi, 1 /*skipExpiredFields*/) != C_ERR) {
- if (flags & OBJ_HASH_KEY) {
- addHashIteratorCursorToReply(c, &hi, OBJ_HASH_KEY);
- count++;
- }
- if (flags & OBJ_HASH_VALUE) {
- addHashIteratorCursorToReply(c, &hi, OBJ_HASH_VALUE);
- count++;
- }
- }
-
- hashTypeResetIterator(&hi);
- if (server.memory_tracking_per_slot)
- updateSlotAllocSize(c->db, getKeySlot(c->argv[1]->ptr), oldsize, hashTypeAllocSize(o));
-
- /* Make sure we returned the right number of elements. */
- if (flags & OBJ_HASH_KEY && flags & OBJ_HASH_VALUE) count /= 2;
- serverAssert(count == length);
-}
-
-void hkeysCommand(client *c) {
- genericHgetallCommand(c,OBJ_HASH_KEY);
-}
-
-void hvalsCommand(client *c) {
- genericHgetallCommand(c,OBJ_HASH_VALUE);
-}
-
-void hgetallCommand(client *c) {
- genericHgetallCommand(c,OBJ_HASH_KEY|OBJ_HASH_VALUE);
-}
-
-void hexistsCommand(client *c) {
- kvobj *o;
- if ((o = lookupKeyReadOrReply(c,c->argv[1],shared.czero)) == NULL ||
- checkType(c,o,OBJ_HASH)) return;
-
- addReply(c,hashTypeExists(c->db,o,c->argv[2]->ptr,HFE_LAZY_EXPIRE, NULL) ?
- shared.cone : shared.czero);
-}
-
-void hscanCommand(client *c) {
- kvobj *o;
- unsigned long long cursor;
- size_t oldsize = 0;
-
- if (parseScanCursorOrReply(c,c->argv[2],&cursor) == C_ERR) return;
- if ((o = lookupKeyReadOrReply(c,c->argv[1],shared.emptyscan)) == NULL ||
- checkType(c,o,OBJ_HASH)) return;
-
- if (server.memory_tracking_per_slot)
- oldsize = hashTypeAllocSize(o);
- scanGenericCommand(c,o,cursor);
- if (server.memory_tracking_per_slot)
- updateSlotAllocSize(c->db, getKeySlot(c->argv[1]->ptr), oldsize, hashTypeAllocSize(o));
-}
-
-static void hrandfieldReplyWithListpack(client *c, unsigned int count, listpackEntry *keys, listpackEntry *vals) {
- for (unsigned long i = 0; i < count; i++) {
- if (vals && c->resp > 2)
- addReplyArrayLen(c,2);
- if (keys[i].sval)
- addReplyBulkCBuffer(c, keys[i].sval, keys[i].slen);
- else
- addReplyBulkLongLong(c, keys[i].lval);
- if (vals) {
- if (vals[i].sval)
- addReplyBulkCBuffer(c, vals[i].sval, vals[i].slen);
- else
- addReplyBulkLongLong(c, vals[i].lval);
- }
- }
-}
-
-/* How many times bigger should be the hash compared to the requested size
- * for us to not use the "remove elements" strategy? Read later in the
- * implementation for more info. */
-#define HRANDFIELD_SUB_STRATEGY_MUL 3
-
-/* If client is trying to ask for a very large number of random elements,
- * queuing may consume an unlimited amount of memory, so we want to limit
- * the number of randoms per time. */
-#define HRANDFIELD_RANDOM_SAMPLE_LIMIT 1000
-
-void hrandfieldWithCountCommand(client *c, long l, int withvalues) {
- unsigned long count, size;
- int uniq = 1;
- kvobj *hash;
- size_t oldsize = 0;
-
- if ((hash = lookupKeyReadOrReply(c,c->argv[1],shared.emptyarray))
- == NULL || checkType(c,hash,OBJ_HASH)) return;
-
- if(l >= 0) {
- count = (unsigned long) l;
- } else {
- count = -l;
- uniq = 0;
- }
-
- /* Delete all expired fields. If the entire hash got deleted then return empty array. */
- if (hashTypeExpireIfNeeded(c->db, hash)) {
- addReply(c, shared.emptyarray);
- return;
- }
-
- /* Delete expired fields */
- size = hashTypeLength(hash, 0);
-
- /* If count is zero, serve it ASAP to avoid special cases later. */
- if (count == 0) {
- addReply(c,shared.emptyarray);
- return;
- }
-
- if (server.memory_tracking_per_slot)
- oldsize = hashTypeAllocSize(hash);
-
- /* CASE 1: The count was negative, so the extraction method is just:
- * "return N random elements" sampling the whole set every time.
- * This case is trivial and can be served without auxiliary data
- * structures. This case is the only one that also needs to return the
- * elements in random order. */
- if (!uniq || count == 1) {
- if (withvalues && c->resp == 2)
- addReplyArrayLen(c, count*2);
- else
- addReplyArrayLen(c, count);
- if (hash->encoding == OBJ_ENCODING_HT) {
- while (count--) {
- dictEntry *de = dictGetFairRandomKey(hash->ptr);
- Entry *entry = dictGetKey(de);
- sds fieldStr = entryGetField(entry);
- if (withvalues && c->resp > 2)
- addReplyArrayLen(c,2);
- addReplyBulkCBuffer(c, fieldStr, sdslen(fieldStr));
- if (withvalues) {
- sds value = entryGetValue(entry);
- addReplyBulkCBuffer(c, value, sdslen(value));
- }
- if (c->flags & CLIENT_CLOSE_ASAP)
- break;
- }
- } else if (hash->encoding == OBJ_ENCODING_LISTPACK ||
- hash->encoding == OBJ_ENCODING_LISTPACK_EX)
- {
- listpackEntry *keys, *vals = NULL;
- unsigned long limit, sample_count;
- unsigned char *lp = hashTypeListpackGetLp(hash);
- int tuple_len = hash->encoding == OBJ_ENCODING_LISTPACK ? 2 : 3;
-
- limit = count > HRANDFIELD_RANDOM_SAMPLE_LIMIT ? HRANDFIELD_RANDOM_SAMPLE_LIMIT : count;
- keys = zmalloc(sizeof(listpackEntry)*limit);
- if (withvalues)
- vals = zmalloc(sizeof(listpackEntry)*limit);
- while (count) {
- sample_count = count > limit ? limit : count;
- count -= sample_count;
- lpRandomPairs(lp, sample_count, keys, vals, tuple_len);
- hrandfieldReplyWithListpack(c, sample_count, keys, vals);
- if (c->flags & CLIENT_CLOSE_ASAP)
- break;
- }
- zfree(keys);
- zfree(vals);
- }
- goto out;
- }
-
- /* Initiate reply count, RESP3 responds with nested array, RESP2 with flat one. */
- long reply_size = count < size ? count : size;
- if (withvalues && c->resp == 2)
- addReplyArrayLen(c, reply_size*2);
- else
- addReplyArrayLen(c, reply_size);
-
- /* CASE 2:
- * The number of requested elements is greater than the number of
- * elements inside the hash: simply return the whole hash. */
- if(count >= size) {
- hashTypeIterator hi;
- hashTypeInitIterator(&hi, hash);
- while (hashTypeNext(&hi, 0) != C_ERR) {
- if (withvalues && c->resp > 2)
- addReplyArrayLen(c,2);
- addHashIteratorCursorToReply(c, &hi, OBJ_HASH_KEY);
- if (withvalues)
- addHashIteratorCursorToReply(c, &hi, OBJ_HASH_VALUE);
- }
- hashTypeResetIterator(&hi);
- goto out;
- }
-
- /* CASE 2.5 listpack only. Sampling unique elements, in non-random order.
- * Listpack encoded hashes are meant to be relatively small, so
- * HRANDFIELD_SUB_STRATEGY_MUL isn't necessary and we rather not make
- * copies of the entries. Instead, we emit them directly to the output
- * buffer.
- *
- * And it is inefficient to repeatedly pick one random element from a
- * listpack in CASE 4. So we use this instead. */
- if (hash->encoding == OBJ_ENCODING_LISTPACK ||
- hash->encoding == OBJ_ENCODING_LISTPACK_EX)
- {
- unsigned char *lp = hashTypeListpackGetLp(hash);
- int tuple_len = hash->encoding == OBJ_ENCODING_LISTPACK ? 2 : 3;
- listpackEntry *keys, *vals = NULL;
- keys = zmalloc(sizeof(listpackEntry)*count);
- if (withvalues)
- vals = zmalloc(sizeof(listpackEntry)*count);
- serverAssert(lpRandomPairsUnique(lp, count, keys, vals, tuple_len) == count);
- hrandfieldReplyWithListpack(c, count, keys, vals);
- zfree(keys);
- zfree(vals);
- goto out;
- }
-
- /* CASE 3:
- * The number of elements inside the hash of type dict is not greater than
- * HRANDFIELD_SUB_STRATEGY_MUL times the number of requested elements.
- * In this case we create an array of dictEntry pointers from the original hash,
- * and subtract random elements to reach the requested number of elements.
- *
- * This is done because if the number of requested elements is just
- * a bit less than the number of elements in the hash, the natural approach
- * used into CASE 4 is highly inefficient. */
- if (count*HRANDFIELD_SUB_STRATEGY_MUL > size) {
- /* Hashtable encoding (generic implementation) */
- dict *ht = hash->ptr;
- dictIterator di;
- dictEntry *de;
- unsigned long idx = 0;
-
- /* Allocate a temporary array of pointers to stored key-values in dict and
- * assist it to remove random elements to reach the right count. */
- struct FieldValPair {
- sds field;
- sds value;
- } *pairs = zmalloc(sizeof(struct FieldValPair) * size);
-
- /* Add all the elements into the temporary array. */
- dictInitIterator(&di, ht);
- while((de = dictNext(&di)) != NULL) {
- Entry *e = dictGetKey(de);
- pairs[idx++] = (struct FieldValPair) {entryGetField(e), entryGetValue(e)};
- }
- dictResetIterator(&di);
-
- /* Remove random elements to reach the right count. */
- while (size > count) {
- unsigned long toDiscardIdx = rand() % size;
- pairs[toDiscardIdx] = pairs[--size];
- }
-
- /* Reply with what's in the array */
- for (idx = 0; idx < size; idx++) {
- if (withvalues && c->resp > 2)
- addReplyArrayLen(c,2);
- addReplyBulkCBuffer(c, pairs[idx].field, sdslen(pairs[idx].field));
- if (withvalues)
- addReplyBulkCBuffer(c, pairs[idx].value, sdslen(pairs[idx].value));
- }
-
- zfree(pairs);
- }
-
- /* CASE 4: We have a big hash compared to the requested number of elements.
- * In this case we can simply get random elements from the hash and add
- * to the temporary hash, trying to eventually get enough unique elements
- * to reach the specified count. */
- else {
- /* Allocate temporary dictUnique to find unique elements. Just keep ref
- * to key-value from the original hash. This dict relaxes hash function
- * to be based on field's pointer */
- dictType uniqueDictType = { .hashFunction = dictPtrHash };
- dict *dictUnique = dictCreate(&uniqueDictType);
- dictExpand(dictUnique, count);
-
- /* Hashtable encoding (generic implementation) */
- unsigned long added = 0;
-
- while(added < count) {
- dictEntry *de = dictGetFairRandomKey(hash->ptr);
- serverAssert(de != NULL);
- Entry *e = dictGetKey(de);
- sds field = entryGetField(e);
- sds value = entryGetValue(e);
-
- /* Try to add the object to the dictionary. If it already exists
- * free it, otherwise increment the number of objects we have
- * in the result dictionary. */
- if (dictAdd(dictUnique, field, value) != DICT_OK)
- continue;
-
- added++;
-
- /* We can reply right away, so that we don't need to store the value in the dict. */
- if (withvalues && c->resp > 2)
- addReplyArrayLen(c,2);
-
- addReplyBulkCBuffer(c, field, sdslen(field));
- if (withvalues)
- addReplyBulkCBuffer(c, value, sdslen(value));
- }
-
- /* Release memory */
- dictRelease(dictUnique);
- }
-out:
- if (server.memory_tracking_per_slot)
- updateSlotAllocSize(c->db, getKeySlot(c->argv[1]->ptr), oldsize, hashTypeAllocSize(hash));
-}
-
-/*
- * HRANDFIELD - Return a random field from the hash value stored at key.
- * CLI usage: HRANDFIELD key [<count> [WITHVALUES]]
- *
- * Considerations for the current imp of HRANDFIELD & HFE feature:
- * HRANDFIELD might access any of the fields in the hash as some of them might
- * be expired. And so the Implementation of HRANDFIELD along with HFEs
- * might be one of the two options:
- * 1. Expire hash-fields before diving into handling HRANDFIELD.
- * 2. Refine HRANDFIELD cases to deal with expired fields.
- *
- * Regarding the first option, as reference, the command RANDOMKEY also declares
- * on O(1) complexity, yet might be stuck on a very long (but not infinite) loop
- * trying to find non-expired keys. Furthermore RANDOMKEY also evicts expired keys
- * along the way even though it is categorized as a read-only command. Note that
- * the case of HRANDFIELD is more lightweight versus RANDOMKEY since HFEs have
- * much more effective and aggressive active-expiration for fields behind.
- *
- * The second option introduces additional implementation complexity to HRANDFIELD.
- * We could further refine HRANDFIELD cases to differentiate between scenarios
- * with many expired fields versus few expired fields, and adjust based on the
- * percentage of expired fields. However, this approach could still lead to long
- * loops or necessitate expiring fields before selecting them. For the “lightweight”
- * cases it is also expected to have a lightweight expiration.
- *
- * Considering the pros and cons, and the fact that HRANDFIELD is an infrequent
- * command (particularly with HFEs) and the fact we have effective active-expiration
- * behind for hash-fields, it is better to keep it simple and choose the option #1.
- */
-void hrandfieldCommand(client *c) {
- long l;
- int withvalues = 0;
- kvobj *hash;
- CommonEntry ele;
- size_t oldsize = 0;
-
- if (c->argc >= 3) {
- if (getRangeLongFromObjectOrReply(c,c->argv[2],-LONG_MAX,LONG_MAX,&l,NULL) != C_OK) return;
- if (c->argc > 4 || (c->argc == 4 && strcasecmp(c->argv[3]->ptr,"withvalues"))) {
- addReplyErrorObject(c,shared.syntaxerr);
- return;
- } else if (c->argc == 4) {
- withvalues = 1;
- if (l < -LONG_MAX/2 || l > LONG_MAX/2) {
- addReplyError(c,"value is out of range");
- return;
- }
- }
- hrandfieldWithCountCommand(c, l, withvalues);
- return;
- }
-
- /* Handle variant without <count> argument. Reply with simple bulk string */
- if ((hash = lookupKeyReadOrReply(c,c->argv[1],shared.null[c->resp]))== NULL ||
- checkType(c,hash,OBJ_HASH)) {
- return;
- }
-
- /* Delete all expired fields. If the entire hash got deleted then return null. */
- if (hashTypeExpireIfNeeded(c->db, hash)) {
- addReply(c,shared.null[c->resp]);
- return;
- }
-
- if (server.memory_tracking_per_slot)
- oldsize = hashTypeAllocSize(hash);
- hashTypeRandomElement(hash,hashTypeLength(hash, 0),&ele,NULL);
- if (server.memory_tracking_per_slot)
- updateSlotAllocSize(c->db, getKeySlot(c->argv[1]->ptr), oldsize, hashTypeAllocSize(hash));
-
- if (ele.sval)
- addReplyBulkCBuffer(c, ele.sval, ele.slen);
- else
- addReplyBulkLongLong(c, ele.lval);
-}
-
-/*-----------------------------------------------------------------------------
- * Hash Field with optional expiry (based on entry)
- *----------------------------------------------------------------------------*/
-
-static ExpireMeta* hentryGetExpireMeta(const eItem e) {
- /* extract the expireMeta from the field (entry) */
- return entryRefExpiryMeta((Entry *)e);
-}
-
-/* Remove TTL from the field. Assumed ExpireMeta is attached and has valid value */
-static void hfieldPersist(robj *hashObj, Entry *entry) {
- uint64_t fieldExpireTime = entryGetExpiry(entry);
- if (fieldExpireTime == EB_EXPIRE_TIME_INVALID)
- return;
-
- /* if field is set with expire, then dict must has HFE metadata attached */
- dict *d = hashObj->ptr;
- htMetadataEx *dictExpireMeta = htGetMetadataEx(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, entry);
-
- /* 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. */
-}
-
-/*-----------------------------------------------------------------------------
- * 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;
- Entry *e = item;
- kvobj *kv = expCtx->hashObj;
- size_t oldsize = 0;
- sds key = kvobjGetKey(kv);
-
- if (server.memory_tracking_per_slot)
- oldsize = hashTypeAllocSize(kv);
- sds field = entryGetField(e);
- propagateHashFieldDeletion(expCtx->db, key, field, sdslen(field));
-
- /* update keysizes */
- unsigned long l = hashTypeLength(expCtx->hashObj, 0);
- updateKeysizesHist(expCtx->db, getKeySlot(key), OBJ_HASH, l, l - 1);
-
- serverAssert(hashTypeDelete(expCtx->hashObj, field) == 1);
- if (server.memory_tracking_per_slot)
- updateSlotAllocSize(expCtx->db, getKeySlot(key), oldsize, hashTypeAllocSize(kv));
- server.stat_expired_subkeys++;
- return ACT_REMOVE_EXP_ITEM;
-}
-
-/* Retrieve the ExpireMeta associated with the hash.
- * The caller is responsible for ensuring that it is indeed attached. */
-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;
- htMetadataEx *dictExpireMeta = htGetMetadataEx(d);
- return &dictExpireMeta->expireMeta;
- } else {
- serverPanic("Unknown encoding: %d", hashObj->encoding);
- }
-}
-
-/* Generic structure to hold parsed arguments for all hash field commands */
-typedef struct {
- /* FIELDS arguments */
- int fieldsPos; /* Position of FIELDS keyword (-1 if not found) */
- int numFieldsPos; /* Position of numfields argument */
- int firstFieldPos; /* Position of first field */
- int fieldCount; /* Number of fields */
-
- /* HEXPIRE family arguments */
- int expireTimePos; /* Position of expire time argument */
- long long expireTime; /* Parsed expire time */
- int expireCondition; /* HFE_NX, HFE_XX, HFE_GT, HFE_LT */
-} HashCommandArgs;
-
-/* Parser for HEXPIRE family commands with flexible keyword ordering.
- * Returns C_OK on success, C_ERR on error (with reply sent). */
-static int parseHashCommandArgs(client *c, HashCommandArgs *args,
- long long basetime, int unit)
-{
- memset(args, 0, sizeof(*args));
- args->fieldsPos = -1;
- args->expireTimePos = 2;
-
- if (parseExpireTime(c, c->argv[2], unit, basetime, &args->expireTime) != C_OK) {
- return C_ERR;
- }
-
- /* Parse remaining arguments starting from position 3 */
- for (int i = 3; i < c->argc; i++) {
- char *arg = c->argv[i]->ptr;
-
- /* FIELDS keyword - supported by ALL hash field commands */
- if (!strcasecmp(arg, "FIELDS")) {
- if (args->fieldsPos != -1) {
- addReplyError(c, "FIELDS keyword specified multiple times");
- return C_ERR;
- }
-
- if (i >= c->argc - 2) {
- addReplyError(c, "FIELDS requires at least numfields and one field argument");
- return C_ERR;
- }
-
- args->fieldsPos = i;
- args->numFieldsPos = i + 1;
- long numFields;
- if (getRangeLongFromObjectOrReply(c, c->argv[args->numFieldsPos], 1, INT_MAX,
- &numFields, "Parameter `numFields` should be greater than 0") != C_OK)
- return C_ERR;
-
- args->fieldCount = (int)numFields;
- args->firstFieldPos = i + 2;
-
- /* Check bounds - we must have exactly the right number of fields */
- if (args->firstFieldPos + args->fieldCount > c->argc) {
- addReplyError(c, "wrong number of arguments");
- return C_ERR;
- }
-
- /* Skip over the field arguments */
- i = args->firstFieldPos + args->fieldCount - 1;
- continue;
- }
-
- /* Expiration condition keywords - validation moved outside loop for performance */
- if (!strcasecmp(arg, "NX")) {
- args->expireCondition |= HFE_NX;
- continue;
- } else if (!strcasecmp(arg, "XX")) {
- args->expireCondition |= HFE_XX;
- continue;
- } else if (!strcasecmp(arg, "GT")) {
- args->expireCondition |= HFE_GT;
- continue;
- } else if (!strcasecmp(arg, "LT")) {
- args->expireCondition |= HFE_LT;
- continue;
- }
-
- addReplyErrorFormat(c, "unknown argument: %s", (char*) c->argv[i]->ptr);
- return C_ERR;
- }
-
- if (__builtin_popcount(args->expireCondition & (HFE_NX|HFE_XX|HFE_GT|HFE_LT)) > 1) {
- addReplyError(c, "Multiple condition flags specified");
- return C_ERR;
- }
-
- return C_OK;
-}
-
-/* HTTL key <FIELDS count field [field ...]> */
-static void httlGenericCommand(client *c, const char *cmd, long long basetime, int unit){
- UNUSED(cmd);
- kvobj *hashObj;
- long numFields = 0, numFieldsAt = 3;
-
- /* Read the hash object */
- hashObj = lookupKeyRead(c->db, c->argv[1]);
- if (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, "The `numfields` parameter must match the number of arguments");
- return;
- }
-
- /* Non-existing keys and empty hashes are the same thing. It also means
- * fields in the command don't exist in the hash key. */
- if (!hashObj) {
- addReplyArrayLen(c, numFields);
- for (int i = 0; i < numFields; i++) {
- addReplyLongLong(c, HFE_GET_NO_FIELD);
- }
- 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;
- size_t oldsize = dictMemUsage(d);
-
- 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;
- }
-
- Entry *entry = dictGetKey(de);
- uint64_t expire = entryGetExpiry(entry);
- 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));
- }
- if (server.memory_tracking_per_slot)
- updateSlotAllocSize(c->db, getKeySlot(c->argv[1]->ptr), oldsize, dictMemUsage(d));
- 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.
- *
- * PROPAGATE TO REPLICA:
- * The command will be translated into HPEXPIREAT and the expiration time will be
- * converted to absolute time in milliseconds.
- *
- * As we need to propagate H(P)EXPIRE(AT) command to the replica, each field that
- * is mentioned in the command should be categorized into one of the four options:
- * 1. Field’s expiration time updated successfully - Propagate it to replica as
- * part of the HPEXPIREAT command.
- * 2. The field got deleted since the time is in the past - propagate also HDEL
- * command to delete the field. Also remove the field from the propagated
- * HPEXPIREAT command.
- * 3. Condition not met for the field - Remove the field from the propagated
- * HPEXPIREAT command.
- * 4. Field doesn't exists - Remove the field from propagated HPEXPIREAT command.
- *
- * If none of the provided fields match option #1, that is provided time of the
- * command is in the past, then avoid propagating the HPEXPIREAT command to the
- * replica.
- *
- * This approach is aligned with existing EXPIRE command. If a given key has already
- * expired, then DEL will be propagated instead of EXPIRE command. If condition
- * not met, then command will be rejected. Otherwise, EXPIRE command will be
- * propagated for given key.
- */
-static void hexpireGenericCommand(client *c, long long basetime, int unit) {
- HashCommandArgs args;
- int fieldsNotSet = 0, updated = 0, deleted = 0;
- int64_t oldlen, newlen;
- robj *keyArg = c->argv[1];
- size_t oldsize = 0;
-
- /* Read the hash object */
- kvobj *hashObj = lookupKeyWrite(c->db, keyArg);
- if (checkType(c, hashObj, OBJ_HASH))
- return;
-
- /* Parse arguments using flexible keyword-based parsing */
- if (parseHashCommandArgs(c, &args, basetime, unit) != C_OK)
- return;
-
- /* Non-existing keys and empty hashes are the same thing. It also means
- * fields in the command don't exist in the hash key. */
- if (!hashObj) {
- addReplyArrayLen(c, args.fieldCount);
- for (int i = 0; i < args.fieldCount; i++) {
- addReplyLongLong(c, HSETEX_NO_FIELD);
- }
- return;
- }
-
- oldlen = hashTypeLength(hashObj, 0);
- if (server.memory_tracking_per_slot)
- oldsize = hashTypeAllocSize(hashObj);
-
- HashTypeSetEx exCtx;
- hashTypeSetExInit(keyArg, hashObj, c, c->db, args.expireCondition, &exCtx);
- addReplyArrayLen(c, args.fieldCount);
-
- /* Lazy allocation of fieldsToRemove - only allocate when failures occur */
- int *fieldsToRemove = NULL;
- int removeCount = 0;
-
- for (int i = 0; i < args.fieldCount; i++) {
- int fieldPos = args.firstFieldPos + i;
- sds field = c->argv[fieldPos]->ptr;
- SetExRes res = hashTypeSetEx(hashObj, field, args.expireTime, &exCtx);
- updated += (res == HSETEX_OK);
- deleted += (res == HSETEX_DELETED);
-
- if (unlikely(res != HSETEX_OK)) {
- if (fieldsToRemove == NULL) {
- fieldsToRemove = zmalloc(sizeof(int) * (args.fieldCount > 0 ? args.fieldCount : 1));
- }
- /* Remember this field position for later removal from propagation */
- fieldsToRemove[removeCount++] = fieldPos;
- fieldsNotSet = 1;
- }
-
- addReplyLongLong(c, res);
- }
-
- hashTypeSetExDone(&exCtx);
- if (server.memory_tracking_per_slot)
- updateSlotAllocSize(c->db, getKeySlot(keyArg->ptr), oldsize, hashTypeAllocSize(hashObj));
-
- if (deleted + updated > 0) {
- server.dirty += deleted + updated;
- keyModified(c, c->db, keyArg, hashObj, 1);
- notifyKeyspaceEvent(NOTIFY_HASH, deleted ? "hdel" : "hexpire",
- keyArg, c->db->id);
- }
-
- newlen = (int64_t) hashTypeLength(hashObj, 0);
- if (newlen == 0) {
- newlen = -1;
- /* Del key but don't update KEYSIZES. Else it will decr wrong bin in histogram */
- dbDeleteSkipKeysizesUpdate(c->db, keyArg);
- notifyKeyspaceEvent(NOTIFY_GENERIC, "del", keyArg, c->db->id);
- }
-
- if (oldlen != newlen)
- updateKeysizesHist(c->db, getKeySlot(c->argv[1]->ptr), OBJ_HASH,
- oldlen, newlen);
-
- /* Avoid propagating command if not even one field was updated (Either because
- * the time is in the past, and corresponding HDELs were sent, or conditions
- * not met) then it is useless and invalid to propagate command with no fields */
- if (updated == 0) {
- preventCommandPropagation(c);
- zfree(fieldsToRemove);
- return;
- }
-
- /* Handle propagation using command rewriting
- * Rewrite to canonical HPEXPIREAT command */
- if (c->cmd->proc != hpexpireatCommand) {
- rewriteClientCommandArgument(c, 0, shared.hpexpireat);
-
- robj *expireTimeObj = createStringObjectFromLongLong(args.expireTime);
- rewriteClientCommandArgument(c, args.expireTimePos, expireTimeObj);
- decrRefCount(expireTimeObj);
- }
-
- /* For partial failures, remove failed fields from the original command */
- if (fieldsNotSet) {
- for (int i = removeCount - 1; i >= 0; i--) {
- rewriteClientCommandArgument(c, fieldsToRemove[i], NULL);
- }
- robj *newFieldCount = createStringObjectFromLongLong(updated);
- rewriteClientCommandArgument(c, args.fieldsPos + 1, newFieldCount);
- decrRefCount(newFieldCount);
- }
-
- if (fieldsToRemove)
- zfree(fieldsToRemove);
-}
-
-/* HPEXPIRE key milliseconds [ NX | XX | GT | LT] FIELDS numfields <field [field ...]> */
-void hpexpireCommand(client *c) {
- hexpireGenericCommand(c,commandTimeSnapshot(),UNIT_MILLISECONDS);
-}
-
-/* HEXPIRE key seconds [NX | XX | GT | LT] FIELDS numfields <field [field ...]> */
-void hexpireCommand(client *c) {
- hexpireGenericCommand(c,commandTimeSnapshot(),UNIT_SECONDS);
-}
-
-/* HEXPIREAT key unix-time-seconds [NX | XX | GT | LT] FIELDS numfields <field [field ...]> */
-void hexpireatCommand(client *c) {
- hexpireGenericCommand(c,0,UNIT_SECONDS);
-}
-
-/* HPEXPIREAT key unix-time-milliseconds [NX | XX | GT | LT] FIELDS numfields <field [field ...]> */
-void hpexpireatCommand(client *c) {
- hexpireGenericCommand(c,0,UNIT_MILLISECONDS);
-}
-
-/* for each specified field: get the remaining time to live in seconds*/
-/* HTTL key FIELDS numfields <field [field ...]> */
-void httlCommand(client *c) {
- httlGenericCommand(c, "httl", commandTimeSnapshot(), UNIT_SECONDS);
-}
-
-/* HPTTL key FIELDS numfields <field [field ...]> */
-void hpttlCommand(client *c) {
- httlGenericCommand(c, "hpttl", commandTimeSnapshot(), UNIT_MILLISECONDS);
-}
-
-/* HEXPIRETIME key FIELDS numfields <field [field ...]> */
-void hexpiretimeCommand(client *c) {
- httlGenericCommand(c, "hexpiretime", 0, UNIT_SECONDS);
-}
-
-/* HPEXPIRETIME key FIELDS numfields <field [field ...]> */
-void hpexpiretimeCommand(client *c) {
- httlGenericCommand(c, "hexpiretime", 0, UNIT_MILLISECONDS);
-}
-
-/* HPERSIST key FIELDS numfields <field [field ...]> */
-void hpersistCommand(client *c) {
- long numFields = 0, numFieldsAt = 3;
- int changed = 0; /* Used to determine whether to send a notification. */
-
- /* Read the hash object */
- kvobj *hashObj = lookupKeyWrite(c->db, c->argv[1]);
- if (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, "The `numfields` parameter must match the number of arguments");
- return;
- }
-
- /* Non-existing keys and empty hashes are the same thing. It also means
- * fields in the command don't exist in the hash key. */
- if (!hashObj) {
- addReplyArrayLen(c, numFields);
- for (int i = 0; i < numFields; i++) {
- addReplyLongLong(c, HFE_PERSIST_NO_FIELD);
- }
- 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;
- size_t oldsize = 0;
-
- 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;
- }
-
- if (server.memory_tracking_per_slot)
- oldsize = hashTypeAllocSize(hashObj);
- listpackExUpdateExpiry(hashObj, field, fptr, vptr, HASH_LP_NO_TTL);
- if (server.memory_tracking_per_slot)
- updateSlotAllocSize(c->db, getKeySlot(c->argv[1]->ptr), oldsize, hashTypeAllocSize(hashObj));
- addReplyLongLong(c, HFE_PERSIST_OK);
- changed = 1;
- }
- } else if (hashObj->encoding == OBJ_ENCODING_HT) {
- dict *d = hashObj->ptr;
- size_t oldsize = dictMemUsage(d);
-
- 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;
- }
-
- Entry *entry = dictGetKey(de);
- uint64_t expire = entryGetExpiry(entry);
- 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, entry);
- addReplyLongLong(c, HFE_PERSIST_OK);
- changed = 1;
- }
- if (server.memory_tracking_per_slot)
- updateSlotAllocSize(c->db, getKeySlot(c->argv[1]->ptr), oldsize, dictMemUsage(d));
- } 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);
- keyModified(c, c->db, c->argv[1], hashObj, 1);
- server.dirty++;
- }
-}