Skip to content

Commit d875bf6

Browse files
yzc-yzczuiderkwastgusakovy
committed
Fix leak when shrinking a hashtable without entries (#2288)
Fixes #2271 When we shrink a hash table and it is empty, we do it without iterating over it to rehash the entries. However, there may still be empty child buckets (`used[0]==0 && child_buckets[0]!=0`). These were leaked in this case. This fix is to check for child buckets and don't skip the incremental rehashing if any child buckets exist. The incremental rehashing pass will free them. An additional fix is to compact bucket chains in scan when the scan callback has deleted some entries. This was already implemented for the case when rehashing is ongoing but it was missing in the case rehashing is not ongoing. Additionally, a test case for #2257 was added. --------- Signed-off-by: yzc-yzc <[email protected]> Co-authored-by: Viktor Söderqvist <[email protected]> Co-authored-by: Yakov Gusakov <[email protected]>
1 parent 2831722 commit d875bf6

File tree

2 files changed

+46
-2
lines changed

2 files changed

+46
-2
lines changed

src/hashtable.c

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -649,7 +649,7 @@ static int resize(hashtable *ht, size_t min_capacity, int *malloc_failed) {
649649
if (ht->type->rehashingStarted) ht->type->rehashingStarted(ht);
650650

651651
/* If the old table was empty, the rehashing is completed immediately. */
652-
if (ht->tables[0] == NULL || ht->used[0] == 0) {
652+
if (ht->tables[0] == NULL || (ht->used[0] == 0 && ht->child_buckets[0] == 0)) {
653653
rehashingCompleted(ht);
654654
} else if (ht->type->instant_rehashing) {
655655
while (hashtableIsRehashing(ht)) {
@@ -1718,7 +1718,9 @@ size_t hashtableScanDefrag(hashtable *ht, size_t cursor, hashtableScanFunction f
17181718
if (!hashtableIsRehashing(ht)) {
17191719
/* Emit entries at the cursor index. */
17201720
size_t mask = expToMask(ht->bucket_exp[0]);
1721-
bucket *b = &ht->tables[0][cursor & mask];
1721+
size_t idx = cursor & mask;
1722+
size_t used_before = ht->used[0];
1723+
bucket *b = &ht->tables[0][idx];
17221724
do {
17231725
if (b->presence != 0) {
17241726
int pos;
@@ -1736,6 +1738,11 @@ size_t hashtableScanDefrag(hashtable *ht, size_t cursor, hashtableScanFunction f
17361738
b = next;
17371739
} while (b != NULL);
17381740

1741+
/* If any entries were deleted, fill the holes. */
1742+
if (ht->used[0] < used_before) {
1743+
compactBucketChain(ht, idx, 0);
1744+
}
1745+
17391746
/* Advance cursor. */
17401747
cursor = nextCursor(cursor, mask);
17411748
} else {

tests/unit/expire.tcl

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -951,6 +951,43 @@ start_server {tags {"expire"}} {
951951
}
952952
}
953953

954+
start_server {tags {expire} overrides {hz 100}} {
955+
test {Active expiration triggers hashtable shrink} {
956+
set persistent_keys 5
957+
set volatile_keys 100
958+
set total_keys [expr $persistent_keys + $volatile_keys]
959+
960+
for {set i 0} {$i < $persistent_keys} {incr i} {
961+
r set "key_$i" "value_$i"
962+
}
963+
for {set i 0} {$i < $volatile_keys} {incr i} {
964+
r psetex "expire_key_${i}" 100 "expire_value_${i}"
965+
}
966+
set table_size_before_expire [main_hash_table_size]
967+
968+
# Verify keys are set
969+
assert_equal $total_keys [r dbsize]
970+
971+
# Wait for active expiration
972+
wait_for_condition 100 50 {
973+
[r dbsize] eq $persistent_keys
974+
} else {
975+
fail "Keys not expired"
976+
}
977+
978+
# Wait for the table to shrink and active rehashing finish
979+
wait_for_condition 100 50 {
980+
[main_hash_table_size] < $table_size_before_expire
981+
} else {
982+
puts [r debug htstats 9]
983+
fail "Table didn't shrink"
984+
}
985+
986+
# Verify server is still responsive
987+
assert_equal [r ping] {PONG}
988+
} {} {needs:debug}
989+
}
990+
954991
start_cluster 1 0 {tags {"expire external:skip cluster"}} {
955992
test "expire scan should skip dictionaries with lot's of empty buckets" {
956993
r debug set-active-expire 0

0 commit comments

Comments
 (0)