fix(store): keep records-by-key index in sync on prefix delete - #1
fix(store): keep records-by-key index in sync on prefix delete#1cbenhagen wants to merge 1 commit into
Conversation
`remove_prefix_filtered` deleted rows from the `records` table only and never removed the matching rows from the `records-by-key` secondary index. Since it is the production prefix-delete path (reached via `Replica::delete_prefix`), every prefix-deleted record leaked an orphan index row that was never reclaimed, causing unbounded store growth and dangling pointers in key-range queries. Remove the corresponding `records-by-key` row for each deleted record, keeping the index 1:1 with `records`. Add regression tests asserting `records_by_key.len() == records.len()` after a delete.
|
Caution Review failedPull request was closed or merged during review No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
📝 WalkthroughWalkthrough
Changesrecords_by_key Index Sync Fix
Estimated code review effort🎯 2 (Simple) | ⏱️ ~10 minutes Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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 |
Description
Fixes a secondary-index leak in the redb (
fs-store) replica store.The store keeps two tables that must stay 1:1:
records-1(namespace, author, key)records-by-key-1(namespace, key, author)()StoreInstance::remove_prefix_filtered(src/store/fs.rs) is the production prefix-deletepath. It is called from
Ranger::putwhenever an entry supersedes its prefix children, whichis exactly what
Replica::delete_prefixtriggers. It removed rows fromrecordsonly and neverremoved the matching rows from
records_by_key. Every prefix-deleted record therefore leaked anorphan index row that was never reclaimed.
The only code that removed from both tables was
entry_remove, which is#[cfg(test)]and neverruns in production, so there was no live code path that kept the index in sync on delete.
Consequences:
records-by-key-1are never reclaimed andaccumulate on every delete-heavy namespace.
RecordsByKeyRangewalksrecords-by-key-1anddereferences into
records-1; orphans are dangling pointers that, at best, waste work and, atworst, surface as lookup misses.
Changes
remove_prefix_filterednow drains theextract_from_ifiterator into the set of removed keys(which performs the deletion from
records) and then removes the corresponding row fromrecords_by_keyfor each one, keeping the index 1:1 withrecords. The collect-first stepreleases the mutable borrow of
tables.recordsbeforetables.records_by_keyis mutated.records_by_key.len() == records.len()after a delete:test_remove_prefix_filtered_cleans_by_key_indexexercisesremove_prefix_filtereddirectly and confirms records under unrelated prefixes are untouched.
test_delete_prefix_keeps_by_key_index_in_syncdrives the same path end-to-end throughReplica::delete_prefix.Breaking Changes
None. This is a behavioural bug fix internal to the
fs-storebackend; no public API changes.Notes & open questions
accumulated in existing stores. Because
records-by-key-1is a pure derived index, an existingstore can be repaired offline (with the scheduler stopped) by clearing
records-by-key-1andre-inserting
(namespace, key, author) -> ()for every row inrecords-1. A migration could beadded if we want this to self-heal on open; left out of this PR to keep it focused.
entry_removeremains#[cfg(test)]. The single-entry remove is only used by tests, and theprefix path is the one that runs in production. Worth considering whether both deletes should
route through one shared helper so the index-cleanup can't drift out of the live path again.
docs.redb,count(records-by-key-1) > count(records-1)meansthe leak is present and the delta is the orphan count.
Change checklist
Summary by CodeRabbit
Bug Fixes
Tests