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:
@@ -207,10 +207,58 @@ public static class ServiceCollectionExtensions
|
||||
var mux = system.ActorOf(DependencyMuxActor.Props(), DependencyMuxActorName);
|
||||
registry.Register<DependencyMuxActorKey>(mux);
|
||||
|
||||
// Continuous-historization recorder — gated on ContinuousHistorization:Enabled AND the
|
||||
// gateway-backed IHistorianValueWriter + the durable IHistorizationOutbox being registered
|
||||
// (the Host registers both ONLY when historization is enabled and the ServerHistorian gateway
|
||||
// is configured). The recorder taps the dependency mux's value fan-out, so it is spawned after
|
||||
// (and fed) the same `mux` ref the DriverHostActor uses. It is spawned BEFORE the applier so
|
||||
// the applier's historized-ref subscription sink can wrap this recorder's IActorRef and feed it
|
||||
// the add/remove delta of historized refs on every deploy (closing the T18 ref-feed gap).
|
||||
IActorRef? continuousRecorder = null;
|
||||
var continuousOptions = resolver.GetService<ContinuousHistorizationOptions>();
|
||||
if (continuousOptions is { Enabled: true })
|
||||
{
|
||||
var valueWriter = resolver.GetService<IHistorianValueWriter>();
|
||||
var outbox = resolver.GetService<IHistorizationOutbox>();
|
||||
if (valueWriter is not null && outbox is not null)
|
||||
{
|
||||
// Initial ref set is EMPTY: the deployed address space (and thus the historized-ref
|
||||
// set) is built later at deploy time, not here. The applier's per-deploy add/remove
|
||||
// feed populates the recorder's interest from that point on.
|
||||
continuousRecorder = system.ActorOf(
|
||||
ContinuousHistorizationRecorder.Props(
|
||||
dependencyMux: mux,
|
||||
writer: valueWriter,
|
||||
outbox: outbox,
|
||||
historizedRefs: Array.Empty<string>(),
|
||||
drainBatchSize: continuousOptions.DrainBatchSize,
|
||||
drainInterval: TimeSpan.FromSeconds(continuousOptions.DrainIntervalSeconds),
|
||||
minBackoff: TimeSpan.FromSeconds(continuousOptions.MinBackoffSeconds),
|
||||
maxBackoff: TimeSpan.FromSeconds(continuousOptions.MaxBackoffSeconds)),
|
||||
ContinuousHistorizationRecorderActorName);
|
||||
registry.Register<ContinuousHistorizationRecorderKey>(continuousRecorder);
|
||||
}
|
||||
else
|
||||
{
|
||||
loggerFactory.CreateLogger("ZB.MOM.WW.OtOpcUa.Runtime.ServiceCollectionExtensions")
|
||||
.LogWarning("ContinuousHistorization is enabled but IHistorianValueWriter and/or IHistorizationOutbox are not registered; the recorder will not be spawned. Expected only in misconfigured deployments or test harnesses.");
|
||||
}
|
||||
}
|
||||
|
||||
// Historized-ref subscription sink fed by the applier on every deploy. When the recorder was
|
||||
// spawned, an adapter wraps its IActorRef (a non-blocking Tell of the add/remove delta);
|
||||
// otherwise the Null no-op sink, so the applier behaves identically when historization is off.
|
||||
IHistorizedTagSubscriptionSink historizedSubscriptions = continuousRecorder is not null
|
||||
? new ActorHistorizedTagSubscriptionSink(continuousRecorder)
|
||||
: NullHistorizedTagSubscriptionSink.Instance;
|
||||
|
||||
// OPC UA publish actor — pinned dispatcher, owns the address-space side of the
|
||||
// pipeline. AddressSpaceApplier is constructed here so the actor + applier share the
|
||||
// same sink reference (when DeferredAddressSpaceSink swaps later, both see it).
|
||||
var applier = new AddressSpaceApplier(addressSpaceSink, loggerFactory.CreateLogger<AddressSpaceApplier>());
|
||||
var applier = new AddressSpaceApplier(
|
||||
addressSpaceSink,
|
||||
loggerFactory.CreateLogger<AddressSpaceApplier>(),
|
||||
historizedSubscriptions: historizedSubscriptions);
|
||||
var publishActor = system.ActorOf(
|
||||
OpcUaPublishActor.Props(
|
||||
sink: addressSpaceSink,
|
||||
@@ -247,46 +295,6 @@ public static class ServiceCollectionExtensions
|
||||
HistorianAdapterActor.Props(historianSink, roleInfo.LocalNode),
|
||||
HistorianAdapterActorName);
|
||||
registry.Register<HistorianAdapterActorKey>(historian);
|
||||
|
||||
// Continuous-historization recorder — gated on ContinuousHistorization:Enabled AND the
|
||||
// gateway-backed IHistorianValueWriter + the durable IHistorizationOutbox being registered
|
||||
// (the Host registers both ONLY when historization is enabled and the ServerHistorian gateway
|
||||
// is configured). The recorder taps the dependency mux's value fan-out, so it is spawned after
|
||||
// (and fed) the same `mux` ref the DriverHostActor uses.
|
||||
//
|
||||
// HISTORIZED-REF SET — DOCUMENTED GAP (T18 minimal wiring). The deployed address space (and
|
||||
// thus the set of historized tag refs) is built later at deploy time, not here at actor-spawn
|
||||
// time, so there is no clean ref set to resolve in WithOtOpcUaRuntimeActors. Per the plan, T18
|
||||
// spawns the recorder with an EMPTY initial ref set and registers its key; populating the refs
|
||||
// (a later SetHistorizedRefs feed driven off the deployed composition) is the remaining wiring
|
||||
// and a tracked follow-on. With an empty set the recorder registers interest in nothing and
|
||||
// historizes nothing until that feed lands — the actor + outbox + writer + meters are wired.
|
||||
var continuousOptions = resolver.GetService<ContinuousHistorizationOptions>();
|
||||
if (continuousOptions is { Enabled: true })
|
||||
{
|
||||
var valueWriter = resolver.GetService<IHistorianValueWriter>();
|
||||
var outbox = resolver.GetService<IHistorizationOutbox>();
|
||||
if (valueWriter is not null && outbox is not null)
|
||||
{
|
||||
var recorder = system.ActorOf(
|
||||
ContinuousHistorizationRecorder.Props(
|
||||
dependencyMux: mux,
|
||||
writer: valueWriter,
|
||||
outbox: outbox,
|
||||
historizedRefs: Array.Empty<string>(),
|
||||
drainBatchSize: continuousOptions.DrainBatchSize,
|
||||
drainInterval: TimeSpan.FromSeconds(continuousOptions.DrainIntervalSeconds),
|
||||
minBackoff: TimeSpan.FromSeconds(continuousOptions.MinBackoffSeconds),
|
||||
maxBackoff: TimeSpan.FromSeconds(continuousOptions.MaxBackoffSeconds)),
|
||||
ContinuousHistorizationRecorderActorName);
|
||||
registry.Register<ContinuousHistorizationRecorderKey>(recorder);
|
||||
}
|
||||
else
|
||||
{
|
||||
loggerFactory.CreateLogger("ZB.MOM.WW.OtOpcUa.Runtime.ServiceCollectionExtensions")
|
||||
.LogWarning("ContinuousHistorization is enabled but IHistorianValueWriter and/or IHistorizationOutbox are not registered; the recorder will not be spawned. Expected only in misconfigured deployments or test harnesses.");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return builder;
|
||||
|
||||
Reference in New Issue
Block a user