Clear the Vector Set index pointer on the diskless replication stream-in path - #1993
Closed
tiagonapoli wants to merge 5 commits into
Closed
Clear the Vector Set index pointer on the diskless replication stream-in path#1993tiagonapoli wants to merge 5 commits into
tiagonapoli wants to merge 5 commits into
Conversation
added 2 commits
July 29, 2026 11:06
Diskless replication streams raw Tsavorite log records verbatim, and a Vector Set's index record persists the native DiskANN handle IndexPtr in its value. A replica taking a diskless full sync therefore ends up with the primary's handle in its own copy of the record. Nothing clears it on that path. GarnetRecordTriggers.OnDiskRead is the only hook that zeroes IndexPtr and it only fires for records faulted in from disk, never for records streamed straight into memory. Since VectorManager.NeedsRecreate is exactly indexPtr == 0, a foreign non-zero pointer is indistinguishable from a healthy local one, so the lazy recreate is skipped and the raw value reaches the P/Invoke. Existing cluster Vector Set coverage cannot catch this. Those tests go through SimpleSetupClusterAsync, which never enables diskless sync, and they attach replicas before writing, so each replica builds its own index from replicated VADD payloads. The assertion is structural rather than behavioural because cluster tests run every node in one process, where the primary's handle is still mapped and valid. The replica silently answers out of the primary's live index and every black-box read looks correct; the same aliasing only becomes the observed SIGSEGV once the nodes are separate processes. Tests assert that the replica took an actual streaming full sync, so they cannot pass vacuously by degrading to an incremental AOF replay. The no-full-sync control passes and pins the full sync down as the cause. Grants Garnet.test.cluster.vectorsets access to Garnet.server internals so the persisted record can be read without going through ReadVectorIndex, which would lazily rebuild the index and rewrite the field under test.
…-in path A Vector Set index record persists the native DiskANN handle IndexPtr in its value, and diskless replication streams raw Tsavorite log records verbatim. A replica taking a full sync therefore adopted the primary's handle and, because VectorManager.NeedsRecreate is exactly indexPtr == 0, treated it as a healthy local index: the lazy rebuild was skipped and the foreign address was handed to DiskANN. In production that faults the replica with SIGSEGV; where the address happens to resolve to another live index it silently answers out of the wrong one. Clear the pointer as the record enters the replica's store, so the index is recreated on first touch. This is the same thing OnDiskRead already does for records faulted in from disk, and SetContextForMigration already does for slot migration. Diskless replication was the one path that carried the handle across nodes untouched. ClearIndexPointer becomes public for this; Garnet.cluster already consumes the public ReadIndex and SetContextForMigration on the migration path. Verified against the TLA+ model in the companion investigation: sanitizing the stream-in path is the FixSanitize scenario, which TLC showed to be sufficient on its own. Validating the pointer at the call site was also sufficient but strictly more invasive, and quiescing readers was NOT sufficient, since the handle was never valid in this address space. Turns the three ClusterVectorSetDisklessSyncTests green. Full Garnet.test.cluster.vectorsets (85) and Garnet.test.cluster.replication.disklesssync (29) suites pass.
tiagonapoli
commented
Jul 29, 2026
| /// <c>MC_Vec_QuiesceOnly_Buggy</c> scenario still failed, which is why none of these tests need to | ||
| /// race anything: a quiet, fully serialized read after the sync completes is enough. | ||
| /// </para> | ||
| /// </summary> |
Collaborator
Author
There was a problem hiding this comment.
reduce verbosity - too many comments
no need to say that was a regression test
…efects New test project covering Vector Set replication end to end: diskless full sync, disk-based checkpoint full sync, restart/recovery, failover and async replay. Assertions verify the replica actually holds its own DiskANN index (not the primary's raw handle) and that every VADD'd element and vector payload is present, via VINFO/VDIM/VEMB/VSIM rather than index pointers alone. Fixes two product defects the new tests uncovered: 1. AofProcessor.Dispose() permanently shut down VectorManager's replication replay channel. AofProcessor is transient during startup AOF replay but VectorManager is owned by the database, so after a --recover start every replicated VADD was silently discarded. Dispose now only drains. 2. VectorManager's recovery state (recoveredIndexes/recoveredMetadata) was released after the startup recovery, but a replica re-recovers the store on every disk-based full sync. The second recovery threw NullReferenceException inside the record-trigger walk, aborting it after the first record, so the remaining Vector Set records kept the primary's raw DiskANN handle. State is now cleared instead of released, and ResumePostRecovery runs after a replica's disk-based recovery. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9cdb19eb-7ffa-44cb-ab05-566f44d79d70
tiagonapoli
commented
Jul 29, 2026
| // clears it on this path: GarnetRecordTriggers.OnDiskRead only fires for records | ||
| // faulted in from disk. Left alone, VectorManager.NeedsRecreate would see a | ||
| // non-zero pointer, skip the rebuild, and hand a foreign address to DiskANN. | ||
| if (diskLogRecord.RecordType == VectorManager.RecordType) |
Collaborator
Author
There was a problem hiding this comment.
Add RangeIndex management for this as well
- ClusterVectorSetAsyncReplayTests: 150/4 truncates to 37, so the interleaved loop wrote 148 elements while the assertion expected 150. Per-round counts are now the constants and the totals are derived from them. - AssertSearchesAgree demanded identical VSIM result lists. A replica that rebuilt its index has a DiskANN graph built in a different insertion order, so an approximate search legitimately diverges in the tail. It now checks what must actually hold: an exact query for an element's own embedding returns that element, every neighbour is a real member of the set, and at least half of a random query's neighbourhood agrees with the primary. - PrimaryRebuildsVectorSetIndexAfterRestart waited for a replica whose replication link died with the old primary process. The post-restart replication leg now uses a spare node that attaches to the recovered primary, which is the case the test is actually about. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9cdb19eb-7ffa-44cb-ab05-566f44d79d70
Collapse the multi-paragraph doc comments across the Vector Set replication suite and the accompanying product fixes down to one to three lines each, keeping only the constraints that are not evident from the code. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9cdb19eb-7ffa-44cb-ab05-566f44d79d70
Collaborator
Author
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.
Problem
A Vector Set index record persists the native DiskANN handle
IndexPtrin its value (offset 8 of the 56-byteVectorManager.Index). Diskless replication streams raw Tsavorite log records verbatim (ReplicationSnapshotIterator→DiskLogRecord.Serialize), so a replica taking a full sync adopts the primary's handle.Nothing clears it on that path:
GarnetRecordTriggers.OnDiskReadis the only hook that zeroesIndexPtr, and it only fires for records faulted in from disk. Streamed records land directly in memory, so it never runs.VectorManager.NeedsRecreateis exactlyindexPtr == 0, so a foreign non-zero pointer is indistinguishable from a healthy local one and the lazyService.RecreateIndexrebuild is skipped.In production this faults the replica with
SIGSEGVinsideNativeDiskANNMethods.card, and the replica crash-loops so it never converges. Where the address happens to resolve to another live index instead of unmapped memory, the read succeeds and silently answers out of the wrong vector set.This only affects the replication path. VADD/VREM replicate vector payloads, not native bytes, so a replica that is attached before the writes rebuilds its own index locally and is fine. It takes a full sync of an already-populated Vector Set to hit this.
Fix
Clear the pointer as the record enters the replica's store, in
NetworkClusterSync— the single point where streamed records are ingested:The index is then recreated on first touch. This is the same thing
OnDiskReadalready does for disk-faulted records, andSetContextForMigrationalready does for slot migration — diskless replication was the one path that carried the handle across nodes untouched.VectorManager.ClearIndexPointerbecomespublicfor this;Garnet.clusteralready consumes the publicReadIndex/SetContextForMigrationon the migration path in the same way.Tests
New
test/cluster/Garnet.test.cluster.vectorsets/VectorSets/ClusterVectorSetDisklessSyncTests.cs:VectorSetReadableOnReplicaAfterDisklessFullSyncVectorSetsStayPartitionedAcrossDisklessFullSyncVectorSetReadableOnReplicaWithoutFullSync(control)Existing cluster Vector Set coverage cannot catch this defect: those tests go through
SimpleSetupClusterAsync, which never passesenableDisklessSync, and they attach replicas before writing, so each replica builds its own index from replicated VADD payloads.Why the assertion is structural rather than behavioural. Cluster tests run every node inside a single process, so the primary's handle is still mapped and valid when the replica dereferences it. The replica quietly answers out of the primary's live index and every black-box read returns the correct answer — the SIGSEGV cannot be reproduced in-process, it needs two address spaces. The tests therefore assert the invariant that is actually violated: a node may only dereference a handle it allocated itself. Against unfixed
mainthe replica holds the byte-identical pointer:The record is read via
Read_MainStorerather thanVectorManager.ReadVectorIndex, because the latter consultsNeedsRecreateand would rebuild the index, rewriting the field under test.The tests also assert
recoverFullSync:Truefrom the sync metadata, so they cannot pass vacuously by silently degrading into an incremental AOF replay. The no-full-sync control passes throughout and pins the full sync down as the cause.Garnet.test.cluster.vectorsetsis added toInternalsVisibleToso the test can read the persisted record.Validation
mainand pass with the fix.Garnet.test.cluster.vectorsets— 85/85 pass.Garnet.test.cluster.replication.disklesssync— 29/29 pass.Releasebuild across both target frameworks, 0 warnings.Background
Found while reproducing a customer report on a live 1-shard / 3-replica cluster, where replicas were terminating with
status=11/SEGVand crash-looping. The faulting managed stack from a captured minidump wasNativeDiskANNMethods.card←StorageSession.VectorSetInfo←RespServerSession.NetworkVINFO.The lifetime of a Vector Set index across primary, replica, AOF and full sync was then modelled in TLA+ to confirm the mechanism and to choose between candidate fixes. TLC reproduced the production crash as a 6-step counterexample (
SyncReset→PrimaryCreate→SyncSnapshot→BeginRead→Deref) and, across seven scenarios, showed that:vectorSetLocksare already correct.The three tests above are the executable form of those counterexamples.