fix(historian-sidecar): close active TCP client on cancel so RunAsync unwinds
v2-ci / build (push) Failing after 33s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests) (push) Has been skipped
v2-ci / build (push) Failing after 33s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests) (push) Has been skipped
The net48 sidecar's TcpFrameServer.RunOneConnectionAsync registered the cancellation token to Stop() only the listener (to unblock a parked AcceptTcpClientAsync), but never closed the active client. On net48 NetworkStream.ReadAsync ignores the CancellationToken, so while the frame loop is parked reading an idle connected client, cancelling the token cannot unblock it — only closing the socket can. RunAsync therefore never returned on Ctrl-C/service-stop while a connection was open (Program.Main's RunAsync().GetAwaiter().GetResult() would hang until NSSM force-killed). Register the cancel to Close() the active client, and convert the resulting cancel-time read/handshake exception to OperationCanceledException so RunAsync unwinds cleanly without logging it as a connection failure or counting it toward MaxConsecutiveFailures. Caught by the first-ever net48 execution of TcpRoundTripTests on the Windows VM (these only compile on macOS): SingleActive_SecondClientHelloCompletesOnly AfterFirstCloses deadlocked in teardown. Full net48 historian suite now green (122 passed, 0 failed, 2 skipped); all 6 TcpRoundTrip tests pass.
This commit is contained in:
@@ -81,6 +81,13 @@ public sealed class TcpFrameServer : IDisposable
|
||||
|
||||
using (client)
|
||||
{
|
||||
// net48's NetworkStream.ReadAsync ignores the CancellationToken, so cancelling the
|
||||
// token alone cannot unblock the frame loop when it's parked reading an idle client —
|
||||
// only closing the socket does. Register the cancel to Close() the active client so
|
||||
// RunAsync actually unwinds on shutdown (mirrors the listener.Stop() above that
|
||||
// unblocks a parked AcceptTcpClientAsync). Without this, RunAsync().GetAwaiter() in
|
||||
// Program.Main never returns on Ctrl-C/service-stop while a connection is open.
|
||||
using var clientReg = linked.Token.Register(() => { try { client.Close(); } catch { /* ignore */ } });
|
||||
client.NoDelay = true;
|
||||
Stream stream = client.GetStream();
|
||||
SslStream? ssl = null;
|
||||
@@ -129,6 +136,14 @@ public sealed class TcpFrameServer : IDisposable
|
||||
await handler.HandleAsync(frame.Value.Kind, frame.Value.Body, writer, linked.Token).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch (Exception) when (linked.Token.IsCancellationRequested)
|
||||
{
|
||||
// The clientReg cancel callback closed the socket mid-read/handshake (net48 read
|
||||
// doesn't observe the token); surface it as cancellation so RunAsync's
|
||||
// OperationCanceledException path unwinds cleanly instead of logging a connection
|
||||
// failure and counting it toward MaxConsecutiveFailures.
|
||||
throw new OperationCanceledException(linked.Token);
|
||||
}
|
||||
finally { ssl?.Dispose(); }
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user