fix(historian-gateway): historize EVERY aliased tag sharing a mux ref (FU-3 review — close silent-drop)
Code review found a residual silent-data-loss path: a single driver ref (mux ref) can back SEVERAL historized equipment tags via aliasing (identical machines sharing a register — DriverHostActor._nodeIdByDriverRef is a HashSet), each with its own HistorianTagname. The muxRef->single-name map collapsed last-wins, so under alias + divergent overrides only one historian tag got the value and the rest were silently dropped — the exact failure class FU-3 exists to eliminate. Model the fan as muxRef -> HashSet<historianName> and append ONE outbox entry per name in OnValueChangedAsync (a per-name append failure drops only that name and continues). Convergence removes/adds each (muxRef, name) pair individually from the per-ref set, dropping the mux key only when its last name is removed — so removing one alias leaves the shared ref fanning for the others with no mux churn. Tests: aliased-refs-each-get-the-value (one fan → both historian names written), removing-one-alias-keeps-the-ref-registered, and the override-rename test now feeds a value post-rename to prove the write target actually moved to the new name. Runtime 350/0, OpcUaServer 327/0; 0 warnings. Claude-Session: https://claude.ai/code/session_012SDSQ3AcaXqPcBtDESBRii
This commit is contained in:
+72
-2
@@ -143,15 +143,16 @@ public sealed class ContinuousHistorizationRecorderTests : TestKit
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Override_rename_with_same_mux_ref_updates_target_without_mux_churn()
|
||||
public async Task Override_rename_with_same_mux_ref_updates_target_without_mux_churn()
|
||||
{
|
||||
// An override changing while the driver FullName (mux ref) stays the same: removed+added carry the
|
||||
// SAME mux ref with different historian names. The recorder must update the write target but NOT
|
||||
// re-register the mux (the fanned key-set is unchanged).
|
||||
var mux = CreateTestProbe();
|
||||
var writer = new FakeValueWriter { Succeed = true };
|
||||
|
||||
var rec = Sys.ActorOf(ContinuousHistorizationRecorder.Props(
|
||||
mux.Ref, new FakeValueWriter(), new InMemoryOutbox(), historizedRefs: Array.Empty<string>()));
|
||||
mux.Ref, writer, new InMemoryOutbox(), historizedRefs: Array.Empty<string>()));
|
||||
mux.ExpectMsg<DependencyMuxActor.RegisterInterest>(); // PreStart (empty)
|
||||
|
||||
rec.Tell(new ContinuousHistorizationRecorder.UpdateHistorizedRefs(
|
||||
@@ -164,6 +165,75 @@ public sealed class ContinuousHistorizationRecorderTests : TestKit
|
||||
new[] { new HistorizedTagRef("Area/Pump1", "HIST.New") },
|
||||
new[] { new HistorizedTagRef("Area/Pump1", "HIST.Old") }));
|
||||
mux.ExpectNoMsg(TimeSpan.FromMilliseconds(300));
|
||||
|
||||
// ...and the WRITE TARGET actually updated: a value now historizes under HIST.New, never HIST.Old.
|
||||
rec.Tell(new VirtualTagActor.DependencyValueChanged("Area/Pump1", 9.0, DateTime.UtcNow));
|
||||
await AwaitAssertAsync(() =>
|
||||
Assert.Contains(writer.Snapshot(), w => w.Tag == "HIST.New" && w.Value == 9.0));
|
||||
Assert.DoesNotContain(writer.Snapshot(), w => w.Tag == "HIST.Old");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Aliased_refs_with_distinct_overrides_each_get_the_value()
|
||||
{
|
||||
// Two historized tags share ONE driver ref ("Area/Pump1" — identical machines sharing a register),
|
||||
// each with its OWN HistorianTagname. The mux fans a SINGLE value for the shared ref; BOTH historian
|
||||
// tags must be written. A muxRef→single-name map would silently drop one (the FU-3 review's Critical).
|
||||
var mux = CreateTestProbe();
|
||||
var writer = new FakeValueWriter { Succeed = true };
|
||||
var outbox = new InMemoryOutbox();
|
||||
|
||||
var rec = Sys.ActorOf(ContinuousHistorizationRecorder.Props(
|
||||
mux.Ref, writer, outbox, historizedRefs: Array.Empty<string>()));
|
||||
mux.ExpectMsg<DependencyMuxActor.RegisterInterest>(); // PreStart (empty)
|
||||
|
||||
rec.Tell(new ContinuousHistorizationRecorder.UpdateHistorizedRefs(
|
||||
new[]
|
||||
{
|
||||
new HistorizedTagRef("Area/Pump1", "HIST.MachineA.Temp"),
|
||||
new HistorizedTagRef("Area/Pump1", "HIST.MachineB.Temp"),
|
||||
},
|
||||
Array.Empty<HistorizedTagRef>()));
|
||||
|
||||
// ONE mux ref is registered (both aliases share it).
|
||||
var reg = mux.ExpectMsg<DependencyMuxActor.RegisterInterest>();
|
||||
Assert.Equal(new[] { "Area/Pump1" }, reg.TagRefs);
|
||||
|
||||
rec.Tell(new VirtualTagActor.DependencyValueChanged("Area/Pump1", 71.0, DateTime.UtcNow));
|
||||
|
||||
// BOTH historian names receive the single fanned value.
|
||||
await AwaitAssertAsync(() =>
|
||||
{
|
||||
var snap = writer.Snapshot();
|
||||
Assert.Contains(snap, w => w.Tag == "HIST.MachineA.Temp" && w.Value == 71.0);
|
||||
Assert.Contains(snap, w => w.Tag == "HIST.MachineB.Temp" && w.Value == 71.0);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Removing_one_alias_keeps_the_shared_mux_ref_registered_for_the_other()
|
||||
{
|
||||
// Removing ONE alias's historian name from a shared mux ref must NOT drop the mux registration —
|
||||
// the ref is still fanned for the surviving alias (key-set unchanged → no mux churn).
|
||||
var mux = CreateTestProbe();
|
||||
|
||||
var rec = Sys.ActorOf(ContinuousHistorizationRecorder.Props(
|
||||
mux.Ref, new FakeValueWriter(), new InMemoryOutbox(), historizedRefs: Array.Empty<string>()));
|
||||
mux.ExpectMsg<DependencyMuxActor.RegisterInterest>(); // PreStart (empty)
|
||||
|
||||
rec.Tell(new ContinuousHistorizationRecorder.UpdateHistorizedRefs(
|
||||
new[]
|
||||
{
|
||||
new HistorizedTagRef("Area/Pump1", "HIST.A"),
|
||||
new HistorizedTagRef("Area/Pump1", "HIST.B"),
|
||||
},
|
||||
Array.Empty<HistorizedTagRef>()));
|
||||
mux.ExpectMsg<DependencyMuxActor.RegisterInterest>(); // {Area/Pump1}
|
||||
|
||||
// Drop only HIST.A — Area/Pump1 still fans for HIST.B, so the key-set is unchanged: no mux churn.
|
||||
rec.Tell(new ContinuousHistorizationRecorder.UpdateHistorizedRefs(
|
||||
Array.Empty<HistorizedTagRef>(), new[] { new HistorizedTagRef("Area/Pump1", "HIST.A") }));
|
||||
mux.ExpectNoMsg(TimeSpan.FromMilliseconds(300));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
Reference in New Issue
Block a user