fix(runtime): #869 review round 3 — key a native-ack revert to the ack that failed
A pending local ack can outlive its own delivery (sources that send no Acknowledge back; a Clear now only carries it through), so a later failed re-ack — or a stale Galaxy confirmation timing out after the alarm re-occurred and was acked again — could revert an ack it does not own. MarkAcknowledged now issues a sequence (0 when the ack changed nothing), carried on RouteAlarmAck and echoed on AlarmAckFailed; TryRevertAcknowledge reverts only on a matching sequence. Claude-Session: https://claude.ai/code/session_01GT67AD99zQQqpLCVm3p7xk
This commit is contained in:
+19
@@ -83,6 +83,25 @@ public sealed class DriverInstanceActorAckConfirmationTests : RuntimeActorTestBa
|
||||
router.ExpectMsg<DriverInstanceActor.AlarmAckFailed>(Timeout, cancellationToken: TestContext.Current.CancellationToken).ConditionNodeId.ShouldBe("n-bad");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// lmxopcua#869 review — the failure echoes the routed ack's <see cref="DriverInstanceActor.RouteAlarmAck.AckSequence"/>,
|
||||
/// which is what lets the host revert only the acknowledge that failed (keyed in
|
||||
/// <c>NativeAlarmProjector.TryRevertAcknowledge(nodeId, ackSequence, …)</c>).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void A_failed_confirmation_echoes_the_ack_sequence()
|
||||
{
|
||||
var driver = new ConfirmingDriver();
|
||||
var actor = SpawnConnected(driver);
|
||||
driver.Confirmation = () => Task.FromException(new DriverOperationRefusedException("unconfirmed"));
|
||||
var router = CreateTestProbe();
|
||||
|
||||
actor.Tell(Ack("n-seq") with { AckSequence = 42 }, router.Ref);
|
||||
|
||||
router.ExpectMsg<DriverInstanceActor.AlarmAckFailed>(Timeout, cancellationToken: TestContext.Current.CancellationToken)
|
||||
.AckSequence.ShouldBe(42L);
|
||||
}
|
||||
|
||||
private static DriverInstanceActor.RouteAlarmAck Ack(string conditionNodeId) =>
|
||||
new("Tank.Hi", "c", "alice", ConditionNodeId: conditionNodeId);
|
||||
|
||||
|
||||
@@ -630,6 +630,32 @@ public class NativeAlarmProjectorTests
|
||||
sut.TryRevertAcknowledge("n1", out _).ShouldBeFalse("an Acknowledge transition states the ack — authoritative");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// lmxopcua#869 review — a pending ack can outlive its own delivery (a source that sends no Acknowledge back,
|
||||
/// across a Clear that only carried it), so a revert is keyed to the failed ack's sequence: a later re-ack that
|
||||
/// changed nothing (sequence 0) or a stale earlier ack's failure does not undo it. Positive control: the
|
||||
/// matching sequence does.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void A_revert_only_undoes_the_acknowledge_that_failed()
|
||||
{
|
||||
var sut = new NativeAlarmProjector();
|
||||
sut.Project("n1", Evt(AlarmTransitionKind.Raise));
|
||||
var first = sut.MarkAcknowledged("n1");
|
||||
first.ShouldBeGreaterThan(0);
|
||||
sut.Project("n1", Evt(AlarmTransitionKind.Clear)); // first ack delivered; nothing settles it
|
||||
|
||||
var reAck = sut.MarkAcknowledged("n1"); // a re-ack of the acked, cleared condition
|
||||
reAck.ShouldBe(0L, "it changed nothing, so its failure has nothing to undo");
|
||||
sut.TryRevertAcknowledge("n1", reAck, out _).ShouldBeFalse("a later re-ack's failure must not revert the first");
|
||||
|
||||
sut.Project("n1", Evt(AlarmTransitionKind.Raise, msg: "again")); // a new occurrence (un-acks)
|
||||
var second = sut.MarkAcknowledged("n1");
|
||||
sut.TryRevertAcknowledge("n1", first, out _).ShouldBeFalse("a stale earlier ack's failure must not revert the newer one");
|
||||
sut.TryRevertAcknowledge("n1", second, out var reverted).ShouldBeTrue();
|
||||
reverted!.Acknowledged.ShouldBeFalse();
|
||||
}
|
||||
|
||||
private static AlarmEventArgs Evt(
|
||||
AlarmTransitionKind kind,
|
||||
AlarmSeverity sev = AlarmSeverity.High,
|
||||
|
||||
Reference in New Issue
Block a user