Skip to content

fix(test): harden LocalArtifactServer disposal against Windows shutdown aborts (#2071) - #2072

Merged
Aaronontheweb merged 3 commits into
devfrom
fix/artifact-server-disposal
Aug 27, 2026
Merged

fix(test): harden LocalArtifactServer disposal against Windows shutdown aborts (#2071)#2072
Aaronontheweb merged 3 commits into
devfrom
fix/artifact-server-disposal

Conversation

@Aaronontheweb

Copy link
Copy Markdown
Collaborator

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 DisposeAsync call and the server's in-flight I/O, both surfaced because DisposeAsync unconditionally awaited the serve loop — rethrowing any fault into the disposing test:

  1. Accept side: Stop()/Close() while a GetContextAsync() is pending aborts the accept. The exception surfaces after IsListening flips to false, making the when (!_listener.IsListening) guard racy so the exception escaped uncaught.
  2. Handle side: an in-flight request aborted by Stop()/Close() throws on the response stream inside HandleAsync, escaping the loop and faulting _serveLoop.

Because each test gets a fresh LocalArtifactServer instance (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-agnostic ArcticInt8... test could be blamed.

Fix

  • Treat any HttpListenerException on the accept side as a graceful stop (drop the racy IsListening gate).
  • Contain response-stream aborts inside HandleAsync.
  • Swallow serve-loop faults in 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 EmbeddingModelProvisionerTests class 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

…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
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)
{
}
Comment thread src/Netclaw.Embeddings.Tests/LocalArtifactServer.cs Fixed
Comment thread src/Netclaw.Embeddings.Tests/LocalArtifactServer.cs Fixed
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.
}
@Aaronontheweb
Aaronontheweb merged commit 18d5247 into dev Aug 27, 2026
35 of 38 checks passed
@Aaronontheweb
Aaronontheweb deleted the fix/artifact-server-disposal branch August 27, 2026 12:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Flaky test: EmbeddingModelProvisionerTests fails on Windows CI (HttpListener I/O aborted)

1 participant