fix(persistence): a restart no longer flattens every compact encoding - #794
fix(persistence): a restart no longer flattens every compact encoding#794TinDang97 wants to merge 1 commit into
Conversation
RDB decode rebuilt every container in its full form, so a listpack hash, a listpack list, an intset and a set listpack all came back as hashtable/linkedlist/hashtable on the first reload. Redis preserves all of them across DEBUG RELOAD. The memory cost is not incidental. The SADD-builds-a-listpack win (#787, set 404.5 B/key) reverted to 978.2 B/key -- 1.85x back to 4.48x vs redis 7.4.2 -- the moment the server came back up, which makes every small-container encoding win in this campaign conditional on this fix. ## The trap: two decoders, and the obvious one is not the one that runs `src/persistence/rdb.rs` has TWO decode paths. `read_entry_zero_copy` is a second, hand-rolled decoder that builds each container inline and never calls `value_codec::decode_value_body`. Hooking `decode_value_body` alone therefore changed nothing observable: the restart path -- including `load_from_bytes`, the AOF RDB preamble -- goes through `read_entry_zero_copy`, and the guard test still failed with all four types flattened. Both funnels are now covered. ## Change - `value_codec::compact_after_decode` re-derives the compact encoding against thresholds already in the tree (LISTPACK_MAX_ENTRIES, LISTPACK_MAX_ELEMENT_SIZE, INTSET_MAX_ENTRIES). - Applied at `read_entry_zero_copy`'s single Entry funnel and, via `decode_value_body_compacting`, at the `value_codec` path. - No wire-format change: every listpack variant already maps to the same `ValueType` tag as its full form (`value_type_of`), so files stay readable in both directions. They are not byte-identical -- a listpack preserves insertion order where a HashMap does not. - `HashWithTtl` is deliberately not compacted: a listpack carries no TTL sidecar. ## Deliberately NOT the cold/spill path `ValueKind::classify_cold` accepts only the canonical full forms, so a cold-decoded `SetListpack` falls through its `_ => Err(WrongType)` arm and answers WRONGTYPE for a perfectly valid set. The first attempt compacted the shared `decode_value_body` and turned 11 cold-tier tests red for exactly that reason. Compaction is therefore opt-in, RDB-path only. Widening `classify_cold` so the cold tier compacts too is a named follow-up, as is the `redis_rdb.rs` read side used by DUMP/RESTORE and replica full sync. ## Tests `tests/restart_preserves_compact_encoding.rs` writes one key of each compact type plus a 200-field hash as a negative control -- without it, a bug that compacted EVERYTHING would pass -- restarts a real server on the same --dir, and asserts the target encoding of all five. It asserts the post-restart encoding absolutely rather than before == after, so it is meaningful on main, where a small string set legitimately gains a listpack here. Three existing tests asserted the OLD behaviour and are corrected: persistence::rdb::tests::test_round_trip_{hash,list,set} matched RedisValueRef::Hash/List/Set and panicked "Expected hash" the moment decode started returning the compact form. They now assert the listpack variant AND keep every data assertion, so they check strictly more than before -- the encoding as well as the contents. Lib suite 5137 passed / 0 failed. Refs #787 author: Tin Dang
|
ⓘ Qodo reviews are paused because the subscription is no longer active. Ask your workspace admin to reactivate the subscription to resume reviews. Manage billing |
Three existing tests asserted the old behaviour
They now assert the listpack variant and keep every data assertion, so they check strictly more than before: encoding and contents, including that list order survives the re-derivation. Local gate on the final tree: fmt, clippy |
|
Warning Review limit reachedNext included review available in 51 minutes. View limit detailsLimit details: You’ve used the included review currently available. You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Review configuration: ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Team Run ID: 📒 Files selected for processing (4)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
The bug
RDB decode rebuilt every container in its full form, so a listpack hash, a listpack
list, an intset and a set listpack all came back flattened on the first reload:
DEBUG RELOADRedis preserves every one of these. moon preserved none.
This is the critical path for the whole memory campaign. The #787 SADD-listpack win
(set 978.2 -> 404.5 B/key on Linux) reverted to 978.2 — 1.85x back to 4.48x vs redis
7.4.2 — the moment the server came back up. Every per-type encoding win is conditional on
this fix, and every RSS figure measured on a never-restarted server is an upper bound, not
a steady state.
The trap: two decoders, and the obvious one is not the one that runs
src/persistence/rdb.rshas two decode paths:value_codec::decode_value_body— the discoverable one.read_entry_zero_copy— a hand-rolled decoder that builds each container inline andnever calls
value_codecat all.#2 is the path a restart actually takes, including
load_from_bytes, the AOF RDBpreamble. My first attempt hooked only #1: everything compiled, the lib suite stayed
green, and the guard test still failed with all four types flattened. Both funnels are
covered now. (Three
CompactValue::from_redis_valuefunnels exist in that file; thestring one needs no compaction.)
Change
value_codec::compact_after_decodere-derives the compact encoding against thresholdsalready in the tree (
LISTPACK_MAX_ENTRIES128,LISTPACK_MAX_ELEMENT_SIZE64,INTSET_MAX_ENTRIES512), applied at both RDB funnels.No wire-format change. Every listpack variant already maps to the same
ValueTypetagas its full form (
value_codec::value_type_of), so files stay readable in bothdirections. They are not byte-identical — a listpack preserves insertion order where a
HashMapdoes not; the type tag and field/value encoding are unchanged.HashWithTtlis deliberately not compacted: a listpack carries no TTL sidecar.Deliberately NOT the cold/spill path
ValueKind::classify_coldaccepts only the canonical full forms, so a cold-decodedSetListpackfalls through its_ => Err(WrongType)arm and would answer WRONGTYPE fora perfectly valid set. The first version of this fix compacted the shared
decode_value_bodyand turned 11 cold-tier tests red for exactly that reason. Compactionis therefore opt-in (
decode_value_body_compacting), RDB-path only.Two named follow-ups: widening
classify_coldso the cold tier compacts too, and theredis_rdb.rsread side used by DUMP/RESTORE and replica full sync — the latter means areplica can currently hold more memory than its master for identical data.
Tests — red/green
tests/restart_preserves_compact_encoding.rswrites one key of each compact type plus a200-field hash as a negative control (without it, a bug that compacted everything
would pass),
BGREWRITEAOFs, restarts a real server on the same--dir, and asserts thetarget encoding of all five.
It asserts the post-restart encoding absolutely rather than
before == after, becausebefore == afteris branch-dependent: a small string set is ahashtableon main and alistpackon #787's branch. Stated absolutely, the test is meaningful on both.Red:
h: was listpack, after restart hashtable / l: ... linkedlist / si: ... hashtable.Green after the fix.
Refs #787
author: Tin Dang