feat(mesh-phase4): DriverHostActor runs ConfigDb-free (LocalDb alarm store, guarded acks)

Nullable ConfigDb factory; UpsertNodeDeploymentState no-ops when absent
(central persists acks from the ApplyAck); scripted-alarm condition state
served from the LocalDb store instead of the ConfigDb-backed Ef store.
Combines Phase-4 Tasks 4 + 6. Removes the interim dbFactory! cast.

Claude-Session: https://claude.ai/code/session_01GASWkNEi68FSCtvr6rLoEW
This commit is contained in:
Joseph Doherty
2026-07-23 12:39:41 -04:00
parent 4f4cdd05ec
commit d907160747
7 changed files with 466 additions and 20 deletions
@@ -18,6 +18,7 @@ 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.AlarmHistorian;
using ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms;
using ZB.MOM.WW.OtOpcUa.Core.Scripting;
using ZB.MOM.WW.OtOpcUa.Core.VirtualTags;
using ZB.MOM.WW.OtOpcUa.OpcUaServer;
@@ -282,6 +283,12 @@ public static class ServiceCollectionExtensions
// harnesses) rather than given a null-object, so DriverHostActor skips caching outright
// instead of pretending to cache into a sink that drops everything.
var deploymentArtifactCache = resolver.GetService<IDeploymentArtifactCache>();
// Per-cluster mesh Phase 4: scripted-alarm condition-state store. Registered by the Host's
// AddOtOpcUaLocalDb on driver-role nodes (the replicated LocalDb store), so condition state
// persists with no ConfigDb; null elsewhere (admin-only graphs, test harnesses) → the actor
// falls back to an EfAlarmConditionStateStore over dbFactory, or skips the alarm host when
// there is no ConfigDb either.
var alarmStateStore = resolver.GetService<IAlarmStateStore>();
// Per-cluster mesh Phase 3: ConfigSource:Mode selects where this driver reads config.
// FetchAndCache pulls the artifact from central over gRPC and reads only the LocalDb cache;
// Direct (default) reads central SQL as before. The fetcher is resolved only in FetchAndCache
@@ -462,14 +469,12 @@ public static class ServiceCollectionExtensions
// ackRouter: under ClusterClient mode the node comm actor relays ApplyAcks across
// the boundary; under Dps it stays null and the host publishes on the
// deployment-acks topic exactly as before.
// dbFactory! — KNOWN INTERIM. A driver-only node genuinely passes a null factory here
// (ConfigDb is now gated on hasAdmin, and driver-only nodes are forced to FetchAndCache),
// so DriverHostActor's ConfigDb-touching paths — scripted-alarm state persistence
// (EfAlarmConditionStateStore) and UpsertNodeDeploymentState — fault-and-swallow until
// Task 4 makes the factory nullable + guards those sites (and removes this !) and Task 6
// re-homes the alarm store to LocalDb. Inert in practice: no driver-only node is
// configured to boot until the Task 10 rig change.
DriverHostActor.Props(dbFactory!, roleInfo.LocalNode,
// dbFactory is nullable (Phase 4): a driver-only node passes null here — ConfigDb is
// gated on hasAdmin and driver-only nodes are forced to FetchAndCache. DriverHostActor
// guards every ConfigDb-touching path on it (UpsertNodeDeploymentState no-ops; central
// persists acks from the ApplyAck) and serves scripted-alarm state from the LocalDb
// alarmStateStore instead of an EfAlarmConditionStateStore.
DriverHostActor.Props(dbFactory, roleInfo.LocalNode,
ackRouter: useClusterClient ? nodeComm : null,
driverFactory: driverFactory, localRoles: roleInfo.LocalRoles,
dependencyMux: mux,
@@ -484,7 +489,8 @@ public static class ServiceCollectionExtensions
redundancyRoleView: redundancyRoleView,
replicationPeerHost: replicationPeerHost,
fetchAndCacheMode: fetchAndCacheMode,
artifactFetcher: artifactFetcher),
artifactFetcher: artifactFetcher,
alarmStateStore: alarmStateStore),
DriverHostActorName);
registry.Register<DriverHostActorKey>(driverHost);