diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests/TwoNodeClusterHarness.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests/TwoNodeClusterHarness.cs index 9bdb9c59..15175be0 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests/TwoNodeClusterHarness.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests/TwoNodeClusterHarness.cs @@ -456,6 +456,41 @@ public sealed class TwoNodeClusterHarness : IAsyncDisposable $"B up={Akka.Cluster.Cluster.Get(b).State.Members.Count(m => m.Status == MemberStatus.Up)}"); } + /// + /// Stops a node's hosted services, then disposes it. + /// + /// + /// + /// is not optional, and disposal is not a substitute for + /// it. Host.DisposeAsync only disposes the service provider; it never invokes + /// . Akka.Hosting terminates the + /// from that callback, so a disposed-but-never-stopped node + /// leaves a live ActorSystem — remoting transport, dispatcher thread pools and all — + /// rooted for the lifetime of the test process. + /// + /// + /// Every test here starts two of them, so the cost compounded across the suite: a single + /// test host was measured at ~30 GB RSS, which starved timeout-based assertions + /// into false failures that looked exactly like real regressions. + /// + /// + /// The node to stop and dispose. + private static async Task ShutDownAsync(WebApplication node) + { + // Best-effort: a node already stopped (or mid-CoordinatedShutdown from a failover test) + // throws here, and a failure to stop must never mask the test's own assertion failure. + try + { + await node.StopAsync(TimeSpan.FromSeconds(10)); + } + catch + { + // fall through to disposal + } + + await node.DisposeAsync(); + } + private static int AllocateFreePort() { using var listener = new TcpListener(System.Net.IPAddress.Parse(LoopbackHost), 0); @@ -468,8 +503,8 @@ public sealed class TwoNodeClusterHarness : IAsyncDisposable /// Asynchronously disposes both nodes and cleans up the SQL database if used. public async ValueTask DisposeAsync() { - if (NodeB is not null) await NodeB.DisposeAsync(); - if (NodeA is not null) await NodeA.DisposeAsync(); + if (NodeB is not null) await ShutDownAsync(NodeB); + if (NodeA is not null) await ShutDownAsync(NodeA); if (_sqlConnString is not null) { try { await DropSqlDatabaseAsync(_sqlConnString); } diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests.csproj b/tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests.csproj index c678f2e0..9ae4ff79 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests.csproj +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests.csproj @@ -30,4 +30,17 @@ + + + false + false + +