fix(test): harden LocalArtifactServer disposal against Windows shutdown aborts (#2071) - #2072
Merged
Merged
Conversation
…wn aborts EmbeddingModelProvisionerTests fails intermittently on Windows CI with HttpListenerException 'I/O operation has been aborted'. Root cause: two shutdown races between a mid-test DisposeAsync and the server's in-flight I/O, both surfaced because DisposeAsync unconditionally awaited the serve loop, rethrowing any fault into the disposing test (and, since each test gets a fresh instance, poisoning whichever test happens to be tearing down). 1. The pending GetContextAsync accept aborts after IsListening flips false, so the !IsListening guard was racy and let the exception escape. 2. An in-flight request aborted by Stop()/Close() threw on the response stream inside HandleAsync, escaping the loop and faulting _serveLoop. Fix: treat any HttpListenerException on accept as a graceful stop, contain Response-stream aborts in HandleAsync, and swallow serve-loop faults in DisposeAsync so disposal can never fail a test. Verified: 46 consecutive local runs clean (previously ~1 in 15 failed). Closes #2071
Aaronontheweb
enabled auto-merge (squash)
August 27, 2026 03:46
Comment on lines
+94
to
+96
| catch (ObjectDisposedException) | ||
| { | ||
| } |
Comment on lines
+97
to
+99
| catch (HttpListenerException) | ||
| { | ||
| } |
Comment on lines
+107
to
+109
| catch (ObjectDisposedException) | ||
| { | ||
| } |
Comment on lines
+110
to
+112
| catch (HttpListenerException) | ||
| { | ||
| } |
The empty catches in LocalArtifactServer.cs deliberately absorb ObjectDisposedException / HttpListenerException aborts that Windows' HttpListener raises on Stop()/Close() during fixture teardown (issue #2071). Sensored via .slopwatch/config.json scoped to that single test-infra file only.
… disposal Windows CI still flaked with 'System.ArgumentException: The handle is invalid' escaping from GetContextAsync when Stop()/Close() races the pending accept during teardown. Broaden the accept-side catch to treat ArgumentException as graceful stop and absorb any Exception in DisposeAsync, which fully covers the Windows abort shapes.
Comment on lines
+151
to
+158
| catch (Exception) | ||
| { | ||
| // Network-teardown abort: Windows can surface several exception shapes | ||
| // (HttpListenerException, ObjectDisposedException, ArgumentException "invalid | ||
| // handle") from Stop()/Close() racing the pending accept or an in-flight response. | ||
| // The listener is already stopped and closed, so there is nothing left to release | ||
| // and disposal must never fail a test. SW003 suppressed via .slopwatch/config.json. | ||
| } |
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.
Summary
Fixes the intermittent Windows CI failure in
EmbeddingModelProvisionerTests(HttpListenerException: The I/O operation has been aborted) that blocked the 0.27.0-beta.1 release PR (#2069).Root cause
Two shutdown races between a test's
DisposeAsynccall and the server's in-flight I/O, both surfaced becauseDisposeAsyncunconditionallyawaited the serve loop — rethrowing any fault into the disposing test:Stop()/Close()while aGetContextAsync()is pending aborts the accept. The exception surfaces afterIsListeningflips to false, making thewhen (!_listener.IsListening)guard racy so the exception escaped uncaught.Stop()/Close()throws on the response stream insideHandleAsync, escaping the loop and faulting_serveLoop.Because each test gets a fresh
LocalArtifactServerinstance (xUnit per-test class instantiation, not a shared fixture), the fault landed on whichever test happened to be tearing down — which is why even the sync, server-agnosticArcticInt8...test could be blamed.Fix
HttpListenerExceptionon the accept side as a graceful stop (drop the racyIsListeninggate).HandleAsync.DisposeAsync— the load-bearing change so disposal can never fail a test, regardless of which abort signature surfaces.Verification
Stress-tested locally: 46 consecutive runs of the
EmbeddingModelProvisionerTestsclass and full project all green. Previously reproduced the flake at ~1 in 15 runs, including the distinct in-flight on the response stream.Closes #2071