docs(src): fix CommentChecker findings across src/ — 1,032 → 5 (all false positives)
Comment/XML-doc changes only, no code logic: - strip project-tracking references (WP*, P*-*, row-NN, #NN, Task N) from comments, keeping the explanatory rationale - add missing summaries, <param>, <returns>, <typeparam> and <inheritdoc/> - replace misused <inheritdoc/> with real docs; where <inheritdoc/> was combined with useful remarks, move the rationale into // comments - drop stale replicationActor <param> on DeploymentManagerActor ctor Remaining 5 are false positives (CWE-1236 x2, failures-1, year-0001, M365). Build 0 errors / 0 warnings.
This commit is contained in:
@@ -261,7 +261,7 @@ public class AkkaHostedService : IHostedService
|
||||
TimeSpan transportHeartbeat,
|
||||
TimeSpan transportFailure)
|
||||
{
|
||||
// Simultaneous-cold-start split-brain guard (Gitea #33): when enabled the node must NOT
|
||||
// Simultaneous-cold-start split-brain guard: when enabled the node must NOT
|
||||
// auto-join from its config seeds — Akka's FirstSeedNodeProcess is exactly what races the
|
||||
// peer and forms two 1-node clusters. Emit an EMPTY seed list so the ActorSystem comes up
|
||||
// unjoined, and ClusterBootstrapCoordinator issues the single reachability-gated
|
||||
@@ -471,7 +471,7 @@ akka {{
|
||||
debugStreamService?.SetActorSystem(_actorSystem!);
|
||||
|
||||
// Wire up the aggregated live alarm cache with the ActorSystem so it can host
|
||||
// per-site aggregator actors on the active central node (plan #10, Task 4).
|
||||
// per-site aggregator actors on the active central node.
|
||||
var siteAlarmLiveCache = _serviceProvider.GetService<ZB.MOM.WW.ScadaBridge.Communication.SiteAlarmLiveCacheService>();
|
||||
siteAlarmLiveCache?.SetActorSystem(_actorSystem!);
|
||||
|
||||
@@ -511,10 +511,10 @@ akka {{
|
||||
// All central singletons below are created through SingletonRegistrar.Start,
|
||||
// which pins the actor to the active central node (ClusterSingletonManager), exposes a
|
||||
// stable address (ClusterSingletonProxy), and — crucially — registers a PhaseClusterLeave
|
||||
// GracefulStop drain task so an in-flight EF write drains before handover (review 01
|
||||
// [Medium]: notification-outbox and audit-log-ingest previously had NO drain task). Names
|
||||
// GracefulStop drain task so an in-flight EF write drains before handover (notification-outbox and
|
||||
// audit-log-ingest previously had NO drain task). Names
|
||||
// are unchanged ({name}-singleton / {name}-proxy). Site singletons use the same
|
||||
// registrar with `role: siteRole` (Task 11 / round-2 N5).
|
||||
// registrar with `role: siteRole`.
|
||||
var outbox = SingletonRegistrar.Start(
|
||||
_actorSystem!, "notification-outbox",
|
||||
Props.Create(() => new ZB.MOM.WW.ScadaBridge.NotificationOutbox.NotificationOutboxActor(
|
||||
@@ -825,18 +825,18 @@ akka {{
|
||||
? () => clusterNodeProvider.SelfIsPrimary
|
||||
: null;
|
||||
|
||||
// Deployment Manager — role-scoped singleton via SingletonRegistrar
|
||||
// (review 01 round-2 N5): previously hand-rolled with bare PoisonPill
|
||||
// Deployment Manager — role-scoped singleton via SingletonRegistrar:
|
||||
// previously hand-rolled with bare PoisonPill
|
||||
// termination and NO PhaseClusterLeave drain, so in-flight SQLite
|
||||
// writes (static overrides, native_alarm_state) could be cut off on
|
||||
// graceful failover. Names unchanged: deployment-manager-singleton /
|
||||
// deployment-manager-proxy.
|
||||
// WP2.6a: the shared cache backing SiteExternalSystemRepository reads
|
||||
// The shared cache backing SiteExternalSystemRepository reads
|
||||
// (registered singleton by AddSiteRuntime); DeploymentManagerActor invalidates
|
||||
// it after an artifact deploy applies external-system changes.
|
||||
var externalSystemCache = _serviceProvider
|
||||
.GetService<ZB.MOM.WW.ScadaBridge.SiteRuntime.Repositories.ExternalSystemDefinitionCache>();
|
||||
// Task 10 (site-secrets artifact deploy): the same per-node singleton AddSiteRuntime
|
||||
// Site-secrets artifact deploy: the same per-node singleton AddSiteRuntime
|
||||
// registers (and AddHostedService runs periodically) is reconciled SYNCHRONOUSLY by
|
||||
// HandleDeployArtifacts right after it writes a shipped secret set, so the deploy
|
||||
// reply's SecretsApplied reflects this node's own apply/verify outcome immediately
|
||||
@@ -909,7 +909,7 @@ akka {{
|
||||
// singleton is what guarantees queries always read the actively-written copy;
|
||||
// replication is what gives the singleton HISTORY to read after it moves —
|
||||
// without it, a failover would restart the event log empty at exactly the moment
|
||||
// an operator is investigating an incident (WP3.2 design memo, Decision (b)).
|
||||
// an operator is investigating an incident (design decision).
|
||||
var eventLogQueryService = _serviceProvider.GetService<SiteEventLogging.IEventLogQueryService>();
|
||||
if (eventLogQueryService != null)
|
||||
{
|
||||
@@ -1064,7 +1064,7 @@ akka {{
|
||||
// Raised from Warning to Error (register row 73). This is a misconfiguration,
|
||||
// not a degraded-but-expected state: no configuration makes the fetcher
|
||||
// legitimately absent on a site node, and the consequence is that neither the
|
||||
// file heal nor the row-67 runtime heal will ever run on this node.
|
||||
// file heal nor the runtime heal will ever run on this node.
|
||||
_logger.LogError(
|
||||
"No IDeploymentConfigFetcher available; SiteReconciliationActor not created for "
|
||||
+ "site {SiteId} node {Node} — startup self-heal is DISABLED (replication "
|
||||
|
||||
@@ -8,7 +8,7 @@ using ZB.MOM.WW.ScadaBridge.ClusterInfrastructure;
|
||||
namespace ZB.MOM.WW.ScadaBridge.Host.Actors;
|
||||
|
||||
/// <summary>
|
||||
/// Runs the simultaneous-cold-start split-brain guard (Gitea #33): when
|
||||
/// Runs the simultaneous-cold-start split-brain guard: when
|
||||
/// <c>ScadaBridge:Cluster:BootstrapGuard:Enabled</c>, the node starts with NO config seed nodes
|
||||
/// (<c>AkkaHostedService.BuildHocon</c> emits an empty seed list), and this coordinator picks the join
|
||||
/// order and issues the single <see cref="Akka.Cluster.Cluster.JoinSeedNodes"/> once the ActorSystem is
|
||||
@@ -56,7 +56,9 @@ public sealed class ClusterBootstrapCoordinator : IHostedService
|
||||
_log = log ?? throw new ArgumentNullException(nameof(log));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
/// <summary>When the bootstrap guard is enabled, decides the join order and issues the single seed-node join once the ActorSystem is up; otherwise does nothing.</summary>
|
||||
/// <param name="cancellationToken">Token that signals startup should be abandoned.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (!_clusterOptions.BootstrapGuard.Enabled)
|
||||
@@ -69,7 +71,9 @@ public sealed class ClusterBootstrapCoordinator : IHostedService
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
/// <summary>Cancels any in-flight join attempt and waits for it to finish.</summary>
|
||||
/// <param name="cancellationToken">Token that signals shutdown should no longer wait.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
public async Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await _cts.CancelAsync().ConfigureAwait(false);
|
||||
|
||||
@@ -13,8 +13,7 @@ namespace ZB.MOM.WW.ScadaBridge.Host.Actors;
|
||||
/// left the two busiest singletons (notification-outbox, audit-log-ingest)
|
||||
/// without drain tasks (review 01 [Medium]). The optional <c>role</c> maps to
|
||||
/// <see cref="ClusterSingletonManagerSettings"/>/<see cref="ClusterSingletonProxySettings"/>
|
||||
/// <c>.WithRole</c>, so role-scoped site singletons get the same drain
|
||||
/// (review 01 round-2 N5).
|
||||
/// <c>.WithRole</c>, so role-scoped site singletons get the same drain.
|
||||
/// </summary>
|
||||
internal static class SingletonRegistrar
|
||||
{
|
||||
|
||||
@@ -6,6 +6,11 @@ namespace ZB.MOM.WW.ScadaBridge.Host;
|
||||
public static class CentralListenerIsolation
|
||||
{
|
||||
/// <summary>Runs after routing and before authentication or endpoint dispatch.</summary>
|
||||
/// <param name="app">The application builder to add the middleware to.</param>
|
||||
/// <param name="grpcPort">The local port reserved for gRPC endpoints.</param>
|
||||
/// <param name="metricsPort">The dedicated metrics port, or null when metrics share the HTTP ports.</param>
|
||||
/// <param name="httpPorts">The local ports serving ordinary HTTP traffic.</param>
|
||||
/// <returns>The same application builder, for chaining.</returns>
|
||||
public static IApplicationBuilder UseCentralListenerIsolation(this IApplicationBuilder app,
|
||||
int grpcPort, int? metricsPort, IReadOnlyCollection<int> httpPorts)
|
||||
=> app.Use(async (context, next) =>
|
||||
|
||||
@@ -84,7 +84,9 @@ internal sealed class InboundApiKeyStoreImportService(
|
||||
/// <summary>SQL Server "Violation of PRIMARY KEY constraint" — the only error worth one retry.</summary>
|
||||
private const int PrimaryKeyViolation = 2627;
|
||||
|
||||
/// <inheritdoc/>
|
||||
/// <summary>Runs the one-off SQLite-to-store key import at host startup; failures are logged, never thrown.</summary>
|
||||
/// <param name="cancellationToken">Cancellation token for the startup import.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var configured = storeOptions.Value.ImportFromSqlitePath;
|
||||
@@ -124,7 +126,9 @@ internal sealed class InboundApiKeyStoreImportService(
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
/// <summary>No shutdown work is required.</summary>
|
||||
/// <param name="cancellationToken">Cancellation token (unused).</param>
|
||||
/// <returns>A completed task.</returns>
|
||||
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
|
||||
private async Task<ApiKeyImportResult> ImportWithOneRetryAsync(string path, CancellationToken ct)
|
||||
|
||||
@@ -10,12 +10,15 @@ public sealed class InboundApiKeyStoreOptions
|
||||
{
|
||||
public const string SectionPath = "ScadaBridge:InboundApi:ApiKeyStore";
|
||||
|
||||
/// <summary>The selected inbound API key store backend; defaults to <see cref="InboundApiKeyStoreProvider.Sqlite"/>.</summary>
|
||||
public InboundApiKeyStoreProvider Provider { get; set; } = InboundApiKeyStoreProvider.Sqlite;
|
||||
|
||||
/// <summary>SQLite file to import once when <see cref="Provider"/> is SqlServer; defaults to the library's <c>SqlitePath</c>. Renamed <c>*.imported</c> afterwards.</summary>
|
||||
public string? ImportFromSqlitePath { get; set; }
|
||||
|
||||
/// <summary>Reads <c>Provider</c> from configuration (case-insensitive). Throws <see cref="InvalidOperationException"/> naming the legal values on an unknown one, so a typo fails the boot with a readable message instead of silently falling back to SQLite.</summary>
|
||||
/// <param name="configuration">The configuration to read the provider setting from.</param>
|
||||
/// <returns>The configured provider, or <see cref="InboundApiKeyStoreProvider.Sqlite"/> when unset.</returns>
|
||||
public static InboundApiKeyStoreProvider ReadProvider(IConfiguration configuration)
|
||||
{
|
||||
var value = configuration[$"{SectionPath}:Provider"];
|
||||
|
||||
@@ -7,8 +7,8 @@ using ZB.MOM.WW.ScadaBridge.SiteCallAudit;
|
||||
namespace ZB.MOM.WW.ScadaBridge.Host;
|
||||
|
||||
/// <summary>
|
||||
/// Boot-time cross-validation of the "rate-fold interval triple" (arch-review P5-5,
|
||||
/// Low). <see cref="KpiHistoryOptions.SampleInterval"/>,
|
||||
/// Boot-time cross-validation of the "rate-fold interval triple":
|
||||
/// <see cref="KpiHistoryOptions.SampleInterval"/>,
|
||||
/// <see cref="NotificationOutboxOptions.DeliveredKpiWindow"/>, and
|
||||
/// <see cref="SiteCallAuditOptions.KpiInterval"/> are three independently-owned config
|
||||
/// keys, bound in three different component projects with no compile-time relationship
|
||||
@@ -23,7 +23,7 @@ namespace ZB.MOM.WW.ScadaBridge.Host;
|
||||
/// <c>KpiHistoryRepository.FoldHourlyRollupsAsync</c>'s per-hour <c>Sum</c> over those
|
||||
/// samples is correct for <em>any</em> value of the other two keys — a widened
|
||||
/// <c>DeliveredKpiWindow</c> can no longer silently over-count the hourly rollup the way
|
||||
/// it did before P5-5.
|
||||
/// it did before the rescale.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>What still depends on it.</b> The rescale trades exact per-sample fidelity for
|
||||
@@ -54,6 +54,8 @@ public sealed class KpiRateFoldIntervalCrossValidator : OptionsValidatorBase<Kpi
|
||||
/// Creates the validator with the two sibling options it cross-checks against
|
||||
/// <see cref="KpiHistoryOptions"/>.
|
||||
/// </summary>
|
||||
/// <param name="notificationOutboxOptions">Notification Outbox options supplying <c>DeliveredKpiWindow</c>.</param>
|
||||
/// <param name="siteCallAuditOptions">Site Call Audit options supplying <c>KpiInterval</c>.</param>
|
||||
public KpiRateFoldIntervalCrossValidator(
|
||||
IOptions<NotificationOutboxOptions> notificationOutboxOptions,
|
||||
IOptions<SiteCallAuditOptions> siteCallAuditOptions)
|
||||
|
||||
@@ -77,7 +77,7 @@ var configuration = new ConfigurationBuilder()
|
||||
// The expander MUST read the SAME store the running node will serve — SecretsRegistration.
|
||||
// UsesCentralSharedStore is the single predicate deciding that, shared with the runtime
|
||||
// registration in AddScadaBridgeSecrets, so the two can never disagree. On a Central node with
|
||||
// Secrets:Store:Provider=SqlServer that is the SHARED SQL-Server store (Task 5): an expander left
|
||||
// Secrets:Store:Provider=SqlServer that is the SHARED SQL-Server store: an expander left
|
||||
// on SQLite there would resolve pre-host ${secret:} references (e.g. ScadaBridge:Security:
|
||||
// JwtSigningKey) from a store the node does not actually serve, so a value rotated through the
|
||||
// shared store would never reach the pre-host config, and two central nodes could disagree on
|
||||
@@ -312,7 +312,7 @@ try
|
||||
// for ScriptArtifactsChanged. The Transport bundle importer publishes
|
||||
// post-commit; the Inbound API's ScriptArtifactChangeSubscriber (a hosted
|
||||
// service registered by AddInboundAPI below) consumes ApiMethod notifications
|
||||
// and invalidates the compiled-handler cache (wired in plan R2-06 T1; the
|
||||
// and invalidates the compiled-handler cache (the
|
||||
// per-request revision check remains the correctness fallback).
|
||||
// SharedScript/Template notifications currently have NO consumer — nothing at
|
||||
// central caches compiled artifacts of those kinds. Central-only.
|
||||
@@ -345,7 +345,7 @@ try
|
||||
// the KpiHistoryRecorderActor cluster singleton (started in
|
||||
// AkkaHostedService). Observability/best-effort: NOT readiness-gated.
|
||||
builder.Services.AddKpiHistory(builder.Configuration);
|
||||
// P5-5 (arch-review Low): cross-validates the rate-fold interval triple
|
||||
// Cross-validates the rate-fold interval triple
|
||||
// (KpiHistoryOptions.SampleInterval, NotificationOutboxOptions.DeliveredKpiWindow,
|
||||
// SiteCallAuditOptions.KpiInterval) at boot. Rides KpiHistoryOptions' existing
|
||||
// ValidateOnStart trigger set by AddKpiHistory above.
|
||||
@@ -388,7 +388,7 @@ try
|
||||
// Local SQLite store by default. With Secrets:Store:Provider=SqlServer this central node's
|
||||
// store becomes the SHARED SQL-Server database instead — no hub, no follower sweep, just
|
||||
// both central nodes reading and writing the same rows, so a clustered pair can never ship
|
||||
// sites a drifted secret set (Task 5). The Layer-A pre-host expander above resolves this
|
||||
// sites a drifted secret set. The Layer-A pre-host expander above resolves this
|
||||
// same predicate (SecretsRegistration.UsesCentralSharedStore) so it always reads the store
|
||||
// this node actually serves. See SecretsRegistration for why the SQL gate exists (eager
|
||||
// options validation) and why the connection string can never itself be a ${secret:}
|
||||
@@ -569,7 +569,7 @@ try
|
||||
builder.Services.AddSingleton<Akka.Actor.ActorSystem>(sp =>
|
||||
sp.GetRequiredService<AkkaHostedService>().GetOrCreateActorSystem());
|
||||
|
||||
// Simultaneous-cold-start split-brain guard (Gitea #33, dark switch
|
||||
// Simultaneous-cold-start split-brain guard (dark switch
|
||||
// ScadaBridge:Cluster:BootstrapGuard:Enabled, default off). Registered AFTER the
|
||||
// AkkaHostedService/ActorSystem bridge so its StartAsync resolves the system the main path
|
||||
// built; when the guard is off it no-ops (Akka auto-joined from config seeds), so registering
|
||||
@@ -727,7 +727,7 @@ try
|
||||
// connection to the right pipeline by listener/protocol.
|
||||
app.MapGrpcService<ZB.MOM.WW.ScadaBridge.Communication.Grpc.CentralControlGrpcService>();
|
||||
|
||||
// The pull-only secrets hub that used to be mapped here (scadaproj#3) is retired — Task 11.
|
||||
// The pull-only secrets hub that used to be mapped here is retired.
|
||||
// Sites receive secrets exclusively via Deploy Artifacts; there is no hub endpoint to map
|
||||
// on central and nothing left for a site to sweep. See SecretsRegistration.
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ public enum SecretsNodeRole
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// <b>Task 11 retired ALL secrets replication in ScadaBridge</b> — both the pull-only gRPC hub
|
||||
/// <b>ALL secrets replication is retired in ScadaBridge</b> — both the pull-only gRPC hub
|
||||
/// (central hosts, sites sweep) and the shared-SQL-Server <c>ReplicatingSecretStore</c> path that
|
||||
/// preceded it. ScadaBridge is hub-and-spoke — one central cluster plus N separate site clusters —
|
||||
/// and every topology now gives each SITE node a permanently LOCAL, undecorated SQLite store, so a
|
||||
@@ -293,7 +293,7 @@ public static class SecretsRegistration
|
||||
/// <remarks>
|
||||
/// A Site node (<paramref name="role"/> = <see cref="SecretsNodeRole.Site"/>) always resolves
|
||||
/// to the plain local SQLite store — there is no replicating or decorated store left for a
|
||||
/// site to be given. This is deliberate (Task 11): <c>DeployedSecretsMirror</c> deletes a name
|
||||
/// site to be given. This is deliberate: <c>DeployedSecretsMirror</c> deletes a name
|
||||
/// from the local store directly, and a replication decorator would have published that
|
||||
/// delete fleet-wide.
|
||||
/// </remarks>
|
||||
|
||||
@@ -24,7 +24,10 @@ namespace ZB.MOM.WW.ScadaBridge.Host.Services;
|
||||
public sealed class CentralSharedSeamAuditWriter(ICentralAuditWriter central)
|
||||
: ZB.MOM.WW.Audit.IAuditWriter
|
||||
{
|
||||
/// <inheritdoc />
|
||||
/// <summary>Forwards the audit event to the central direct-write audit path.</summary>
|
||||
/// <param name="auditEvent">The audit event to write.</param>
|
||||
/// <param name="cancellationToken">Token to cancel the write.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
public Task WriteAsync(AuditEvent auditEvent, CancellationToken cancellationToken = default)
|
||||
=> central.WriteAsync(auditEvent, cancellationToken);
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ internal static class SiteKestrelSetup
|
||||
/// this node holding a half-open stream that will never produce another byte and never fail
|
||||
/// either. For LocalDb sync that is the expensive one: the handler sits inside its single-stream
|
||||
/// guard, so the rebooted peer's reconnects are refused while this node still reports itself
|
||||
/// replication-connected (ScadaBridge arch-review 2026-08-19, P4-3).
|
||||
/// replication-connected.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// 60s costs one frame per idle minute per connection and bounds detection at roughly
|
||||
@@ -46,6 +46,9 @@ internal static class SiteKestrelSetup
|
||||
/// governs the gRPC port and the metrics port alike. That is the intent: the metrics listener
|
||||
/// also speaks HTTP/2, and a scrape connection has nothing to lose from a ping.
|
||||
/// </remarks>
|
||||
/// <param name="options">The Kestrel server options to configure.</param>
|
||||
/// <param name="grpcPort">Port for the HTTP/2 gRPC listener.</param>
|
||||
/// <param name="metricsPort">Port for the metrics listener.</param>
|
||||
public static void Configure(KestrelServerOptions options, int grpcPort, int metricsPort)
|
||||
{
|
||||
options.Limits.Http2.KeepAlivePingDelay = Http2KeepAlivePingDelay;
|
||||
|
||||
@@ -154,6 +154,8 @@ public static class SiteLocalDbLegacyMigrator
|
||||
/// falling back to the code default. Relative paths resolve against the process working
|
||||
/// directory — NOT the data volume — because that is where the old code actually put them.
|
||||
/// </summary>
|
||||
/// <param name="config">The application configuration.</param>
|
||||
/// <returns>The absolute path of the legacy tracking database.</returns>
|
||||
internal static string ResolveTrackingPath(IConfiguration config)
|
||||
{
|
||||
var connectionString =
|
||||
@@ -184,6 +186,8 @@ public static class SiteLocalDbLegacyMigrator
|
||||
}
|
||||
|
||||
/// <summary>Resolves the legacy event-log path from the OLD key, falling back to the code default.</summary>
|
||||
/// <param name="config">The application configuration.</param>
|
||||
/// <returns>The absolute path of the legacy event-log database.</returns>
|
||||
internal static string ResolveEventLogPath(IConfiguration config)
|
||||
{
|
||||
var path = config["ScadaBridge:SiteEventLog:DatabasePath"] ?? DefaultEventLogPath;
|
||||
@@ -203,6 +207,8 @@ public static class SiteLocalDbLegacyMigrator
|
||||
/// mounted data volume (<c>./data/</c>), so on the docker rig there is a real file here
|
||||
/// with real buffered messages — this migration is not the usual no-op.
|
||||
/// </summary>
|
||||
/// <param name="config">The application configuration.</param>
|
||||
/// <returns>The absolute path of the legacy store-and-forward database.</returns>
|
||||
internal static string ResolveStoreAndForwardPath(IConfiguration config)
|
||||
{
|
||||
var path = config["ScadaBridge:StoreAndForward:SqliteDbPath"] ?? DefaultStoreAndForwardPath;
|
||||
@@ -222,6 +228,8 @@ public static class SiteLocalDbLegacyMigrator
|
||||
/// to the code default. Like store-and-forward — and unlike the two Phase 1 paths — this
|
||||
/// default is inside the mounted data volume, so a real deployment has real config here.
|
||||
/// </summary>
|
||||
/// <param name="config">The application configuration.</param>
|
||||
/// <returns>The absolute path of the legacy site configuration database.</returns>
|
||||
internal static string ResolveSiteStoragePath(IConfiguration config)
|
||||
{
|
||||
var path = config["ScadaBridge:Database:SiteDbPath"] ?? DefaultSiteStoragePath;
|
||||
@@ -471,10 +479,12 @@ public static class SiteLocalDbLegacyMigrator
|
||||
/// <para>
|
||||
/// This never shows up on Linux or macOS: POSIX rename does not care about open handles,
|
||||
/// so the docker rig and developer machines migrate cleanly. It reproduces on the first
|
||||
/// Windows site node that boots with a pre-Phase-2 database beside it — which is every
|
||||
/// Windows site node that boots with a pre-consolidation database beside it — which is every
|
||||
/// Windows install being upgraded, exactly once.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <param name="legacyPath">The path of the legacy SQLite file.</param>
|
||||
/// <returns>A read-only, non-pooled SQLite connection string for the file.</returns>
|
||||
internal static string LegacyReadOnlyConnectionString(string legacyPath)
|
||||
=> $"Data Source={legacyPath};Mode=ReadOnly;Pooling=False";
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@ public static class SiteLocalDbSetup
|
||||
/// masquerade as the other's.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <c>secret_mirror_status</c> is the twelfth entry (review finding M1). It is per-node
|
||||
/// <c>secret_mirror_status</c> is the twelfth entry. It is per-node
|
||||
/// DATA — one row per node, keyed by <c>node_name</c>, each written only by its own node —
|
||||
/// but it is replicated precisely so the active node can read the standby's row and put
|
||||
/// it on the site health report (only the active node sends one). Explicit primary key,
|
||||
@@ -95,7 +95,7 @@ public static class SiteLocalDbSetup
|
||||
|
||||
/// <summary>
|
||||
/// Creates the site node's tables, opts them into change capture when this node
|
||||
/// replicates, and migrates any pre-Phase-1 databases in.
|
||||
/// replicates, and migrates any pre-consolidation databases in.
|
||||
/// </summary>
|
||||
/// <param name="db">The LocalDb instance being initialized.</param>
|
||||
/// <param name="config">
|
||||
@@ -148,8 +148,7 @@ public static class SiteLocalDbSetup
|
||||
// ELEVENTH entry to ReplicatedTables on an already-replicating site streams a
|
||||
// snapshot of only that one table, where through 0.2.x the flag was a single
|
||||
// per-database bit and the same edit would have re-streamed all eleven in full, in
|
||||
// both directions (the same holds for secret_mirror_status, the twelfth, added by
|
||||
// review finding M1). Nothing about the first boot changes: enabling replication on a
|
||||
// both directions (the same holds for secret_mirror_status, the twelfth). Nothing about the first boot changes: enabling replication on a
|
||||
// site that has been running without it seeds all twelve at once, the flagged set is
|
||||
// therefore every registered table, and the library sends that as an ordinary full
|
||||
// snapshot. A hypothetical "register the whole list in one call" API would silently
|
||||
@@ -169,7 +168,7 @@ public static class SiteLocalDbSetup
|
||||
else
|
||||
{
|
||||
// Not merely "do not register": actively remove capture this node must not pay for.
|
||||
// The file may have been registered by an EARLIER build (every build before WP1.3
|
||||
// The file may have been registered by an EARLIER build (every earlier build
|
||||
// registered unconditionally), in which case its triggers are still there, still
|
||||
// running two INSERTs plus a json_object of the full row inside every write
|
||||
// transaction, still appending to an oplog with no reader. DeregisterReplicated is
|
||||
@@ -202,7 +201,7 @@ public static class SiteLocalDbSetup
|
||||
// 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.
|
||||
// Deliberately OUTSIDE the guard: an unreplicated node still has to absorb its
|
||||
// pre-Phase-1 files, and there is no peer for its rows to be invisible to.
|
||||
// pre-consolidation files, and there is no peer for its rows to be invisible to.
|
||||
SiteLocalDbLegacyMigrator.Migrate(db, config);
|
||||
}
|
||||
|
||||
@@ -252,6 +251,8 @@ public static class SiteLocalDbSetup
|
||||
/// <c>docs/deployment/topology-guide.md</c>.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <param name="config">The node configuration to inspect.</param>
|
||||
/// <returns><see langword="true"/> when replication is configured on this node; otherwise <see langword="false"/>.</returns>
|
||||
internal static bool ReplicationIsConfigured(IConfiguration config)
|
||||
{
|
||||
var section = config.GetSection("LocalDb:Replication");
|
||||
@@ -261,8 +262,7 @@ public static class SiteLocalDbSetup
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets <c>auto_vacuum = INCREMENTAL</c> on the consolidated LocalDb file — arch-review
|
||||
/// 2026-08-19 P4-6 — but ONLY when that file does not exist yet.
|
||||
/// Sets <c>auto_vacuum = INCREMENTAL</c> on the consolidated LocalDb file, but ONLY when that file does not exist yet.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
|
||||
@@ -92,7 +92,7 @@ public static class SiteServiceRegistration
|
||||
// The shim is gone; SiteLocalDbDirectoryTests still pins the outcome here, since
|
||||
// what this host needs is the guarantee, not any particular owner of it.
|
||||
//
|
||||
// arch-review 2026-08-19 P4-6: MUST run before AddZbLocalDb — its singleton
|
||||
// MUST run before AddZbLocalDb — its singleton
|
||||
// factory is lazy, but the ordering still has to be enforced here rather than
|
||||
// relied upon, because it is what makes "PRAGMA auto_vacuum = INCREMENTAL" take
|
||||
// effect at all. SQLite only honors that pragma on a table-less database, and
|
||||
@@ -116,7 +116,7 @@ public static class SiteServiceRegistration
|
||||
|
||||
services.AddDataConnectionLayer();
|
||||
// A Site node's secret store is ALWAYS the local SQLite store — never a replication
|
||||
// decorator. Task 11 retired both the pull-only gRPC secrets hub (central hosts, sites
|
||||
// decorator. Both were retired: the pull-only gRPC secrets hub (central hosts, sites
|
||||
// sweep) and the shared-SQL-Server replication mode that preceded it. This is deliberate,
|
||||
// not merely simplification: ZB.MOM.WW.ScadaBridge.SiteRuntime.Secrets.DeployedSecretsMirror
|
||||
// deletes a name from the local store directly the moment a deploy artifact stops
|
||||
@@ -199,7 +199,7 @@ public static class SiteServiceRegistration
|
||||
services.AddSingleton<Akka.Actor.ActorSystem>(sp =>
|
||||
sp.GetRequiredService<AkkaHostedService>().GetOrCreateActorSystem());
|
||||
|
||||
// Simultaneous-cold-start split-brain guard (Gitea #33, dark switch
|
||||
// Simultaneous-cold-start split-brain guard (dark switch
|
||||
// ScadaBridge:Cluster:BootstrapGuard:Enabled, default off). This is the case the guard was
|
||||
// built for — a shared power event that powers up both site VMs together. Registered AFTER
|
||||
// the AkkaHostedService/ActorSystem bridge (mirrors the Central composition root in
|
||||
@@ -221,8 +221,7 @@ public static class SiteServiceRegistration
|
||||
return new AkkaClusterNodeProvider(akkaService, siteRole);
|
||||
});
|
||||
|
||||
// The EventLogPurgeService runs on every site host node and, as of arch-review
|
||||
// 2026-08-19 P4-4, purges its own site_events file on EVERY node — the old
|
||||
// The EventLogPurgeService runs on every site host node and purges its own site_events file on EVERY node — the old
|
||||
// early-exit-on-standby gate this delegate drove is gone (DataConnectionActor
|
||||
// writes site_events on both site nodes, so a standby that never purged would grow
|
||||
// unbounded on an unreplicated site). Register it to delegate to
|
||||
|
||||
@@ -27,7 +27,7 @@ public static class StartupValidator
|
||||
var seedNodes = configuration.GetSection("ScadaBridge:Cluster:SeedNodes").Get<List<string>>();
|
||||
|
||||
// Secrets replication (the gRPC hub, and the SQL-Server ReplicatingSecretStore mode that
|
||||
// preceded it) is retired in ScadaBridge (Task 11) — sites receive secrets exclusively via
|
||||
// preceded it) is retired in ScadaBridge — sites receive secrets exclusively via
|
||||
// Deploy Artifacts, and a replicated site store would publish DeployedSecretsMirror's local
|
||||
// deletes fleet-wide. Checked unconditionally (both roles): a Site used to be the gRPC
|
||||
// hub's follower half, so the flag was never Central-only.
|
||||
@@ -56,7 +56,7 @@ public static class StartupValidator
|
||||
.Require("ScadaBridge:Node:RemotingPort",
|
||||
_ => portValid,
|
||||
"must be 1-65535")
|
||||
// Retired for BOTH roles (Task 11) — a Site could set this too, as the gRPC hub's
|
||||
// Retired for BOTH roles — a Site could set this too, as the gRPC hub's
|
||||
// follower half. Deliberately NOT folded into a role-scoped block below.
|
||||
.Require("Secrets:Replication:Enabled",
|
||||
_ => !secretsReplicationEnabled,
|
||||
@@ -192,8 +192,8 @@ public static class StartupValidator
|
||||
+ "endpoint under ScadaBridge:Communication:CentralGrpcEndpoints "
|
||||
+ "(e.g. http://scadabridge-central-a:8083). Central nodes leave it empty.");
|
||||
|
||||
// The site hot-path audit writer's SQLite file (arch-review remediation
|
||||
// WP1.2). SqliteAuditWriterOptions.DatabasePath has no default (mirrors
|
||||
// The site hot-path audit writer's SQLite file (arch-review remediation).
|
||||
// SqliteAuditWriterOptions.DatabasePath has no default (mirrors
|
||||
// LocalDb:Path) — an unset value would previously fall back to a bare
|
||||
// "auditlog.db" resolved relative to the process CWD, which on the docker
|
||||
// rig is the container's ephemeral overlayfs, not the mounted /app/data
|
||||
@@ -245,6 +245,8 @@ public static class StartupValidator
|
||||
}
|
||||
|
||||
/// <summary>Non-fatal configuration findings. Collected separately from <see cref="Validate"/> because this runs before the logger exists; Program.cs logs each entry at Warning once Serilog is configured.</summary>
|
||||
/// <param name="configuration">The application configuration to inspect.</param>
|
||||
/// <returns>The warning messages to log; empty when there are none.</returns>
|
||||
public static IReadOnlyList<string> CollectWarnings(IConfiguration configuration)
|
||||
{
|
||||
var role = configuration.GetSection("ScadaBridge:Node")["Role"];
|
||||
@@ -270,7 +272,7 @@ public static class StartupValidator
|
||||
// both central nodes to deploy the SAME secret set; a per-node store means each node has
|
||||
// silently drifted the moment anyone edits a secret on only one of them.
|
||||
//
|
||||
// No Secrets:Replication:Enabled gate here any more (Task 11): that flag is now a hard
|
||||
// No Secrets:Replication:Enabled gate here any more: that flag is now a hard
|
||||
// Validate() failure on either role (the gRPC hub and SQL-Server secrets replication are
|
||||
// both retired), so CollectWarnings never runs with it true on a config that also passed
|
||||
// Validate — the old gate that suppressed this warning when replication was on is dead code
|
||||
@@ -284,8 +286,8 @@ public static class StartupValidator
|
||||
+ "deploy its own secret set to sites; set Secrets:Store:Provider=SqlServer");
|
||||
}
|
||||
|
||||
// Review round 1, Minor #4: a config that still carries Secrets:Replication or
|
||||
// Secrets:GrpcHub child keys — left over from before Task 11 retired the hub — is not
|
||||
// A config that still carries Secrets:Replication or
|
||||
// Secrets:GrpcHub child keys — left over from before the hub was retired — is not
|
||||
// itself an error (Validate already rejects Enabled=true outright), but every key under
|
||||
// either section is now silently ignored. That is worth a Warning so an operator cleans
|
||||
// QA/prod appsettings rather than assuming a still-present Mode/BearerToken/Endpoint value
|
||||
@@ -307,7 +309,7 @@ public static class StartupValidator
|
||||
|
||||
/// <summary>
|
||||
/// Whether <c>Secrets:Replication:Enabled</c> is "set" — read as a raw string rather than
|
||||
/// <c>IConfiguration.GetValue<bool></c> (review round 1, Minor #3), which throws a raw
|
||||
/// <c>IConfiguration.GetValue<bool></c> which throws a raw
|
||||
/// binder <see cref="InvalidOperationException"/> on a non-boolean value like <c>"yes"</c> or
|
||||
/// <c>"1"</c>, so a malformed leftover key would surface as an unrelated parse crash instead of
|
||||
/// the designed retirement message in <see cref="Validate"/>. Any value other than absent,
|
||||
|
||||
Reference in New Issue
Block a user