feat(historian-gateway): feed historized refs to the recorder on deploy (close continuous-historization ref-feed gap)
v2-ci / build (pull_request) Failing after 39s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (pull_request) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (pull_request) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (pull_request) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (pull_request) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (pull_request) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests) (pull_request) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests) (pull_request) Has been skipped
v2-ci / build (pull_request) Failing after 39s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (pull_request) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (pull_request) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (pull_request) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (pull_request) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (pull_request) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests) (pull_request) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests) (pull_request) Has been skipped
The ContinuousHistorizationRecorder was spawned with an EMPTY historized-ref set, so it registered interest in nothing and historized nothing. This feeds it the currently-historized tag refs on every address-space deploy/redeploy so its DependencyMuxActor interest converges to exactly the historized set (the same refs the EnsureTags provisioning hook resolves: override-or-FullName). Design — delta convergence (the plan is a pure DIFF): - New seam IHistorizedTagSubscriptionSink (Core.Abstractions/Historian) with a Null no-op singleton, mirroring how IHistorianProvisioning decouples the T15 hook. AddressSpaceApplier gains a DEFAULTED ctor param (Null sink) so all ~80 existing call sites + the production site compile unchanged. - Apply() only ever sees a plan diff (an incremental/surgical apply carries a delta, not the full set), so the applier feeds an add/remove DELTA computed from AddedEquipmentTags / RemovedEquipmentTags / ChangedEquipmentTags. The recorder keeps the full set and re-registers it. The feed is a single non-blocking Tell behind the sink, wrapped in try/catch so a faulting feed never blocks or breaks a deploy (same discipline as the provisioning hook). - Recorder.UpdateHistorizedRefs(added, removed) converges the tracked set, then — only when it actually changed — sends ONE RegisterInterest with the full set (the mux's RegisterInterest is a full-REPLACE) or one UnregisterInterest when it drains to empty (the mux has no per-ref unregister). An unchanged delta is a no-op (no mux churn). - DI: the recorder is now spawned BEFORE the applier so the adapter (ActorHistorizedTagSubscriptionSink) can wrap its IActorRef; the Null sink is used when continuous historization is off/unwired. Tests: recorder convergence (add-from-empty, add+remove converge, idempotent, drain-to-empty unregisters); applier feeds resolved added refs, removed+renamed deltas, and survives a throwing sink. Build clean (0 warnings on touched projects); Runtime/OpcUaServer/Gateway/AdminUI suites green. Claude-Session: https://claude.ai/code/session_012SDSQ3AcaXqPcBtDESBRii
This commit is contained in:
+70
@@ -36,6 +36,76 @@ public sealed class ContinuousHistorizationRecorderTests : TestKit
|
||||
Assert.Contains("Pump1.Temp", reg.TagRefs);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdateHistorizedRefs_from_empty_registers_the_added_refs()
|
||||
{
|
||||
var mux = CreateTestProbe();
|
||||
|
||||
var rec = Sys.ActorOf(ContinuousHistorizationRecorder.Props(
|
||||
mux.Ref, new FakeValueWriter(), new InMemoryOutbox(), historizedRefs: Array.Empty<string>()));
|
||||
|
||||
// PreStart registers the (empty) initial set first.
|
||||
var initial = mux.ExpectMsg<DependencyMuxActor.RegisterInterest>();
|
||||
Assert.Empty(initial.TagRefs);
|
||||
|
||||
rec.Tell(new ContinuousHistorizationRecorder.UpdateHistorizedRefs(
|
||||
new[] { "Pump1.Temp", "Pump2.Flow" }, Array.Empty<string>()));
|
||||
|
||||
var reg = mux.ExpectMsg<DependencyMuxActor.RegisterInterest>();
|
||||
Assert.Equal(new[] { "Pump1.Temp", "Pump2.Flow" }, reg.TagRefs.OrderBy(x => x, StringComparer.Ordinal));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdateHistorizedRefs_converges_adding_and_removing()
|
||||
{
|
||||
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[] { "A", "B" }, Array.Empty<string>()));
|
||||
var first = mux.ExpectMsg<DependencyMuxActor.RegisterInterest>();
|
||||
Assert.Equal(new[] { "A", "B" }, first.TagRefs.OrderBy(x => x, StringComparer.Ordinal));
|
||||
|
||||
// Add "C", remove "A" → converge to {B, C}. The mux's RegisterInterest is a full-REPLACE, so the
|
||||
// recorder sends ONE RegisterInterest carrying exactly the converged set (C registered, A gone).
|
||||
rec.Tell(new ContinuousHistorizationRecorder.UpdateHistorizedRefs(new[] { "C" }, new[] { "A" }));
|
||||
var second = mux.ExpectMsg<DependencyMuxActor.RegisterInterest>();
|
||||
Assert.Equal(new[] { "B", "C" }, second.TagRefs.OrderBy(x => x, StringComparer.Ordinal));
|
||||
Assert.DoesNotContain("A", second.TagRefs);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdateHistorizedRefs_is_idempotent_when_the_set_is_unchanged()
|
||||
{
|
||||
var mux = CreateTestProbe();
|
||||
|
||||
var rec = Sys.ActorOf(ContinuousHistorizationRecorder.Props(
|
||||
mux.Ref, new FakeValueWriter(), new InMemoryOutbox(), historizedRefs: new[] { "A", "B" }));
|
||||
var initial = mux.ExpectMsg<DependencyMuxActor.RegisterInterest>(); // PreStart {A, B}
|
||||
Assert.Equal(new[] { "A", "B" }, initial.TagRefs.OrderBy(x => x, StringComparer.Ordinal));
|
||||
|
||||
// A delta with no net effect (add an already-present ref, remove an absent one) → no mux churn.
|
||||
rec.Tell(new ContinuousHistorizationRecorder.UpdateHistorizedRefs(new[] { "A" }, new[] { "Z" }));
|
||||
mux.ExpectNoMsg(TimeSpan.FromMilliseconds(300));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdateHistorizedRefs_draining_to_empty_unregisters_all_interest()
|
||||
{
|
||||
var mux = CreateTestProbe();
|
||||
|
||||
var rec = Sys.ActorOf(ContinuousHistorizationRecorder.Props(
|
||||
mux.Ref, new FakeValueWriter(), new InMemoryOutbox(), historizedRefs: new[] { "A" }));
|
||||
mux.ExpectMsg<DependencyMuxActor.RegisterInterest>(); // PreStart {A}
|
||||
|
||||
// The mux has no per-ref unregister; an empty converged set drops ALL interest in one message.
|
||||
rec.Tell(new ContinuousHistorizationRecorder.UpdateHistorizedRefs(Array.Empty<string>(), new[] { "A" }));
|
||||
mux.ExpectMsg<DependencyMuxActor.UnregisterInterest>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DependencyValueChanged_appends_to_outbox_then_drains_to_writer()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user