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
@@ -23,6 +23,8 @@ using ZB.MOM.WW.OtOpcUa.Host.Logging;
using ZB.MOM.WW.OtOpcUa.Host.Observability;
using ZB.MOM.WW.OtOpcUa.Host.OpcUa;
using ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway;
using ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Recorder;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions.Historian;
using ZB.MOM.WW.OtOpcUa.OpcUaServer;
using ZB.MOM.WW.OtOpcUa.Runtime.Historian;
using ZB.MOM.WW.OtOpcUa.Runtime.Scripting;
@@ -124,6 +126,49 @@ if (hasDriver)
builder.Configuration,
(opts, sp) => GatewayHistorian.CreateDataSource(opts, sp));
// Continuous historization of driver (non-Galaxy) tag values. Gated on ContinuousHistorization:Enabled
// AND the ServerHistorian gateway being configured: the recorder drains driver-tag live values to the
// SAME single gateway's WriteLiveValues SQL path, sourcing endpoint/key/TLS from the ServerHistorian
// section (this section carries only the recorder + outbox knobs). When both are on, register the durable
// crash-safe outbox + the gateway-backed live-value writer here; WithOtOpcUaRuntimeActors (below) spawns
// the recorder actor itself, gated on the same options.
var continuousHistorizationOptions = builder.Configuration
.GetSection(ContinuousHistorizationOptions.SectionName).Get<ContinuousHistorizationOptions>()
?? new ContinuousHistorizationOptions();
foreach (var warning in continuousHistorizationOptions.Validate())
Log.Warning("ContinuousHistorization misconfiguration detected at startup: {Warning}", warning);
if (serverHistorianOptions.Enabled && continuousHistorizationOptions.Enabled)
{
// Register the bound options so WithOtOpcUaRuntimeActors can gate the recorder spawn on Enabled.
builder.Services.AddSingleton(continuousHistorizationOptions);
// Durable, crash-safe FasterLog outbox (the historization crash boundary). Built via the factory so
// the container OWNS disposal (FasterLogHistorizationOutbox is IDisposable). Binding the observable
// outbox depth/dropped gauges here (once, on first resolution) keeps the live instance behind them.
builder.Services.AddSingleton<IHistorizationOutbox>(_ =>
{
var commitMode = Enum.TryParse<HistorizationCommitMode>(
continuousHistorizationOptions.CommitMode, ignoreCase: true, out var parsedMode)
? parsedMode
: HistorizationCommitMode.PerEntry;
var outbox = new FasterLogHistorizationOutbox(
continuousHistorizationOptions.OutboxPath,
commitMode,
continuousHistorizationOptions.CommitIntervalMs,
continuousHistorizationOptions.Capacity);
ContinuousHistorizationMetrics.BindOutbox(outbox);
return outbox;
});
// Gateway-backed live-value writer over its OWN gRPC channel to the same single gateway (a second
// channel to a co-located sidecar is cheap — the gateway pools the historian sessions server-side).
builder.Services.AddSingleton<IHistorianValueWriter>(sp =>
new GatewayHistorianValueWriter(
HistorianGatewayClientAdapter.Create(
serverHistorianOptions, sp.GetRequiredService<ILoggerFactory>()),
sp.GetRequiredService<ILogger<GatewayHistorianValueWriter>>()));
}
// Bind every cross-platform driver factory before AddAkka resolves IDriverFactory — replaces
// the F7-default NullDriverFactory with a real DriverFactoryRegistryAdapter so DriverHostActor
// can materialise real IDriver instances on deploy.