[V1] Backport #1949: Fix NetworkWriter.AsyncFlushPages hanging FlushEvent waiters on send failure - #1999
Conversation
…ng FlushEvent waiters on send failure AsyncFlushPages's catch block logs the exception and disposes the network handler, but never invokes AsyncFlushPageCallback for the page it failed to send. Callers blocked on the corresponding FlushEvent -- notably GarnetClient.InternalExecuteNoResponse, which cluster PUBLISH forwarding uses on Garnet's network-processing thread -- are therefore never released and hang indefinitely once a send throws. release/v1 carries the identical gap, so this backports the fix merged to main in microsoft#1949 along with its regression test. Applied by hand rather than cherry-picked: the test file lives at test/Garnet.test/ on this branch (test/standalone/Garnet.test/ on main) and this branch's fixture derives from AllureTestBase. The production diff is identical to main's; in the test, only two doc-comment sentences that referenced main's own review history were reworded, with no change to test logic. Test-only note: the test reaches NetworkWriter's private networkSender field to inject a throwing sender, so Garnet.client gains an InternalsVisibleTo entry for Garnet.test. The key is the one already used by other projects on this branch, so no new signing material is introduced. Verified on net10.0 Debug (macOS/arm64): the new test fails without the NetworkWriter change (3s cancellation-token timeout -- the FlushEvent waiter is never released) and passes with it (96ms). GarnetClientTests: 20/20. All three *GarnetClientTests fixtures: 69/69. net8.0 builds clean (0 warnings, with TreatWarningsAsErrors set repo-wide) but its tests were not run locally -- no net8.0 runtime in this environment -- so CI is the first place that TFM and the Linux/Windows legs get exercised. Co-authored-by: Vasileios Zois <96085550+vazois@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Backports the main-branch fix for a hang in NetworkWriter.AsyncFlushPages on send failure into release/v1, and adds a regression test to ensure FlushEvent waiters are released even when SendResponse throws.
Changes:
- Ensure
AsyncFlushPagessignals page completion (viaAsyncFlushPageCallback) even whennetworkSender.SendResponse(...)throws. - Add a regression test that simulates a send failure and verifies
FlushEvent.WaitAsync(...)is released. - Grant
Garnet.testaccess toGarnet.clientinternals viaInternalsVisibleToto enable the test’s injection.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| test/Garnet.test/GarnetClientTests.cs | Adds regression coverage validating FlushEvent is signaled on send failure by injecting a throwing network sender. |
| libs/client/NetworkWriter.cs | Calls AsyncFlushPageCallback in the send-failure catch path to prevent FlushEvent waiters from hanging. |
| libs/client/Garnet.client.csproj | Adds InternalsVisibleTo for Garnet.test to allow the new test to access NetworkWriter internals. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
The 4 red The catch block this PR touches logs That zero is meaningful rather than a logging-plumbing artifact: the same string does appear on #1998 (job 90737676753), with the stack running through The same failures without this change:
Also worth noting the failure sets are disjoint across the four legs — no test fails in more than one — while the other four cluster legs pass on the same commit. And A re-run of the 4 failed jobs whenever convenient would be appreciated. Separately, a correction I owe you, @vazois. On #1949 you asked whether the fix could double-invoke the flush callback and recreate the original bug. I answered that every exception path in
It does matter which page: What actually keeps this benign is narrower than what I claimed: the throw-after-callback window is effectively confined to teardown, and The robust version would be making page completion idempotent (an interlocked flag on |
Backport of #1949 to
release/v1, as promised in #1949 (comment). Sorry for the lag, @vazois — #1949 landed onmainwhile I was away from it.The gap on
release/v1AsyncFlushPages's catch block logs the exception and disposes the network handler, but never invokesAsyncFlushPageCallbackfor the page whose send failed. Anything blocked on the correspondingFlushEventis therefore never released — in particularGarnetClient.InternalExecuteNoResponse, which cluster PUBLISH forwarding calls on Garnet's own network-processing thread.I checked this branch directly rather than assuming the fix was still needed: its catch block is character-for-character what
main's was before #1949, so the hang is reachable here too. This is the sibling half of #1929 — the other half was already backported as #1953.Deviations from #1949
Applied by hand, not cherry-picked, because the test file moved between branches. The exact differences:
libs/client/NetworkWriter.cs,libs/client/Garnet.client.csproj) — byte-identical to what landed onmainin 2255520.test/Garnet.test/GarnetClientTests.cshere,test/standalone/Garnet.test/GarnetClientTests.csonmain.GarnetClientTestsderives fromAllureTestBaseand carries[AllureNUnit]; that is pre-existing branch infrastructure, untouched.main's copy describes "a thread parked influshEvent.Wait" and refers to "the previousWait()-on-a-background-thread approach" — both are leftovers from an earlier iteration of that test during Fix NetworkWriter.AsyncFlushPages hanging FlushEvent waiters on send failure #1949's review and describe code that no longer exists. I reworded them to describe what the test actually does. Test logic is byte-identical; this is the only semantic difference in the file.Before porting the test I diffed every API it touches across the 245 commits separating the branches:
INetworkSenderis byte-identical (so theThrowingNetworkSenderstub needs no adjustment),CompletionEventdiffers only by a comment and an addedToString()withWaitAsyncpresent on both,NetworkWriter's ctor differs only bymain's extraPoolOwnerTypeparam (not used by the test), andGarnetClient's ctor differs only bymain's extraclientNameparam (the test uses named arguments).The narrow
AsyncFlushPageCallbackdouble-invocation edge case disclosed in #1949's description applies identically here and is likewise not addressed.Test-only note
The test reaches
NetworkWriter's privatenetworkSenderfield to inject a sender that throws, soGarnet.clientgains anInternalsVisibleToentry forGarnet.test— same as onmain. The key is the one already used byGarnet.host,Tsavorite.core,GarnetServerandGarnetJSONon this branch, so no new signing material enters a shipping v1 package.Verification
On this branch,
net10.0Debug:NetworkWriter.cschange: fails after 3s — the cancellation token fires because theFlushEventwaiter is never released. I reverted just that hunk and re-ran to confirm this, rather than assuming the test would have caught the bug.GarnetClientTests: 20/20. All three*GarnetClientTestsfixtures (GarnetClientTests,RespListGarnetClientTests,RespSortedSetGarnetClientTests): 69/69.dotnet format --verify-no-changes: clean on both touched projects.To be upfront about the limits: my environment is macOS/arm64 with only the .NET 10 runtime installed.
net8.0builds cleanly there (0 warnings, withTreatWarningsAsErrorsset repo-wide) but I could not run its tests, and I have no Linux or Windows coverage — CI on this PR is the first place those get exercised.