037798b367
Tasks 14, 15 and 16, landed as ONE commit. PLAN DEFECT: these three tasks cannot compile separately. SiteReplicationActor takes a ReplicationService and calls ReplaceAllAsync (Task 14 deletes both); DeploymentManagerActor Tells message types declared in ReplicationMessages.cs (Task 15 deletes it); AkkaHostedService constructs the actor (Task 16). Any ordering leaves a broken intermediate. Combining them also strengthens the invariant Task 14 already stated for itself — the two mechanisms never both run, and never neither. Registered 8 tables in SiteLocalDbSetup.OnReady: sf_messages plus the 7 site config tables. notification_lists and smtp_configurations are deliberately NOT registered — permanently empty by design, so registering them would open a standing replication channel whose only historical payload was plaintext SMTP passwords. Migrate stays the LAST call in OnReady, after all registrations, so migrated rows enter the oplog through live capture triggers. Deleted: SiteReplicationActor, ReplicationMessages.cs, ReplicationService, StoreAndForwardStorage.ReplaceAllAsync, and 6 test files. ReplaceAllAsync is not merely unused but unsafe to keep: a mass DELETE on a now-replicated table would be captured and shipped to the peer. Kept ActiveNodeEvaluator (delivery gate + heartbeat still need it) with its doc corrected, and activeNodeCheck in AkkaHostedService (SiteCommunicationActor). The positional-argument hazard the plan flagged was real: removing DeploymentManagerActor's optional IActorRef? replicationActor shifted 6 trailing optionals, and 4 test call sites bound the wrong arguments with no compile error at some positions. Converted them to named arguments where possible — Props.Create builds an expression tree, which rejects out-of-position named args, so the rest are padded positionally with a comment saying why. The Task 7 'not yet registered' test was INVERTED rather than deleted, and is exact in both directions: too few means a table silently stops replicating, too many means the SMTP tables leak. Added a separate security-named test for those two, and a composite-PK test (LWW keys on the full PK, so a truncated key set would collapse distinct rows). The convergence suites now get their registrations from the real OnReady — their temporary harness registration is deleted, so they prove the cutover rather than agreeing with themselves. Verified: build 0 warnings; SiteRuntime 512, StoreAndForward 130, Host 330, AuditLog 355, ExternalSystemGateway 142, HealthMonitoring 97, LocalDb integration 16 — all pass, 0 failures. Claude-Session: https://claude.ai/code/session_01BL2Vu1ESDQ9SCN4gVKkdts
92 lines
4.6 KiB
C#
92 lines
4.6 KiB
C#
using Microsoft.Extensions.Configuration;
|
|
using ZB.MOM.WW.LocalDb;
|
|
using ZB.MOM.WW.ScadaBridge.SiteEventLogging;
|
|
using ZB.MOM.WW.ScadaBridge.SiteRuntime.Persistence;
|
|
using ZB.MOM.WW.ScadaBridge.SiteRuntime.Tracking;
|
|
using ZB.MOM.WW.ScadaBridge.StoreAndForward;
|
|
|
|
namespace ZB.MOM.WW.ScadaBridge.Host;
|
|
|
|
/// <summary>
|
|
/// Initializes the consolidated site database — the single <c>ZB.MOM.WW.LocalDb</c>-managed
|
|
/// file that holds the site node's replicated state.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// <b>Ordering is load-bearing.</b> Table DDL must run BEFORE
|
|
/// <c>RegisterReplicated</c>, and every row that should replicate must be written
|
|
/// AFTER it. Capture is trigger-based change-data-capture: the library neither
|
|
/// captures nor snapshots rows that already existed when the triggers were installed,
|
|
/// so a row written before registration is invisible to the peer forever — silently,
|
|
/// with no error anywhere.
|
|
/// </para>
|
|
/// <para>
|
|
/// Runs inside the <c>AddZbLocalDb</c> onReady callback, once, before any caller
|
|
/// receives the <see cref="ILocalDb"/> singleton. onReady is a synchronous
|
|
/// <c>Action<ILocalDb></c>, hence the direct <c>CreateConnection()</c> rather than
|
|
/// blocking on the async execute API. A throw here propagates out of the first
|
|
/// <c>GetRequiredService<ILocalDb>()</c> and fails host startup, which is the
|
|
/// intent: a site node that cannot establish its schema must not come up half-working.
|
|
/// </para>
|
|
/// </remarks>
|
|
public static class SiteLocalDbSetup
|
|
{
|
|
/// <summary>
|
|
/// Creates the site node's replicated tables, opts them into change capture, and
|
|
/// migrates any pre-Phase-1 databases in.
|
|
/// </summary>
|
|
/// <param name="db">The LocalDb instance being initialized.</param>
|
|
/// <param name="config">Configuration, for the legacy migrator's old path keys and node name.</param>
|
|
public static void OnReady(ILocalDb db, IConfiguration config)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(db);
|
|
ArgumentNullException.ThrowIfNull(config);
|
|
|
|
using (var connection = db.CreateConnection())
|
|
{
|
|
OperationTrackingSchema.Apply(connection);
|
|
SiteEventLogSchema.Apply(connection);
|
|
|
|
// Phase 2: the site's configuration tables and the store-and-forward buffer
|
|
// now live in this file too.
|
|
SiteStorageSchema.Apply(connection);
|
|
StoreAndForwardSchema.Apply(connection);
|
|
}
|
|
|
|
// Both tables qualify: each has an explicit primary key (RegisterReplicated
|
|
// rejects tables without one) and no BLOB columns (which json_object cannot
|
|
// capture). Registration is idempotent and installs the capture triggers.
|
|
db.RegisterReplicated("OperationTracking");
|
|
db.RegisterReplicated("site_events");
|
|
|
|
// Phase 2: the store-and-forward buffer and the seven site configuration tables.
|
|
// These replaced the bespoke SiteReplicationActor and StoreAndForward
|
|
// ReplicationService, which shipped hand-written Add/Remove/Park/Requeue operations
|
|
// over Akka; both were deleted in the same commit that added these lines, so the
|
|
// two mechanisms never ran at once.
|
|
//
|
|
// Both composite-PK tables are fine: RegisterReplicated orders multi-column PKs by
|
|
// ordinal. No Phase 2 table has a BLOB column, which it would reject.
|
|
db.RegisterReplicated("sf_messages");
|
|
db.RegisterReplicated("deployed_configurations");
|
|
db.RegisterReplicated("static_attribute_overrides");
|
|
db.RegisterReplicated("shared_scripts");
|
|
db.RegisterReplicated("external_systems");
|
|
db.RegisterReplicated("database_connections");
|
|
db.RegisterReplicated("data_connection_definitions");
|
|
db.RegisterReplicated("native_alarm_state");
|
|
|
|
// notification_lists and smtp_configurations are created but deliberately NOT
|
|
// registered. They are permanently empty by design — the site-side write paths were
|
|
// removed on 2026-07-10, the legacy migrator skips them, and the active node's
|
|
// artifact apply purges them on every deploy. Registering them would open a standing
|
|
// replication channel whose only historical payload was plaintext SMTP passwords, in
|
|
// exchange for replicating nothing. Anyone adding them here should first establish
|
|
// that a site has a legitimate reason to hold SMTP credentials at all.
|
|
|
|
// AFTER registration, so migrated rows enter the oplog and reach the peer like
|
|
// any other write. Before it, they would be invisible to replication forever.
|
|
SiteLocalDbLegacyMigrator.Migrate(db, config);
|
|
}
|
|
}
|