10 KiB
System review remediation (2026-10-21)
Decision and execution record. Source: eight read-only parallel slice reviews of main @ 513fa1aa (findings in the review directory 00-SUMMARY.md + 01..08), excluding items already known from the 2026-09-09 arch review, the deferred-work register and CLAUDE.md. Remediation base for wave 2 docs: 1627513f.
Scope
8 slices (SiteRuntime; site data path DCL/S&F/event log; Communication + cluster; Deploy/Template/ScriptAnalysis/Transport; Management/InboundAPI/Security/ESG/Host; Audit/SiteCalls/Outbox/Notify/KPI/Health; Central UI + CLI; ConfigDB/Commons + gap inventory). ~107 findings: 4 High, ~30 Medium, ~73 Low. Recurring themes: non-collectible Roslyn loads, tracked whole-graph template reads for id/name lookups, uncached LDAP bind + N+1 role mapping, reconciliation paths that replay everything row-at-a-time, options validated but never read, sticky process-wide failure caches.
Owner decisions
- CLI auth = bearer per invocation. One LDAP bind at
POST /auth/token, thenAuthorization: Beareron every management/audit/debug-stream call; Basic remains the fallback. Roles in a token are authoritative until expiry (<=JwtExpiryMinutes, 15); rotatingJwtSigningKeyinvalidates all tokens;StartupValidatorrequires a >= 32-byte key on Central;/auth/tokenanswers 404 underDisableLogin. Database:MachineDataDbremoved (a required boot key nothing read).- Config audit stores summaries + optional retention (script body = length + SHA-256;
AfterStateJsoncapped 256 K;ConfigAudit:RetentionDaysdefault 0 = keep). - Bound Test Run gate: Test Run against a deployed instance needs Deployer + site scope and writes an audit row (closes the Flow Debugger F2/F3 asymmetry).
- Overrides seed, not fire: persisted static overrides loaded at actor start seed script state without firing scripts; alarms still evaluate.
Lanes (one line each)
- A SiteRuntime: actor-name gate (
ActorNameRules) + guardedActorOfso a bad name can never restart the singleton (High); overrides republished/seeded on start; per-site serialised artifact apply with 10-min watchdog; metadata-only reconcile sweep; batchedRoute.To().Get/SetAttributes; DCL connection actor names ASCII-sanitised with collision rejection; startup warm/parallel compile. - B Scripts: compile-cache eviction counter/warning + configurable bound (
ScriptCompileCacheMaxEntries), faulted compiles never cached, scheduler slot reuse, shed-log rate limiting, native-alarm deletes batched into the flush. - C DCL: same-adapter reconnect advances the session generation at issue time (High); MxGateway subscription ids
{session}:{handle};IDataConnection.WriteBatchAndWaitAsyncremoved;MxAlarmStreamReopenDebounce200 ms;ClientNamePrefixnow live; OPC UA discovery async and bounded byOperationTimeoutMs. - D Store-and-forward / event log: per-lane ranked paging, gauge re-seed; event-log reads off the write lock with
KeywordSearchMaxWindow; cap-trim count refresh every 10th batch, one vacuum per tick. - E Communication: failback probe is a real readiness RPC (sentinel
$scadabridge-failback-probe, 5-min probation); site-address refresh single-flight with deadline + sequence; streaming consumers re-resolve addresses on reopen; reconcile cancellation flows from the gRPC call; malformed address keeps the old channel (High); deadSiteContactscode deleted. - F Script analysis / Template Engine: compile gate no longer emits/loads assemblies; trust verdicts cached by code hash;
BlockingWaitAnalyzerwarning;ActorNameRulesat save; lean template projections. - G Deployment: hash-only revision computation; single
FlattenSessionfor bulk deploy pre-filter + prepare; reconcile fixed round trips with 3 retries; system artifact deployment records purged underTerminalDeploymentRecordRetention. - H Site audit: reconciliation pull serves only un-acked rows; new site-only
PushAbandoned/Rejectedstates;IngestAck.rejected_event_ids(field 2); persisted high-water; set-based paged central ingest (High); paged purge; fail-fast writer; per-site state pruning. - I Site calls / outbox / notify / KPI: node-local
OperationTrackingCentralAckledger for un-acked pull; 128-row upsert chunks; conditional-aggregate KPI queries; outbox per-attempt timestamps, Pending/Retrying split claim,DispatchSweepBudget2 min; SMTPMaxConcurrentConnectionsclamps Email parallelism; OAuth2 token cache eviction; KPI fold hour-at-a-time. - J Host / Inbound / ESG: single-flight method compile and warm-up service, relay nonce single-use, in-memory request buffering,
ApiMethodCachepruning, JSON GET/DELETE query params, central definition cache (DefinitionCacheTtl10 s), secret-store fault vs missing PSK, NodeOptions-bound ports. - K Security / CLI: bearer-per-invocation,
RoleMappingCache(30 s, invalidated post-commit by the management handlers), 10 s display-state memo, dead refresh members removed fromJwtTokenService. - L Central UI: blocking-wait warning as Monaco marker, bounded script-analysis payload/concurrency, Topology behind
PollGate, lean projections, bounded caches/rows, safe-invoke fire-and-forget, negative TTL for a faulted pending-updates flight. - M Management: ListInstances SQL paging/prefix search, 409
IMPORT_IN_PROGRESSsingle-flight for large bodies,CompileForPublishonce per save, site address + native-alarm-source name validation, terminalPendingSecuredWritespurge (SecuredWriteTerminalRetention90 d). - N Transport: import validates site gRPC addresses and actor-safe names as blockers; compressed decrypted content pinned on the session and zeroed on every release; template library read once;
StaleInstanceIdscovers derived/composing templates;Transport:SchemaVersionMajorremoved. - O/P ConfigDB: deterministic config-audit paging,
InstanceLocatorsingle projected join, dead repository members deleted, config-audit purge +ConfigAuditOptionsbinding, migrationReviewRemediationIndexes. - NuGet:
System.Security.Cryptography.Xml/Pkcsand SignalR.Common pinned to 10.0.13.
Adversarial-review catches (fixed before merge)
- C reconnect race: the first fix advanced the session generation when the reconnect result was applied, so a new session dying before the actor processed that result had its
Disconnected(stamped with the old generation) dropped as stale — leaving the actor Connected on a dead session with frozen tags. The generation now advances when the reconnect attempt is issued. - H central-fault refusals: central answers DB faults with an empty/partial ack, which the new refusal counting treated as per-row refusals (a SQL maintenance window would mark rows
Rejected). Central now names only row-intrinsic SQL failures inIngestAck.rejected_event_ids; the served high-water mark is persisted so owed rows retire after a site restart; transient faults never count toward reconciliation abandonment. - A data-connection actor names: the actor-name gate covered instances/scripts/alarms but not DCL connection actors; names are sanitised and collisions rejected at deploy.
- I outbox double-send: the new Pending/Retrying split claim back-filled Pending with an offset (
Skip(n)); an operator Retry flipping a Parked row to Pending mid-claim shifted the offset so one notification was claimed twice and delivered twice. The back-fill is now a keyset continuation plus a distinct-id guard. - Also: failback probe sentinel changed to an illegal actor name; alarm aggregator reconcile no longer reopens during a pending backoff reconnect (double subscribe);
RoleMappingCacheinvalidation after commit (lane K review M1).
Deferred to the register
Rows 128-135 of docs/plans/2026-07-08-deferred-work-register.md: TLS production profile; ExternalDefinitionLookupCache invalidation; whole-graph load on template member add/update (04/P6 partial); idle SiteStreamGrpcClientFactory client eviction; compile-cache eviction Meter; dedicated rejected-rows health field; oversize audit row wedge (accepted); possibly redundant config-audit indexes. Row 36 annotated (same free-text LIKE shape on Instances/config audit; ListInstances now prefix-paged). Row 117 closed (already fixed by arch-review MG-6).
Rollout notes
- Not deployed: the docker rig is rebuilt from main; QA (vq03/vq04) and prod (vd03) are not deployed.
- Migration:
ReviewRemediationIndexesaddsAuditLogIX_AuditLog_Kind_OccurredandIX_AuditLog_Actor_Occurred(partition-aligned) plus three compositeAuditLogEntriesindexes. Production appliesdocs/plans/sql/ReviewRemediationIndexes.sqlby hand (index builds on a large partitionedAuditLog: schedule off-hours). Not yet applied to QA/prod. - Mixed version: upgrade central first (new
IngestAckfield 2 and proto additions are additive; an old central ignoresrejected_event_idsand an old site never rejects). Pair upgrades stay both-nodes-together for LocalDb sites. - Config changes: CLI hosts need a central that serves
/auth/token(older centrals fall back to Basic automatically); Central must have a >= 32-byteJwtSigningKeyor fails startup validation; removeDatabase:MachineDataDbandTransport:SchemaVersionMajorfrom appsettings (ignored if left). - New keys (all defaulted):
ScadaBridge:ConfigAudit:{RetentionDays 0, PurgeBatchSize 5000, MaxBatchesPerTick 200, PurgeInterval 1d};Security:RoleMappingCacheSeconds 30;SiteEventLog:{KeywordSearchMaxWindow 7d, CapTrimCountRefreshBatches 10};AuditLogsite writer/reconciler{MaxRejectedPushAttempts 10, PurgeBatchSize 5000, PurgeVacuumPagesPerBatch 1000, MaxPagesPerTick 50};SiteRuntime:ScriptCompileCacheMaxEntries 1024;NotificationOutbox:DispatchSweepBudget 2m;ManagementService:{SecuredWriteTerminalRetention 90d, SecuredWriteTerminalPurgeInterval 1h}; ESGDefinitionCacheTtl 10s(central); DCLMxAlarmStreamReopenDebounce 200ms;DeploymentManager:TerminalDeploymentRecordRetentiongoverns system artifact deployment record purge. - Behaviour changes operators will notice:
instance list --searchis prefix-only; Test Run against a deployed instance needs Deployer + site scope; deploys reject non-actor-safe names (rename before redeploy); a redeployed instance's persisted overrides no longer re-fire value-change scripts on start.