fix(runtime): the native-alarm projector learns a routed client acknowledgement (#641 review)
#641 keeps the projector's prior state across a deploy instead of resetting it to the acked default. The projector never saw a client's ack (the SDK applies it to the node and the host only routes it), so a Clear after ack+deploy now projected Acked=false and un-acknowledged the operator's ack - previously masked by the per-deploy reset. Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
@@ -2018,6 +2018,12 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers, IWithUnboundedS
|
||||
return;
|
||||
}
|
||||
|
||||
// Gitea #641 review — the node is already acknowledged (the SDK applies the client's ack); tell the
|
||||
// projector too, or the next driver Clear projects (Active=false, Acked=prev=false) and un-acknowledges it.
|
||||
// Most sources (MTConnect, FOCAS) send no Acknowledge transition back, and the projector now survives a
|
||||
// deploy rather than resetting to its acked default.
|
||||
_nativeAlarmProjector.MarkAcknowledged(msg.ConditionNodeId);
|
||||
|
||||
if (!_children.TryGetValue(target.DriverInstanceId, out var entry))
|
||||
{
|
||||
_log.Debug("DriverHost {Node}: driver {Driver} not running for alarm ack of {Cond} — dropped",
|
||||
|
||||
@@ -40,6 +40,17 @@ public sealed class NativeAlarmProjector
|
||||
StatusCode: e.QualityStatusCode ?? OpcUaStatus.Good);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gitea #641 review — record a client acknowledgement the node manager applied to <paramref name="nodeId"/>
|
||||
/// and routed toward the driver. The projector otherwise never sees it, and a later Clear would project the
|
||||
/// stale unacknowledged prior. An untracked condition already defaults to acknowledged.
|
||||
/// </summary>
|
||||
/// <param name="nodeId">The acknowledged condition node id.</param>
|
||||
public void MarkAcknowledged(string nodeId)
|
||||
{
|
||||
if (_prior.TryGetValue(nodeId, out var p)) _prior[nodeId] = (p.Active, true);
|
||||
}
|
||||
|
||||
/// <summary>Clears all tracked per-node state.</summary>
|
||||
public void Clear() => _prior.Clear();
|
||||
|
||||
|
||||
+27
@@ -473,6 +473,33 @@ public sealed class DriverHostActorNativeAlarmTests : RuntimeActorTestBase
|
||||
cleared.State.Acknowledged.ShouldBeFalse("nobody acknowledged it");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gitea #641 review — a CLIENT acknowledgement is applied to the node by the SDK and only ROUTED here; the
|
||||
/// projector must learn of it. Before #641 the projector was cleared on every deploy, so a Clear arriving
|
||||
/// after one defaulted Acked to true and happened to be right for a client-acked alarm; now that the
|
||||
/// projector survives the deploy, a projector that never heard the ack projects the Clear as UNacked and
|
||||
/// un-acknowledges the alarm the operator acknowledged.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void A_clear_after_a_client_acknowledge_and_a_redeploy_stays_acknowledged()
|
||||
{
|
||||
var db = NewInMemoryDbFactory();
|
||||
var deploymentId = SeedV3AlarmDeployment(db, RevA, Driver: "drv-1", Tag: "temp_hi");
|
||||
var (actor, publish, coordinator) = SpawnHostAndApplyWithCoordinator(db, deploymentId);
|
||||
|
||||
actor.Tell(Transition(AlarmTransitionKind.Raise));
|
||||
publish.ExpectMsg<OpcUaPublishActor.AlarmStateUpdate>(TimeSpan.FromSeconds(5)).State.Acknowledged.ShouldBeFalse();
|
||||
|
||||
// The client's Acknowledge on the node, routed toward the driver (no transition comes back for it).
|
||||
actor.Tell(new DriverHostActor.RouteNativeAlarmAck(AlarmRawPath, "seen", "op"));
|
||||
Redeploy(db, actor, publish, coordinator);
|
||||
|
||||
actor.Tell(Transition(AlarmTransitionKind.Clear));
|
||||
var cleared = publish.FishForMessage<OpcUaPublishActor.AlarmStateUpdate>(_ => true, TimeSpan.FromSeconds(5));
|
||||
cleared.State.Active.ShouldBeFalse();
|
||||
cleared.State.Acknowledged.ShouldBeTrue("the operator acknowledged it before it cleared");
|
||||
}
|
||||
|
||||
private static DriverInstanceActor.AttributeAlarmPublished Transition(AlarmTransitionKind kind) =>
|
||||
new("drv-1", new AlarmEventArgs(
|
||||
new StubAlarmHandle(), "Temp", AlarmRawPath, "OffNormalAlarm", kind.ToString(), AlarmSeverity.High, Ts,
|
||||
|
||||
Reference in New Issue
Block a user