d907160747
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
111 lines
5.8 KiB
C#
111 lines
5.8 KiB
C#
using Microsoft.Extensions.Configuration;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using ZB.MOM.WW.LocalDb;
|
|
using ZB.MOM.WW.LocalDb.Replication;
|
|
using ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms;
|
|
using ZB.MOM.WW.OtOpcUa.Runtime.DeploymentCache;
|
|
using ZB.MOM.WW.OtOpcUa.Runtime.ScriptedAlarms;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Host.Configuration;
|
|
|
|
/// <summary>
|
|
/// Single registration point for the node-local LocalDb subsystem: the embedded SQLite store
|
|
/// that caches the deployed-configuration artifact, plus the optional gRPC replication that
|
|
/// mirrors it to the node's redundant pair peer.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// Exists as a named extension rather than inline <c>AddZbLocalDb</c> calls in
|
|
/// <c>Program.cs</c> so the wiring is covered by a DI resolution test — <c>Program.cs</c> is
|
|
/// top-level statements and cannot be exercised directly, which is exactly how a
|
|
/// "registered but never resolvable" defect ships unnoticed. This family has shipped that
|
|
/// defect three times (Secrets 0.2.0, Secrets 0.2.2, ScadaBridge #22).
|
|
/// </para>
|
|
/// <para>
|
|
/// <b>Driver-role nodes only.</b> Admin-only nodes have no deployed configuration to cache,
|
|
/// and registering here would impose the <c>LocalDb:Path</c>-required-or-no-boot constraint
|
|
/// on them for nothing.
|
|
/// </para>
|
|
/// <para>
|
|
/// <b>Storage is unconditional; replication is default-OFF.</b> A node with no
|
|
/// <c>PeerAddress</c> and no <c>SyncListenPort</c> is simply a fast local SQLite file —
|
|
/// <c>AddZbLocalDbReplication</c> registers the engine but it stays idle. Replication is
|
|
/// opt-in per pair because there is no production distribution story for the sync
|
|
/// <c>ApiKey</c> yet (the same open question the Secrets KEK has).
|
|
/// </para>
|
|
/// </remarks>
|
|
public static class LocalDbRegistration
|
|
{
|
|
/// <summary>Configuration section holding <c>LocalDbOptions</c>.</summary>
|
|
public const string LocalDbSectionPath = "LocalDb";
|
|
|
|
/// <summary>Configuration section holding <c>ReplicationOptions</c>.</summary>
|
|
public const string ReplicationSectionPath = "LocalDb:Replication";
|
|
|
|
/// <summary>
|
|
/// Configuration key carrying the dedicated h2c sync listener's port. Zero or absent means
|
|
/// no listener is bound and the passive sync endpoint is not mapped.
|
|
/// </summary>
|
|
public const string SyncListenPortKey = "LocalDb:SyncListenPort";
|
|
|
|
/// <summary>
|
|
/// The port the dedicated cleartext-HTTP/2 sync listener should bind, or <c>0</c> when the
|
|
/// node should not listen at all. Defaults to <c>0</c> — absent configuration must mean
|
|
/// "off".
|
|
/// </summary>
|
|
/// <param name="configuration">The application configuration.</param>
|
|
/// <returns>The configured sync port, or <c>0</c>.</returns>
|
|
public static int SyncListenPort(IConfiguration configuration)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(configuration);
|
|
return configuration.GetValue(SyncListenPortKey, defaultValue: 0);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Registers the local database (with the deployment-cache schema applied and registered for
|
|
/// replication) and the replication engine.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// <see cref="LocalDbSetup.OnReady"/> runs inside <c>AddZbLocalDb</c>'s singleton factory,
|
|
/// before the first caller receives the <c>ILocalDb</c> — so every consumer is guaranteed
|
|
/// to see the tables created and their capture triggers installed.
|
|
/// </para>
|
|
/// <para>
|
|
/// <c>AddZbLocalDbReplication</c> is called unconditionally rather than behind an
|
|
/// <c>Enabled</c> flag because the library already models "off" as "no
|
|
/// <c>PeerAddress</c>": the initiator background service idles and the passive endpoint
|
|
/// is only reachable if <c>Program.cs</c> maps it, which it does only when
|
|
/// <see cref="SyncListenPortKey"/> is set. Adding a second gate on top would give two
|
|
/// ways to express the same thing and a way for them to disagree.
|
|
/// </para>
|
|
/// </remarks>
|
|
/// <param name="services">The service collection to add to.</param>
|
|
/// <param name="configuration">The application configuration.</param>
|
|
/// <returns>The same <paramref name="services"/> instance, for chaining.</returns>
|
|
public static IServiceCollection AddOtOpcUaLocalDb(
|
|
this IServiceCollection services,
|
|
IConfiguration configuration)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(services);
|
|
ArgumentNullException.ThrowIfNull(configuration);
|
|
|
|
services.AddZbLocalDb(configuration, db => LocalDbSetup.OnReady(db, configuration));
|
|
services.AddZbLocalDbReplication(configuration);
|
|
|
|
// The consumer-facing seam. WithOtOpcUaRuntimeActors resolves this optionally and threads it
|
|
// into DriverHostActor; registering the store without this leaves the cache tables present,
|
|
// replicating, and never written to.
|
|
services.AddSingleton<IDeploymentArtifactCache, LocalDbDeploymentArtifactCache>();
|
|
|
|
// Per-cluster mesh Phase 4: scripted-alarm condition state served from the replicated LocalDb
|
|
// store instead of the ConfigDb-backed EF store, so a driver-only node (no ConfigDb) keeps its
|
|
// Part 9 condition state and mirrors it to its pair peer. Driver-role only, alongside the cache —
|
|
// admin-only nodes never register LocalDb. WithOtOpcUaRuntimeActors resolves it optionally and
|
|
// threads it into DriverHostActor's ScriptedAlarm engine. Singleton to match ILocalDb + the cache.
|
|
services.AddSingleton<IAlarmStateStore, LocalDbAlarmConditionStateStore>();
|
|
|
|
return services;
|
|
}
|
|
}
|