From f63f20f69ea7c27ca13fd70d6a7edf6650aa5675 Mon Sep 17 00:00:00 2001 From: Simon Cropp Date: Tue, 11 Aug 2026 13:08:33 +1000 Subject: [PATCH] Give the socket tests a thread pool that can answer them AttachedViewerTests failed intermittently on CI with "No response for Inline", on a different platform and a different test each run, and never locally. ServerFixture drives the real ViewerClient, whose send is synchronous, so a test holds a pool thread for the whole exchange while the server needs threads of its own to accept the connection and answer it. Two per exchange, against the four or so a runner starts with. Enough of them at once and the answer waits on the pool adding roughly one thread a second, past the client's three second timeout. Confirmed rather than assumed. With DOTNET_ThreadPool_ForceMaxWorkerThreads=2 the whole suite fails that way four runs out of four, while the class on its own passes nine out of nine, which is the tell: it needs the rest of the suite competing for the pool. With the floor forced to one and the maximum left alone, the intermittent shape comes back exactly as CI shows it. A send that answers in 26ms unstarved took 516ms with the pool merely busy. Raising the floor removes the scarcity rather than the blocking. The blocking is real, but it belongs to the client the tray and an attached viewer use, and nothing outside a test host runs dozens of those at once. Five runs of the suite under the conditions that used to fail now pass. --- .../ModuleInitializer.cs | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/DiffEngineViewer.Tests/ModuleInitializer.cs b/src/DiffEngineViewer.Tests/ModuleInitializer.cs index 1dd21197..e9e334d1 100644 --- a/src/DiffEngineViewer.Tests/ModuleInitializer.cs +++ b/src/DiffEngineViewer.Tests/ModuleInitializer.cs @@ -11,5 +11,29 @@ public static void Initialize() // Before anything can touch DiffTools, which resolves and caches on first use. Only points // the resolver at the local build; nothing here launches anything. ManualViewer.Register(); + RaiseThreadPoolFloor(); + } + + /// + /// The socket tests need two pool threads per exchange, and a CI runner starts with about four + /// in total. + /// + /// drives the real ViewerClient, whose send is synchronous, + /// so a test blocks a pool thread for the whole exchange while the server needs threads of its + /// own to accept the connection and answer it. Run enough of those at once on a four core + /// runner and the answer waits on the pool's hill climb, which adds roughly one thread a + /// second, past the client's three second timeout. That surfaced as an intermittent "No + /// response for Inline", on a different platform and a different test each run. + /// + /// + /// Raising the floor removes the scarcity rather than the blocking. The blocking is real, but + /// it belongs to the client the tray and an attached viewer actually use, and no process runs + /// dozens of those at once — a test host is the only thing that does. + /// + /// + static void RaiseThreadPoolFloor() + { + ThreadPool.GetMinThreads(out var workers, out var completionPorts); + ThreadPool.SetMinThreads(Math.Max(workers, 32), completionPorts); } }