feat(historian-gateway): wire ContinuousHistorizationRecorder into DI + hosted lifecycle + meters

Bind ContinuousHistorizationOptions (Enabled/OutboxPath/CommitMode/
CommitIntervalMs/DrainBatchSize/DrainIntervalSeconds/Capacity/backoff) with a
warn-only Validate(); gated on Enabled AND the ServerHistorian gateway being
configured, the Host registers the durable FasterLogHistorizationOutbox (container
-disposed) + a gateway-backed GatewayHistorianValueWriter, and binds outbox
depth/dropped observable gauges on the central scraped meter. WithOtOpcUaRuntimeActors
spawns the recorder (over the same dependency-mux ref) when the options + writer +
outbox resolve, registering ContinuousHistorizationRecorderKey. Spawned with an EMPTY
historized-ref set: the deployed address space builds later, so ref population is a
documented follow-on (a later SetHistorizedRefs feed) — T18 wires the actor + outbox
+ writer + meters; the ref feed is the known remaining gap.

Claude-Session: https://claude.ai/code/session_012SDSQ3AcaXqPcBtDESBRii
This commit is contained in:
Joseph Doherty
2026-06-26 18:47:20 -04:00
parent 97528c500f
commit 2a5c717755
6 changed files with 384 additions and 0 deletions
@@ -11,6 +11,7 @@ using ZB.MOM.WW.OtOpcUa.Commons.Interfaces;
using ZB.MOM.WW.OtOpcUa.Commons.OpcUa;
using ZB.MOM.WW.OtOpcUa.Configuration;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions.Historian;
using ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian;
using ZB.MOM.WW.OtOpcUa.Core.Scripting;
using ZB.MOM.WW.OtOpcUa.Core.VirtualTags;
@@ -33,6 +34,7 @@ public static class ServiceCollectionExtensions
public const string DependencyMuxActorName = "dependency-mux";
public const string OpcUaPublishActorName = "opcua-publish";
public const string PeerProbeSupervisorName = "peer-probe-supervisor";
public const string ContinuousHistorizationRecorderActorName = "continuous-historization-recorder";
/// <summary>
/// Registers shared runtime services. Currently binds <see cref="IAlarmHistorianSink"/>
@@ -245,6 +247,46 @@ 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;
@@ -258,5 +300,9 @@ public sealed class HistorianAdapterActorKey { }
public sealed class DependencyMuxActorKey { }
public sealed class OpcUaPublishActorKey { }
/// <summary>Marker key for the per-node ContinuousHistorizationRecorder (spawned only when
/// <c>ContinuousHistorization:Enabled=true</c> and the gateway value-writer + outbox are registered).</summary>
public sealed class ContinuousHistorizationRecorderKey { }
/// <summary>Marker key for the per-node PeerProbeSupervisor.</summary>
public sealed class PeerProbeSupervisorKey { }