docs(comments): strip internal task/milestone/bundle bookkeeping from code comments
Remove project bookkeeping citations from shipped code comments across the solution: hyphenated task IDs (WP-14, StoreAndForward-025), milestone/task/ issue refs (M3, Task 4, Audit Log #23, #21), Bundle X task-bundle labels, and C/D/K/S/T phase labels. Comment text only — no code logic, string/log literals, or XML-doc structure changed. Genuine descriptions are preserved (only the citation is stripped), and technical lookalikes are retained (UTF-8, SHA-256, T00:00:00, M365, UTC-5, pre-C4/pre-C5 schema versions). Flagged by the new CommentChecker TaskReferenceInComment / TrackingReferenceInComment checks plus targeted grep passes; full solution builds clean, append-only guard tests pass.
This commit is contained in:
@@ -24,7 +24,7 @@ namespace ZB.MOM.WW.ScadaBridge.Host.Actors;
|
||||
/// Creates the actor system on start, registers actors, and triggers
|
||||
/// CoordinatedShutdown on stop.
|
||||
///
|
||||
/// WP-3: Transport heartbeat is explicitly configured in HOCON from CommunicationOptions.
|
||||
/// Transport heartbeat is explicitly configured in HOCON from CommunicationOptions.
|
||||
/// </summary>
|
||||
public class AkkaHostedService : IHostedService
|
||||
{
|
||||
@@ -39,7 +39,7 @@ public class AkkaHostedService : IHostedService
|
||||
/// Guards the one-time creation of <see cref="_actorSystem"/> in
|
||||
/// <see cref="GetOrCreateActorSystem"/> so <see cref="StartAsync"/> and a concurrent
|
||||
/// health-probe resolution of the DI <see cref="ActorSystem"/> singleton race to create
|
||||
/// it exactly once (HOST-021).
|
||||
/// it exactly once.
|
||||
/// </summary>
|
||||
private readonly object _actorSystemLock = new();
|
||||
/// <summary>
|
||||
@@ -51,14 +51,14 @@ public class AkkaHostedService : IHostedService
|
||||
private readonly List<IDisposable> _trackedDisposables = new();
|
||||
|
||||
/// <summary>
|
||||
/// NotificationService-020 guard: sentinel that flips to <c>true</c> the
|
||||
/// Sentinel that flips to <c>true</c> the
|
||||
/// first time a Notification-category S&F delivery handler is registered
|
||||
/// on this hosted service instance. <see cref="StoreAndForwardService.RegisterDeliveryHandler"/>
|
||||
/// is last-write-wins on category, so a future code change that introduces
|
||||
/// a second registration path (e.g. a role-branch + helper that both call
|
||||
/// the registration) would silently overwrite the canonical
|
||||
/// <c>NotificationForwarder</c> handler with whatever the loser registers —
|
||||
/// the prior NS-001 fix did exactly this, and was silently superseded
|
||||
/// a prior fix did exactly this, and was silently superseded
|
||||
/// when the central-only redesign moved delivery to <c>NotificationOutbox</c>.
|
||||
/// This sentinel makes the duplicate noisy at startup so a maintainer
|
||||
/// re-introducing the second path sees it immediately.
|
||||
@@ -99,7 +99,7 @@ public class AkkaHostedService : IHostedService
|
||||
/// <returns>A task representing the asynchronous operation.</returns>
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
// HOST-021: create (or reuse) the externally-owned, process-singleton ActorSystem. A
|
||||
// Create (or reuse) the externally-owned, process-singleton ActorSystem. A
|
||||
// health probe may already have created it via the DI singleton bridge
|
||||
// (GetOrCreateActorSystem) before this hosted service's StartAsync ran; either way the
|
||||
// call yields the one instance and sets _actorSystem. Actor registration below then
|
||||
@@ -132,7 +132,7 @@ public class AkkaHostedService : IHostedService
|
||||
/// whichever runs first creates the system exactly once.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// HOST-021: the <see cref="ActorSystem"/> is an externally-owned process singleton — its
|
||||
/// The <see cref="ActorSystem"/> is an externally-owned process singleton — its
|
||||
/// lifecycle is this hosted service's (created here, torn down via
|
||||
/// <c>CoordinatedShutdown</c> in <see cref="StopAsync"/>). It MUST be registered in DI as a
|
||||
/// <b>singleton resolved through this method</b>, never as a transient/scoped factory:
|
||||
@@ -165,7 +165,7 @@ public class AkkaHostedService : IHostedService
|
||||
// For site nodes, include a site-specific role (e.g., "site-SiteA") alongside the base role
|
||||
var roles = BuildRoles();
|
||||
|
||||
// Host-006: HOCON is assembled in a dedicated builder that quotes/escapes every
|
||||
// HOCON is assembled in a dedicated builder that quotes/escapes every
|
||||
// interpolated value, so a hostname, seed node or strategy containing a quote,
|
||||
// backslash or whitespace cannot corrupt the configuration document.
|
||||
var hocon = BuildHocon(_nodeOptions, _clusterOptions, roles,
|
||||
@@ -195,13 +195,13 @@ public class AkkaHostedService : IHostedService
|
||||
/// Builds the Akka HOCON configuration document. Every interpolated value is
|
||||
/// routed through <see cref="QuoteHocon"/> (string values) so a hostname,
|
||||
/// seed-node URI, role or split-brain strategy containing a quote, backslash or
|
||||
/// whitespace cannot corrupt the document or be silently misparsed (Host-006).
|
||||
/// whitespace cannot corrupt the document or be silently misparsed.
|
||||
///
|
||||
/// Host-012: the <c>keep-oldest down-if-alone</c> flag is emitted from
|
||||
/// The <c>keep-oldest down-if-alone</c> flag is emitted from
|
||||
/// <see cref="ClusterOptions.DownIfAlone"/> rather than hard-coded, so the bound
|
||||
/// configuration value is actually consumed.
|
||||
///
|
||||
/// Host-013: every duration is rendered via <see cref="DurationHocon"/> in
|
||||
/// Every duration is rendered via <see cref="DurationHocon"/> in
|
||||
/// milliseconds, so sub-second cluster timing values (e.g. a 750ms heartbeat) are
|
||||
/// preserved exactly instead of being rounded to whole seconds.
|
||||
/// </summary>
|
||||
@@ -275,7 +275,7 @@ akka {{
|
||||
/// HOCON parser accepts a <c>ms</c> suffix, so emitting whole milliseconds
|
||||
/// preserves sub-second configuration exactly — a 750ms heartbeat stays 750ms
|
||||
/// rather than being rounded to <c>1s</c> (or, for sub-half-second values,
|
||||
/// silently collapsing to a degenerate <c>0s</c>) — Host-013.
|
||||
/// silently collapsing to a degenerate <c>0s</c>).
|
||||
/// </summary>
|
||||
private static string DurationHocon(TimeSpan duration)
|
||||
{
|
||||
@@ -354,7 +354,7 @@ akka {{
|
||||
|
||||
/// <summary>
|
||||
/// Registers central-side actors including the CentralCommunicationActor.
|
||||
/// WP-4: Central communication actor routes all 8 message patterns to sites.
|
||||
/// Central communication actor routes all 8 message patterns to sites.
|
||||
/// </summary>
|
||||
private void RegisterCentralActors()
|
||||
{
|
||||
@@ -400,7 +400,7 @@ akka {{
|
||||
.GetRequiredService<IOptions<ZB.MOM.WW.ScadaBridge.NotificationOutbox.NotificationOutboxOptions>>().Value;
|
||||
var outboxLogger = _serviceProvider.GetRequiredService<ILoggerFactory>()
|
||||
.CreateLogger<ZB.MOM.WW.ScadaBridge.NotificationOutbox.NotificationOutboxActor>();
|
||||
// M4 Bundle B: central direct-write audit writer for dispatcher attempt
|
||||
// Central direct-write audit writer for dispatcher attempt
|
||||
// + terminal events. Resolved once from the root provider — the writer
|
||||
// is a singleton and stateless, opening per-call DI scopes internally
|
||||
// to resolve the scoped IAuditLogRepository.
|
||||
@@ -433,12 +433,12 @@ akka {{
|
||||
commService?.SetNotificationOutbox(outboxProxy);
|
||||
_logger.LogInformation("NotificationOutbox singleton created and registered with CentralCommunicationActor");
|
||||
|
||||
// Audit Log (#23) — central singleton mirrors the Notification Outbox
|
||||
// Audit Log — central singleton mirrors the Notification Outbox
|
||||
// pattern. The IngestAuditEvents gRPC handler lives on SiteStreamGrpcServer
|
||||
// (Communication.Grpc); a central node hosting that server (M6 reconciliation
|
||||
// (Communication.Grpc); a central node hosting that server (reconciliation
|
||||
// path) hands the proxy in via SetAuditIngestActor below. When the gRPC
|
||||
// server is not registered (current central topology), the host still
|
||||
// brings the singleton up so a Bundle H in-process test (or a future
|
||||
// brings the singleton up so an in-process test (or a future
|
||||
// direct caller) can Ask the proxy without further wiring.
|
||||
// IAuditLogRepository is a SCOPED EF Core service, so the singleton
|
||||
// actor takes the root IServiceProvider and creates a fresh scope per
|
||||
@@ -471,15 +471,15 @@ akka {{
|
||||
// Hand the proxy to the SiteStreamGrpcServer (if registered on this node)
|
||||
// so the IngestAuditEvents RPC routes incoming site batches to the singleton.
|
||||
// The gRPC server is currently only registered on Site nodes; on a central
|
||||
// node this resolves to null and the wiring is a no-op until M6 (which
|
||||
// brings central-hosted gRPC + a real site→central client).
|
||||
// node this resolves to null and the wiring is a no-op unless a future
|
||||
// central-hosted gRPC server (or a real site→central client) is added.
|
||||
var grpcServer = _serviceProvider.GetService<ZB.MOM.WW.ScadaBridge.Communication.Grpc.SiteStreamGrpcServer>();
|
||||
grpcServer?.SetAuditIngestActor(auditIngestProxy);
|
||||
_logger.LogInformation(
|
||||
"AuditLogIngestActor singleton created (gRPC server bound: {GrpcBound})",
|
||||
grpcServer is not null);
|
||||
|
||||
// Audit Log (#23) M6 Bundle E (T7): subscribe the per-site stalled
|
||||
// Subscribe the per-site stalled
|
||||
// telemetry tracker to the actor system EventStream NOW that the
|
||||
// system exists. The tracker mirrors every
|
||||
// SiteAuditTelemetryStalledChanged publication (from
|
||||
@@ -504,11 +504,11 @@ akka {{
|
||||
_logger.LogInformation("SiteAuditTelemetryStalledTracker subscribed to EventStream");
|
||||
}
|
||||
|
||||
// Site Call Audit (#22) — central singleton mirrors the AuditLogIngest
|
||||
// and NotificationOutbox patterns. M3's dual-write transaction routes
|
||||
// Site Call Audit — central singleton mirrors the AuditLogIngest
|
||||
// and NotificationOutbox patterns. The dual-write transaction routes
|
||||
// SiteCalls upserts through AuditLogIngestActor's own scope-per-message
|
||||
// ISiteCallAuditRepository resolution, so this singleton is not on the
|
||||
// M3 happy-path hot path; it exists so direct-write callers Ask through
|
||||
// happy-path hot path; it exists so direct-write callers Ask through
|
||||
// a stable cluster proxy without further wiring. The central→site
|
||||
// Retry/Discard relay now lives in this actor (see the
|
||||
// RegisterCentralCommunication wiring below); the reconciliation puller
|
||||
@@ -532,12 +532,12 @@ akka {{
|
||||
var siteCallAuditSingletonManager =
|
||||
_actorSystem!.ActorOf(siteCallAuditSingletonProps, "site-call-audit-singleton");
|
||||
|
||||
// SiteCallAudit-002 graceful-handover hook. The default singleton handover
|
||||
// Graceful-handover hook. The default singleton handover
|
||||
// path waits for the actor's `ReceiveAsync` task to complete before
|
||||
// signalling `HandOverDone` to the new oldest node — so an in-flight
|
||||
// EF `UpsertAsync` IS waited for during a *clean* coordinated shutdown
|
||||
// (the cluster-leave phase below fires before the singleton terminates).
|
||||
// The risk the finding tracks is the seam between in-flight async work
|
||||
// The risk here is the seam between in-flight async work
|
||||
// and the cluster-leave + singleton-stop sequence: we bound it by
|
||||
// issuing an explicit `GracefulStop` to the singleton manager early
|
||||
// in `cluster-leave`, with a timeout that lets the running upsert + SQL
|
||||
@@ -546,7 +546,7 @@ akka {{
|
||||
// coordinated shutdown indefinitely — exceeding it falls through to
|
||||
// the existing PoisonPill termination path. Same pattern is suitable
|
||||
// for the NotificationOutbox singleton; not added here to keep this
|
||||
// change minimal (out of NS-020's scope).
|
||||
// change minimal.
|
||||
var siteCallAuditShutdown = Akka.Actor.CoordinatedShutdown.Get(_actorSystem);
|
||||
siteCallAuditShutdown.AddTask(
|
||||
Akka.Actor.CoordinatedShutdown.PhaseClusterLeave,
|
||||
@@ -577,7 +577,7 @@ akka {{
|
||||
// SetNotificationOutbox wiring above.
|
||||
commService?.SetSiteCallAudit(siteCallAuditProxy);
|
||||
|
||||
// Task 5 (#22): hand the CentralCommunicationActor to the SiteCallAudit
|
||||
// Hand the CentralCommunicationActor to the SiteCallAudit
|
||||
// actor so it can relay operator Retry/Discard on parked cached calls to
|
||||
// the owning site (over the per-site ClusterClient via SiteEnvelope).
|
||||
// Mirrors the RegisterAuditIngest / RegisterNotificationOutbox wiring;
|
||||
@@ -588,7 +588,7 @@ akka {{
|
||||
_logger.LogInformation(
|
||||
"SiteCallAuditActor singleton created and registered with CentralCommunicationActor");
|
||||
|
||||
// Audit Log (#23) M6 Bundle B/C — start the two central-only maintenance
|
||||
// Start the two central-only maintenance
|
||||
// singletons that were fully implemented but never instantiated: the
|
||||
// daily AuditLog partition-switch purge (AuditLogPurgeActor) and the
|
||||
// periodic per-site audit-event reconciliation pull
|
||||
@@ -699,7 +699,7 @@ akka {{
|
||||
_actorSystem.ActorOf(auditReconProxyProps, "site-audit-reconciliation-proxy");
|
||||
_logger.LogInformation("SiteAuditReconciliationActor singleton created");
|
||||
|
||||
// KPI History (#26, M6) — central singleton that periodically samples the
|
||||
// KPI History — central singleton that periodically samples the
|
||||
// Notification Outbox / Site Call Audit point-in-time KPIs into the
|
||||
// KpiHistorySamples table and runs the daily retention purge. Mirrors the
|
||||
// audit-log-purge singleton pattern above: a ClusterSingletonManager pins
|
||||
@@ -832,7 +832,7 @@ akka {{
|
||||
var dmLogger = _serviceProvider.GetRequiredService<ILoggerFactory>()
|
||||
.CreateLogger<DeploymentManagerActor>();
|
||||
|
||||
// WP-34: Create DCL Manager Actor for tag subscriptions
|
||||
// Create DCL Manager Actor for tag subscriptions
|
||||
var dclFactory = _serviceProvider.GetService<ZB.MOM.WW.ScadaBridge.DataConnectionLayer.IDataConnectionFactory>();
|
||||
var dclOptions = _serviceProvider.GetService<IOptions<ZB.MOM.WW.ScadaBridge.DataConnectionLayer.DataConnectionOptions>>()?.Value
|
||||
?? new ZB.MOM.WW.ScadaBridge.DataConnectionLayer.DataConnectionOptions();
|
||||
@@ -841,7 +841,7 @@ akka {{
|
||||
{
|
||||
var healthCollector = _serviceProvider.GetRequiredService<ZB.MOM.WW.ScadaBridge.HealthMonitoring.ISiteHealthCollector>();
|
||||
var siteEventLogger = _serviceProvider.GetService<ZB.MOM.WW.ScadaBridge.SiteEventLogging.ISiteEventLogger>();
|
||||
// T17: the verify-endpoint probe builds an OPC UA ApplicationConfiguration directly,
|
||||
// The verify-endpoint probe builds an OPC UA ApplicationConfiguration directly,
|
||||
// so the manager needs the same deployment-wide OpcUaGlobalOptions the
|
||||
// DataConnectionFactory feeds to RealOpcUaClient when creating connections.
|
||||
var opcUaGlobalOptions = _serviceProvider
|
||||
@@ -854,7 +854,7 @@ akka {{
|
||||
_logger.LogInformation("Data Connection Layer manager actor created");
|
||||
}
|
||||
|
||||
// T17 / D6 — per-node OPC UA certificate-store actor. Created on EVERY
|
||||
// Per-node OPC UA certificate-store actor. Created on EVERY
|
||||
// site node (NOT a singleton) at a well-known name so the Deployment
|
||||
// Manager singleton can fan a trust/remove out to BOTH nodes' PKI stores
|
||||
// (node-a + node-b) and keep them in lock-step across failover. It needs
|
||||
@@ -875,7 +875,7 @@ akka {{
|
||||
|
||||
// Notify-and-fetch: the deployment config fetcher pulls a deployment's flattened
|
||||
// config from central over HTTP. Used by BOTH the active singleton
|
||||
// (RefreshDeploymentCommand, Task 10) AND the standby replication path — the active
|
||||
// (RefreshDeploymentCommand) AND the standby replication path — the active
|
||||
// node now replicates only the deployment id and the standby fetches the config
|
||||
// itself, so a large config never crosses the intra-site Akka hop. Resolve once.
|
||||
var deploymentConfigFetcher =
|
||||
@@ -933,7 +933,7 @@ akka {{
|
||||
|
||||
var dmProxy = _actorSystem.ActorOf(proxyProps, "deployment-manager-proxy");
|
||||
|
||||
// WP-4: Create SiteCommunicationActor for receiving messages from central
|
||||
// Create SiteCommunicationActor for receiving messages from central
|
||||
var siteCommActor = _actorSystem.ActorOf(
|
||||
Props.Create(() => new SiteCommunicationActor(
|
||||
_nodeOptions.SiteId!,
|
||||
@@ -974,7 +974,7 @@ akka {{
|
||||
if (storeAndForwardService != null)
|
||||
{
|
||||
// Initialize SQLite schema and start the retry timer. Must complete before
|
||||
// any actor or HTTP handler touches the service. Host-005: awaited rather
|
||||
// any actor or HTTP handler touches the service. Awaited rather
|
||||
// than blocked via GetAwaiter().GetResult() — no thread-pool starvation /
|
||||
// sync-context deadlock risk, and exceptions surface as their original type.
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
@@ -1010,9 +1010,9 @@ akka {{
|
||||
// NotificationSubmitAck as the outcome (accepted → delivered; not accepted
|
||||
// or timeout → throw → transient → keep buffering). Central owns SMTP.
|
||||
//
|
||||
// NotificationService-020: register exactly once. The sentinel guard
|
||||
// Register exactly once. The sentinel guard
|
||||
// catches a second registration path that re-introduces the dead
|
||||
// NS-001 site-SMTP handler — see the sentinel's XML doc above for the
|
||||
// site-SMTP handler — see the sentinel's XML doc above for the
|
||||
// historical context. Throwing here is intentional: a silent overwrite
|
||||
// by a future maintainer would invert the design back to site-side
|
||||
// delivery (NotificationForwarder vs. NotificationDeliveryService).
|
||||
@@ -1070,7 +1070,7 @@ akka {{
|
||||
contacts.Count, _nodeOptions.SiteId);
|
||||
}
|
||||
|
||||
// Task 18c — per-node startup reconciliation. Created on EVERY site node (NOT a
|
||||
// Per-node startup reconciliation. Created on EVERY site node (NOT a
|
||||
// singleton) so a standby that was DOWN during a deploy self-heals on its next
|
||||
// restart: it reports its local deployed inventory to central via the
|
||||
// SiteCommunicationActor Ask, fetches the gap (missing/stale) over HTTP, and
|
||||
@@ -1103,7 +1103,7 @@ akka {{
|
||||
+ "startup self-heal disabled (replication remains the primary path)");
|
||||
}
|
||||
|
||||
// Audit Log (#23) — site-side telemetry actor that drains the SQLite
|
||||
// Audit Log — site-side telemetry actor that drains the SQLite
|
||||
// Pending queue and pushes to central via IngestAuditEvents. Not a
|
||||
// cluster singleton: each site is its own cluster, and the actor reads
|
||||
// node-local SQLite (no replication). The Props are bound to the
|
||||
@@ -1111,7 +1111,7 @@ akka {{
|
||||
// batch SQLite read + gRPC push never contend with the default
|
||||
// dispatcher used by hot-path actors.
|
||||
//
|
||||
// Per Bundle E's brief: the SiteAuditTelemetryActor takes its
|
||||
// The SiteAuditTelemetryActor takes its
|
||||
// collaborators through its constructor, so we resolve them from DI
|
||||
// and pass them in via Props.Create rather than relying on a future
|
||||
// FactoryProvider. The real site→central client is constructed and
|
||||
@@ -1122,7 +1122,7 @@ akka {{
|
||||
.GetRequiredService<IOptions<ZB.MOM.WW.ScadaBridge.AuditLog.Site.Telemetry.SiteAuditTelemetryOptions>>();
|
||||
var siteAuditQueue = _serviceProvider
|
||||
.GetRequiredService<ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services.ISiteAuditQueue>();
|
||||
// Audit Log (#23) Task 2 follow-up: the production site→central audit
|
||||
// Audit Log follow-up: the production site→central audit
|
||||
// push uses the ClusterClient transport via the SiteCommunicationActor,
|
||||
// not the DI-resolved NoOpSiteStreamAuditClient. The NoOp default stays
|
||||
// correct for central/test composition roots (no SiteCommunicationActor);
|
||||
@@ -1137,7 +1137,7 @@ akka {{
|
||||
var siteAuditLogger = _serviceProvider.GetRequiredService<ILoggerFactory>()
|
||||
.CreateLogger<ZB.MOM.WW.ScadaBridge.AuditLog.Site.Telemetry.SiteAuditTelemetryActor>();
|
||||
|
||||
// AuditLog-001: resolve the site-local operation tracking store so the
|
||||
// Resolve the site-local operation tracking store so the
|
||||
// actor can run the combined-telemetry cached-drain in parallel with
|
||||
// the audit-only drain. The store is registered by AddSiteRuntime on
|
||||
// site composition roots; on central it is intentionally absent and
|
||||
@@ -1162,9 +1162,9 @@ akka {{
|
||||
siteTrackingStore is not null);
|
||||
|
||||
// Gate gRPC subscriptions until the actor system and SiteStreamManager are
|
||||
// initialized (REQ-HOST-7).
|
||||
// initialized.
|
||||
//
|
||||
// Host-009: SetReady asserts a deliberately narrow contract. By this point the
|
||||
// SetReady asserts a deliberately narrow contract. By this point the
|
||||
// actor system exists, SiteStreamManager.Initialize has run, and every
|
||||
// role actor (SiteCommunicationActor, deployment-manager singleton,
|
||||
// SiteReplicationActor, the ClusterClient) has been created with ActorOf —
|
||||
@@ -1178,14 +1178,14 @@ akka {{
|
||||
// handshake has completed". Streams opened before SetReady are already
|
||||
// rejected by SiteStreamGrpcServer with StatusCode.Unavailable.
|
||||
var grpcServer = _serviceProvider.GetService<ZB.MOM.WW.ScadaBridge.Communication.Grpc.SiteStreamGrpcServer>();
|
||||
// Audit Log (#23 M6): hand the site-local SqliteAuditWriter (which
|
||||
// Audit Log: hand the site-local SqliteAuditWriter (which
|
||||
// implements ISiteAuditQueue) to the gRPC server so the PullAuditEvents
|
||||
// reconciliation RPC can serve central's pulls. Both the writer and the
|
||||
// gRPC server are singletons — wiring this here keeps the dependency
|
||||
// direction one-way (Host knows both; Communication doesn't reach back
|
||||
// into AuditLog).
|
||||
grpcServer?.SetSiteAuditQueue(siteAuditQueue);
|
||||
// Site Call Audit (#22): hand the site-local OperationTrackingStore to
|
||||
// Site Call Audit: hand the site-local OperationTrackingStore to
|
||||
// the gRPC server so the PullSiteCalls reconciliation RPC can serve
|
||||
// central's self-heal pulls. siteTrackingStore is resolved above with
|
||||
// GetService — present on site composition roots, null on central — so
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<link href="/ZB.MOM.WW.ScadaBridge.Host.styles.css" rel="stylesheet" />
|
||||
<link href="_content/ZB.MOM.WW.ScadaBridge.CentralUI/css/site.css" rel="stylesheet" />
|
||||
<script>
|
||||
// T34b: apply the persisted theme to <html> BEFORE first paint so a
|
||||
// Apply the persisted theme to <html> BEFORE first paint so a
|
||||
// dark-mode user never sees a light flash. Mirrors window.sbTheme.get/apply
|
||||
// (theme.js) — same key, same rule — but inlined so it runs without waiting
|
||||
// on the deferred script include. localStorage access is guarded for
|
||||
|
||||
@@ -5,7 +5,7 @@ using ZB.MOM.WW.ScadaBridge.InboundAPI;
|
||||
namespace ZB.MOM.WW.ScadaBridge.Host.Health;
|
||||
|
||||
/// <summary>
|
||||
/// InboundAPI-008 / InboundAPI-022: production implementation of
|
||||
/// Production implementation of
|
||||
/// <see cref="IActiveNodeGate"/> backed by the running Akka.NET cluster.
|
||||
///
|
||||
/// The inbound API is "Central cluster only (active node)" — a standby central
|
||||
|
||||
@@ -6,7 +6,7 @@ using Microsoft.Extensions.Logging;
|
||||
namespace ZB.MOM.WW.ScadaBridge.Host.Health;
|
||||
|
||||
/// <summary>
|
||||
/// M2.14 (#28): readiness check that verifies every <b>required central cluster
|
||||
/// Readiness check that verifies every <b>required central cluster
|
||||
/// singleton</b> is reachable from this node, satisfying the "required cluster
|
||||
/// singletons running (if applicable)" clause of REQ-HOST-4a. Register it
|
||||
/// <see cref="ZB.MOM.WW.Health.ZbHealthTags.Ready"/>-tagged in the Central-role
|
||||
|
||||
@@ -7,17 +7,17 @@ namespace ZB.MOM.WW.ScadaBridge.Host;
|
||||
/// <summary>
|
||||
/// Builds the Serilog <see cref="LoggerConfiguration"/> for the Host process.
|
||||
///
|
||||
/// REQ-HOST-8 / Host-011: the configured minimum level comes from
|
||||
/// The configured minimum level comes from
|
||||
/// <c>ScadaBridge:Logging:MinimumLevel</c> (bound to <see cref="LoggingOptions"/>) so an
|
||||
/// operator editing that key changes the effective log level.
|
||||
///
|
||||
/// REQ-HOST-8 / Host-014: the console and file sinks are read from the standard
|
||||
/// The console and file sinks are read from the standard
|
||||
/// <c>Serilog</c> configuration section via <c>ReadFrom.Configuration</c> — the sink
|
||||
/// set, console output template, file path and rolling interval are all
|
||||
/// configuration-driven (defined in <c>appsettings.json</c>), not hard-coded. The
|
||||
/// explicit <c>MinimumLevel.Is</c> below pins the floor from <see cref="LoggingOptions"/>.
|
||||
///
|
||||
/// Host-020: <c>ScadaBridge:Logging:MinimumLevel</c> is the single source of truth
|
||||
/// <c>ScadaBridge:Logging:MinimumLevel</c> is the single source of truth
|
||||
/// for the floor — the explicit <c>MinimumLevel.Is</c> call deliberately runs
|
||||
/// AFTER <c>ReadFrom.Configuration</c> so a <c>Serilog:MinimumLevel</c> entry in
|
||||
/// configuration is overridden. To make that precedence visible (so an operator
|
||||
@@ -43,7 +43,7 @@ public static class LoggerConfigurationFactory
|
||||
|
||||
/// <summary>
|
||||
/// Test-visible overload of <see cref="Build(IConfiguration, string, string, string)"/>
|
||||
/// that routes the Host-020 precedence warning through a caller-supplied
|
||||
/// that routes the precedence warning through a caller-supplied
|
||||
/// writer so unit tests can capture it. Production calls the four-arg
|
||||
/// overload which uses <see cref="Console.Error"/>.
|
||||
/// </summary>
|
||||
@@ -51,7 +51,7 @@ public static class LoggerConfigurationFactory
|
||||
/// <param name="nodeRole">Role label added as a log enrichment property.</param>
|
||||
/// <param name="siteId">Site identifier added as a log enrichment property.</param>
|
||||
/// <param name="nodeHostname">Hostname added as a log enrichment property.</param>
|
||||
/// <param name="warningWriter">Writer that receives the one-shot Host-020 override-warning when both keys are present.</param>
|
||||
/// <param name="warningWriter">Writer that receives the one-shot override-warning when both keys are present.</param>
|
||||
/// <returns>The fully configured <see cref="LoggerConfiguration"/> ready to create the Serilog logger.</returns>
|
||||
internal static LoggerConfiguration Build(
|
||||
IConfiguration configuration,
|
||||
@@ -65,7 +65,7 @@ public static class LoggerConfigurationFactory
|
||||
|
||||
var minimumLevel = ParseLevel(loggingOptions.MinimumLevel, warningWriter);
|
||||
|
||||
// Host-020: warn once if the operator also set a Serilog:MinimumLevel —
|
||||
// Warn once if the operator also set a Serilog:MinimumLevel —
|
||||
// they almost certainly expected it to take effect, but the explicit
|
||||
// MinimumLevel.Is call below silently overrides it. The warning is
|
||||
// emitted only when the conflicting key is actually present (a bare
|
||||
@@ -95,7 +95,7 @@ public static class LoggerConfigurationFactory
|
||||
/// Parses a Serilog <see cref="LogEventLevel"/> name, falling back to
|
||||
/// <see cref="LogEventLevel.Information"/> for null/blank/unrecognised values.
|
||||
///
|
||||
/// Host-022: when an operator sets <c>ScadaBridge:Logging:MinimumLevel</c> to a
|
||||
/// When an operator sets <c>ScadaBridge:Logging:MinimumLevel</c> to a
|
||||
/// value that doesn't parse (e.g. the typo "Informaiton"), the helper must NOT
|
||||
/// throw — startup has to succeed so the rest of the system can come up — but
|
||||
/// it MUST make the silent fallback visible. The logger is not yet built at
|
||||
|
||||
@@ -40,7 +40,7 @@ var configuration = new ConfigurationBuilder()
|
||||
.AddCommandLine(args)
|
||||
.Build();
|
||||
|
||||
// WP-11: Full startup validation — fail fast before any DI or actor system setup
|
||||
// Full startup validation — fail fast before any DI or actor system setup
|
||||
StartupValidator.Validate(configuration);
|
||||
|
||||
// Read node options for Serilog enrichment
|
||||
@@ -48,9 +48,9 @@ var nodeRole = configuration["ScadaBridge:Node:Role"]!;
|
||||
var nodeHostname = configuration["ScadaBridge:Node:NodeHostname"] ?? "unknown";
|
||||
var siteId = configuration["ScadaBridge:Node:SiteId"] ?? "central";
|
||||
|
||||
// WP-14: Serilog structured logging.
|
||||
// Host-011: minimum level is driven by ScadaBridge:Logging:MinimumLevel (LoggingOptions).
|
||||
// Host-014: console and file sinks are defined in the `Serilog` configuration
|
||||
// Serilog structured logging.
|
||||
// Minimum level is driven by ScadaBridge:Logging:MinimumLevel (LoggingOptions).
|
||||
// Console and file sinks are defined in the `Serilog` configuration
|
||||
// section (appsettings.json) and applied via ReadFrom.Configuration inside the
|
||||
// factory — the sink set, output template, file path and rolling interval are all
|
||||
// configuration-driven per REQ-HOST-8, not hard-coded here.
|
||||
@@ -67,10 +67,10 @@ try
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
builder.Configuration.AddConfiguration(configuration);
|
||||
|
||||
// WP-14: Serilog
|
||||
// Serilog
|
||||
builder.Host.UseSerilog();
|
||||
|
||||
// WP-17: Windows Service support (no-op when not running as a Windows Service)
|
||||
// Windows Service support (no-op when not running as a Windows Service)
|
||||
builder.Host.UseWindowsService();
|
||||
|
||||
// Shared components
|
||||
@@ -86,32 +86,32 @@ try
|
||||
// AddNotificationService() SMTP machinery above. AddNotificationOutbox binds
|
||||
// NotificationOutboxOptions via BindConfiguration, so no explicit Configure is needed.
|
||||
builder.Services.AddNotificationOutbox();
|
||||
// Transport (#24) — central-only bundle export/import pipeline. Binds
|
||||
// Transport — central-only bundle export/import pipeline. Binds
|
||||
// TransportOptions from ScadaBridge:Transport via BindConfiguration; no
|
||||
// explicit Configure needed.
|
||||
builder.Services.AddTransport();
|
||||
// Audit Log (#23) — central node owns the AuditLogIngestActor singleton +
|
||||
// Audit Log — central node owns the AuditLogIngestActor singleton +
|
||||
// IAuditLogRepository. The site writer chain is still registered (lazy
|
||||
// singletons) but is never resolved on a central node.
|
||||
builder.Services.AddAuditLog(builder.Configuration);
|
||||
// #23 M6-T5 Bundle D — central-only hosted service that rolls
|
||||
// Central-only hosted service that rolls
|
||||
// pf_AuditLog_Month forward monthly. Depends on IPartitionMaintenance
|
||||
// (registered below by AddConfigurationDatabase).
|
||||
builder.Services.AddAuditLogCentralMaintenance(builder.Configuration);
|
||||
// #23 M6 Bundle B/C — central-only registration backing the two
|
||||
// Central-only registration backing the two
|
||||
// maintenance singletons started in AkkaHostedService: the production
|
||||
// ISiteEnumerator + IPullAuditEventsClient (gRPC) used by the
|
||||
// SiteAuditReconciliationActor, plus the AuditLogPurgeOptions /
|
||||
// SiteAuditReconciliationOptions bindings consumed by both singletons.
|
||||
// Central-only by design (it dials sites), kept out of AddAuditLog.
|
||||
builder.Services.AddAuditLogCentralReconciliationClient(builder.Configuration);
|
||||
// Site Call Audit (#22) — central node owns the SiteCallAuditActor
|
||||
// singleton (M3 Bundle F). The extension itself currently registers
|
||||
// Site Call Audit — central node owns the SiteCallAuditActor
|
||||
// singleton. The extension itself currently registers
|
||||
// nothing — actor Props are constructed inline in AkkaHostedService —
|
||||
// but the call is here for symmetry with the other audit composition
|
||||
// roots so future per-actor DI lands without touching Program.cs.
|
||||
builder.Services.AddSiteCallAudit();
|
||||
// KPI History (#26, M6) — central-only. Binds KpiHistoryOptions from
|
||||
// KPI History — central-only. Binds KpiHistoryOptions from
|
||||
// ScadaBridge:KpiHistory and registers the validated options consumed by
|
||||
// the KpiHistoryRecorderActor cluster singleton (started in
|
||||
// AkkaHostedService). Observability/best-effort: NOT readiness-gated.
|
||||
@@ -144,7 +144,7 @@ try
|
||||
// The POST /api/{methodName} endpoint authenticates Bearer tokens
|
||||
// (sbk_<keyId>_<secret>) and authorizes by scope == method name through this
|
||||
// verifier. The legacy peppered-HMAC X-API-Key path — the SQL Server ApiKey
|
||||
// entity, ApiKeyValidator, and IApiKeyHasher — was retired in re-arch C5; the
|
||||
// entity, ApiKeyValidator, and IApiKeyHasher — was retired; the
|
||||
// ScadaBridge:InboundApi:ApiKeyPepper config key is now consumed only as the
|
||||
// library verifier's pepper secret (PepperSecretName below).
|
||||
//
|
||||
@@ -175,7 +175,7 @@ try
|
||||
|
||||
builder.Services.AddZbApiKeyAuth(builder.Configuration, apiKeyStoreSection);
|
||||
|
||||
// Inbound-API key re-arch (C1), additive: expose the library admin facade
|
||||
// Inbound-API key re-arch, additive: expose the library admin facade
|
||||
// (ApiKeyAdminCommands) and the app-side management seam (IInboundApiKeyAdmin)
|
||||
// in the SAME container as AddZbApiKeyAuth, so CLI + CentralUI later create /
|
||||
// list / enable / disable / delete inbound keys and edit their method-scopes
|
||||
@@ -201,7 +201,7 @@ try
|
||||
?? throw new InvalidOperationException("ScadaBridge:Database:ConfigurationDb connection string is required for Central role.");
|
||||
builder.Services.AddConfigurationDatabase(configDbConnectionString);
|
||||
|
||||
// WP-12: Health checks for readiness gating — shared ZB.MOM.WW.Health probes.
|
||||
// Health checks for readiness gating — shared ZB.MOM.WW.Health probes.
|
||||
// Check names and the ready/active tier split are preserved: database + akka-cluster
|
||||
// carry the Ready tag (/health/ready), active-node carries the Active tag (/health/active).
|
||||
// The Akka checks resolve ActorSystem from DI via the transient bridge registered below;
|
||||
@@ -216,7 +216,7 @@ try
|
||||
failureStatus: null,
|
||||
tags: new[] { ZbHealthTags.Ready },
|
||||
args: AkkaClusterStatusPolicy.Default)
|
||||
// M2.14 (#28): readiness ALSO reflects "required cluster singletons running"
|
||||
// Readiness ALSO reflects "required cluster singletons running"
|
||||
// (REQ-HOST-4a). Probes each central singleton's local ClusterSingletonProxy
|
||||
// with a bounded Identify and degrades to Unhealthy if any required singleton
|
||||
// is unreachable. Registered inside the Central-role branch (this is it) so the
|
||||
@@ -233,11 +233,11 @@ try
|
||||
failureStatus: null,
|
||||
tags: new[] { ZbHealthTags.Active });
|
||||
|
||||
// WP-13: Akka.NET bootstrap via hosted service
|
||||
// Akka.NET bootstrap via hosted service
|
||||
builder.Services.AddSingleton<AkkaHostedService>();
|
||||
builder.Services.AddHostedService(sp => sp.GetRequiredService<AkkaHostedService>());
|
||||
|
||||
// HOST-021: bridge the AkkaHostedService-owned ActorSystem to DI as a SINGLETON via
|
||||
// Bridge the AkkaHostedService-owned ActorSystem to DI as a SINGLETON via
|
||||
// GetOrCreateActorSystem(). The shared ZB.MOM.WW.Health Akka checks resolve ActorSystem
|
||||
// from DI, per probe, inside a child scope. ActorSystem is IDisposable, so a TRANSIENT
|
||||
// (or scoped) bridge is captured-and-disposed by each probe's scope — disposing the live
|
||||
@@ -249,7 +249,7 @@ try
|
||||
builder.Services.AddSingleton<Akka.Actor.ActorSystem>(sp =>
|
||||
sp.GetRequiredService<AkkaHostedService>().GetOrCreateActorSystem());
|
||||
|
||||
// InboundAPI-022: register the production IActiveNodeGate implementation so
|
||||
// Register the production IActiveNodeGate implementation so
|
||||
// standby-node gating is actually enforced (the InboundApiEndpointFilter
|
||||
// consults IActiveNodeGate and defaults to "allow" when none is registered,
|
||||
// which leaves the design's "central cluster only (active node)" guarantee
|
||||
@@ -285,12 +285,12 @@ try
|
||||
.GetRequiredService<ILoggerFactory>()
|
||||
.CreateLogger(typeof(MigrationHelper).FullName!);
|
||||
|
||||
// Host-010: tolerate a database that is briefly unreachable at boot
|
||||
// Tolerate a database that is briefly unreachable at boot
|
||||
// (e.g. app and DB containers starting together) with a bounded
|
||||
// exponential backoff before failing fatally.
|
||||
// Host-015: only connection-class (transient) faults are retried — a
|
||||
// Only connection-class (transient) faults are retried — a
|
||||
// schema-version mismatch is permanent and must fail fast on attempt 1.
|
||||
// Host-019: thread the host's ApplicationStopping token into both the
|
||||
// Thread the host's ApplicationStopping token into both the
|
||||
// migration call itself and the inter-attempt Task.Delay so a SIGTERM
|
||||
// during the bounded-retry window (~2 min worst-case) tears down
|
||||
// cleanly instead of being ignored until the loop exhausts.
|
||||
@@ -316,7 +316,7 @@ try
|
||||
app.UseAuthorization();
|
||||
app.UseAntiforgery();
|
||||
|
||||
// Audit Log #23 (M4 Bundle D, T8): emit one InboundRequest/InboundAuthFailure
|
||||
// Emit one InboundRequest/InboundAuthFailure
|
||||
// audit row per call into the inbound API. Placed AFTER UseAuthentication/
|
||||
// UseAuthorization so any HttpContext.User the framework populates is in
|
||||
// place, and scoped to the /api/ prefix so it never observes the Central UI,
|
||||
@@ -324,7 +324,7 @@ try
|
||||
// is responsible for stashing the resolved API key name on
|
||||
// HttpContext.Items (see AuditWriteMiddleware.AuditActorItemKey) AFTER its
|
||||
// in-handler API key validation succeeds.
|
||||
// InboundAPI-025: scope the audit middleware to the inbound API method
|
||||
// Scope the audit middleware to the inbound API method
|
||||
// route (/api/{methodName}) and explicitly exclude the management/audit
|
||||
// sub-trees that share the /api prefix. Without these exclusions the
|
||||
// middleware would emit a spurious ApiInbound audit row for every
|
||||
@@ -343,7 +343,7 @@ try
|
||||
&& HttpMethods.IsPost(ctx.Request.Method),
|
||||
branch => branch.UseAuditWriteMiddleware());
|
||||
|
||||
// WP-12: Map the canonical three-tier health endpoints in one call:
|
||||
// Map the canonical three-tier health endpoints in one call:
|
||||
// /health/ready — Ready-tagged checks (database + akka-cluster). REQ-HOST-4a defines
|
||||
// readiness as cluster membership + DB connectivity, explicitly NOT
|
||||
// cluster leadership, so the leader-only active-node check is excluded
|
||||
@@ -365,11 +365,11 @@ try
|
||||
app.MapCentralUI<ZB.MOM.WW.ScadaBridge.Host.Components.App>();
|
||||
app.MapInboundAPI();
|
||||
app.MapManagementAPI();
|
||||
// Audit Log #23 (M8): CLI-facing /api/audit/{query,export} routes. Same
|
||||
// Audit Log: CLI-facing /api/audit/{query,export} routes. Same
|
||||
// Basic-Auth + LDAP mechanism as /management; gated on the OperationalAudit
|
||||
// / AuditExport role sets.
|
||||
app.MapAuditAPI();
|
||||
// Notify-and-fetch deploy (#2/#3): site-facing token-gated fetch of a staged
|
||||
// Notify-and-fetch deploy: site-facing token-gated fetch of a staged
|
||||
// deployment's flattened config. Machine-to-machine — AllowAnonymous, gated
|
||||
// solely by the per-deployment X-Deployment-Token (no central FallbackPolicy).
|
||||
app.MapDeploymentConfigAPI();
|
||||
@@ -394,10 +394,10 @@ try
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
builder.Configuration.AddConfiguration(configuration);
|
||||
|
||||
// WP-14: Serilog
|
||||
// Serilog
|
||||
builder.Host.UseSerilog();
|
||||
|
||||
// WP-17: Windows Service support (no-op when not running as a Windows Service)
|
||||
// Windows Service support (no-op when not running as a Windows Service)
|
||||
builder.Host.UseWindowsService();
|
||||
|
||||
// Read GrpcPort from config (NodeOptions already has default 8083)
|
||||
@@ -449,7 +449,7 @@ try
|
||||
// Map gRPC service — resolves the singleton SiteStreamGrpcServer from DI
|
||||
app.MapGrpcService<ZB.MOM.WW.ScadaBridge.Communication.Grpc.SiteStreamGrpcServer>();
|
||||
|
||||
// Host-017 / REQ-HOST-7: site-shutdown ordering. ApplicationStopping
|
||||
// REQ-HOST-7: site-shutdown ordering. ApplicationStopping
|
||||
// fires BEFORE IHostedService.StopAsync runs, so the gRPC server
|
||||
// refuses new streams (Unavailable) and cancels every active stream
|
||||
// here — clients observe a clean Cancelled and reconnect — and only
|
||||
|
||||
@@ -47,7 +47,7 @@ public static class SiteServiceRegistration
|
||||
var siteDbPath = config["ScadaBridge:Database:SiteDbPath"] ?? "site.db";
|
||||
services.AddSiteRuntime($"Data Source={siteDbPath}");
|
||||
services.AddDataConnectionLayer();
|
||||
// Audit Log #23 (M3 Bundle F): adapter that surfaces the site id to
|
||||
// Adapter that surfaces the site id to
|
||||
// StoreAndForwardService through DI WITHOUT introducing a
|
||||
// StoreAndForward → HealthMonitoring project-reference cycle. Must be
|
||||
// registered BEFORE AddStoreAndForward so the S&F factory resolves a
|
||||
@@ -58,7 +58,7 @@ public static class SiteServiceRegistration
|
||||
services.AddStoreAndForward();
|
||||
services.AddSiteEventLogging();
|
||||
|
||||
// Site Event Logging (#12) M2.16 (#30) — bridge ISiteEventLogger.FailedWriteCount
|
||||
// Site Event Logging — bridge ISiteEventLogger.FailedWriteCount
|
||||
// into the site health report as a point-in-time SiteEventLogWriteFailures field.
|
||||
// Must come AFTER both AddSiteHealthMonitoring (registers ISiteHealthCollector) and
|
||||
// AddSiteEventLogging (registers ISiteEventLogger). The outer Func<IServiceProvider, …>
|
||||
@@ -68,24 +68,24 @@ public static class SiteServiceRegistration
|
||||
services.AddSiteEventLogHealthMetricsBridge(
|
||||
sp => () => sp.GetRequiredService<ISiteEventLogger>().FailedWriteCount);
|
||||
|
||||
// Audit Log (#23) — site-side hot-path writer + telemetry collaborators.
|
||||
// Audit Log — site-side hot-path writer + telemetry collaborators.
|
||||
// The SiteAuditTelemetryActor itself is registered by AkkaHostedService
|
||||
// in the site-role block; this call wires every DI dependency it (and
|
||||
// ScriptRuntimeContext, when Bundle F lands) reaches for.
|
||||
// ScriptRuntimeContext) reaches for.
|
||||
services.AddAuditLog(config);
|
||||
|
||||
// Audit Log (#23) M2 Bundle G — bridge FallbackAuditWriter primary
|
||||
// Bridge FallbackAuditWriter primary
|
||||
// failures into the site health report payload as
|
||||
// SiteAuditWriteFailures. Must come AFTER both AddSiteHealthMonitoring
|
||||
// (registers ISiteHealthCollector) and AddAuditLog (registers the
|
||||
// NoOp default this call replaces).
|
||||
services.AddAuditLogHealthMetricsBridge();
|
||||
|
||||
// WP-13: Akka.NET bootstrap via hosted service
|
||||
// Akka.NET bootstrap via hosted service
|
||||
services.AddSingleton<AkkaHostedService>();
|
||||
services.AddHostedService(sp => sp.GetRequiredService<AkkaHostedService>());
|
||||
|
||||
// HOST-021: bridge the AkkaHostedService-owned ActorSystem to DI as a SINGLETON via
|
||||
// Bridge the AkkaHostedService-owned ActorSystem to DI as a SINGLETON via
|
||||
// GetOrCreateActorSystem(). The shared ZB.MOM.WW.Health Akka checks resolve ActorSystem
|
||||
// from DI, per probe, inside a child scope. ActorSystem is IDisposable, so a TRANSIENT
|
||||
// (or scoped) bridge is captured-and-disposed by each probe's scope — disposing the live
|
||||
@@ -106,7 +106,7 @@ public static class SiteServiceRegistration
|
||||
return new AkkaClusterNodeProvider(akkaService, siteRole);
|
||||
});
|
||||
|
||||
// SiteEventLogging-019 / #29 (M2.15): the EventLogPurgeService runs on every
|
||||
// The EventLogPurgeService runs on every
|
||||
// site host node but consults this optional gate each tick and early-exits on
|
||||
// the standby. Register it to delegate to IClusterNodeProvider.SelfIsPrimary
|
||||
// (the canonical "this node is Up AND cluster leader" check) so purge runs ONLY
|
||||
@@ -146,7 +146,7 @@ public static class SiteServiceRegistration
|
||||
services.Configure<NotificationOptions>(config.GetSection("ScadaBridge:Notification"));
|
||||
services.Configure<LoggingOptions>(config.GetSection("ScadaBridge:Logging"));
|
||||
|
||||
// Audit Log (#23) — exposes ScadaBridge:Node:NodeName to downstream audit
|
||||
// Audit Log — exposes ScadaBridge:Node:NodeName to downstream audit
|
||||
// writers so they can stamp the SourceNode column. Registered here in
|
||||
// shared bootstrap because every node (central + site) needs it.
|
||||
services.AddSingleton<INodeIdentityProvider, NodeIdentityProvider>();
|
||||
|
||||
@@ -3,14 +3,14 @@ namespace ZB.MOM.WW.ScadaBridge.Host;
|
||||
/// <summary>
|
||||
/// Bounded retry-with-backoff for startup preconditions.
|
||||
///
|
||||
/// Host-010 / REQ-HOST-4a: a Central node applies/validates database migrations
|
||||
/// REQ-HOST-4a: a Central node applies/validates database migrations
|
||||
/// before the host begins serving traffic. In container orchestration the database
|
||||
/// and the app frequently start together, so the database may be briefly
|
||||
/// unreachable. Rather than crashing the process on the first connection failure,
|
||||
/// the migration step is wrapped in this bounded exponential backoff: it tolerates a
|
||||
/// short outage and only fails fatally once attempts are exhausted.
|
||||
///
|
||||
/// Host-015: only <em>transient</em> faults are retried. The optional
|
||||
/// Only <em>transient</em> faults are retried. The optional
|
||||
/// <c>isTransient</c> predicate classifies each exception; a permanent failure
|
||||
/// (e.g. a database schema-version mismatch — which no amount of waiting can fix)
|
||||
/// is rethrown immediately rather than being retried for minutes before the
|
||||
@@ -42,7 +42,7 @@ public static class StartupRetry
|
||||
/// <summary>
|
||||
/// Executes an asynchronous operation with bounded exponential backoff, retrying only transient faults.
|
||||
/// Overload that forwards the retry-loop cancellation token to the operation itself —
|
||||
/// Host-019: needed so callers (e.g. the database-migration step) can honour
|
||||
/// needed so callers (e.g. the database-migration step) can honour
|
||||
/// <c>IHostApplicationLifetime.ApplicationStopping</c> inside the operation as well
|
||||
/// as inside the inter-attempt <c>Task.Delay</c>.
|
||||
/// </summary>
|
||||
@@ -63,7 +63,7 @@ public static class StartupRetry
|
||||
Func<Exception, bool>? isTransient = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Default: treat every exception as transient (preserves the pre-Host-015
|
||||
// Default: treat every exception as transient (preserves the pre-existing
|
||||
// behaviour for callers that do not classify faults).
|
||||
isTransient ??= static _ => true;
|
||||
|
||||
@@ -94,7 +94,7 @@ public static class StartupRetry
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Transient-fault classifier for the database-migration startup step (Host-015).
|
||||
/// Transient-fault classifier for the database-migration startup step.
|
||||
/// Returns <c>true</c> only for connection-class faults that a brief wait can
|
||||
/// resolve — a SQL connection/transport error or a timeout — and <c>false</c>
|
||||
/// for everything else (notably schema-validation <see cref="InvalidOperationException"/>s
|
||||
|
||||
@@ -63,7 +63,7 @@ public static class StartupValidator
|
||||
.Require("ScadaBridge:Database:MachineDataDb",
|
||||
_ => !string.IsNullOrEmpty(configuration.GetSection("ScadaBridge:Database")["MachineDataDb"]),
|
||||
"connection string required for Central")
|
||||
// Task 1.4: the LDAP server key moved into the nested Security:Ldap
|
||||
// The LDAP server key moved into the nested Security:Ldap
|
||||
// sub-section (bound to the shared LdapOptions). Validate the nested key so
|
||||
// the pre-host preflight still fails fast on a missing LDAP server for
|
||||
// Central. The full LDAP option set (SearchBase / ServiceAccountDn /
|
||||
@@ -75,7 +75,7 @@ public static class StartupValidator
|
||||
.Require("ScadaBridge:Security:JwtSigningKey",
|
||||
_ => !string.IsNullOrEmpty(configuration.GetSection("ScadaBridge:Security")["JwtSigningKey"]),
|
||||
"required for Central")
|
||||
// Review #4 (fail-fast pepper validation): the inbound API-key pepper
|
||||
// The inbound API-key pepper
|
||||
// backs the peppered-HMAC secret compare in the shared
|
||||
// ZB.MOM.WW.Auth.ApiKeys verifier (wired by AddZbApiKeyAuth at the
|
||||
// Central composition root). A missing or too-short pepper does not
|
||||
@@ -97,13 +97,13 @@ public static class StartupValidator
|
||||
// collisions + SiteDbPath + seed-node-port loop, in the original order.
|
||||
.When(role == "Site", p =>
|
||||
{
|
||||
// Host-007 / REQ-HOST-4: GrpcPort range, then GrpcPort vs RemotingPort.
|
||||
// GrpcPort range, then GrpcPort vs RemotingPort.
|
||||
p.Require("ScadaBridge:Node:GrpcPort", _ => grpcValid, "must be 1-65535");
|
||||
// Identical GrpcPort/RemotingPort make Kestrel and Akka.Remote contend
|
||||
// for the same TCP port. Uses the resolved GrpcPort, including 8083.
|
||||
p.Require("ScadaBridge:Node:GrpcPort", _ => port != grpcPort, "must differ from RemotingPort");
|
||||
|
||||
// Host-007 / REQ-HOST-4: MetricsPort range, then MetricsPort vs both ports.
|
||||
// MetricsPort range, then MetricsPort vs both ports.
|
||||
p.Require("ScadaBridge:Node:MetricsPort", _ => metricsValid, "must be 1-65535");
|
||||
// The Kestrel metrics (HTTP/1.1) listener port must differ from BOTH the
|
||||
// Akka remoting port and the gRPC port. Uses the resolved MetricsPort (8084 default).
|
||||
@@ -114,7 +114,7 @@ public static class StartupValidator
|
||||
_ => !string.IsNullOrEmpty(configuration.GetSection("ScadaBridge:Database")["SiteDbPath"]),
|
||||
"required for Site nodes");
|
||||
|
||||
// Host-004: a seed node must reference an Akka.Remote endpoint, never the
|
||||
// A seed node must reference an Akka.Remote endpoint, never the
|
||||
// Kestrel HTTP/2 gRPC port. A seed entry whose port equals this node's
|
||||
// GrpcPort would make a joining node attempt an Akka.Remote TCP
|
||||
// association against the gRPC listener and fail.
|
||||
|
||||
@@ -4,7 +4,7 @@ using ZB.MOM.WW.ScadaBridge.StoreAndForward;
|
||||
namespace ZB.MOM.WW.ScadaBridge.Host;
|
||||
|
||||
/// <summary>
|
||||
/// Audit Log #23 (M3 Bundle F): Host-side adapter implementing the
|
||||
/// Host-side adapter implementing the
|
||||
/// optional <see cref="IStoreAndForwardSiteContext"/> the Store-and-Forward
|
||||
/// service consults to stamp cached-call audit telemetry with the site id.
|
||||
/// </summary>
|
||||
|
||||
Reference in New Issue
Block a user