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;
|
||||||
using ZB.MOM.WW.Health.Akka;
|
using ZB.MOM.WW.Health.Akka;
|
||||||
using ZB.MOM.WW.Health.EntityFrameworkCore;
|
using ZB.MOM.WW.Health.EntityFrameworkCore;
|
||||||
|
using ZB.MOM.WW.LocalDb.Replication;
|
||||||
using ZB.MOM.WW.ScadaBridge.AuditLog;
|
using ZB.MOM.WW.ScadaBridge.AuditLog;
|
||||||
using ZB.MOM.WW.ScadaBridge.CentralUI;
|
using ZB.MOM.WW.ScadaBridge.CentralUI;
|
||||||
using ZB.MOM.WW.ScadaBridge.ClusterInfrastructure;
|
using ZB.MOM.WW.ScadaBridge.ClusterInfrastructure;
|
||||||
@@ -516,7 +517,8 @@ try
|
|||||||
builder.Services.AddGrpc();
|
builder.Services.AddGrpc();
|
||||||
builder.Services.AddSingleton<ZB.MOM.WW.ScadaBridge.Communication.Grpc.SiteStreamGrpcServer>();
|
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);
|
SiteServiceRegistration.Configure(builder.Services, builder.Configuration);
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
@@ -535,6 +537,14 @@ try
|
|||||||
// Map gRPC service — resolves the singleton SiteStreamGrpcServer from DI
|
// Map gRPC service — resolves the singleton SiteStreamGrpcServer from DI
|
||||||
app.MapGrpcService<ZB.MOM.WW.ScadaBridge.Communication.Grpc.SiteStreamGrpcServer>();
|
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
|
// Site-shutdown ordering. ApplicationStopping
|
||||||
// fires BEFORE IHostedService.StopAsync runs, so the gRPC server
|
// fires BEFORE IHostedService.StopAsync runs, so the gRPC server
|
||||||
// refuses new streams (Unavailable) and cancels every active stream
|
// 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.SiteRuntime;
|
||||||
using ZB.MOM.WW.ScadaBridge.StoreAndForward;
|
using ZB.MOM.WW.ScadaBridge.StoreAndForward;
|
||||||
using ZB.MOM.WW.LocalDb;
|
using ZB.MOM.WW.LocalDb;
|
||||||
|
using ZB.MOM.WW.LocalDb.Replication;
|
||||||
using ZB.MOM.WW.Secrets.DependencyInjection;
|
using ZB.MOM.WW.Secrets.DependencyInjection;
|
||||||
using ZB.MOM.WW.Telemetry;
|
using ZB.MOM.WW.Telemetry;
|
||||||
|
|
||||||
@@ -56,12 +57,22 @@ public static class SiteServiceRegistration
|
|||||||
//
|
//
|
||||||
// Storage is registered UNCONDITIONALLY and needs no flag: with no peer
|
// Storage is registered UNCONDITIONALLY and needs no flag: with no peer
|
||||||
// configured, LocalDb is simply a local SQLite file and the replication
|
// configured, LocalDb is simply a local SQLite file and the replication
|
||||||
// initiator idles. Replication itself is opt-in via LocalDb:Replication:PeerAddress
|
// initiator idles.
|
||||||
// and is wired in Program.cs, which owns the gRPC host the sync endpoint needs.
|
|
||||||
//
|
//
|
||||||
// Design: scadaproj docs/plans/2026-07-19-scadabridge-localdb-design.md
|
// Design: scadaproj docs/plans/2026-07-19-scadabridge-localdb-design.md
|
||||||
services.AddZbLocalDb(config, SiteLocalDbSetup.OnReady);
|
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();
|
services.AddDataConnectionLayer();
|
||||||
// Local SQLite store by default; a local store replicating against a shared SQL-Server hub
|
// 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.
|
// 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.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using ZB.MOM.WW.LocalDb;
|
using ZB.MOM.WW.LocalDb;
|
||||||
|
using ZB.MOM.WW.LocalDb.Replication;
|
||||||
using ZB.MOM.WW.ScadaBridge.Host;
|
using ZB.MOM.WW.ScadaBridge.Host;
|
||||||
using ZB.MOM.WW.ScadaBridge.Host.Actors;
|
using ZB.MOM.WW.ScadaBridge.Host.Actors;
|
||||||
|
|
||||||
@@ -120,6 +121,35 @@ public class SiteLocalDbWiringTests : IDisposable
|
|||||||
Assert.Same(first, second);
|
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]
|
[Fact]
|
||||||
public void Site_LocalDb_CreatesTheConfiguredFile()
|
public void Site_LocalDb_CreatesTheConfiguredFile()
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user