src/t_hash.c C 6,911 lines View on github.com → Search inside
File is large — showing lines 1–2,000 of 6,911.
1/*2 * Copyright (c) 2009-Present, Redis Ltd.3 * All rights reserved.4 * 5 * Copyright (c) 2024-present, Valkey contributors.6 * All rights reserved.7 *8 * Licensed under your choice of (a) the Redis Source Available License 2.09 * (RSALv2); or (b) the Server Side Public License v1 (SSPLv1); or (c) the10 * GNU Affero General Public License v3 (AGPLv3).11 */1213#include "server.h"14#include "bio.h"15#include "redisassert.h"16#include "ebuckets.h"17#include "entry.h"18#include "vector.h"19#include <math.h>2021/* Threshold for HEXPIRE and HPERSIST to be considered whether it is worth to22 * update the expiration time of the hash object in global HFE DS. */23#define HASH_NEW_EXPIRE_DIFF_THRESHOLD 40002425/* Reserve 2 bits out of hash-field expiration time for possible future lightweight26 * indexing/categorizing of fields. It can be achieved by hacking HFE as follows:27 *28 *    HPEXPIREAT key [ 2^47 + USER_INDEX ] FIELDS numfields field [field …]29 *30 * Redis will also need to expose kind of HEXPIRESCAN and HEXPIRECOUNT for this31 * idea. Yet to be better defined.32 *33 * HFE_MAX_ABS_TIME_MSEC constraint must be enforced only at API level. Internally,34 * the expiration time can be up to EB_EXPIRE_TIME_MAX for future readiness.35 */36#define HFE_MAX_ABS_TIME_MSEC (EB_EXPIRE_TIME_MAX >> 2)3738typedef enum GetFieldRes {39    /* common (Used by hashTypeGet* value family) */40    GETF_OK = 0,            /* The field was found. */41    GETF_NOT_FOUND,         /* The field was not found. */42    GETF_EXPIRED,           /* Logically expired (Might be lazy deleted or not) */43    GETF_EXPIRED_HASH,      /* Delete hash since retrieved field was expired and44                             * it was the last field in the hash. */45} GetFieldRes;4647typedef listpackEntry CommonEntry; /* extend usage beyond lp */4849#define FIELDS_STACK_SIZE 645051/* A vec with an embedded stack buffer, used to collect field robj pointers52 * for subkey notifications without heap allocation in the common case. */53typedef struct fieldvec { vec v; void *buf[FIELDS_STACK_SIZE]; } fieldvec;5455static inline vec *fieldvecInit(fieldvec *fv, size_t cap) {56    vecInit(&fv->v, fv->buf, FIELDS_STACK_SIZE);57    vecReserve(&fv->v, cap);58    return &fv->v;59}6061/* hash field expiration (HFE) funcs */62static ExpireAction onFieldExpire(eItem item, void *ctx);63static ExpireMeta* hentryGetExpireMeta(const eItem field);64static void hexpireGenericCommand(client *c, long long basetime, int unit);65static void hfieldPersist(robj *hashObj, Entry *entry);66static void propagateHashFieldDeletion(redisDb *db, sds key, char *field, size_t fieldLen);6768/* hash dictType funcs */69static void dictEntryDestructor(dict *d, void *entry);70static size_t hashDictMetadataBytes(dict *d);71static size_t hashDictWithExpireMetadataBytes(dict *d);72static void hashDictWithExpireOnRelease(dict *d);73static kvobj* hashTypeLookupWriteOrCreate(client *c, robj *key);74static int hashTypeCanConvertTmplToListpack(robj *o);75static void hashTypeConvertTmplToListpackOrHT(robj *o, int lp_enc, int with_hfe);7677/*-----------------------------------------------------------------------------78 * Define dictType of hash79 *80 * - Stores fields as entry objects (field-value pairs) with optional expiration81 * - Note that small hashes are represented with listpacks82 * - Once expiration is set for a field, the dict instance and corresponding83 *   dictType are replaced with a dict containing metadata for Hash Field84 *   Expiration (HFE) and using dictType `entryHashDictTypeWithHFE`85 * - Dict uses no_value=1 since entry contains both field and value86 *----------------------------------------------------------------------------*/87dictType entryHashDictType = {88    dictSdsHash,                                /* lookup hash function */89    NULL,                                       /* key dup */90    NULL,                                       /* val dup */91    dictSdsKeyCompare,                          /* lookup key compare */92    dictEntryDestructor,                       /* key destructor */93    NULL,                                       /* val destructor (value is in entry) */94    .dictMetadataBytes = hashDictMetadataBytes,95    .no_value = 1,                              /* entry contains both field and value */96    .keys_are_odd = 1,                          /* entry pointers (SDS) are always odd */97};9899/* Define alternative dictType of hash with hash-field expiration (HFE) support */100dictType entryHashDictTypeWithHFE = {101    dictSdsHash,                                /* lookup hash function */102    NULL,                                       /* key dup */103    NULL,                                       /* val dup */104    dictSdsKeyCompare,                          /* lookup key compare */105    dictEntryDestructor,                       /* key destructor */106    NULL,                                       /* val destructor (value is in entry) */107    .dictMetadataBytes = hashDictWithExpireMetadataBytes,108    .onDictRelease = hashDictWithExpireOnRelease,109    .no_value = 1,                              /* entry contains both field and value */110    .keys_are_odd = 1,                          /* entry pointers (SDS) are always odd */111};112113/*-----------------------------------------------------------------------------114 * Hash Field Expiration (HFE) Feature115 *116 * Each hash instance maintains its own set of hash field expiration within its117 * private ebuckets DS. In order to support HFE active expire cycle across hash118 * instances, hashes with associated HFE will be also registered in a global119 * ebuckets DS with expiration time value that reflects their next minimum120 * time to expire (db->subexpires). The global HFE Active expiration will be121 * triggered from activeExpireCycle() function and in turn will invoke "local"122 * HFE Active sub-expiration for each hash instance that has expired fields.123 *----------------------------------------------------------------------------*/124EbucketsType subexpiresBucketsType = {125    .onDeleteItem = NULL,126    .getExpireMeta = hashGetExpireMeta,   /* get ExpireMeta attached to each hash */127    .itemsAddrAreOdd = 0,                 /* Addresses of dict are even */128};129130/* htExpireMetadata - ebuckets-type for hash fields with time-Expiration. ebuckets131 * instance Will be attached to each hash that has at least one field with expiry132 * time. */133EbucketsType hashFieldExpireBucketsType = {134    .onDeleteItem = NULL,135    .getExpireMeta = hentryGetExpireMeta, /* get ExpireMeta attached to each field */136    .itemsAddrAreOdd = 1,                 /* Addresses of hfield (entry/sds) are odd!! */137};138139/* OnFieldExpireCtx passed to OnFieldExpire() */140typedef struct OnFieldExpireCtx {141    robj *hashObj;142    redisDb *db;143    int activeEx; /* 1 for active expire, 0 for lazy expire */144    vec *vexpired; /* Expired fields vector */145} OnFieldExpireCtx;146147/* The implementation of hashes by dict was modified from storing fields as sds148 * strings to store "entry" objects (field-value pairs with optional expiration).149 * The entry structure unifies field and value into a single allocation, with150 * optional expiration metadata. This is simpler than the previous mstr approach151 * and provides better memory locality.152 */153154/* Used by hpersistCommand() */155typedef enum SetPersistRes {156    HFE_PERSIST_NO_FIELD =     -2,   /* No such hash-field */157    HFE_PERSIST_NO_TTL =       -1,   /* No TTL attached to the field */158    HFE_PERSIST_OK =            1159} SetPersistRes;160161static inline int isDictWithMetaHFE(dict *d) {162    return d->type == &entryHashDictTypeWithHFE;163}164165/*-----------------------------------------------------------------------------166 * setex* - Set field's expiration167 *168 * Setting expiration time to fields might be time-consuming and complex since169 * each update of expiration time, not only updates `ebuckets` of corresponding170 * hash, but also might update `ebuckets` of global HFE DS. It is required to opt171 * sequence of field updates with expirartion for a given hash, such that only172 * once done, the global HFE DS will get updated.173 *174 * To do so, follow the scheme:175 * 1. Call hashTypeSetExInit() to initialize the HashTypeSetEx struct.176 * 2. Call hashTypeSetEx() one time or more, for each field/expiration update.177 * 3. Call hashTypeSetExDone() for notification and update of global HFE.178 *----------------------------------------------------------------------------*/179180/* Returned value of hashTypeSetEx() */181typedef enum SetExRes {182    HSETEX_OK =                1,   /* Expiration time set/updated as expected */183    HSETEX_NO_FIELD =         -2,   /* No such hash-field */184    HSETEX_NO_CONDITION_MET =  0,   /* Specified NX | XX | GT | LT condition not met */185    HSETEX_DELETED =           2,   /* Field deleted because the specified time is in the past */186} SetExRes;187188/* Used by httlGenericCommand() */189typedef enum GetExpireTimeRes {190    HFE_GET_NO_FIELD =          -2, /* No such hash-field */191    HFE_GET_NO_TTL =            -1, /* No TTL attached to the field */192} GetExpireTimeRes;193194typedef enum ExpireSetCond {195    HFE_NX = 1<<0,196    HFE_XX = 1<<1,197    HFE_GT = 1<<2,198    HFE_LT = 1<<3199} ExpireSetCond;200201/* Used by hashTypeSetEx() for setting fields or their expiry  */202typedef struct HashTypeSetEx {203204    /*** config ***/205    ExpireSetCond expireSetCond;        /* [XX | NX | GT | LT] */206207    /*** metadata ***/208    uint64_t minExpire;                 /* if uninit EB_EXPIRE_TIME_INVALID */209    redisDb *db;210    robj *key, *hashObj;211    uint64_t minExpireFields;           /* Trace updated fields and their previous/new212                                         * minimum expiration time. If minimum recorded213                                         * is above minExpire of the hash, then we don't214                                         * have to update global HFE DS */215216    /* Optionally provide client for notification */217    client *c;218    const char *cmd;219} HashTypeSetEx;220221int hashTypeSetExInit(robj *key, kvobj *kvo, client *c, redisDb *db,222                      ExpireSetCond expireSetCond, HashTypeSetEx *ex);223224SetExRes hashTypeSetEx(robj *o, sds field, uint64_t expireAt, HashTypeSetEx *exInfo);225226void hashTypeSetExDone(HashTypeSetEx *e);227228/*-----------------------------------------------------------------------------229 * Accessor functions for dictType of hash230 *----------------------------------------------------------------------------*/231232static void dictEntryDestructor(dict *d, void *entry) {233    size_t usable;234    size_t *alloc_size = htGetMetadataSize(d);235236    /* If attached TTL to the field, then remove it from hash's private ebuckets. */237    if (entryGetExpiry(entry) != EB_EXPIRE_TIME_INVALID) {238        htMetadataEx *dictExpireMeta = htGetMetadataEx(d);239        ebRemove(&dictExpireMeta->hfe, &hashFieldExpireBucketsType, entry);240    }241242    entryFree(entry, &usable);243    *alloc_size -= usable;244245    /* Don't have to update global HFE DS. It's unnecessary. Implementing this246     * would introduce significant complexity and overhead for an operation that247     * isn't critical. In the worst case scenario, the hash will be efficiently248     * updated later by an active-expire operation, or it will be removed by the249     * hash's dbGenericDelete() function. */250}251252static size_t hashDictMetadataBytes(dict *d) {253    UNUSED(d);254    return sizeof(size_t);255}256257static size_t hashDictWithExpireMetadataBytes(dict *d) {258    UNUSED(d);259    /* expireMeta of the hash, ref to ebuckets and pointer to hash's key */260    return sizeof(htMetadataEx);261}262263static void hashDictWithExpireOnRelease(dict *d) {264    /* for sure allocated with metadata. Otherwise, this func won't be registered */265    htMetadataEx *dictExpireMeta = htGetMetadataEx(d);266    ebDestroy(&dictExpireMeta->hfe, &hashFieldExpireBucketsType, NULL);267}268269/*-----------------------------------------------------------------------------270 * Hash template registry internals271 *272 * Many hashes often share the exact same set of field names (e.g. rows of one273 * "schema"). Instead of repeating the field names in every key, a single274 * immutable template stores the names once and every matching hash references275 * it, keeping only its own values. This is a memory optimization aimed at large276 * populations of hashes with a stable, identical field set.277 *278 * A template is identified by its exact set of field names and is never279 * changed once created. So adding a field (HSET) or removing one (HDEL) does280 * not edit the template: the hash just switches to the template for the new281 * field set, reusing it if it already exists or creating it otherwise (see282 * hashTemplateGetOrCreateWithHash). This makes changing the field set more283 * costly than on a plain hash, so the feature is meant for many hashes that284 * keep the same fields mostly stable.285 *286 * Fields are kept sorted inside the template by sdscmplen (length, then bytes;287 * see hashTemplateValidateFields). A field lookup binary-searches the sorted288 * names for its index (hashTemplateFieldIndex), then reads the value stored at289 * that same index.290 *291 * Two value encodings reference a template:292 *   OBJ_ENCODING_TMPL_LP    - o->ptr is a listpack laid out as293 *                             [template_id (varint)][v0][v1]...[vN-1]. Values294 *                             are packed inline and the leading id maps back to295 *                             the template. Compact, used for small hashes.296 *                             Lookup: lpSeek(lp, idx + 1) (the +1 skips the id).297 *   OBJ_ENCODING_TMPL_ARRAY - o->ptr is a hashTemplateArray { tmpl_id; sds298 *                             values[] } holding the values in template field299 *                             order. Used once listpack limits are exceeded (or300 *                             there are many fields). Lookup: values[idx].301 *302 * Registry: htemplates->by_fields maps a sorted field set to its template, and303 * htemplates->by_id maps a small integer id to the template. TMPL_LP stores304 * this id inline rather than an 8-byte pointer, so a key's reference to its305 * template can be as little as ~2 bytes. RDB save uses it the same way: each306 * template is written once and every key just references it by this id.307 *308 * Lifetime and threading: each referencing key holds a key_refcount to its309 * template and a client can also hold a hold_refcount (e.g. a fieldset310 * registered with HIMPORT PREPARE). A single template may back a huge number of311 * keys and those keys are often freed in bulk in the background by a BIO312 * lazyfree thread (e.g. background trim after ASM). The registry is main thread313 * only, so a BIO thread must not touch it: instead the BIO lazyfree thread records314 * per-template key drop counts and publishes them to the main thread, which 315 * applies the drops and frees any template reaching zero refs in cron (see bio_pending_drops).316 *----------------------------------------------------------------------------*/317318#define HASH_TMPL_STACK_ENTRIES 128319320/* Global template registry; file-local alias of server.htemplates. */321static hashTemplateRegistry *htemplates = NULL;322323/* Forward declarations for template helpers. */324static hashTemplate *hashTemplateGetOrCreateWithHash(uint64_t hash, sds *fields, unsigned long long field_count);325static void hashTemplateDecrKeyRef(hashTemplate *tmpl);326static hashTemplate *hashTemplateLpGetTemplate(unsigned char *lp);327static uint64_t hashTemplateLpGetTemplateId(unsigned char *lp);328static unsigned char *hashTemplateLpCreate(hashTemplate *tmpl, sds *values);329static hashTemplateArray *hashTemplateArrayCreate(hashTemplate *tmpl, sds *values, int take);330static hashTemplate *hashTemplateArrayGetTemplate(hashTemplateArray *hta);331332/* Keys freed in BIO lazyfree thread sets a dict (template id->key_ref_count). 333 * Later, it is applied on main thread in cron, key refs are actually dropped. */334static redisAtomic uintptr_t bio_pending_drops = 0;335336static uint64_t pendingDropHash(const void *key) { return (uint64_t)(uintptr_t)key; }337static dictType pendingDropDictType = { pendingDropHash, NULL, NULL, NULL, NULL, NULL, NULL };338339/* BIO thread: Increment deleted key count for the template id. */340static void hashTemplateRecordPendingDrop(uint64_t id) {341    serverAssert(bioIsLazyfreeWorker());342343    uintptr_t cur;344    atomicExchangeAcquire(bio_pending_drops, 0, cur);345346    dict *d = cur ? (dict *)cur : dictCreate(&pendingDropDictType);347    dictEntry *e;348    dictEntry *de = dictAddRaw(d, (void *)(uintptr_t)id, &e);349    350    /* Increment deleted key count for the template id */351    if (de) 352        dictSetUnsignedIntegerVal(de, 1);353    else    354        dictSetUnsignedIntegerVal(e, dictGetUnsignedIntegerVal(e) + 1);355    atomicSetRelease(bio_pending_drops, (uintptr_t)d);356}357358/* Sum of the 'n' sds lengths, or -1 if any exceeds hash-max-listpack-value. */359static ssize_t hashTypeSdsArrayLpBytes(sds *arr, unsigned long long n) {360    size_t sum = 0;361    for (unsigned long long i = 0; i < n; i++) {362        size_t len = sdslen(arr[i]);363        if (len > server.hash_max_listpack_value) return -1;364        sum += len;365    }366    return (ssize_t)sum;367}368369/* Do 'n' sds strings fit a listpack? */370static int hashTypeSdsArrayFitsLp(sds *arr, unsigned long long n) {371    if (n > server.hash_max_listpack_entries) return 0;372    ssize_t sum = hashTypeSdsArrayLpBytes(arr, n);373    return sum >= 0 && lpSafeToAdd(NULL, sum);374}375376/* True if the hash uses one of the template encodings. */377static inline int hashTypeIsTemplate(const robj *o) {378    return o->encoding == OBJ_ENCODING_TMPL_LP ||379           o->encoding == OBJ_ENCODING_TMPL_ARRAY;380}381382/* by_id is a chunked array: fixed-size chunks of template pointers. */383#define TMPL_CHUNK_SIZE 128  /* template ids per chunk */384385typedef struct tmplIdChunk {386    hashTemplate *slots[TMPL_CHUNK_SIZE]; /* id % TMPL_CHUNK -> template, NULL if free */387    unsigned int used;               /* used slots; chunk freed when 0 */388} tmplIdChunk;389390/* Get chunk holding 'id', allocating it on demand. */391static tmplIdChunk *tmplIdGetOrCreateChunk(size_t id) {392    size_t chunk_idx = id / TMPL_CHUNK_SIZE;393    if (chunk_idx >= htemplates->by_id_cap) {394        size_t old = htemplates->by_id_cap;395        size_t ncap = old ? old * 2 : 8;396        while (ncap <= chunk_idx) ncap *= 2;397398        htemplates->by_id = zrealloc(htemplates->by_id, ncap * sizeof(tmplIdChunk *));399        memset(htemplates->by_id + old, 0, (ncap - old) * sizeof(tmplIdChunk *));400        htemplates->by_id_cap = ncap;401    }402    if (htemplates->by_id[chunk_idx] == NULL) {403        htemplates->by_id[chunk_idx] = zcalloc(sizeof(tmplIdChunk));404        htemplates->by_id_chunks++;405    }406    return htemplates->by_id[chunk_idx];407}408409/* Get lowest free id. Caller guarantees a gap exists. Chunks below410 * by_id_free_chunk_hint are full, the scan starts there. */411static size_t tmplIdGetLowestFree(void) {412    size_t chunk_idx = htemplates->by_id_free_chunk_hint;413#ifdef DEBUG_ASSERTIONS414    /* Chunks below the hint must be full. */415    for (size_t i = 0; i < chunk_idx; i++)416        serverAssert(htemplates->by_id[i] && htemplates->by_id[i]->used == TMPL_CHUNK_SIZE);417#endif418    while (chunk_idx < htemplates->by_id_cap && htemplates->by_id[chunk_idx] &&419           htemplates->by_id[chunk_idx]->used == TMPL_CHUNK_SIZE) {420        chunk_idx++;421    }422    htemplates->by_id_free_chunk_hint = chunk_idx;423    tmplIdChunk *chunk = chunk_idx < htemplates->by_id_cap ? htemplates->by_id[chunk_idx] : NULL;424    size_t id = chunk_idx * TMPL_CHUNK_SIZE;425    while (chunk && chunk->slots[id % TMPL_CHUNK_SIZE] != NULL) id++;426    return id;427}428429/* Store tmpl in by_id under the lowest free id and return that id (the caller430 * sets tmpl->id). Low ids keep the TMPL_LP listpack id ~2 bytes per key. */431static uint64_t tmplIdAllocate(hashTemplate *tmpl) {432    int no_gaps = dictSize(htemplates->by_fields) == htemplates->by_id_next;433    size_t id = no_gaps ? htemplates->by_id_next++ : tmplIdGetLowestFree();434    /* Every lower id is in use, the hint can move up. */435    if (no_gaps) htemplates->by_id_free_chunk_hint = id / TMPL_CHUNK_SIZE;436    tmplIdChunk *chunk = tmplIdGetOrCreateChunk(id);437    chunk->slots[id % TMPL_CHUNK_SIZE] = tmpl;438    chunk->used++;439    return id;440}441442/* Recycle a template ID when template is freed. */443static void tmplIdRecycle(uint64_t id) {444    size_t chunk_idx = id / TMPL_CHUNK_SIZE;445    tmplIdChunk *chunk = htemplates->by_id[chunk_idx];446    chunk->slots[id % TMPL_CHUNK_SIZE] = NULL;447    if (htemplates->by_id_free_chunk_hint > chunk_idx)448        htemplates->by_id_free_chunk_hint = chunk_idx;449    /* Free the chunk once it holds no live ids so the id space shrinks. */450    if (--chunk->used == 0) {451        zfree(chunk);452        htemplates->by_id[chunk_idx] = NULL;453        htemplates->by_id_chunks--;454    }455456    /* This runs from the registry's key destructor, dictSize()==1 means it is457     * the last one and the whole by_id array can be released. */458    if (dictSize(htemplates->by_fields) == 1) {459        for (size_t i = 0; i < htemplates->by_id_cap; i++)460            zfree(htemplates->by_id[i]);461        zfree(htemplates->by_id);462        htemplates->by_id = NULL;463        htemplates->by_id_cap = 0;464        htemplates->by_id_chunks = 0;465        htemplates->by_id_next = 0;466        htemplates->by_id_free_chunk_hint = 0;467    }468}469470/* Lookup template by ID. Returns NULL if invalid. */471hashTemplate *hashTemplateGetById(uint64_t id) {472    size_t chunk_idx = id / TMPL_CHUNK_SIZE;473    if (chunk_idx >= htemplates->by_id_cap) return NULL;474    tmplIdChunk *chunk = htemplates->by_id[chunk_idx];475    return chunk ? chunk->slots[id % TMPL_CHUNK_SIZE] : NULL;476}477478/* Defrag the template struct and re-point every reference479 * to it (by_id slot, by_fields key, by_fields_lp value).*/480void hashTemplateDefrag(hashTemplate *tmpl, dictEntry *bf, monotime endtime) {481    /* Field-name array. */482    sds *newfields = activeDefragAlloc(tmpl->fields);483    if (newfields) tmpl->fields = newfields;484485    /* Field names, checking the clock every 128 so a wide template cannot486     * blow the time budget. On timeout, defrag_field keeps the position, the487     * leftover is picked up when the template is scanned again. */488    unsigned long long i = tmpl->defrag_field;489    long iterations = 0;490    while (i < tmpl->field_count) {491        sds newsds = activeDefragSds(tmpl->fields[i]);492        if (newsds) tmpl->fields[i] = newsds;493        server.stat_active_defrag_scanned++;494        i++;495        if (++iterations > 128) {496            if (getMonotonicUs() > endtime) break;497            iterations = 0;498        }499    }500    tmpl->defrag_field = (i >= tmpl->field_count) ? 0 : i;501502    /* fields_lp blob (the by_fields_lp key). */503    dictEntry *lp = NULL;504    if (tmpl->fields_lp) {505        unsigned char *oldlp = tmpl->fields_lp;506        unsigned char *newlp = activeDefragAlloc(oldlp);507        if (newlp) {508            /* oldlp is freed, find its entry by pointer. */509            uint64_t hash = dictGetHash(htemplates->by_fields_lp, newlp);510            lp = dictFindByHashAndPtr(htemplates->by_fields_lp, oldlp, hash);511            serverAssert(lp);512            tmpl->fields_lp = newlp;513            dictSetKey(htemplates->by_fields_lp, lp, newlp);514        }515    }516517    /* The struct itself: by_id slot, by_fields key, by_fields_lp value. */518    uint64_t id = tmpl->id;519    hashTemplate *newtmpl = activeDefragAlloc(tmpl);520    if (!newtmpl) return;521522    tmplIdChunk *chunk = htemplates->by_id[id / TMPL_CHUNK_SIZE];523    chunk->slots[id % TMPL_CHUNK_SIZE] = newtmpl;524525    dictSetKey(htemplates->by_fields, bf, newtmpl);526    if (newtmpl->fields_lp) {527        if (!lp) lp = dictFind(htemplates->by_fields_lp, newtmpl->fields_lp);528        serverAssert(lp);529        dictSetVal(htemplates->by_fields_lp, lp, newtmpl);530    }531}532533/* Defrag by_id top array (once, at idx 0) and the chunk at 'idx'. */534int hashTemplateDefragByIdChunk(unsigned long chunk_idx) {535    if (htemplates->by_id == NULL || chunk_idx >= htemplates->by_id_cap) 536        return 0;537    if (chunk_idx == 0) {538        tmplIdChunk **newarr = activeDefragAlloc(htemplates->by_id);539        if (newarr) htemplates->by_id = newarr;540    }541    tmplIdChunk *chunk = htemplates->by_id[chunk_idx];542    if (chunk) {543        tmplIdChunk *newchunk = activeDefragAlloc(chunk);544        if (newchunk) htemplates->by_id[chunk_idx] = newchunk;545    }546    return 1;547}548549/* HIMPORT SET propagation usually sends RESTORE commands with the same few550 * field sets. ASM may send RESTORE commands with thousands of different field551 * sets because keys arrive in arbitrary order. The cache holds at most one blob552 * per template, so its size is naturally bounded by the template count. The553 * explicit cap only protects against misuse that creates an excessive number554 * of templates. */555#define HASH_TMPL_FIELDS_LP_CACHE_MAX_BYTES (16 * 1024 * 1024)556557/* Cache 'fields_lp' as tmpl's fields-listpack (blob -> template), taking558 * ownership on success. Returns 0 without consuming fields_lp if tmpl is already559 * indexed or the cache is full. */560int hashTemplateIndexFieldsLp(hashTemplate *tmpl, unsigned char *fields_lp) {561    if (tmpl->fields_lp) return 0;562563    size_t cached_bytes = htemplates->fields_lp_cache_bytes;564    serverAssert(cached_bytes <= HASH_TMPL_FIELDS_LP_CACHE_MAX_BYTES);565566    size_t bytes = lpBytes(fields_lp);567    if (bytes > HASH_TMPL_FIELDS_LP_CACHE_MAX_BYTES - cached_bytes)568        return 0;569570    tmpl->fields_lp = fields_lp;571    tmpl->fields_lp_last_used = server.mstime;572    serverAssert(dictAdd(htemplates->by_fields_lp, fields_lp, tmpl) == DICT_OK);573    htemplates->fields_lp_cache_bytes += bytes;574    htemplates->total_mem_size += bytes;575    return 1;576}577578/* Build a fresh fields listpack (caller owns it). */579static unsigned char *hashTemplateBuildFieldsLp(hashTemplate *tmpl) {580    listpackEntry stack_ent[HASH_TMPL_STACK_ENTRIES];581    listpackEntry *ent = (tmpl->field_count <= HASH_TMPL_STACK_ENTRIES) ?582                         stack_ent : zmalloc(sizeof(listpackEntry) * tmpl->field_count);583    for (unsigned long long i = 0; i < tmpl->field_count; i++) {584        ent[i].sval = (unsigned char *)tmpl->fields[i];585        ent[i].slen = sdslen(tmpl->fields[i]);586    }587    unsigned char *lp = lpNewWithEntries(ent, tmpl->field_count);588    if (ent != stack_ent) zfree(ent);589    return lp;590}591592/* Return tmpl's fields listpack, building it on first use. '*cache' requests593 * indexing the blob for RESTORE lookup and is cleared if the cache is full,594 * in which case the caller owns the returned blob. */595unsigned char *hashTemplateGetFieldsLp(hashTemplate *tmpl, int *cache) {596    if (!*cache) return hashTemplateBuildFieldsLp(tmpl);597    tmpl->fields_lp_last_used = server.mstime;598    if (tmpl->fields_lp) return tmpl->fields_lp;599    unsigned char *lp = hashTemplateBuildFieldsLp(tmpl);600    if (!hashTemplateIndexFieldsLp(tmpl, lp)) *cache = 0;601    return lp;602}603604/* Lookup a template by its fields listpack. Returns NULL if not indexed. */605hashTemplate *hashTemplateGetByFieldsLp(unsigned char *fields_lp) {606    dictEntry *de = dictFind(htemplates->by_fields_lp, fields_lp);607    if (!de) return NULL;608    hashTemplate *tmpl = dictGetVal(de);609    tmpl->fields_lp_last_used = server.mstime;610    return tmpl;611}612613/* Compute SipHash for a single field. */614static uint64_t computeFieldHash(sds field) {615    return dictGenHashFunction(field, sdslen(field));616}617618/* Compute commutative hash for a set of fields.619 * Uses sum of per-field SipHashes so fields can be620 * incrementally added/removed:621 *   add:    hash += computeFieldHash(new_field)622 *   remove: hash -= computeFieldHash(removed_field) */623static uint64_t computeFieldsHash(sds *fields, unsigned long long field_count) {624    uint64_t hash = 0;625    for (unsigned long long i = 0; i < field_count; i++)626        hash += computeFieldHash(fields[i]);627    return hash;628}629630/* Template registry dict callbacks. Key is hashTemplate*, value is same. */631static uint64_t templateFieldsHashFunc(const void *key) {632    const hashTemplate *tmpl = key;633    return tmpl->hash;634}635636static int templateFieldsKeyCompare(dictCmpCache *cache, 637                                    const void *k1,638                                    const void *k2)639{640    UNUSED(cache);641    const hashTemplate *t1 = k1;642    const hashTemplate *t2 = k2;643644    if (t1->hash != t2->hash) return 0;645    if (t1->field_count != t2->field_count) return 0;646647    for (unsigned long long i = 0; i < t1->field_count; i++) {648        if (sdscmplen(t1->fields[i], t2->fields[i]) != 0)649            return 0;650    }651    return 1;652}653654/* Drop tmpl's cached fields listpack. */655static void hashTemplateDropFieldsLp(hashTemplate *tmpl) {656    if (tmpl->fields_lp == NULL) return;657658    unsigned char *lp = tmpl->fields_lp;659    size_t bytes = lpBytes(lp);660    serverAssert(dictDelete(htemplates->by_fields_lp, lp) == DICT_OK);661    serverAssert(htemplates->fields_lp_cache_bytes >= bytes);662    htemplates->fields_lp_cache_bytes -= bytes;663    htemplates->total_mem_size -= bytes;664    lpFree(lp);665    tmpl->fields_lp = NULL;666}667668static void templateFieldsKeyDestructor(dict *d, void *key) {669    UNUSED(d);670    hashTemplate *tmpl = key;671    tmplIdRecycle(tmpl->id);672    hashTemplateDropFieldsLp(tmpl);673    for (unsigned long long i = 0; i < tmpl->field_count; i++)674        sdsfree(tmpl->fields[i]);675    zfree(tmpl->fields);676    htemplates->total_mem_size -= tmpl->mem_size;677    zfree(tmpl);678}679680static dictType templateFieldsDictType = {681    templateFieldsHashFunc,        /* hash function */682    NULL,                          /* key dup */683    NULL,                          /* val dup */684    templateFieldsKeyCompare,      /* key compare */685    templateFieldsKeyDestructor,   /* key destructor (key=tmpl) */686    NULL,                          /* val destructor (val=same as key) */687    NULL                           /* allow to expand */688};689690/* by_fields_lp dict: key is the fields listpack, value is the hashTemplate* */691static uint64_t templateFieldsLpHashFunc(const void *key) {692    return dictGenHashFunction(key, lpBytes((unsigned char *)key));693}694695static int templateFieldsLpKeyCompare(dictCmpCache *cache,696                                      const void *k1, const void *k2)697{698    UNUSED(cache);699    size_t l1 = lpBytes((unsigned char *)k1), l2 = lpBytes((unsigned char *)k2);700    return (l1 == l2) && (memcmp(k1, k2, l1) == 0);701}702703static dictType templateFieldsLpDictType = {704    templateFieldsLpHashFunc,      /* hash function */705    NULL,                          /* key dup */706    NULL,                          /* val dup */707    templateFieldsLpKeyCompare,    /* key compare */708    NULL,                          /* key destructor (blob owned by template) */709    NULL,                          /* val destructor */710    NULL                           /* allow to expand */711};712713/* Initialize hash templates registry. */714void hashTemplatesInit(void) {715    if (htemplates) return;716    htemplates = zcalloc(sizeof(hashTemplateRegistry));717    htemplates->by_fields = dictCreate(&templateFieldsDictType);718    htemplates->by_fields_lp = dictCreate(&templateFieldsLpDictType);719    server.htemplates = htemplates;720}721722/* Create a new hash tmpl, fields must be pre-sorted. */723static hashTemplate *hashTemplateCreateInternal(uint64_t hash, sds *fields,724                                                unsigned long long field_count) 725{726    /* Fields must be strictly ascending; catch unsorted/dup sets in test builds. */727    debugServerAssert(hashTemplateValidateFields(fields, field_count));728729    hashTemplate *tmpl = zmalloc(sizeof(*tmpl));730    tmpl->hash = hash;731    tmpl->hold_refcount = 0;732    tmpl->key_refcount = 0;733    tmpl->field_count = field_count;734    tmpl->fields = zmalloc(sizeof(sds) * field_count);735    tmpl->fields_lp = NULL; /* Lazy built on first save/lookup due to RESTORE. */736    tmpl->fields_lp_last_used = 0;737    tmpl->defrag_field = 0;738    tmpl->mem_size = sizeof(*tmpl) + sizeof(sds) * field_count;739740    for (unsigned long long i = 0; i < field_count; i++) {741        tmpl->fields[i] = sdsdup(fields[i]);742        tmpl->mem_size += sdsZmallocSize(tmpl->fields[i]);743    }744    htemplates->total_mem_size += tmpl->mem_size;745746    /* Whether DUMP can serialize the field names as one listpack blob. */747    tmpl->fits_in_listpack = hashTypeSdsArrayFitsLp(tmpl->fields, tmpl->field_count);748    tmpl->id = tmplIdAllocate(tmpl);749    return tmpl;750}751752/* Get or create a template (computes the fields-hash). fields must be pre-sorted. */753hashTemplate *hashTemplateGetOrCreate(sds *fields, unsigned long long field_count) {754    return hashTemplateGetOrCreateWithHash(755        computeFieldsHash(fields, field_count), fields, field_count);756}757758/* Get or create a template, reusing a fields-hash the caller already has. The759 * fields-hash is commutative (a sum of per-field hashes), so an HSET/HDEL that760 * adds or removes one field can update it in O(1) (hash +/- that field's hash)761 * and pass it here instead of rescanning every field. fields must be pre-sorted. */762static hashTemplate *hashTemplateGetOrCreateWithHash(uint64_t hash, sds *fields,763                                              unsigned long long field_count) {764    /* Fields must be strictly ascending; catch unsorted/dup sets in test builds. */765    debugServerAssert(hashTemplateValidateFields(fields, field_count));766767    hashTemplate query = {768        .hash = hash,769        .field_count = field_count,770        .fields = fields771    };772773    dictEntry *de = dictFind(htemplates->by_fields, &query);774    if (de) return dictGetKey(de);775776    hashTemplate *tmpl = hashTemplateCreateInternal(hash, fields, field_count);777    dictAdd(htemplates->by_fields, tmpl, NULL);778    return tmpl;779}780781/* Registry lookup without creating on miss. fields must be pre-sorted. */782static hashTemplate *hashTemplateFindByFields(uint64_t hash, sds *fields,783                                              unsigned long long field_count) {784    debugServerAssert(hashTemplateValidateFields(fields, field_count));785786    hashTemplate query = { .hash = hash, .field_count = field_count, .fields = fields };787    dictEntry *de = dictFind(htemplates->by_fields, &query);788    return de ? dictGetKey(de) : NULL;789}790791/* Bump key_refcount when a hash key starts using tmpl. */792void hashTemplateIncrKeyRef(hashTemplate *tmpl) {793    tmpl->key_refcount++;794    htemplates->total_key_refs++;795}796797/* Bump hold_refcount for a non-key holder (a client's prepared fieldset from798 * HIMPORT PREPARE, or an in-progress RDB load). */799void hashTemplateIncrHoldRef(hashTemplate *tmpl) {800    tmpl->hold_refcount++;801}802803/* Free the template when no key and no holder reference it. */804static void hashTemplateFreeIfUnreferenced(hashTemplate *tmpl) {805    if (tmpl->key_refcount == 0 && tmpl->hold_refcount == 0)806        dictDelete(htemplates->by_fields, tmpl);807}808809/* Drop one holdref (a client's prepared fieldset, or an in-progress RDB load).810 * When the last holder leaves, free tmpl if no keys reference it either. */811void hashTemplateDecrHoldRef(hashTemplate *tmpl) {812    serverAssert(tmpl->hold_refcount > 0);813    tmpl->hold_refcount--;814    if (tmpl->hold_refcount > 0) return;815816    hashTemplateFreeIfUnreferenced(tmpl);817}818819/* True if the calling thread is the one allowed to mutate the registry: the820 * main thread or a thread holding the module GIL. Any other thread (a BIO821 * lazyfree job) must defer the drop using hashTemplateRecordPendingDrop() */822static inline int canThreadWriteRegistry(void) {823    return pthread_equal(pthread_self(), server.main_thread_id) || moduleThreadHoldsGIL();824}825826/* Decrement key_refcount when freeing a hash key. */827static void hashTemplateDecrKeyRef(hashTemplate *tmpl) {828    serverAssert(canThreadWriteRegistry());829    serverAssert(tmpl);830    serverAssert(tmpl->key_refcount > 0);831    htemplates->total_key_refs--;832    if (--tmpl->key_refcount == 0) hashTemplateFreeIfUnreferenced(tmpl);833}834835/* Apply the key ref drops recorded by the BIO lazyfree thread. Processes836 * incrementally with a time limit to avoid blocking the main thread when837 * draining large batches (e.g. after FLUSHALL). */838static void hashTemplateApplyPendingDrops(void) {839    static dict *batch = NULL;840    static dictIterator it;841842    /* Acquire new batch if not already draining. */843    if (batch == NULL) {844        uintptr_t cur;845        atomicExchangeAcquire(bio_pending_drops, 0, cur);846        if (cur == 0) return;847        batch = (dict *)cur;848        dictInitSafeIterator(&it, batch);849    }850851    /* Time limit: spread work across multiple cron cycles to avoid spikes. */852    long long start = ustime();853    long long timelimit = 1000000 / server.hz / 20;  /* 5% of a hz cycle */854    if (timelimit <= 0) timelimit = 1;855856    int i = 0;857    dictEntry *de;858    while ((de = dictNext(&it)) != NULL) {859        hashTemplate *tmpl = hashTemplateGetById((uint64_t)(uintptr_t)dictGetKey(de));860        uint64_t n = dictGetUnsignedIntegerVal(de);861        serverAssert(tmpl != NULL && tmpl->key_refcount >= n);862        htemplates->total_key_refs -= n;863        tmpl->key_refcount -= n;864        if (tmpl->key_refcount == 0) hashTemplateFreeIfUnreferenced(tmpl);865        /* Check the clock every 64 drops. */866        if ((++i & 63) == 0 && ustime() - start > timelimit) return;867    }868    dictResetIterator(&it);869    dictRelease(batch);870    batch = NULL;871}872873/* Idle time (ms) after which an unused fields_lp blob is deleted which was874 * generated due to DUMP/RESTORE propagation master->repica or ASM */875#define HASH_TEMPLATE_FIELDS_LP_IDLE_MS 5000876877/* Max idle blobs collected per batch. */878#define FIELDS_LP_BATCH 64879880typedef struct {881    hashTemplate *tmpls[FIELDS_LP_BATCH];882    int collected; /* idle blobs gathered into tmpls[] */883    int scanned;   /* blobs scanned this window */884} fieldsLpReclaimCtx;885886/* dictScan callback: collect templates whose fields_lp is idle. */887static void fieldsLpReclaimCollect(void *privdata, const dictEntry *de, dictEntry **plink) {888    UNUSED(plink);889    fieldsLpReclaimCtx *ctx = privdata;890    if (ctx->collected >= FIELDS_LP_BATCH) return;891892    hashTemplate *tmpl = dictGetVal((dictEntry *)de);893    if (tmpl->fields_lp == NULL) return;894    ctx->scanned++;895    if (server.mstime - tmpl->fields_lp_last_used < HASH_TEMPLATE_FIELDS_LP_IDLE_MS) return; /* still in use */896    ctx->tmpls[ctx->collected++] = tmpl;897}898899/* Reclaim fields_lp blobs idle longer than HASH_TEMPLATE_FIELDS_LP_IDLE_MS.900 * These blobs are built lazily during DUMP/RESTORE/ASM for O(1) template lookup;901 * once idle they are dead weight (rebuilt on the next DUMP if needed). */902static void hashTemplatesCleanupFieldsLpCron(void) {903    static unsigned long cursor = 0;904    dict *d = htemplates ? htemplates->by_fields_lp : NULL;905    if (!d || dictSize(d) == 0) return;906907    long long start = ustime();908    long long timelimit = 1000000 / server.hz / 200;  /* 0.5% of a hz cycle (~0.5ms) */909    if (timelimit <= 0) timelimit = 1;910911    while (1) {912        /* Collect a small batch, then free it */913        fieldsLpReclaimCtx ctx = { .collected = 0 };914        int steps = 0;915916        do {917            cursor = dictScan(d, cursor, fieldsLpReclaimCollect, &ctx);918        } while (cursor != 0 && ++steps < 16 && ctx.collected < FIELDS_LP_BATCH);919920        /* Blobs present but none idle: nothing to reclaim now, stop (e.g. ASM). */921        if (ctx.collected == 0 && ctx.scanned > 0) break;922923        for (int i = 0; i < ctx.collected; i++) {924            hashTemplate *tmpl = ctx.tmpls[i];925            hashTemplateDropFieldsLp(tmpl);926        }927928        if (cursor == 0) break; /* completed a full sweep */929        if (ustime() - start > timelimit) break;   /* out of time budget */930    }931}932933/* Template registry maintenance, once per serverCron cycle. */934void hashTemplatesCron(void) {935    if (!htemplates) return;936    hashTemplateApplyPendingDrops();937    hashTemplatesCleanupFieldsLpCron();938}939940/* Get number of templates in the registry. */941size_t hashTemplateRegistrySize(void) {942    if (!htemplates || !htemplates->by_fields) return 0;943    return dictSize(htemplates->by_fields);944}945946/* Get total number of template-based keys (sum of all key_refcounts). */947size_t hashTemplateKeyCount(void) {948    if (!htemplates) return 0;949    return htemplates->total_key_refs;950}951952/* Memory held by the shared template registry, reported as953 * used_memory_hash_templates (INFO) and hash.templates (MEMORY STATS). */954size_t hashTemplatesMemUsage(void) {955    if (!htemplates) return 0;956    return htemplates->total_mem_size + sizeof(*htemplates) +957           dictMemUsage(htemplates->by_fields) +958           dictMemUsage(htemplates->by_fields_lp) +959           htemplates->by_id_cap * sizeof(tmplIdChunk *) +960           htemplates->by_id_chunks * sizeof(tmplIdChunk);961}962963/* Find field index in tmpl using binary search (fields are sorted).964 * Returns the index (>= 0) if found, otherwise -(insert_pos + 1) where965 * insert_pos is the position to splice field into to keep fields sorted. */966long long hashTemplateFieldIndex(hashTemplate *tmpl, sds field) {967    long long lo = 0, hi = (long long)tmpl->field_count - 1;968    while (lo <= hi) {969        long long mid = lo + (hi - lo) / 2;970        int cmp = sdscmplen(field, tmpl->fields[mid]);971        if (cmp == 0) return mid;972        if (cmp < 0) hi = mid - 1;973        else lo = mid + 1;974    }975    return -(lo + 1);976}977978/* Return 1 if fields are strictly ascending (sorted, no duplicates), else 0. */979int hashTemplateValidateFields(sds *fields, unsigned long long field_count) {980    for (unsigned long long i = 1; i < field_count; i++)981        if (sdscmplen(fields[i - 1], fields[i]) >= 0)982            return 0;983    return 1;984}985986/*-----------------------------------------------------------------------------987 * HashTemplateLp functions (OBJ_ENCODING_TMPL_LP)988 *989 * Listpack format: [template_id (varint)][value1][value2]...990 * o->ptr points directly to the listpack991 *----------------------------------------------------------------------------*/992993/* Read the template ID stored as the first listpack entry. */994static uint64_t hashTemplateLpGetTemplateId(unsigned char *lp) {995    unsigned char *p = lpFirst(lp);996    serverAssert(p != NULL); /* id entry always present in a well-formed TMPL_LP */997    long long id;998    if (!lpGetIntegerValue(p, &id))999        serverPanic("TMPL_LP listpack header is not an integer template ID");1000    return (uint64_t)id;1001}10021003static hashTemplate *hashTemplateLpGetTemplate(unsigned char *lp) {1004    hashTemplate *tmpl = hashTemplateGetById(hashTemplateLpGetTemplateId(lp));1005    if (tmpl == NULL)1006        serverPanic("TMPL_LP listpack references unknown template ID");1007    return tmpl;1008}10091010/* Return the shared template of a template-encoded hash (TMPL_LP or TMPL_ARRAY). */1011hashTemplate *hashTypeGetTemplate(robj *o) {1012    serverAssert(hashTypeIsTemplate(o));1013    return (o->encoding == OBJ_ENCODING_TMPL_LP) ?1014        hashTemplateLpGetTemplate(o->ptr) :1015        hashTemplateArrayGetTemplate(o->ptr);1016}10171018/* Replace template ID in listpack. */1019unsigned char *hashTemplateLpSetTemplate(unsigned char *lp, hashTemplate *tmpl) {1020    unsigned char *p = lpFirst(lp);1021    serverAssert(p != NULL); /* id entry always present in a well-formed TMPL_LP */1022    unsigned char *v = lpReplaceInteger(lp, &p, (long long)tmpl->id);1023    serverAssert(v != NULL);1024    return v;1025}10261027/* Get pointer to first value entry (skip template ID). */1028static unsigned char *hashTemplateLpFirstValue(unsigned char *lp) {1029    unsigned char *p = lpFirst(lp);  /* template ID entry */1030    serverAssert(p != NULL);         /* id entry always present */1031    unsigned char *v = lpNext(lp, p);1032    serverAssert(v != NULL);         /* a TMPL_LP always holds >= 1 value */1033    return v;1034}10351036/* Seek the value entry at template field index 'idx' (entry 0 is the template1037 * ID, so value idx lives at idx+1). A well-formed TMPL_LP always has this1038 * entry; so assert here rather than hand a NULL cursor. */1039static unsigned char *hashTemplateLpSeekValue(unsigned char *lp, long long idx) {1040    unsigned char *p = lpSeek(lp, idx + 1);1041    serverAssert(p != NULL);1042    return p;1043}10441045/* Walk a TMPL_LP listpack once, filling vptrs[i] with a pointer to the entry1046 * holding the value of field i (i in [0, field_count)). Lets callers that need1047 * many values index them in O(1) instead of an O(field_count) lpSeek per draw1048 * (e.g. HRANDFIELD with a large count). */1049static void hashTemplateLpCollectValuePtrs(unsigned char *lp, unsigned char **vptrs,1050                                           unsigned long long field_count) {1051    unsigned char *p = hashTemplateLpFirstValue(lp);1052    for (unsigned long long i = 0; i < field_count; i++) {1053        serverAssert(p != NULL);1054        vptrs[i] = p;1055        p = lpNext(lp, p);1056    }1057}10581059/* Create listpack with template ID and values. Increments key_refcount. */1060static unsigned char *hashTemplateLpCreate(hashTemplate *tmpl, sds *values) {1061    hashTemplateIncrKeyRef(tmpl);10621063    unsigned long long n = tmpl->field_count;1064    /* +1 for template ID entry */1065    listpackEntry stack_entries[HASH_TMPL_STACK_ENTRIES + 1];1066    listpackEntry *entries = (n + 1 <= HASH_TMPL_STACK_ENTRIES + 1) ?1067                             stack_entries :1068                             zmalloc(sizeof(listpackEntry) * (n + 1));10691070    /* First entry: template ID */1071    entries[0].lval = (long long)tmpl->id;1072    entries[0].sval = NULL;10731074    /* Remaining entries: values */1075    for (unsigned long long i = 0; i < n; i++) {1076        entries[i + 1].sval = (unsigned char *)values[i];1077        entries[i + 1].slen = sdslen(values[i]);1078    }10791080    unsigned char *lp = lpNewWithEntries(entries, n + 1);1081    serverAssert(lp != NULL);1082    if (entries != stack_entries) zfree(entries);10831084    return lp;1085}10861087typedef struct {1088    sds field;1089    unsigned char *vstr;1090    size_t vlen;1091    long long vll;1092} hashTypeFvPair;10931094/* qsort comparator: order hashTypeFvPair by field name. */1095static int hashTypeFvPairCmp(const void *a, const void *b) {1096    return sdscmplen(((const hashTypeFvPair *)a)->field,1097                     ((const hashTypeFvPair *)b)->field);1098}10991100/* Create TMPL_LP from field-value pairs */1101static unsigned char *hashTemplateLpCreateFromPairs(hashTemplate *tmpl,1102                                                    hashTypeFvPair *pairs,1103                                                    unsigned long long n) {1104    hashTemplateIncrKeyRef(tmpl);11051106    /* +1 for template ID entry */1107    listpackEntry stack_entries[HASH_TMPL_STACK_ENTRIES + 1];1108    listpackEntry *entries = (n + 1 <= HASH_TMPL_STACK_ENTRIES + 1) ?1109                             stack_entries :1110                             zmalloc(sizeof(listpackEntry) * (n + 1));11111112    entries[0].lval = (long long)tmpl->id;  /* template ID */1113    entries[0].sval = NULL;1114    for (unsigned long long i = 0; i < n; i++) {1115        if (pairs[i].vstr) {1116            entries[i + 1].sval = pairs[i].vstr;1117            entries[i + 1].slen = pairs[i].vlen;1118        } else {1119            entries[i + 1].sval = NULL;      /* int-encoded value */1120            entries[i + 1].lval = pairs[i].vll;1121        }1122    }11231124    unsigned char *lp = lpNewWithEntries(entries, n + 1);1125    serverAssert(lp != NULL);1126    if (entries != stack_entries) zfree(entries);11271128    return lp;1129}11301131/* Release a template listpack's key ref and free the listpack. May run1132 * in a BIO lazyfree thread and it may just record the ref count drop to be 1133 * collected by main thread later in hashTemplateApplyPendingDrops() */1134void hashTemplateLpFree(unsigned char *lp) {1135    uint64_t id = hashTemplateLpGetTemplateId(lp);1136    if (!canThreadWriteRegistry()) {1137        hashTemplateRecordPendingDrop(id);1138    } else {1139        hashTemplate *tmpl = hashTemplateGetById(id);1140        if (tmpl == NULL)1141            serverPanic("TMPL_LP listpack references unknown template ID");1142        hashTemplateDecrKeyRef(tmpl);1143    }1144    lpFree(lp);1145}11461147/*-----------------------------------------------------------------------------1148 * hashTemplateArray functions (OBJ_ENCODING_TMPL_ARRAY)1149 *----------------------------------------------------------------------------*/11501151static hashTemplate *hashTemplateArrayGetTemplate(hashTemplateArray *hta) {1152    hashTemplate *tmpl = hashTemplateGetById(hta->tmpl_id);1153    serverAssert(tmpl != NULL);1154    serverAssert(tmpl->field_count == hta->field_count);1155    return tmpl;1156}11571158/* Create a new hashTemplateArray. Increments key_refcount.1159 * If take is set, takes ownership of the SDS strings in values array.1160 * Otherwise copies them with sdsdup. */1161static hashTemplateArray *hashTemplateArrayCreate(hashTemplate *tmpl, sds *values, int take) {1162    unsigned long long n = tmpl->field_count;1163    size_t usable;1164    hashTemplateArray *hta = zmalloc_usable(sizeof(*hta) + sizeof(sds) * n, &usable);1165    hta->tmpl_id = tmpl->id;1166    hta->field_count = n;1167    hta->alloc_size = usable;11681169    for (unsigned long long i = 0; i < n; i++) {1170        hta->values[i] = take ? values[i] : sdsdup(values[i]);1171        hta->alloc_size += sdsAllocSize(hta->values[i]);1172    }11731174    hashTemplateIncrKeyRef(tmpl);1175    return hta;1176}11771178static hashTemplateArray *hashTemplateArrayResize(hashTemplateArray *hta, unsigned long long field_count) {1179    size_t old_usable, new_usable;1180    hta = zrealloc_usable(hta, sizeof(*hta) + sizeof(sds) * field_count,1181                          &new_usable, &old_usable);1182    hta->alloc_size += new_usable;1183    hta->alloc_size -= old_usable;1184    return hta;1185}11861187/* Free a hashTemplateArray (release key ref and free data). May run in a BIO1188 * lazyfree thread: uses the tmpl_id/field_count, never the registry. */1189void hashTemplateArrayFree(hashTemplateArray *hta) {1190#ifdef DEBUG_ASSERTIONS1191    size_t values_size = 0;1192    for (unsigned long long i = 0; i < hta->field_count; i++)1193        values_size += sdsAllocSize(hta->values[i]);1194    debugServerAssert(hta->alloc_size - values_size == zmalloc_usable_size(hta));1195#endif11961197    for (unsigned long long i = 0; i < hta->field_count; i++)1198        sdsfree(hta->values[i]);11991200    if (!canThreadWriteRegistry())1201        hashTemplateRecordPendingDrop(hta->tmpl_id);1202    else1203        hashTemplateDecrKeyRef(hashTemplateGetById(hta->tmpl_id));12041205    zfree(hta);1206}12071208/* Build a hash object that shares `tmpl` and stores the given values.1209 * Two template-backed encodings exist:1210 *   TMPL_LP    - values packed in a listpack (compact, small hashes).1211 *   TMPL_ARRAY - values stored as an sds array (used when listpack limits1212 *                would be exceeded).1213 * If `take` is set, ownership of the value sds strings is transferred here. */1214robj *createHashObjectFromTemplate(hashTemplate *tmpl, sds *values, int take) {1215    robj *o;12161217    /* TMPL_LP if the values fit a listpack (field names live in the template). */1218    if (hashTypeSdsArrayFitsLp(values, tmpl->field_count)) {1219        o = createObject(OBJ_HASH, hashTemplateLpCreate(tmpl, values));1220        o->encoding = OBJ_ENCODING_TMPL_LP;1221        /* The listpack copied the value bytes; if we own them, free them now. */1222        if (take)1223            for (unsigned long long i = 0; i < tmpl->field_count; i++)1224                sdsfree(values[i]);1225    } else {1226        o = createObject(OBJ_HASH, hashTemplateArrayCreate(tmpl, values, take));1227        o->encoding = OBJ_ENCODING_TMPL_ARRAY;1228    }1229    return o;1230}12311232/* Move a template hash to `new_tmpl` whose field set must already match the1233 * values held by the key. */1234static void hashTypeTmplSwitch(robj *o, hashTemplate *old_tmpl, hashTemplate *new_tmpl) {1235    hashTemplateIncrKeyRef(new_tmpl);1236    if (o->encoding == OBJ_ENCODING_TMPL_LP) {1237        o->ptr = hashTemplateLpSetTemplate(o->ptr, new_tmpl);1238    } else {1239        serverAssert(o->encoding == OBJ_ENCODING_TMPL_ARRAY);1240        hashTemplateArray *hta = o->ptr;1241        hta->tmpl_id = new_tmpl->id;1242        hta->field_count = new_tmpl->field_count;1243    }1244    hashTemplateDecrKeyRef(old_tmpl);1245}12461247/* Overwrite the value of the field at template index `idx`. Returns 1 if1248 * `value` was adopted. */1249static int hashTypeTmplWriteValueAt(robj *o, long long idx, sds value, int take) {1250    if (o->encoding == OBJ_ENCODING_TMPL_LP) {1251        unsigned char *lp = o->ptr;1252        unsigned char *p = hashTemplateLpSeekValue(lp, idx);1253        o->ptr = lpReplace(lp, &p, (unsigned char *)value, sdslen(value));1254        serverAssert(o->ptr != NULL);1255        return 0;1256    } else {1257        serverAssert(o->encoding == OBJ_ENCODING_TMPL_ARRAY);1258        hashTemplateArray *hta = o->ptr;1259        serverAssert((unsigned long long)idx < hta->field_count);1260        hta->alloc_size -= sdsAllocSize(hta->values[idx]);1261        sdsfree(hta->values[idx]);1262        hta->values[idx] = take ? value : sdsdup(value);1263        hta->alloc_size += sdsAllocSize(hta->values[idx]);1264        return take != 0;1265    }1266}12671268/* A field an HSET-style command adds to a template hash. */1269typedef struct {1270    sds field;1271    sds value;1272    unsigned long long insert_pos;1273    int arg_idx;          /* argv order, so the last of duplicate fields sets the value */1274} tmplNewField;12751276/* qsort comparator: field name ascending, then argv position. */1277static int tmplNewFieldCmp(const void *a, const void *b) {1278    const tmplNewField *x = a, *y = b;1279    int cmp = sdscmplen(x->field, y->field);1280    return cmp ? cmp : x->arg_idx - y->arg_idx;1281}12821283/* Add new fields to a template hash with a single template switch. `new_fields`1284 * must be sorted by field name and have no duplicates, and insert_pos must be1285 * the field's final position in the new template. Values are copied. */1286static void hashTypeTmplAddFields(redisDb *db, robj *o, hashTemplate *tmpl,1287                                  tmplNewField *new_fields, int num_new_fields)1288{1289    unsigned long long old_field_count = tmpl->field_count;12901291    /* Check if the listpack needs to be converted to array */1292    if (o->encoding == OBJ_ENCODING_TMPL_LP &&1293        old_field_count + num_new_fields > server.hash_max_listpack_entries)1294    {1295        hashTypeConvert(db, o, OBJ_ENCODING_TMPL_ARRAY);1296    }12971298    /* Merge old and new names into the new template's field array. */1299    unsigned long long total_field_count = old_field_count + num_new_fields;1300    sds stack_fields[HASH_TMPL_STACK_ENTRIES];1301    sds *fields_arr = (total_field_count <= HASH_TMPL_STACK_ENTRIES) ? stack_fields :1302                                                                     zmalloc(sizeof(sds) * total_field_count);1303    uint64_t hash = tmpl->hash;1304    unsigned long long old_pos = 0;1305    int added = 0;1306    for (unsigned long long i = 0; i < total_field_count; i++) {1307        if (added < num_new_fields && new_fields[added].insert_pos == i) {1308            fields_arr[i] = new_fields[added].field;1309            hash += computeFieldHash(new_fields[added].field); /* incremental set hash */1310            added++;1311        } else {1312            serverAssert(old_pos < old_field_count);1313            fields_arr[i] = tmpl->fields[old_pos++];1314        }1315    }1316    serverAssert(added == num_new_fields && old_pos == old_field_count);13171318    hashTemplate *new_tmpl = hashTemplateGetOrCreateWithHash(hash, fields_arr, total_field_count);1319    if (fields_arr != stack_fields) zfree(fields_arr);1320    1321    /* Update the values */1322    if (o->encoding == OBJ_ENCODING_TMPL_LP) {1323        unsigned char *lp = o->ptr;1324        for (int i = 0; i < num_new_fields; i++) {1325            sds v = new_fields[i].value;1326            serverAssert(new_fields[i].insert_pos <= old_field_count + (unsigned long long)i);1327            /* Check if this is an insert or an append */1328            if (new_fields[i].insert_pos < old_field_count + (unsigned long long)i) {1329                unsigned char *p = hashTemplateLpSeekValue(lp, new_fields[i].insert_pos);1330                lp = lpInsertString(lp, (unsigned char *)v, sdslen(v), p, LP_BEFORE, NULL);1331            } else {1332                lp = lpAppend(lp, (unsigned char *)v, sdslen(v));1333            }1334            serverAssert(lp != NULL);1335        }1336        o->ptr = lp;1337    } else {1338        serverAssert(o->encoding == OBJ_ENCODING_TMPL_ARRAY);1339        serverAssert(num_new_fields > 0);1340        hashTemplateArray *hta = hashTemplateArrayResize(o->ptr, total_field_count);13411342        /* Open a gap: move the old values from the first insert position1343         * onwards to the end of the array. Then fill each slot in order,1344         * either with the next new value or with the next old one. */1345        unsigned long long first = new_fields[0].insert_pos;1346        serverAssert(first <= old_field_count);1347        memmove(&hta->values[first + num_new_fields], &hta->values[first],1348                sizeof(sds) * (old_field_count - first));1349        old_pos = first + num_new_fields;1350        added = 0;1351        for (unsigned long long i = first; added < num_new_fields; i++) {1352            serverAssert(i < total_field_count);1353            if (new_fields[added].insert_pos == i) {1354                hta->values[i] = sdsdup(new_fields[added].value);1355                hta->alloc_size += sdsAllocSize(hta->values[i]);1356                added++;1357            } else {1358                serverAssert(old_pos < total_field_count);1359                hta->values[i] = hta->values[old_pos++];1360            }1361        }1362        o->ptr = hta;1363    }1364    hashTypeTmplSwitch(o, tmpl, new_tmpl);1365}13661367/* Set the `n` field-value pairs at argv[i * 2] and argv[i * 2 + 1] on a template1368 * hash: an existing field's value is overwritten in place and the fields the1369 * template lacks are all added in a single template switch.1370 * Returns the number of fields added. */1371static int hashTypeTmplSetFields(redisDb *db, robj *o, robj **argv, int n) {1372    hashTemplate *tmpl = hashTypeGetTemplate(o);1373    tmplNewField stack_new_fields[HASH_TMPL_STACK_ENTRIES];1374    tmplNewField *new_fields = (n <= HASH_TMPL_STACK_ENTRIES) ? stack_new_fields :1375                               zmalloc(sizeof(tmplNewField) * n);1376    int num_new_fields = 0;13771378    /* One pass: overwrite the fields that exist, collect the ones that do not.1379     * Binary searching here is the only field lookup the command does. */1380    for (int i = 0; i < n; i++) {1381        sds field = argv[i * 2]->ptr;1382        sds value = argv[i * 2 + 1]->ptr;13831384        long long idx = hashTemplateFieldIndex(tmpl, field);1385        if (idx >= 0) {1386            hashTypeTmplWriteValueAt(o, idx, value, 0); /* Existing field */1387            continue;1388        }13891390        /* Temporarily set insert pos for the existing template.1391         * Later, it will be adjusted for the new template below */1392        new_fields[num_new_fields].insert_pos = -idx - 1;1393        new_fields[num_new_fields].field = field;1394        new_fields[num_new_fields].value = value;1395        new_fields[num_new_fields].arg_idx = i;1396        num_new_fields++;1397    }13981399    if (num_new_fields > 0) {1400        /* Sort and drop duplicates, keeping the last occurrence of a field1401         * given more than once, so its last value wins. A single field needs1402         * neither. */1403        if (num_new_fields > 1) {1404            qsort(new_fields, num_new_fields, sizeof(*new_fields), tmplNewFieldCmp);1405            int unique = 0;1406            for (int i = 0; i < num_new_fields; i++) {1407                /* Skip the entry if the next one is the same field. */1408                if (i + 1 < num_new_fields &&1409                    sdscmplen(new_fields[i].field, new_fields[i + 1].field) == 0)1410                {1411                    continue;1412                }1413                new_fields[unique++] = new_fields[i];1414            }1415            num_new_fields = unique;1416        }14171418        /* So far insert_pos is where the field goes among the OLD template fields.1419         * Every new field placed before it shifts it right by one, so its1420         * final position in the new template is insert_pos + k. */1421        for (int k = 0; k < num_new_fields; k++) {1422            serverAssert(new_fields[k].insert_pos <= tmpl->field_count);1423            new_fields[k].insert_pos += k;1424        }14251426        hashTypeTmplAddFields(db, o, tmpl, new_fields, num_new_fields);1427    }14281429    if (new_fields != stack_new_fields) zfree(new_fields);1430    return num_new_fields;1431}14321433/* Reply with the value of the field at template index `idx`. */1434static void addTmplValueToReply(client *c, robj *o, long long idx) {1435    if (o->encoding == OBJ_ENCODING_TMPL_LP) {1436        unsigned int vlen;1437        long long vll;1438        unsigned char *p = hashTemplateLpSeekValue(o->ptr, idx);1439        unsigned char *vstr = lpGetValue(p, &vlen, &vll);1440        if (vstr)1441            addReplyBulkCBuffer(c, vstr, vlen);1442        else1443            addReplyBulkLongLong(c, vll);1444    } else {1445        serverAssert(o->encoding == OBJ_ENCODING_TMPL_ARRAY);1446        hashTemplateArray *hta = o->ptr;1447        serverAssert((unsigned long long)idx < hta->field_count);1448        sds value = hta->values[idx];1449        addReplyBulkCBuffer(c, value, sdslen(value));1450    }1451}14521453/* qsort comparator for template field indexes. */1454static int tmplIdxCmp(const void *a, const void *b) {1455    long long x = *(const long long *)a;1456    long long y = *(const long long *)b;1457    return (x > y) - (x < y);1458}14591460/* Delete fields from a template hash with a single template switch. `del_indexes`1461 * holds the template indexes of the fields to delete, in any order. Each index1462 * must be valid and given once. */1463static void hashTypeTmplDropFields(robj *o, hashTemplate *tmpl,1464                                   long long *del_indexes,1465                                   unsigned long long num_del_fields)1466{1467    unsigned long long old_field_count = tmpl->field_count;1468    serverAssert(num_del_fields > 0 && num_del_fields <= old_field_count);14691470    /* The walks below expect ascending indexes. */1471    if (num_del_fields > 1)1472        qsort(del_indexes, num_del_fields, sizeof(*del_indexes), tmplIdxCmp);14731474    if (num_del_fields == old_field_count) {1475        /* Losing the last field turns the hash into a plain empty listpack.1476         * Caller will delete the key. */1477        if (o->encoding == OBJ_ENCODING_TMPL_LP) {1478            hashTemplateLpFree(o->ptr);1479        } else {1480            serverAssert(o->encoding == OBJ_ENCODING_TMPL_ARRAY);1481            hashTemplateArrayFree(o->ptr);1482        }1483        o->ptr = lpNew(0);1484        o->encoding = OBJ_ENCODING_LISTPACK;1485        return;1486    }14871488    /* The surviving fields, in order, are the new field set. */1489    unsigned long long new_field_count = old_field_count - num_del_fields;1490    fieldvec fvfields;1491    vec *vfields = fieldvecInit(&fvfields, new_field_count);1492    uint64_t hash = tmpl->hash;1493    unsigned long long kept = 0, dropped = 0;14941495    for (unsigned long long i = 0; i < old_field_count; i++) {1496        if (dropped < num_del_fields && del_indexes[dropped] == (long long)i) {1497            hash -= computeFieldHash(tmpl->fields[i]); /* incremental set hash */1498            dropped++;1499        } else {1500            vecPush(vfields, tmpl->fields[i]);1501        }1502    }1503    serverAssert(vecSize(vfields) == new_field_count && dropped == num_del_fields);15041505    hashTemplate *new_tmpl = hashTemplateGetOrCreateWithHash(hash, (sds *)vecData(vfields), new_field_count);1506    vecRelease(vfields);15071508    /* Update the values */1509    if (o->encoding == OBJ_ENCODING_TMPL_LP) {1510        /* Collect the dropped entries in one walk, then delete them in one pass. */1511        unsigned char *lp = o->ptr;1512        fieldvec fvps;1513        vec *vps = fieldvecInit(&fvps, num_del_fields);1514        /* Seek the first dropped entry; each next one is `distance` entries1515         * after the previous, so step forward instead of a fresh seek. */1516        vecPush(vps, hashTemplateLpSeekValue(lp, del_indexes[0]));1517        for (unsigned long long i = 1; i < num_del_fields; i++) {1518            unsigned long long distance = del_indexes[i] - del_indexes[i - 1];1519            unsigned char *p = lpNextN(lp, vecGet(vps, i - 1), distance);1520            serverAssert(p != NULL);1521            vecPush(vps, p);1522        }1523        o->ptr = lpBatchDelete(lp, (unsigned char **)vecData(vps), num_del_fields);1524        vecRelease(vps);1525    } else {1526        serverAssert(o->encoding == OBJ_ENCODING_TMPL_ARRAY);1527        hashTemplateArray *hta = o->ptr;1528        /* Same walk as the field loop above, this time over the values. */1529        kept = dropped = 0;1530        for (unsigned long long i = 0; i < old_field_count; i++) {1531            if (dropped < num_del_fields && del_indexes[dropped] == (long long)i) {1532                hta->alloc_size -= sdsAllocSize(hta->values[i]);1533                sdsfree(hta->values[i]);1534                dropped++;1535            } else {1536                hta->values[kept++] = hta->values[i];1537            }1538        }1539        serverAssert(kept == new_field_count && dropped == num_del_fields);1540        o->ptr = hashTemplateArrayResize(hta, new_field_count);1541    }1542    hashTypeTmplSwitch(o, tmpl, new_tmpl);1543}15441545/* Delete the `n` fields in argv[] from a template hash in a single template1546 * switch, pushing the fields actually deleted into `vdeleted` in argv order.1547 * With `c`, each value is replied first (HGETDEL), null for a miss or repeat. */1548static void hashTypeTmplDeleteFields(robj *o, robj **argv, int n, vec *vdeleted, client *c) {1549    hashTemplate *tmpl = hashTypeGetTemplate(o);1550    unsigned long long old_field_count = tmpl->field_count;1551    unsigned long long num_del_fields = 0;1552    /* Fields are collected first and dropped all at once at the end. Since the 1553     * hash stays untouched during the scan, a field given twice in argv would be 1554     * found twice: delmark[] marks the fields seen in an earlier argv position. */1555    unsigned char stack_delmark[HASH_TMPL_STACK_ENTRIES] = {0};1556    unsigned char *delmark = NULL;1557    long long stack_del_indexes[HASH_TMPL_STACK_ENTRIES];1558    long long *del_indexes = (n <= HASH_TMPL_STACK_ENTRIES) ? stack_del_indexes :1559                                                              zmalloc(sizeof(long long) * n);1560    for (int i = 0; i < n; i++) {1561        long long idx = hashTemplateFieldIndex(tmpl, argv[i]->ptr);1562        /* Skip missing fields or if a field given twice in argv. */1563        if (idx < 0 || (delmark && delmark[idx])) {1564            if (c) addReplyNull(c);1565            continue;1566        }1567        /* Reply while the values are still in place. */1568        if (c) addTmplValueToReply(c, o, idx);1569        1570        /* Mark the field as deleted */1571        if (n > 1) {1572            if (delmark == NULL) {1573                delmark = (old_field_count <= HASH_TMPL_STACK_ENTRIES) ?1574                          stack_delmark : zcalloc(old_field_count);1575            }1576            serverAssert((unsigned long long)idx < old_field_count);1577            delmark[idx] = 1;1578        }1579        1580        /* Mark field idx to delete */1581        del_indexes[num_del_fields++] = idx;1582        vecPush(vdeleted, argv[i]);1583    }1584    1585    /* Delete the fields */1586    if (num_del_fields > 0)1587        hashTypeTmplDropFields(o, tmpl, del_indexes, num_del_fields);15881589    if (delmark && delmark != stack_delmark) zfree(delmark);1590    if (del_indexes != stack_del_indexes) zfree(del_indexes);1591}15921593/*-----------------------------------------------------------------------------1594 * listpackEx functions1595 *----------------------------------------------------------------------------*/1596/*1597 * If any of hash field expiration command is called on a listpack hash object1598 * for the first time, we convert it to OBJ_ENCODING_LISTPACK_EX encoding.1599 * We allocate "struct listpackEx" which holds listpack pointer and expiry1600 * metadata. In the listpack string, we append another TTL entry for each field1601 * value pair. From now on, listpack will have triplets in it: field-value-ttl.1602 * If TTL is not set for a field, we store 'zero' as the TTL value. 'zero' is1603 * encoded as two bytes in the listpack. Memory overhead of a non-existing TTL1604 * will be two bytes per field.1605 *1606 * Fields in the listpack will be ordered by TTL. Field with the smallest expiry1607 * time will be the first item. Fields without TTL will be at the end of the1608 * listpack. This way, it is easier/faster to find expired items.1609 */16101611#define HASH_LP_NO_TTL 016121613struct listpackEx *listpackExCreate(void) {1614    listpackEx *lpt = zcalloc(sizeof(*lpt));1615    lpt->meta.trash = 1;1616    lpt->lp = NULL;1617    return lpt;1618}16191620static void listpackExFree(listpackEx *lpt) {1621    lpFree(lpt->lp);1622    zfree(lpt);1623}16241625struct lpFindArgs {1626    uint64_t max_to_search; /* [in] Max number of tuples to search */1627    uint64_t expire_time;   /* [in] Find the tuple that has a TTL larger than expire_time */1628    unsigned char *p;       /* [out] First item of the tuple that has a TTL larger than expire_time */1629    int expired;            /* [out] Number of tuples that have TTLs less than expire_time */1630    int index;              /* Internally used */1631    unsigned char *fptr;    /* Internally used, temp ptr */1632};16331634/* Callback for lpFindCb(). Used to find number of expired fields as part of1635 * active expiry or when trying to find the position for the new field according1636 * to its expiry time.*/1637static int cbFindInListpack(const unsigned char *lp, unsigned char *p,1638                            void *user, unsigned char *s, long long slen)1639{1640    (void) lp;1641    struct lpFindArgs *r = user;16421643    r->index++;16441645    if (r->max_to_search == 0)1646        return 0; /* Break the loop and return */16471648    if (r->index % 3 == 1) {1649        r->fptr = p;  /* First item of the tuple. */1650    } else if (r->index % 3 == 0) {1651        serverAssert(!s);16521653        /* Third item of a tuple is expiry time */1654        if (slen == HASH_LP_NO_TTL || (uint64_t) slen >= r->expire_time) {1655            r->p = r->fptr;1656            return 0; /* Break the loop and return */1657        }1658        r->expired++;1659        r->max_to_search--;1660    }16611662    return 1;1663}16641665/* Returns number of expired fields. */1666static uint64_t listpackExExpireDryRun(const robj *o) {1667    serverAssert(o->encoding == OBJ_ENCODING_LISTPACK_EX);16681669    listpackEx *lpt = o->ptr;16701671    struct lpFindArgs r = {1672        .max_to_search = UINT64_MAX,1673        .expire_time = commandTimeSnapshot(),1674    };16751676    lpFindCb(lpt->lp, NULL, &r, cbFindInListpack, 0);1677    return r.expired;1678}16791680/* Returns the expiration time of the item with the nearest expiration. */1681static uint64_t listpackExGetMinExpire(robj *o) {1682    serverAssert(o->encoding == OBJ_ENCODING_LISTPACK_EX);16831684    long long expireAt;1685    unsigned char *fptr;1686    listpackEx *lpt = o->ptr;16871688    /* As fields are ordered by expire time, first field will have the smallest1689     * expiry time. Third element is the expiry time of the first field */1690    fptr = lpSeek(lpt->lp, 2);1691    if (fptr != NULL) {1692        serverAssert(lpGetIntegerValue(fptr, &expireAt));16931694        /* Check if this is a non-volatile field. */1695        if (expireAt != HASH_LP_NO_TTL)1696            return expireAt;1697    }16981699    return EB_EXPIRE_TIME_INVALID;1700}17011702/* Walk over fields and delete the expired ones. */1703void listpackExExpire(redisDb *db, kvobj *kv, ExpireInfo *info) {1704    OnFieldExpireCtx *ctx = info->ctx;1705    serverAssert(kv->encoding == OBJ_ENCODING_LISTPACK_EX);1706    uint64_t expired = 0, min = EB_EXPIRE_TIME_INVALID;1707    unsigned char *ptr;1708    listpackEx *lpt = kv->ptr;17091710    ptr = lpFirst(lpt->lp);17111712    sds key = kvobjGetKey(kv);17131714    while (ptr != NULL && (info->itemsExpired < info->maxToExpire)) {1715        long long val;1716        int64_t flen;1717        unsigned char intbuf[LP_INTBUF_SIZE], *fref;17181719        fref = lpGet(ptr, &flen, intbuf);17201721        ptr = lpNext(lpt->lp, ptr);1722        serverAssert(ptr);1723        ptr = lpNext(lpt->lp, ptr);1724        serverAssert(ptr && lpGetIntegerValue(ptr, &val));17251726        /* Fields are ordered by expiry time. If we reached to a non-expired1727         * or a non-volatile field, we know rest is not yet expired. A field is1728         * expired iff its expire-time is strictly less than `now`, matching1729         * lazy-expiry semantics (see hashTypeIsExpired()) and ebExpire(). */1730        if (val == HASH_LP_NO_TTL || (uint64_t) val >= info->now)1731            break;17321733        /* Collect expired field for subkey notification. */1734        if (ctx->vexpired) {1735            char *fstr = (char *)(fref ? fref : intbuf);1736            vecPush(ctx->vexpired, createStringObject(fstr, flen));1737        }17381739        propagateHashFieldDeletion(db, key, (char *)((fref) ? fref : intbuf), flen);1740        server.stat_expired_subkeys++;1741        if (ctx->activeEx) server.stat_expired_subkeys_active++;17421743        ptr = lpNext(lpt->lp, ptr);17441745        info->itemsExpired++;1746        expired++;1747    }17481749    if (expired) {1750        size_t oldsize = 0;1751        if (server.memory_tracking_enabled)1752            oldsize = kvobjAllocSize(kv);1753        lpt->lp = lpDeleteRange(lpt->lp, 0, expired * 3);1754        if (server.memory_tracking_enabled)1755            updateSlotAllocSize(db, getKeySlot(key), kv, oldsize, kvobjAllocSize(kv));17561757        /* update keysizes */1758        unsigned long l = lpLength(lpt->lp) / 3;1759        updateKeysizesHist(db, OBJ_HASH, l + expired, l);1760    }17611762    min = hashTypeGetMinExpire(kv, 1 /*accurate*/);1763    info->nextExpireTime = min;1764}17651766static void listpackExAddInternal(robj *o, listpackEntry ent[3]) {1767    listpackEx *lpt = o->ptr;17681769    /* Shortcut, just append at the end if this is a non-volatile field. */1770    if (ent[2].lval == HASH_LP_NO_TTL) {1771        lpt->lp = lpBatchAppend(lpt->lp, ent, 3);1772        return;1773    }17741775    struct lpFindArgs r = {1776            .max_to_search = UINT64_MAX,1777            .expire_time = ent[2].lval,1778    };17791780    /* Check if there is a field with a larger TTL. */1781    lpFindCb(lpt->lp, NULL, &r, cbFindInListpack, 0);17821783    /* If list is empty or there is no field with a larger TTL, result will be1784     * NULL. Otherwise, just insert before the found item.*/1785    if (r.p)1786        lpt->lp = lpBatchInsert(lpt->lp, r.p, LP_BEFORE, ent, 3, NULL);1787    else1788        lpt->lp = lpBatchAppend(lpt->lp, ent, 3);1789}17901791/* Add new field ordered by expire time. */1792void listpackExAddNew(robj *o, char *field, size_t flen,1793                      char *value, size_t vlen, uint64_t expireAt) {1794    listpackEntry ent[3] = {1795        {.sval = (unsigned char*) field, .slen = flen},1796        {.sval = (unsigned char*) value, .slen = vlen},1797        {.lval = expireAt}1798    };17991800    listpackExAddInternal(o, ent);1801}18021803/* If expiry time is changed, this function will place field into the correct1804 * position. First, it deletes the field and re-inserts to the listpack ordered1805 * by expiry time. */1806static void listpackExUpdateExpiry(robj *o, sds field,1807                                   unsigned char *fptr,1808                                   unsigned char *vptr,1809                                   uint64_t expire_at) {1810    unsigned int slen = 0;1811    long long val = 0;1812    unsigned char tmp[512] = {0};1813    unsigned char *valstr;1814    sds tmpval = NULL;1815    listpackEx *lpt = o->ptr;18161817    /* Copy value */1818    valstr = lpGetValue(vptr, &slen, &val);1819    if (valstr) {1820        /* Normally, item length in the listpack is limited by1821         * 'hash-max-listpack-value' config. It is unlikely, but it might be1822         * larger than sizeof(tmp). */1823        if (slen > sizeof(tmp))1824            tmpval = sdsnewlen(valstr, slen);1825        else1826            memcpy(tmp, valstr, slen);1827    }18281829    /* Delete field name, value and expiry time */1830    lpt->lp = lpDeleteRangeWithEntry(lpt->lp, &fptr, 3);18311832    listpackEntry ent[3] = {{0}};18331834    ent[0].sval = (unsigned char*) field;1835    ent[0].slen = sdslen(field);18361837    if (valstr) {1838        ent[1].sval = tmpval ? (unsigned char *) tmpval : tmp;1839        ent[1].slen = slen;1840    } else {1841        ent[1].lval = val;1842    }1843    ent[2].lval = expire_at;18441845    listpackExAddInternal(o, ent);1846    sdsfree(tmpval);1847}18481849/* Update field expire time. */1850SetExRes hashTypeSetExpiryListpack(HashTypeSetEx *ex, sds field,1851                                   unsigned char *fptr, unsigned char *vptr,1852                                   unsigned char *tptr, uint64_t expireAt)1853{1854    long long expireTime;1855    uint64_t prevExpire = EB_EXPIRE_TIME_INVALID;18561857    serverAssert(lpGetIntegerValue(tptr, &expireTime));18581859    if (expireTime != HASH_LP_NO_TTL) {1860        prevExpire = (uint64_t) expireTime;1861    }18621863    /* Special value of EXPIRE_TIME_INVALID indicates field should be persisted.*/1864    if (expireAt == EB_EXPIRE_TIME_INVALID) {1865        /* Return error if already there is no ttl. */1866        if (prevExpire == EB_EXPIRE_TIME_INVALID)1867            return HSETEX_NO_CONDITION_MET;1868        listpackExUpdateExpiry(ex->hashObj, field, fptr, vptr, HASH_LP_NO_TTL);1869        return HSETEX_OK;1870    }18711872    if (prevExpire == EB_EXPIRE_TIME_INVALID) {1873        /* For fields without expiry, LT condition is considered valid */1874        if (ex->expireSetCond & (HFE_XX | HFE_GT))1875            return HSETEX_NO_CONDITION_MET;1876    } else {1877        if (((ex->expireSetCond == HFE_GT) && (prevExpire >= expireAt)) ||1878            ((ex->expireSetCond == HFE_LT) && (prevExpire <= expireAt)) ||1879            (ex->expireSetCond == HFE_NX) )1880            return HSETEX_NO_CONDITION_MET;18811882        /* Track of minimum expiration time (only later update global HFE DS) */1883        if (ex->minExpireFields > prevExpire)1884            ex->minExpireFields = prevExpire;1885    }18861887    /* If expired, then delete the field and propagate the deletion.1888     * If replica, continue like the field is valid */1889    if (unlikely(checkAlreadyExpired(expireAt))) {1890        propagateHashFieldDeletion(ex->db, ex->key->ptr, field, sdslen(field));1891        hashTypeDelete(ex->hashObj, field);1892        server.stat_expired_subkeys++;1893        return HSETEX_DELETED;1894    }18951896    if (ex->minExpireFields > expireAt)1897        ex->minExpireFields = expireAt;18981899    listpackExUpdateExpiry(ex->hashObj, field, fptr, vptr, expireAt);1900    return HSETEX_OK;1901}19021903/* Returns 1 if expired */1904int hashTypeIsExpired(const robj *o, uint64_t expireAt) {1905    if (server.allow_access_expired) return 0;19061907    if (o->encoding == OBJ_ENCODING_LISTPACK_EX) {1908        if (expireAt == HASH_LP_NO_TTL)1909            return 0;1910    } else if (o->encoding == OBJ_ENCODING_HT) {1911        if (expireAt == EB_EXPIRE_TIME_INVALID)1912            return 0;1913    } else {1914        serverPanic("Unknown encoding: %d", o->encoding);1915    }19161917    return (mstime_t) expireAt < commandTimeSnapshot();1918}19191920/* Returns listpack pointer of the object. */1921unsigned char *hashTypeListpackGetLp(robj *o) {1922    if (o->encoding == OBJ_ENCODING_LISTPACK)1923        return o->ptr;1924    else if (o->encoding == OBJ_ENCODING_LISTPACK_EX)1925        return ((listpackEx*)o->ptr)->lp;19261927    serverPanic("Unknown encoding: %d", o->encoding);1928}19291930/*-----------------------------------------------------------------------------1931 * Hash type API1932 *----------------------------------------------------------------------------*/19331934/* Check the length of a number of objects to see if we need to convert a1935 * listpack to a real hash. Note that we only check string encoded objects1936 * as their string length can be queried in constant time. */1937void hashTypeTryConversion(redisDb *db, kvobj *o, robj **argv, int start, int end) {1938    int tmpl_lp = (o->encoding == OBJ_ENCODING_TMPL_LP);1939    int target_enc = tmpl_lp ? OBJ_ENCODING_TMPL_ARRAY : OBJ_ENCODING_HT;19401941    /* TMPL_ARRAY and HT don't need conversion checks. */1942    if (o->encoding != OBJ_ENCODING_LISTPACK &&1943        o->encoding != OBJ_ENCODING_LISTPACK_EX &&1944        o->encoding != OBJ_ENCODING_TMPL_LP)1945    {1946        return;1947    }19481949    /* Determine target encoding for conversion. */1950    if (!tmpl_lp) {1951        /* Check field count limit for regular listpack. */1952        size_t new_fields = (end - start + 1) / 2;1953        if (new_fields > server.hash_max_listpack_entries) {1954            hashTypeConvert(db, o, OBJ_ENCODING_HT);1955            dictExpand(o->ptr, new_fields);1956            return;1957        }1958    }19591960    /* Check value sizes (and field sizes for non-TMPL_LP). */1961    size_t sum = 0;1962    for (int i = start; i <= end; i++) {1963        if (tmpl_lp && ((i - start) % 2 == 0))1964            continue;1965        if (!sdsEncodedObject(argv[i]))1966            continue;1967        size_t len = sdslen(argv[i]->ptr);1968        if (len > server.hash_max_listpack_value) {1969            hashTypeConvert(db, o, target_enc);1970            return;1971        }1972        sum += len;1973    }1974    unsigned char *lp = (o->encoding == OBJ_ENCODING_LISTPACK_EX) ?1975                        ((listpackEx*)o->ptr)->lp : o->ptr;1976    if (!lpSafeToAdd(lp, sum))1977        hashTypeConvert(db, o, target_enc);1978}19791980/* Get the value from a listpack encoded hash, identified by field. */1981GetFieldRes hashTypeGetFromListpack(robj *o, sds field,1982                            unsigned char **vstr,1983                            unsigned int *vlen,1984                            long long *vll,1985                            uint64_t *expiredAt)1986{1987    *expiredAt = EB_EXPIRE_TIME_INVALID;1988    unsigned char *zl, *fptr = NULL, *vptr = NULL;19891990    if (o->encoding == OBJ_ENCODING_LISTPACK) {1991        zl = o->ptr;1992        fptr = lpFirst(zl);1993        if (fptr != NULL) {1994            fptr = lpFind(zl, fptr, (unsigned char*)field, sdslen(field), 1);1995            if (fptr != NULL) {1996                /* Grab pointer to the value (fptr points to the field) */1997                vptr = lpNext(zl, fptr);1998                serverAssert(vptr != NULL);1999            }2000        }

Findings

✓ No findings reported for this file.

Get this view in your editor

Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.

Generated by searchcode.com · powered by code_get_file + code_get_findings