LocalDb adoption Phase 1 + 2: consolidate the site database, delete the bespoke replicators #23
@@ -7,6 +7,7 @@ using ZB.MOM.WW.Auth.AspNetCore;
|
||||
using ZB.MOM.WW.Health;
|
||||
using ZB.MOM.WW.Health.Akka;
|
||||
using ZB.MOM.WW.Health.EntityFrameworkCore;
|
||||
using ZB.MOM.WW.LocalDb.Replication;
|
||||
using ZB.MOM.WW.ScadaBridge.AuditLog;
|
||||
using ZB.MOM.WW.ScadaBridge.CentralUI;
|
||||
using ZB.MOM.WW.ScadaBridge.ClusterInfrastructure;
|
||||
@@ -516,7 +517,8 @@ try
|
||||
builder.Services.AddGrpc();
|
||||
builder.Services.AddSingleton<ZB.MOM.WW.ScadaBridge.Communication.Grpc.SiteStreamGrpcServer>();
|
||||
|
||||
// Existing site service registrations
|
||||
// Existing site service registrations (this is also where LocalDb and its
|
||||
// replication engine are registered — see SiteServiceRegistration)
|
||||
SiteServiceRegistration.Configure(builder.Services, builder.Configuration);
|
||||
|
||||
var app = builder.Build();
|
||||
@@ -535,6 +537,14 @@ try
|
||||
// Map gRPC service — resolves the singleton SiteStreamGrpcServer from DI
|
||||
app.MapGrpcService<ZB.MOM.WW.ScadaBridge.Communication.Grpc.SiteStreamGrpcServer>();
|
||||
|
||||
// The passive half of LocalDb replication: the peer node dials THIS endpoint.
|
||||
// It shares the Kestrel h2c listener the site gRPC server already uses, so no
|
||||
// listener or port changes are needed. Mapping it is harmless with no peer
|
||||
// configured — nothing dials it. Inbound auth is the host's job and lands in
|
||||
// Task 8; until then this endpoint is unauthenticated, which is why
|
||||
// replication ships default-OFF.
|
||||
app.MapZbLocalDbSync();
|
||||
|
||||
// Site-shutdown ordering. ApplicationStopping
|
||||
// fires BEFORE IHostedService.StopAsync runs, so the gRPC server
|
||||
// refuses new streams (Unavailable) and cancels every active stream
|
||||
|
||||
@@ -15,6 +15,7 @@ using ZB.MOM.WW.ScadaBridge.SiteEventLogging;
|
||||
using ZB.MOM.WW.ScadaBridge.SiteRuntime;
|
||||
using ZB.MOM.WW.ScadaBridge.StoreAndForward;
|
||||
using ZB.MOM.WW.LocalDb;
|
||||
using ZB.MOM.WW.LocalDb.Replication;
|
||||
using ZB.MOM.WW.Secrets.DependencyInjection;
|
||||
using ZB.MOM.WW.Telemetry;
|
||||
|
||||
@@ -56,12 +57,22 @@ public static class SiteServiceRegistration
|
||||
//
|
||||
// Storage is registered UNCONDITIONALLY and needs no flag: with no peer
|
||||
// configured, LocalDb is simply a local SQLite file and the replication
|
||||
// initiator idles. Replication itself is opt-in via LocalDb:Replication:PeerAddress
|
||||
// and is wired in Program.cs, which owns the gRPC host the sync endpoint needs.
|
||||
// initiator idles.
|
||||
//
|
||||
// Design: scadaproj docs/plans/2026-07-19-scadabridge-localdb-design.md
|
||||
services.AddZbLocalDb(config, SiteLocalDbSetup.OnReady);
|
||||
|
||||
// The replication engine, likewise unconditional but INERT by default: with no
|
||||
// LocalDb:Replication:PeerAddress the initiating SyncBackgroundService starts and
|
||||
// immediately idles, and nothing dials the passive endpoint. A site pair
|
||||
// replicates only once an operator sets a peer on both nodes.
|
||||
//
|
||||
// Registered HERE rather than in Program.cs (where the plan first put it) so the
|
||||
// composition-root tests, which build this graph and not Program.cs, actually
|
||||
// cover it. The endpoint half — MapZbLocalDbSync — has to stay in Program.cs
|
||||
// because it needs the WebApplication.
|
||||
services.AddZbLocalDbReplication(config);
|
||||
|
||||
services.AddDataConnectionLayer();
|
||||
// Local SQLite store by default; a local store replicating against a shared SQL-Server hub
|
||||
// only when Secrets:Replication:Enabled is true AND a hub connection string is present.
|
||||
|
||||
@@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using ZB.MOM.WW.LocalDb;
|
||||
using ZB.MOM.WW.LocalDb.Replication;
|
||||
using ZB.MOM.WW.ScadaBridge.Host;
|
||||
using ZB.MOM.WW.ScadaBridge.Host.Actors;
|
||||
|
||||
@@ -120,6 +121,35 @@ public class SiteLocalDbWiringTests : IDisposable
|
||||
Assert.Same(first, second);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Site_Replication_IsRegistered_AndInertWithNoPeer()
|
||||
{
|
||||
// Task 7's default-OFF pin. This fixture configures NO
|
||||
// LocalDb:Replication:PeerAddress, which is the shipping default, so the
|
||||
// engine must resolve cleanly and sit idle rather than throw, retry, or
|
||||
// report a connection. "Default-off" here means inert, NOT unregistered —
|
||||
// the services are always in the graph.
|
||||
var status = _host.Services.GetService<ISyncStatus>();
|
||||
|
||||
Assert.NotNull(status);
|
||||
Assert.False(status!.Connected);
|
||||
Assert.Null(status.PeerNodeId);
|
||||
Assert.Null(status.LastSyncUtc);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Site_Replication_OplogBacklog_IsZero_NotNull_OnAHealthyIdleNode()
|
||||
{
|
||||
// OplogBacklog is deliberately nullable: null means "unknown" (no provider
|
||||
// wired, or the poll failed) and must never be reported as a healthy 0. On a
|
||||
// node whose local database is present and readable, a real 0 proves the
|
||||
// backlog provider is actually wired to the oplog — a null here would mean
|
||||
// Task 9's health signal is about to publish "unknown" forever.
|
||||
var status = _host.Services.GetRequiredService<ISyncStatus>();
|
||||
|
||||
Assert.Equal(0L, status.OplogBacklog);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Site_LocalDb_CreatesTheConfiguredFile()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user