fix(opcuaclient): #676 live gate A1 — a sustained closed connection is a definitive loss
Live gate A1 (docker-dev, opc-plc restart / SIGINT / kill): the fast path never fired — a real process stop delivers no session status; the keep-alives report BadSecureChannelClosed (0x80860000) once, then BadConnectionClosed (0x80AE0000) interleaved with BadNoCommunication for the whole outage. Reproduced in-process with TcpStallProxy.HardClose (reset every connection, refuse new ones; server and session alive behind it): the identical sequence, and the SDK 1.5.378 session never opens a new connection on its own (48 s with the proxy accepting again, zero attempts) — a closed channel cannot recover in place. The driver now treats BadSecureChannelClosed/BadConnectionClosed as definitive on the SECOND closed-channel keep-alive failure in one outage (a good keep-alive resets the count), raising ConnectionLost ~1 keep-alive after the drop. BadNoCommunication / BadRequestTimeout / BadNotConnected (stall/pause) stay with the watchdog; the stall test still sees no ConnectionLost. New wire test (hard close + reopen ⇒ raised once, SDK never reconnects, reinit brings the identical value back Good) and unit tests (sustained raises once; a single blip that recovers does not) — all red on the previous code. Docs updated.
This commit is contained in:
+48
-2
@@ -105,11 +105,9 @@ public sealed class OpcUaClientConnectionLossSignalTests
|
||||
public static TheoryData<uint> AmbiguousStatuses =>
|
||||
[
|
||||
StatusCodes.BadNoCommunication,
|
||||
StatusCodes.BadConnectionClosed,
|
||||
StatusCodes.BadNotConnected,
|
||||
StatusCodes.BadRequestTimeout,
|
||||
StatusCodes.BadTimeout,
|
||||
StatusCodes.BadSecureChannelClosed,
|
||||
];
|
||||
|
||||
[Theory]
|
||||
@@ -153,6 +151,54 @@ public sealed class OpcUaClientConnectionLossSignalTests
|
||||
raised.ShouldBe(0, "an ambiguous loss may yet resume in place — it belongs to the #527 watchdog");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// #676 live gate A1: a real upstream process stop (docker restart / SIGINT / docker kill) never produced a
|
||||
/// session status — the keep-alives carried <c>BadSecureChannelClosed</c> once, then <c>BadConnectionClosed</c>
|
||||
/// (interleaved with <c>BadNoCommunication</c>) for the whole outage. Measured in-process (hard-closing proxy, the
|
||||
/// server and its session still alive): the SDK 1.5.378 session NEVER opens a new connection on its own, so a
|
||||
/// closed channel is dead for good. Sustained — a second closed-channel failure in the same outage — it raises.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void A_sustained_closed_channel_raises_ConnectionLost_once()
|
||||
{
|
||||
var (drv, _, _, _) = NewConnectedDriver();
|
||||
using var _ = drv;
|
||||
var raised = new List<string>();
|
||||
drv.ConnectionLost += (_, e) => raised.Add(e.Reason);
|
||||
|
||||
drv.RaiseKeepAliveForTest(StatusCodes.BadSecureChannelClosed); // the live sequence …
|
||||
raised.ShouldBeEmpty("one closed-channel failure may be a transient");
|
||||
drv.RaiseKeepAliveForTest(StatusCodes.BadNoCommunication);
|
||||
drv.RaiseKeepAliveForTest(StatusCodes.BadConnectionClosed); // … sustained
|
||||
drv.RaiseKeepAliveForTest(StatusCodes.BadNoCommunication);
|
||||
drv.RaiseKeepAliveForTest(StatusCodes.BadConnectionClosed);
|
||||
|
||||
raised.ShouldHaveSingleItem().ShouldContain("BadConnectionClosed");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0x80860000u)] // BadSecureChannelClosed
|
||||
[InlineData(0x80AE0000u)] // BadConnectionClosed
|
||||
public void A_single_closed_channel_blip_that_recovers_does_not_raise(uint closed)
|
||||
{
|
||||
var (drv, _, _, _) = NewConnectedDriver();
|
||||
using var _ = drv;
|
||||
var raised = 0;
|
||||
drv.ConnectionLost += (_, _) => raised++;
|
||||
|
||||
drv.RaiseKeepAliveForTest(closed);
|
||||
drv.RaiseKeepAliveForTest(StatusCodes.BadNoCommunication);
|
||||
drv.RaiseKeepAliveForTest(StatusCodes.Good); // recovered — the count starts again
|
||||
drv.RaiseKeepAliveForTest(closed);
|
||||
drv.RaiseKeepAliveForTest(StatusCodes.Good);
|
||||
|
||||
raised.ShouldBe(0);
|
||||
// Positive control: the same driver DOES raise once a closed channel is sustained.
|
||||
drv.RaiseKeepAliveForTest(closed);
|
||||
drv.RaiseKeepAliveForTest(closed);
|
||||
raised.ShouldBe(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void An_ambiguous_outage_that_turns_definitive_raises_once()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user