Repository navigation
[MOD-18471] Fix SVS marked-deletion GC race - #1082
Conversation
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 6892476. Configure here.
58f7e1a to
4fadf25
Compare
4fadf25 to
7692f8e
Compare
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #1082 +/- ##
==========================================
+ Coverage 97.52% 97.53% +0.01%
==========================================
Files 143 143
Lines 9697 9704 +7
==========================================
+ Hits 9457 9465 +8
+ Misses 240 239 -1 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
Looks good, in svs.h runGC() body: |
|
I initially used that snapshot/fetch_sub approach, but it has a second race because the snapshot is not synchronized with SVS's compaction cutoff. A deletion can complete after the load and before compact() acquires SVS's internal exclusive lock; compact() then removes that tombstone, but fetch_sub(before) leaves its increment in num_marked_deleted. If it was the last node, the tiered empty-index fast path can leave that stale count indefinitely. The view_data().size() check does not close that window. Taking mainIndexGuard shared around the complete backend deletion (tombstone plus counter increment) aligns it with GC's existing exclusive mainIndexGuard: the deletion either finishes before compaction and is cleared by that GC, or starts after the reset and remains counted for the next GC. |
|
got it. Thanks! |

Summary
GC can finish compacting the SVS index, then a concurrent tiered deletion can increment num_marked_deleted before GC resets the counter to zero. The atomic counter prevents a C++ data race but the unconditional store still loses the logical update because the deletion path previously did not acquire either GC lock. The fix routes all tiered backend deletions through a helper that holds mainIndexGuard shared, while GC already holds that guard exclusively through compaction and the counter reset.
Testing
The before/after tests were run in an isolated build using the repository-pinned SVS revision because the working tree's pre-existing SVS submodule checkout differs from the pinned commit.
Supersedes #1081, which GitHub closed when its head branch was renamed.
Note
Medium Risk
Changes concurrency around tiered SVS deletions and GC counter semantics; scope is localized but affects correctness of when reindex/GC is triggered.
Overview
Fixes a logical race between tiered SVS garbage collection and backend deletions on
num_marked_deleted: aftercompact(), GC could reset the counter to zero while a concurrent deletion (previously unlocked) still incremented it, so the new tombstone was effectively dropped.Tiered SVS now routes every backend delete (
deleteVector, in-place/async overwrites) throughdeleteVectorFromBackend, which takes a sharedmainIndexGuardso deletions cannot interleave with GC’s exclusive hold throughcompact()and the counter reset. GC wrapper docs are updated to reflect that the guard also blocks backend deletions.Test-only:
SVSIndex::setAfterCompactCallbackForTestruns a hook aftercompact()and before the counter reset; a new unit test drives a concurrentdeleteVectorin that window and asserts it blocks until GC finishes, preserving the marked-deletion count across epochs.Reviewed by Cursor Bugbot for commit 7692f8e. Bugbot is set up for automated code reviews on this repo. Configure here.