fix(v3-batch4): reassert skips historian re-record + scripted-alarm redeploy recovery (reassert review M1/M2/LOW-3)
M1 (MEDIUM): the VirtualTag re-assert re-published a stale last value with a fresh deploy-time timestamp, and VirtualTagHostActor.OnResult recorded it to the IHistoryWriter for Historize=true plans — every deploy would append an artificial historian sample (BadInternalError if the last state was Bad) that never corresponded to a real evaluation. Inert today (NullHistoryWriter) but a data-quality bug once a VT history sink binds. Fix: EvaluationResult carries an IsReassert flag (default false), set true in VirtualTagActor.OnReassertValue; OnResult still PUBLISHES the AttributeValueUpdate (to repair the reset node) but SKIPS _history.Record when IsReassert. Regression test VirtualTagHostActorTests.Reassert_of_a_historized_vtag_publishes_but_does_not_re_record_to_the_historian (fails before — count=2 — passes after: count stays 1). LOW-3: corrected the ordering comments in VirtualTagActor.OnReassertValue and VirtualTagHostActor.OnApply. ApplyVirtualTags goes to the VirtualTag HOST, not the publish actor; the ordering holds because the re-assert reaches the publish actor via a multi-hop chain (host -> child ReassertValue -> child -> parent EvaluationResult -> OnResult -> publish actor) and thus lands AFTER RebuildAddressSpace in the shared publish actor's FIFO mailbox. M2 (CONFIRMED REAL — reported as scoped follow-up, not fixed here): the same redeploy-reset race is latent for Part 9 scripted-alarm condition nodes. A full-rebuild deploy clears + re-materialises them fresh (OtOpcUaNodeManager.RebuildAddressSpace clears _alarmConditions @2160; MaterialiseAlarmCondition recreates normal state), but the engine reload does NOT re-emit an unchanged-active condition: ScriptedAlarmEngine.LoadAsync -> EvaluatePredicateToStateAsync (@546-552) computes ApplyPredicate(seed, true) where seed is the persisted state from the DB-backed EfAlarmConditionStateStore, yielding EmissionKind.None, which ScriptedAlarmHostActor.OnEngineEmission (@292) filters. Net: an active alarm with static dependencies under-reports until its next real transition on a full-rebuild deploy. The fix is larger/riskier than the VT case (touches the Core engine emission contract and must publish ONLY the OPC UA node state, NOT the alerts topic, to avoid duplicate AVEVA history rows — the alarm analogue of M1), so per review guidance it is deferred as a scoped follow-up rather than forced into this commit. Claude-Session: https://claude.ai/code/session_01LVneM3eh1UtJxEisFXgmox
This commit is contained in:
@@ -519,6 +519,41 @@ public sealed class VirtualTagHostActorTests : RuntimeActorTestBase
|
||||
mux.ExpectNoMsg(TimeSpan.FromMilliseconds(200));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// M1 (reassert review): a re-assert must PUBLISH (to repair the reset OPC UA node) but must NOT
|
||||
/// re-record to the historian. Otherwise every deploy appends an artificial historian sample carrying a
|
||||
/// stale value with a fresh deploy-time timestamp — a data-quality bug the moment a VT history sink is
|
||||
/// bound. Historize=true survivor: first real evaluation records exactly once; the redeploy's re-assert
|
||||
/// publishes a second time but the historian count stays 1.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Reassert_of_a_historized_vtag_publishes_but_does_not_re_record_to_the_historian()
|
||||
{
|
||||
var publish = CreateTestProbe();
|
||||
var mux = CreateTestProbe();
|
||||
var writer = new CapturingHistoryWriter();
|
||||
var host = Sys.ActorOf(VirtualTagHostActor.Props(publish.Ref, mux.Ref, new ConstOkEvaluator(7.0), writer));
|
||||
|
||||
var plan = new[] { PlanH("vt-1", "eq-1", "speed-rpm", historize: true) };
|
||||
host.Tell(new VirtualTagHostActor.ApplyVirtualTags(plan));
|
||||
|
||||
mux.ExpectMsg<DependencyMuxActor.RegisterInterest>();
|
||||
var child = mux.LastSender;
|
||||
|
||||
// First real evaluation → Good publish + exactly one historian record.
|
||||
child.Tell(new VirtualTagActor.DependencyValueChanged("a", 1, DateTime.UtcNow));
|
||||
publish.ExpectMsg<OpcUaPublishActor.AttributeValueUpdate>();
|
||||
AwaitAssert(() => writer.Calls.Count.ShouldBe(1));
|
||||
|
||||
// Redeploy (unchanged plan) → the survivor re-asserts: a SECOND publish repairs the reset node.
|
||||
host.Tell(new VirtualTagHostActor.ApplyVirtualTags(plan));
|
||||
publish.ExpectMsg<OpcUaPublishActor.AttributeValueUpdate>();
|
||||
|
||||
// The publish landing means the reassert's OnResult turn is complete (Record runs on the same turn,
|
||||
// right after the Tell) — so a second Record would already be visible. It must NOT be: still 1.
|
||||
writer.Calls.Count.ShouldBe(1);
|
||||
}
|
||||
|
||||
/// <summary>Evaluator that always returns <c>Ok(value)</c> — a static-output VirtualTag whose recompute
|
||||
/// is dedup-suppressed after the first publish, so the re-assert path is the only way its reset node
|
||||
/// recovers.</summary>
|
||||
|
||||
Reference in New Issue
Block a user