Repository: valkey-io/valkey
File: src/hashtable.c (C)

Code before:
    if (new_capacity < min_capacity || num_buckets * sizeof(bucket) < num_buckets) {
        /* Overflow */
        return 0;
    }

    signed char old_exp = ht->bucket_exp[hashtableIsRehashing(ht) ? 1 : 0];
    size_t alloc_size = num_buckets * sizeof(bucket);
    if (exp == old_exp) {
        /* Can't resize to same size. */
        return 0;
    }

    /* We can't resize if rehashing is already ongoing. Fast-forward ongoing
     * rehashing before we continue. This can happen only in exceptional
     * scenarios, such as when many insertions are made while rehashing is
     * paused. */
    if (hashtableIsRehashing(ht)) {
        if (hashtableIsRehashingPaused(ht)) return 0;
        while (hashtableIsRehashing(ht)) {
            rehashStep(ht);
        }
    }

    if (ht->type->resizeAllowed) {
        double fill_factor = (double)min_capacity / ((double)numBuckets(old_exp) * ENTRIES_PER_BUCKET);
        if (fill_factor * 100 < MAX_FILL_PERCENT_HARD && !ht->type->resizeAllowed(exp > old_exp ? alloc_size : 0, fill_factor)) {
            /* Resize callback says no. */
            return 0;
        }
    }

    /* Allocate the new hash table. */
    bucket *new_table;
    if (malloc_failed) {
        new_table = ztrycalloc(alloc_size);
        if (new_table == NULL) {
            *malloc_failed = 1;
            return 0;
        }
    } else {
        new_table = zcalloc(alloc_size);
    }
    if (ht->type->trackMemUsage) ht->type->trackMemUsage(ht, alloc_size);
    ht->bucket_exp[1] = exp;
    ht->tables[1] = new_table;
    ht->used[1] = 0;
    ht->rehash_idx = 0;
    if (ht->type->rehashingStarted) ht->type->rehashingStarted(ht);

    /* If the old table was empty, the rehashing is completed immediately. */
    if (ht->tables[0] == NULL || ht->used[0] == 0) {

Review comment (suggestion):
This effectively ignores the allocation size when shrinking. It means we allow memory usage to temporarily grow above maxmemory. This is risky in a system where maxmemory is set close to the available memory. In the worst case, the kernel's OOM killer will kill Valkey.

Code after:
    if (new_capacity < min_capacity || num_buckets * sizeof(bucket) < num_buckets) {
        /* Overflow */
        return 0;
    }

    signed char old_exp = ht->bucket_exp[hashtableIsRehashing(ht) ? 1 : 0];
    size_t alloc_size = num_buckets * sizeof(bucket);
    if (exp == old_exp) {
        /* Can't resize to same size. */
        return 0;
    }

    /* We can't resize if rehashing is already ongoing. Fast-forward ongoing
     * rehashing before we continue. This can happen only in exceptional
     * scenarios, such as when many insertions are made while rehashing is
     * paused. */
    if (hashtableIsRehashing(ht)) {
        if (hashtableIsRehashingPaused(ht)) return 0;
        while (hashtableIsRehashing(ht)) {
            rehashStep(ht);
        }
    }

    if (resize_policy == HASHTABLE_RESIZE_FORBID && ht->tables[0]) {
        /* Refuse to resize if resizing is forbidden and we already have a primary table. */
        return 0;
    }
    if (exp > old_exp && ht->type->resizeAllowed) {
        /* If we're growing the table, let's check if the resizeAllowed callback allows the resize. */
        double fill_factor = (double)min_capacity / ((double)numBuckets(old_exp) * ENTRIES_PER_BUCKET);
        if (fill_factor * 100 < MAX_FILL_PERCENT_HARD && !ht->type->resizeAllowed(alloc_size, fill_factor)) {
            /* Resize callback says no. */
            return 0;
        }
    }

    /* Allocate the new hash table. */
    bucket *new_table;
    if (malloc_failed) {
        new_table = ztrycalloc(alloc_size);
        if (new_table == NULL) {
            *malloc_failed = 1;
            return 0;
        }
    } else {
        new_table = zcalloc(alloc_size);
    }
    if (ht->type->trackMemUsage) ht->type->trackMemUsage(ht, alloc_size);
    ht->bucket_exp[1] = exp;
    ht->tables[1] = new_table;
    ht->used[1] = 0;