PR 1+2.W — Wire HistoryRouter + AlarmConditionService into DI

Server-side singletons threaded through OpcUaApplicationHost → OtOpcUaServer
→ DriverNodeManager construction. New ctor parameters are last-position
optional with null defaults so every existing test construction site
(OpcUaServerIntegrationTests, AlarmSubscribeIntegrationTests, etc.) keeps
working unchanged.

Program.cs:
  AddSingleton<IHistoryRouter, HistoryRouter>();
  AddSingleton<AlarmConditionService>();

The router stays empty after this PR. DriverNodeManager's internal
LegacyDriverHistoryAdapter handles every driver that still implements
IHistoryProvider; PR 3.W will register the Wonderware sidecar as a router
source; PR 7.2 retires the legacy fallback entirely.

44 alarm + history + integration tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joseph Doherty
2026-04-29 14:13:51 -04:00
parent 9365beb966
commit bc7ec746c5
3 changed files with 50 additions and 5 deletions

View File

@@ -5,6 +5,8 @@ using ZB.MOM.WW.OtOpcUa.Configuration.LocalCache;
using ZB.MOM.WW.OtOpcUa.Core.Hosting;
using ZB.MOM.WW.OtOpcUa.Core.OpcUa;
using ZB.MOM.WW.OtOpcUa.Core.Resilience;
using ZB.MOM.WW.OtOpcUa.Server.Alarms;
using ZB.MOM.WW.OtOpcUa.Server.History;
using ZB.MOM.WW.OtOpcUa.Server.Observability;
using ZB.MOM.WW.OtOpcUa.Server.Security;
@@ -40,6 +42,12 @@ public sealed class OpcUaApplicationHost : IAsyncDisposable
private ZB.MOM.WW.OtOpcUa.Core.Abstractions.IReadable? _virtualReadable;
private ZB.MOM.WW.OtOpcUa.Core.Abstractions.IReadable? _scriptedAlarmReadable;
// PR 1+2.W — server-level singletons. Threaded through to OtOpcUaServer + every
// DriverNodeManager. Default null preserves existing test construction sites that
// don't opt into the new server-side history routing or alarm-condition state machine.
private readonly IHistoryRouter? _historyRouter;
private readonly AlarmConditionService? _alarmConditionService;
private readonly ILoggerFactory _loggerFactory;
private readonly ILogger<OpcUaApplicationHost> _logger;
private ApplicationInstance? _application;
@@ -57,7 +65,9 @@ public sealed class OpcUaApplicationHost : IAsyncDisposable
Func<string, string?>? resilienceConfigLookup = null,
Func<string, ZB.MOM.WW.OtOpcUa.Core.OpcUa.EquipmentNamespaceContent?>? equipmentContentLookup = null,
ZB.MOM.WW.OtOpcUa.Core.Abstractions.IReadable? virtualReadable = null,
ZB.MOM.WW.OtOpcUa.Core.Abstractions.IReadable? scriptedAlarmReadable = null)
ZB.MOM.WW.OtOpcUa.Core.Abstractions.IReadable? scriptedAlarmReadable = null,
IHistoryRouter? historyRouter = null,
AlarmConditionService? alarmConditionService = null)
{
_options = options;
_driverHost = driverHost;
@@ -71,6 +81,8 @@ public sealed class OpcUaApplicationHost : IAsyncDisposable
_equipmentContentLookup = equipmentContentLookup;
_virtualReadable = virtualReadable;
_scriptedAlarmReadable = scriptedAlarmReadable;
_historyRouter = historyRouter;
_alarmConditionService = alarmConditionService;
_loggerFactory = loggerFactory;
_logger = logger;
}
@@ -136,7 +148,8 @@ public sealed class OpcUaApplicationHost : IAsyncDisposable
authzGate: _authzGate, scopeResolver: _scopeResolver,
tierLookup: _tierLookup, resilienceConfigLookup: _resilienceConfigLookup,
virtualReadable: _virtualReadable, scriptedAlarmReadable: _scriptedAlarmReadable,
anonymousRoles: _options.AnonymousRoles);
anonymousRoles: _options.AnonymousRoles,
historyRouter: _historyRouter, alarmConditionService: _alarmConditionService);
await _application.Start(_server).ConfigureAwait(false);
_logger.LogInformation("OPC UA server started — endpoint={Endpoint} driverCount={Count}",

View File

@@ -6,6 +6,8 @@ using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Core.Hosting;
using ZB.MOM.WW.OtOpcUa.Core.OpcUa;
using ZB.MOM.WW.OtOpcUa.Core.Resilience;
using ZB.MOM.WW.OtOpcUa.Server.Alarms;
using ZB.MOM.WW.OtOpcUa.Server.History;
using ZB.MOM.WW.OtOpcUa.Server.Security;
namespace ZB.MOM.WW.OtOpcUa.Server.OpcUa;
@@ -34,6 +36,13 @@ public sealed class OtOpcUaServer : StandardServer
private readonly IReadable? _virtualReadable;
private readonly IReadable? _scriptedAlarmReadable;
// PR 1+2.W — server-level singletons shared across every DriverNodeManager.
// Null when the deployment hasn't opted into the new server-side history routing /
// server-side alarm-condition state machine; DriverNodeManager falls back to the
// legacy per-driver IHistoryProvider + IAlarmSource paths in that case.
private readonly IHistoryRouter? _historyRouter;
private readonly AlarmConditionService? _alarmConditionService;
/// <summary>
/// Roles granted to anonymous sessions. When non-empty, <see cref="OnImpersonateUser"/>
/// wraps <c>AnonymousIdentityToken</c> in a <see cref="RoleBasedIdentity"/> carrying
@@ -57,7 +66,9 @@ public sealed class OtOpcUaServer : StandardServer
Func<string, string?>? resilienceConfigLookup = null,
IReadable? virtualReadable = null,
IReadable? scriptedAlarmReadable = null,
IReadOnlyList<string>? anonymousRoles = null)
IReadOnlyList<string>? anonymousRoles = null,
IHistoryRouter? historyRouter = null,
AlarmConditionService? alarmConditionService = null)
{
_driverHost = driverHost;
_authenticator = authenticator;
@@ -69,6 +80,8 @@ public sealed class OtOpcUaServer : StandardServer
_virtualReadable = virtualReadable;
_scriptedAlarmReadable = scriptedAlarmReadable;
_anonymousRoles = anonymousRoles ?? [];
_historyRouter = historyRouter;
_alarmConditionService = alarmConditionService;
_loggerFactory = loggerFactory;
}
@@ -102,7 +115,14 @@ public sealed class OtOpcUaServer : StandardServer
var invoker = new CapabilityInvoker(_pipelineBuilder, driver.DriverInstanceId, () => options, driver.DriverType);
var manager = new DriverNodeManager(server, configuration, driver, invoker, logger,
authzGate: _authzGate, scopeResolver: _scopeResolver,
virtualReadable: _virtualReadable, scriptedAlarmReadable: _scriptedAlarmReadable);
virtualReadable: _virtualReadable, scriptedAlarmReadable: _scriptedAlarmReadable,
historyRouter: _historyRouter, alarmService: _alarmConditionService);
// The router stays empty after PR 1+2.W — DriverNodeManager's internal
// LegacyDriverHistoryAdapter handles every driver that still implements
// IHistoryProvider. PR 3.W will register the Wonderware sidecar as a router
// source; PR 7.2 retires the legacy fallback entirely.
_driverNodeManagers.Add(manager);
}

View File

@@ -17,6 +17,8 @@ using ZB.MOM.WW.OtOpcUa.Driver.Modbus;
using ZB.MOM.WW.OtOpcUa.Driver.S7;
using ZB.MOM.WW.OtOpcUa.Driver.TwinCAT;
using ZB.MOM.WW.OtOpcUa.Server;
using ZB.MOM.WW.OtOpcUa.Server.Alarms;
using ZB.MOM.WW.OtOpcUa.Server.History;
using ZB.MOM.WW.OtOpcUa.Server.Hosting;
using ZB.MOM.WW.OtOpcUa.Server.OpcUa;
using ZB.MOM.WW.OtOpcUa.Server.Phase7;
@@ -137,6 +139,14 @@ builder.Services.AddScoped<EquipmentNamespaceContentLoader>();
// to ACL enforcement accidentally on upgrade.
builder.Services.AddSingleton<AuthorizationBootstrap>();
// PR 1+2.W — server-level history routing + alarm-condition state machine. Singletons
// shared across every DriverNodeManager. The router stays empty after this PR;
// PR 3.W registers the Wonderware historian sidecar as a router source. The alarm
// service runs the Active/Acknowledged/Inactive state machine for any driver that
// declares alarms via AlarmConditionInfo's sub-attribute refs.
builder.Services.AddSingleton<IHistoryRouter, HistoryRouter>();
builder.Services.AddSingleton<AlarmConditionService>();
builder.Services.AddSingleton<OpcUaApplicationHost>(sp =>
{
var registry = sp.GetRequiredService<DriverEquipmentContentRegistry>();
@@ -146,7 +156,9 @@ builder.Services.AddSingleton<OpcUaApplicationHost>(sp =>
sp.GetRequiredService<IUserAuthenticator>(),
sp.GetRequiredService<ILoggerFactory>(),
sp.GetRequiredService<ILogger<OpcUaApplicationHost>>(),
equipmentContentLookup: registry.Get);
equipmentContentLookup: registry.Get,
historyRouter: sp.GetRequiredService<IHistoryRouter>(),
alarmConditionService: sp.GetRequiredService<AlarmConditionService>());
});
builder.Services.AddHostedService<OpcUaServerService>();