Repository navigation
Scope checkpoint metadata I/O completions to their own operation - #2154
Merged
Ted Hart (TedHartMS) merged 8 commits intoSep 25, 2026
Merged
Conversation
DeviceLogCommitCheckpointManager held its completion semaphore and its error code on the instance and passed a null device I/O context, so nothing tied a completion to the operation that issued it. With two metadata operations in flight, one operation's completion released the other's wait: the woken operation copied a buffer the device had not filled yet, and returned that buffer to the pool while its own I/O was still writing into it. SectorAlignedBufferPool zeroes a returned buffer and can hand it to another caller, so this also corrupted the operation that was still running. The shared error field had the same ownership problem in both directions: an operation could observe an error raised by another, or reset one another had not observed yet. Concurrent INFO ALL requests hit this and intermittently reported a valid checkpoint as "Invalid metadata length 0 ... truncated or corrupt", logged "Skipping unreadable index checkpoint", and returned disk_checkpoint_entry:(empty). Give each read and write its own MetadataIoCompletion, holding that operation's semaphore and error code, and pass it as the device I/O context. A completion now releases only its own issuer, which also makes the existing buffer return in the finally block a correct lifetime guarantee rather than a race. Publish the lazily created buffer pool with a compare-exchange so concurrent operations cannot each build one. Also validate the metadata length prefix in the cluster-mode override of GetLogCheckpointMetadata, which sliced by it without the check every base-class reader applies, and dispose its device with using so the added throw cannot leak it. Regression coverage adds a GatedCompletionDevice that defers the underlying I/O, not just its callback, so a test can deliver completions in a chosen order and assert that no operation returns before its own completion fires. The three new tests fail against the previous code. Fixes #2149
Contributor
There was a problem hiding this comment.
🟡 Changes recommended
Buffer-pool publication leaks losing candidates, two tests rely on unspecified semaphore ordering, and cluster validation lacks regression coverage.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
Scopes checkpoint metadata completions and errors per I/O operation to prevent concurrent reads and writes from corrupting results or recycling active buffers.
Changes:
- Adds operation-specific completion state and synchronized buffer-pool publication.
- Validates cluster checkpoint metadata sizes and ensures device disposal.
- Adds gated-device concurrency and error-isolation tests.
File summaries
| File | Description |
|---|---|
DeviceLogCommitCheckpointManager.cs |
Isolates metadata I/O completion state. |
GarnetClusterCheckpointManager.cs |
Validates metadata lengths and disposes devices. |
SimulatedFlakyDevice.cs |
Adds a gated test device. |
FlakyDeviceTests.cs |
Adds concurrent metadata I/O tests. |
Review details
- Files reviewed: 4/4 changed files
- Comments generated: 4
- Review effort level: Balanced (auto)
Note
Copilot is running an experiment and ran this review at Balanced.
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Ted Hart (TedHartMS)
requested a review
from Badrish Chandramouli (badrishc)
September 18, 2026 04:52
Ted Hart (TedHartMS)
marked this pull request as draft
September 18, 2026 18:02
Resolves a conflict in SimulatedFlakyDevice.cs where both sides appended new test devices after ErrorCodeOnReadDevice. Git aligned them on the shared StorageDeviceBase boilerplate (constructor, Initialize, RemoveSegmentAsync, Dispose) and reported four hunks; both additions are kept in full. Main's DeferredCompletionDevice and this branch's GatedCompletionDevice are not redundant. The former holds a single read, selected by index, while writes pass through, and records whether Dispose ran with IO outstanding. The latter gates reads and writes, releases pending operations in an order the test chooses, can complete one with an injected error code, and stamps a shared counter so a test can assert that no caller returned before its own completion. The checkpoint metadata tests need reordered release across two concurrent operations and write gating, which the former does not provide.
Free the losing buffer pool candidate. Its constructor reserves a process-wide slot that sizes every thread's shard array, and only Free returns that slot, so a candidate dropped after losing the publish race raised the slot high-water mark permanently. Make the concurrency regression tests independent of SemaphoreSlim's wake order, which is unspecified. The gate now holds the second caller inside the device call after its operation is captured, so it never reaches its wait; when the first operation's completion is delivered there is exactly one waiter and no ordering choice exists. Previously the tests detected completion theft only if the runtime happened to wake waiters first-in-first-out. Cover the cluster override of GetLogCheckpointMetadata, which gained the metadata length check. The manager is reached through the public ClusterFactory.CreateCheckpointManager rather than by widening the internal type's visibility. The tests assert the named exception, that a failing read does not leak its device, and that valid metadata still round-trips.
…ommit-shared-io-completion
Ted Hart (TedHartMS)
marked this pull request as ready for review
September 23, 2026 22:26
Ted Hart (TedHartMS)
requested review from
Badrish Chandramouli (badrishc)
and removed request for
Badrish Chandramouli (badrishc)
September 23, 2026 22:27
Badrish Chandramouli (badrishc)
approved these changes
Sep 24, 2026
TestContext.Test.Name carries a parameterized test's arguments but not its fixture, so two same-named tests in different fixtures share a key in RunningTests. That loses the disambiguation the unhandled-exception dump exists to provide, and lets one test's TearDown remove an entry belonging to another. Key on FullName in both the SetUp that adds the entry and the TearDown that removes it; the two must agree or the TryRemove assertion fires.
…ommit-shared-io-completion
Badrish Chandramouli (badrishc)
approved these changes
Sep 24, 2026
Ted Hart (TedHartMS)
deleted the
tedhar-fix-devicelogcommit-shared-io-completion
branch
September 25, 2026 01:21
x@01 (x-at-01)
added a commit
to webc-fork/garnet
that referenced
this pull request
Oct 4, 2026
…oint abort in-flight flushes + stale selection on recovery (microsoft#2172), checkpoint metadata I/O scoping (microsoft#2154)
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.
Fixes #2149
Problem
DeviceLogCommitCheckpointManagerkept its completion semaphore and its error code on the instance, and passedcontext: nullto every device I/O:Nothing tied a completion to the operation that issued it. With two metadata operations in flight:
ReadIntoA wakes while its own read is still outstanding and copies its still-zeroed buffer, soGetIndexCheckpointMetadatareads a length prefix of0and rejects a valid checkpoint as truncated.finally { pbuffer.Return(); }then runs while A's read is still writing into that buffer.SectorAlignedBufferPool.Returnzeroes the buffer and can hand it to another caller, so this also corrupts whichever operation was still running. This is a live use-after-return, not merely a stale read.metadataIoErrorCode = 0resets an error another operation has not observed yet, and the post-wait read can surface an error belonging to a different operation.Symptom from the issue: concurrent
INFO ALLintermittently logsSkipping unreadable index checkpoint/Invalid metadata length 0and returnsdisk_checkpoint_entry:(empty)while the on-disk metadata is intact.Fix
Each
ReadInto/WriteIntoallocates its ownMetadataIoCompletion(its ownSemaphoreSlimplus its ownErrorCode) and passes it as the device I/O context;IOCallbackrecords the error on, and releases, only that instance. This mirrors the per-call semaphoreGarnet.cluster.ClusterUtilsalready passes as its I/O context.Because the wait can now only be satisfied by the operation's own callback, the existing
finally { pbuffer.Return(); }becomes a correct buffer-lifetime guarantee instead of a race.Metadata operations stay concurrent —
INFO ALLis a normal concurrent request path, so serializing the whole metadata sequence behind one lock (the issue's other suggested option) was rejected; it would also not fix the buffer-lifetime hazard as directly.Two details worth noting:
SemaphoreSlimis deliberately not disposed. It never allocates a wait handle (AvailableWaitHandleis untouched), and on a synchronous device throw — where no completion is delivered — disposing it would risk a lateReleasefaulting a device I/O thread.Release/Waitsupplies the happens-before edge that publishesErrorCodeto the waiting thread, which is the same guarantee the previous code already relied on.Also in this PR:
bufferPoolcreation was an unsynchronized check-then-act; it is now published withInterlocked.CompareExchange, and a candidate that loses the publish race is freed rather than dropped — its constructor reserves a process-wide pool slot that sizes every thread's shard array, and onlyFreereturns that slot.GarnetClusterCheckpointManager.GetLogCheckpointMetadatasliced by the metadata length prefix without theThrowIfInvalidMetadataSizecheck every base-class reader applies, so a truncated file was silently parsed as empty metadata (or threwArgumentOutOfRangeExceptionfromSliceon a negative size). It now applies the same guard —ThrowIfInvalidMetadataSizebecomesprotected static— and disposes its device withusingso the added throw cannot leak it. The bound is unchanged from the one the base class already applies to this identical file format.TestBasekeys its running-test set byTestContext.Test.FullNamerather thanName.Namecarries a parameterized test's arguments but not its fixture, so same-named tests in different fixtures shared a key — which both blurred the unhandled-exception dump and let one test'sTearDownremove an entry belonging to another.Tests
New
GatedCompletionDevicetest double defers the underlying I/O, not just its callback, so a caller woken by a foreign completion observes its own buffer exactly as it left it. It releases pending operations in a chosen order, can complete one with an injected error code, and stamps a shared monotonic counter when each callback fires so "did this caller return before its own completion?" is assertable without timing.Three tests, covering the four scenarios the issue asks for:
ConcurrentMetadataReadsDoNotConsumeEachOthersCompletionsMetadataWriteDoesNotConsumeAConcurrentReadsCompletionMetadataOperationDoesNotConsumeAnotherOperationsErrorAll three were verified to fail against the pre-fix code with the intended diagnostics (
read 0 returned a buffer that its own read had not filled,the metadata write returned before its own completion fired...,a healthy read reported another operation's error), not merely to pass after it.Detection is structural rather than timing-based: the gate holds the second caller inside the device call once its operation is captured, so it never reaches its wait. When the first completion is delivered there is exactly one waiter, and
SemaphoreSlim— which specifies no wake order — has no ordering choice to make. An earlier revision relied on FIFO wake order and could pass against the broken code by scheduling luck.ClusterCheckpointMetadataTestscovers the cluster override that gained the length check: it asserts the named exception, that a failing read does not leak its device, and that valid metadata still round-trips. The internal manager is reached through the publicClusterFactory.CreateCheckpointManagerrather than by widening its visibility.Validation
dotnet build Garnet.slnxandTsavorite.slnx— 0 warnings, 0 errorsdotnet formatclean onGarnet.slnxandTsavorite.slnxTsavorite.test.hlogFlakyDeviceTests— 22/22 passGarnet.test.clusterClusterCheckpointMetadataTests— 3/3 passGarnet.testCheckpointFailureTests+PortSlotTests— 34/34 passCheckpointManagerTests,SimpleRecoveryTest,LogResumeTests) — 19 passed, 5 skipped (Azure)ClusterSRPrimaryCheckpointAsync,ClusterSRPrimaryCheckpointRetrieve) fail identically on a pristine baseline with no changes applied — a pre-existingTimed out DeleteDirectoryteardown flake on this Windows machine, reported asprimary failure — test itself passed. Parity confirmed, not a regression from this PR.