Repository navigation
[MOD-20706] Reuse HNSW ID holes after shared_ptr finalization - #1087
Open
nonirosenfeldredis wants to merge 14 commits into
Open
nonirosenfeldredis wants to merge 14 commits into
nonirosenfeldredis wants to merge 14 commits into
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #1087 +/- ##
==========================================
+ Coverage 97.37% 97.42% +0.04%
==========================================
Files 145 145
Lines 9560 9740 +180
==========================================
+ Hits 9309 9489 +180
Misses 251 251 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
nonirosenfeldredis
force-pushed
the
reuse-from-cache-stacked
branch
from
October 29, 2026 14:52
a9afaf0 to
982be68
Compare
Re-add the cache of cleared slots: the GC clears the slots of ready swap jobs into a FIFO cache and compacts half of it, and an insert takes the oldest slot instead of appending a fresh id. Reuse is off by default (`setReuseOnInsert`). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
nonirosenfeldredis
force-pushed
the
reuse-from-cache-stacked
branch
from
November 1, 2026 20:23
982be68 to
404f52a
Compare
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 404f52a. Configure here.
Keep detached IDs alive through active operation markers. Clear and cache each slot from the last reference deleter using shared main-index access, and let GC consume all cached entries up to its limit.
Add deterministic tests for inserts, repairs, async and in-place deletion, query timeout, batch depletion, and explicit GC with a waiting finalizer. Rename the reusable ID hole cache and finalizer identifiers; extract both queue-draining helpers.
nonirosenfeldredis
enabled auto-merge
November 2, 2026 04:39
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Describe the changes in the pull request
Allow tiered HNSW inserts to fill reusable ID holes published by a
shared_ptr<idType>finalizer. Insert-time reuse remains disabled by default throughsetReuseOnInsert.After a deleted node's last repair completes, detach it from the graph under shared main-index access. Active query, insert, repair, and delete markers retain references to its ID so a reader with a saved candidate or graph pointer can finish safely. The last reference's deleter clears the slot and publishes it to the FIFO
reusableIdHolescache, using shared main-index access. Batch iterators retain their markers until depletion, reset, or destruction. Finalization does not acquire the main index guard exclusively.Inserts fill the oldest reusable hole under shared main-index access, or append when the cache is empty. GC only consumes finalized cache entries for compaction, up to
maxJobsToRun, regardless of whether insert-time reuse is enabled. It drains the remaining cache when the limit permits, including a single remaining entry.TIERED_HNSW_NUM_ID_HOLESreports the current number of reusable ID holes in the cache. It increases when finalizers publish slots and decreases when inserts reuse them or GC compacts them; deleted nodes still held by readers are excluded.RuntimeMarker usage
RuntimeMarkertracks an operation that may retain an internal ID or graph pointer.attach()registers it in the index'scurrentlyRunninglist underrunningGuard, following the operation-marker approach used by the disk index.Once a repaired node is isolated, its
shared_ptr<idType>is added to every currently active marker'spendingIds. Operations starting afterward cannot reach the isolated node and do not retain that reference.detach()(also called by the destructor) unregisters the marker underrunningGuard, then releases its ID references after releasing both that mutex and its operation's main guard. The last reference invokes the custom deleter, which takes shared main-index access, clears the slot, and publishes it to the cache. This protects saved candidates and graph pointers without an exclusive finalization lock.Which issues this PR fixes
Main objects this PR modified
popPendingFinalizer()centralizes the pending queue pop under its mutex.takeReusableIdHoles(), GC compaction, and cache size statistics.The deterministic query/delete test runs for both top-K and range queries. One hook pauses after the query saves a candidate's graph pointer; another pauses after scanning, before the query releases its marker. A separate thread deletes and repairs the node while the query is paused. The test verifies that deletion completes without an exclusive finalization lock, the hole remains unavailable at both checkpoints, and the original ID becomes reusable when the completed query releases its marker.
Marker lifetime coverage
Validation
Mark if applicable
Note
High Risk
Changes core tiered HNSW deletion, compaction, and concurrent graph traversal lifetimes via new marker/reference counting; correctness bugs could cause use-after-free or graph corruption under parallel queries and async repairs.
Overview
Tiered HNSW defers clearing deleted node slots until no in-flight operation can still hold stale internal IDs or graph pointers. When repairs finish, nodes are isolated under shared locking;
RuntimeMarkertracks queries, inserts, repairs, and deletes (including batch iterators) and holdsshared_ptrreferences so the last reader triggersclearDeletedSlotand publishes the slot into a FIFOreusableIdHolescache—without taking the main index lock exclusively for finalization.Optional insert-time reuse (
setReuseOnInsert, off by default) fills the oldest cleared hole viastoreNewElementInClearedSlotinstead of always growingcurElementCount.compact()(renamed fromdefrag()) still runs swap compaction but only drains already finalized cache entries, with bounded batches unchanged in spirit. Repair scheduling skips neighbors already isolated, and debug info addsTIERED_HNSW_NUM_ID_HOLES. Test-only hooks on candidate graph reads support concurrency tests for query/delete interleaving.Reviewed by Cursor Bugbot for commit 313c775. Bugbot is set up for automated code reviews on this repo. Configure here.