Copy expired-but-unreaped fields in hashTypeDup (#4939)

For a hashtable encoded hash, hashTypeDup() iterates with validation on,
so COPY drops fields whose TTL has passed but that active expiry has not
reaped yet. A replica applies the same COPY with field expiry ignored
and keeps them. The primary's copy never had them, so it never reaps
them or propagates their deletion, and the replica keeps them for as
long as the link stays up.

Ignore TTL while copying, the same way #4704 does for listpack
conversion. The listpack branch already copies everything. The reaper
then deletes the expired fields from the copy and propagates the
deletion.

---------

Signed-off-by: Aman Kumar <amankumar020003@gmail.com>
This commit is contained in:
pirate329 authored and GitHub committed 2026-10-19 18:00:39 +03:00
1 parent 9fdda3152e
commit fbb5ab63bb
2 files changed
+45

No files matched your search

+7
View File
@@ -1113,6 +1113,12 @@ robj *hashTypeDup(robj *o) {
hobj = createObject(OBJ_HASH, ht);
hobj->encoding = OBJ_ENCODING_HASHTABLE;
/* Copy logically-expired fields too, like the listpack branch does. A
* replica applying the same COPY ignores field expiry and keeps them,
* so dropping them here means their deletion is never propagated. The
* reaper deletes them from the copy and propagates it. */
bool prev_state = false;
hashTypeIgnoreTTL(o, true, &prev_state);
hashTypeInitIterator(o, &hi);
while (hashTypeNext(&hi) != C_ERR) {
/* Extract a field-value pair from an original hash object.*/
@@ -1128,6 +1134,7 @@ robj *hashTypeDup(robj *o) {
hashTypeTrackEntry(hobj, entry);
}
hashTypeResetIterator(&hi);
hashTypeIgnoreTTL(o, prev_state, NULL);
} else {
serverPanic("Unknown hash encoding");
}
+38
View File
@@ -5398,6 +5398,44 @@ start_server {tags {"hashexpire external:skip"}} {
assert_equal {} [$replica HGET myhash f1]
assert_equal {f2 f3 f4} [lsort [$replica HKEYS myhash]]
} {} {needs:debug}
# The value size picks the encoding on both sides.
foreach {encoding f2_value} [list listpack v2 hashtable [string repeat x 128]] {
test "COPY of a hash holding an expired-but-unreaped field keeps the replica in sync - $encoding" {
lassign [setup_replication_test $primary $replica $primary_host $primary_port] primary_initial_expired replica_initial_expired
$primary DEBUG SET-ACTIVE-EXPIRE 0
$replica DEBUG SET-ACTIVE-EXPIRE 0
$primary HSET myhash f1 v1 f2 $f2_value
$primary HPEXPIRE myhash 1 FIELDS 1 f1
wait_for_condition 100 10 {
[$primary HEXISTS myhash f1] == 0
} else {
fail "Field f1 was never logically expired on the primary"
}
assert_equal $encoding [$primary object encoding myhash]
# The replica ignores field expiry while applying COPY, so its
# copy has f1. The primary has to copy f1 too, otherwise it
# never propagates the deletion of f1 from the copy.
$primary COPY myhash mycopy
wait_for_ofs_sync $primary $replica
assert_equal $encoding [$primary object encoding mycopy]
assert_equal $encoding [$replica object encoding mycopy]
assert_equal 2 [$primary HLEN mycopy]
assert_equal [$primary HLEN mycopy] [$replica HLEN mycopy]
# The copy must be tracked, otherwise active expiry never reaps f1 from it.
assert_equal 2 [get_keys_with_volatile_items $primary]
$primary DEBUG SET-ACTIVE-EXPIRE 1
$replica DEBUG SET-ACTIVE-EXPIRE 1
wait_for_active_expiry $primary mycopy 1 $primary_initial_expired 2
wait_for_ofs_sync $primary $replica
assert_equal {f2} [$replica HKEYS mycopy]
assert_equal {f2} [$replica HKEYS myhash]
} {} {needs:debug}
}
}
}