af545efdf5
Copies the pre-consolidation store-and-forward queue into the consolidated
database on first boot, then renames the legacy file aside. Rows are in that
file precisely because the historian could not be reached, so dropping them
on upgrade would discard exactly the alarm audit trail the queue exists to
protect.
Runs last in OnReady, after every RegisterReplicated call. It is the only
thing in OnReady that writes rows, and capture is trigger-based: a migration
that ran before registration would recover the backlog locally and never
replicate a line of it, silently and permanently.
Ids are derived from the payload rather than the plan's mig-{node}-{legacyId}
scheme. Node-prefixing solves the collision the legacy AUTOINCREMENT key
would cause -- node A's row 7 and node B's row 7 are different alarms -- but
it preserves a duplication that should be collapsed instead. A warm pair's
two legacy files OVERLAP: HistorianAdapterActor default-writes while its
redundancy role is unknown, so both nodes accepted the same transitions
during every boot window. Prefixed ids would carry those duplicates into the
merged buffer forever; equal-payload ids converge them. The same property
makes a crash between commit and rename harmless under INSERT OR IGNORE.
OnReady now takes IConfiguration rather than offering an overload that skips
the migration. A wiring mistake that silently discarded a node's undelivered
alarm history is not a mistake worth making possible.
The copy is restricted to the columns the legacy table actually has. Naming
a column an older build never wrote throws "no such column", which would
discard every row in the table rather than the one field.
Claude-Session: https://claude.ai/code/session_01GASWkNEi68FSCtvr6rLoEW
102 lines
5.1 KiB
C#
102 lines
5.1 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.Runtime.DeploymentCache;
|
|
|
|
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>();
|
|
|
|
return services;
|
|
}
|
|
}
|