Commit Graph

2414 Commits

Author SHA1 Message Date
Joseph Doherty 95f091cff8 Merge branch 'worktree-agent-a6a0dffcb93fa5007' into arch-review-remediation 2026-08-14 23:43:03 -04:00
Joseph Doherty 950c54c5fc fix(runtime): review findings — recursion-safe run cap, atomic detach counter, summary edge cases, per-row event-log fallback 2026-08-14 23:42:29 -04:00
Joseph Doherty f689f495ef chore(deps): LocalDb 0.2.1 — HLC anchor flush on dereg, bounded sync inbox 2026-08-14 23:37:33 -04:00
Joseph Doherty 3913c6b2ac Merge branch 'worktree-agent-a71052048b4ee11d4' into arch-review-remediation 2026-08-14 23:32:36 -04:00
Joseph Doherty 56c99c92c3 fix(ops): wonder site config gains required audit DB path; explicit LocalDb read-page cap; rate-limited observer drop logging
F1: deploy/wonder-app-vd03/appsettings.Site.json (outside git, WP1.2's
StartupValidator gate applies live on next install/upgrade) was missing the
now-required AuditLog:SiteWriter:DatabasePath, added pointing at
E:\ApiInstall\ScadaBridge\site\data\auditlog.db alongside the file's
existing SiteEventLog/LocalDb paths; scanned deploy/ for other Site-role
appsettings with the same gap (none) and confirmed wonder does not pin
LocalDb:Replication:MaxBatchSize (F2 doesn't apply there).

F2: re-pin an explicit LocalDb:Replication:MaxBatchSize=64 on docker/site-a
node-a and node-b. MaxBatchBytes (2 MB default) only bounds the wire
message via the per-message split in SyncSession.PumpLoopAsync;
MaxBatchSize separately bounds the DB read page in
OplogStore.ReadBatchAboveAsync/SnapshotStreamer, which materializes the
whole page into memory before that split runs. Left at the 500 default, a
reconnect drain of worst-case config_json rows could transiently allocate
~35 MB per read even though every wire message stayed within budget.
Updated the CLAUDE.md LocalDb bullet to stop implying the row cap is fully
redundant with the byte budget (topology-guide.md has no matching claim).

F3: StoreAndForwardService's observer-queue onDropped callback logged a
Warning per dropped item, flooding logs at sweep rate for a stuck observer
with a large queue. LogObserverQueueDrop now logs once immediately on the
first drop of an episode, then throttles to at most one rollup Warning per
minute while drops continue, reporting the count dropped since the last
log; the cumulative ObserverQueueDroppedCount counter is unaffected.
Extended StoreAndForwardServiceTests with
ObserverQueue_ManyDropsInOneEpisode_LogsExactlyOneWarning, which floods the
bounded queue and pins exactly one drop-related Warning log for the
episode via a small CapturingLogger test double.

dotnet build ZB.MOM.WW.ScadaBridge.slnx: 0 warnings, 0 errors.
dotnet test StoreAndForward.Tests: 134/134 passed.
dotnet test Host.Tests: 490/490 passed.
2026-08-14 23:31:52 -04:00
Joseph Doherty cfa6acbf48 fix(test): order the cached-drain MarkForwarded assertions behind the push they follow
CachedDrain_OrphanRow_PastGrace_IsAbandoned_AndTheValidRowStillFlows gated on
IngestCachedTelemetryAsync being received once and then asserted, bare, that the
valid row had been marked Forwarded. The drain does that strictly AFTER the push
returns: OnCachedDrainAsync abandons the orphan (:351), pushes the batch (:366),
then parses the ack and marks the accepted ids (:380). Observing the push
therefore orders nothing with respect to the second MarkForwardedAsync — under a
loaded parallel run the post-push continuation can be scheduled after the poll
that saw the push, and the assertion fails fast with "Actually received no
matching calls" while the orphan's own earlier call is reported as the single
non-matching one.

Reproduced deterministically by delaying only the post-push step, which fails
exactly this test (11 siblings still pass) at ~1.3s into the assembly run —
matching the observed failure's fast-fail signature and pointing at line 430.
With the fix the same injected delay passes; suppressing the valid row's
MarkForwarded entirely still fails the test with the identical message, so the
claim (orphan abandoned in its own call, valid row pushed and marked, exactly
once each with exactly the same arguments) is unchanged in force.

Same unsynchronized-assertion class as c4caebe9, different actor. Test-only; the
drain's abandon/push/mark ordering is correct as written.
2026-08-14 23:28:31 -04:00
Joseph Doherty 49fb75c8ba Merge branch 'worktree-agent-a83bdadbbbbe18c6d' into arch-review-remediation 2026-08-14 23:26:24 -04:00
Joseph Doherty 37f13e2eaa fix(dcl): discard in-flight subscribe results for unsubscribed tags; release the orphaned handle 2026-08-14 23:25:48 -04:00
Joseph Doherty c4caebe9b4 fix(test): remove the unsynchronized audit-attempt assertion in the two dispatcher audit-safety tests
Both NotifyDispatcher_AuditWriter_Throws_DeliveryStillSucceeds and
NotificationDispatch_BrokenAuditWriter_StillTransitionsToDelivered read the
throwing writer's attempt counter with a bare Assert immediately after an
AwaitAssert on the Notifications row reaching Delivered. That assumes the audit
writes happen no later than the operational status write, which the dispatcher
deliberately does NOT guarantee: DeliverOneAsync persists the delivery state
first (NotificationOutboxActor.cs:657) and only then emits the Attempted
(:663) and terminal (:676) audit rows — audit is best-effort and must never
gate the user-facing action. Observing Delivered therefore establishes no
happens-before edge with the writer, and under a loaded full-solution parallel
run the continuation after the DB write can be scheduled after the poll that
saw Delivered, so the counter reads 0 and the test fails with "saw 0".

Reproduced deterministically by delaying only the post-update audit emission,
which yields both observed failure messages verbatim; with the fix in place the
same injected delay passes, and suppressing the emissions entirely still fails
both tests with the identical messages — the claims (delivery despite audit
failure, and attempts >= N) are unchanged in force, only the ordering
assumption is gone.

Test-only change; the update-then-audit ordering predates the remediation
(#23 M4) and is correct as written.
2026-08-14 23:12:06 -04:00
Joseph Doherty b1de9dfdd4 Merge branch 'worktree-agent-ada30dd5d1f68b742' into arch-review-remediation 2026-08-14 22:56:47 -04:00
Joseph Doherty c254d0740e perf(sitelog): sampled per-run events; interval run summaries; site_events replication policy pinned
Implements WP3.2 stage (b) per docs/plans/2026-08-15-site-events-policy-design.md.

- Per-run instance-script Started/Completed Info site events are now off by
  default (SiteRuntimeOptions.PerRunScriptEvents=false) instead of firing on
  every run, closing the dominant site_events writer. Gated at the ScriptRunLauncher
  call sites (moved there from ScriptExecutionActor by WP3.1). Error-level events
  (timeout/failure/stuck-watchdog/recursion-limit) remain unconditional.
- ScriptRunSummaryRecorder accumulates per-(instance, script) run counters and a
  new site-only ScriptRunSummaryFlushService emits one aggregate "script" Info
  site event per ScriptRunSummaryIntervalSeconds (default 300s), top-50-script
  breakdown with an "others" rollup, zero-activity intervals emit nothing.
- Per-script opt-in via PerRunScriptEventScripts ("Instance/Script" exact or
  "Instance/*" wildcard), matched by the new pure ScriptRunEventPolicy. All three
  options are read from IOptionsMonitor<SiteRuntimeOptions> per run, so the
  policy is hot-togglable without a restart.
- Fixed the stale "event log is not replicated" comment at AkkaHostedService.cs
  (~905): site_events IS registered in SiteLocalDbSetup.ReplicatedTables — the
  singleton is what makes queries always hit the actively-written copy;
  replication is what gives the singleton history to read after a failover
  (memo Decision (b)). site_events replication itself is unchanged (still
  registered) and already pinned by
  tests/ZB.MOM.WW.ScadaBridge.Host.Tests/SiteLocalDbCdcRegistrationTests.cs.
- Updated Component-SiteEventLogging.md (Volume Policy section, corrected
  Storage/replication rationale) and Component-SiteRuntime.md (Script Run
  Launch + Error Handling sections).
2026-08-14 22:56:08 -04:00
Joseph Doherty 799fd041ec Merge branch 'worktree-agent-a2b4268818a1b6201' into arch-review-remediation 2026-08-14 22:37:43 -04:00
Joseph Doherty c4fc1f8ecd perf(runtime): split trigger evals from the blocking pool; bounded, deadline-aware execution 2026-08-14 22:36:15 -04:00
Joseph Doherty cca7f1786d chore(deps): LocalDb 0.2.0 — dereg cleanup, late-opt-in baselining, byte-budget replication 2026-08-14 22:22:18 -04:00
Joseph Doherty 312216ff2b docs(plans): script pool split design — WP3.1a 2026-08-14 21:47:48 -04:00
Joseph Doherty 6cfb2dd858 docs(plans): site_events volume policy design — WP3.2a 2026-08-14 21:45:23 -04:00
Joseph Doherty a5882753dd perf(comms+audit): close phase-2 residuals — direct ingest path, monotonic timeouts, synthetic probe, not-reporting set, cursor-exact audit pull 2026-08-14 21:38:23 -04:00
Joseph Doherty 4cd1441984 Merge branch 'worktree-agent-aae48b78656e5a4e0' into arch-review-remediation
# Conflicts:
#	src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Repositories/SiteExternalSystemRepository.cs
2026-08-14 21:16:11 -04:00
Joseph Doherty edd925e277 Merge branch 'worktree-agent-a0098b40576d74cfd' into arch-review-remediation 2026-08-14 21:15:32 -04:00
Joseph Doherty d494911190 Merge branch 'worktree-agent-acc1e4b5202e79d46' into arch-review-remediation 2026-08-14 21:15:31 -04:00
Joseph Doherty b2181b80bd Merge branch 'worktree-agent-ae22af64445b321d4' into arch-review-remediation 2026-08-14 21:15:31 -04:00
Joseph Doherty f25b75a64d Merge branch 'worktree-agent-a7084b23177344196' into arch-review-remediation 2026-08-14 21:15:31 -04:00
Joseph Doherty d93f96d08a Merge branch 'worktree-agent-a3b474b485c0288de' into arch-review-remediation 2026-08-14 21:15:31 -04:00
Joseph Doherty 48b3c40a7f perf(deploy): flatten-session caching, bulk DeploySiteAsync, paged management queries 2026-08-14 21:14:22 -04:00
Joseph Doherty d15c5f02ea perf(dcl): batch subscribe/read/write seam, bounded reconnect, sharded subscriptions 2026-08-14 21:14:04 -04:00
Joseph Doherty 2ce0ad7ed1 perf(comms): alarms-only seed, capped buffers, at-least-once audit pull 2026-08-14 21:10:19 -04:00
Joseph Doherty 5db2a810c0 perf(central): set-based ingest, aligned partition purge, KPI query shapes, EF hygiene 2026-08-14 21:07:12 -04:00
Joseph Doherty a212283104 perf(misc): cached hot-path lookups, bounded observer queue, alarm-priority stream path
WP2.6 (arch-review remediation, cross-cutting misc):
- SiteExternalSystemRepository: name/ID-indexed ExternalSystemDefinitionCache replaces
  the fetch-all + reverse-map scan on every by-ID/method lookup; loaded once per
  redeploy, invalidated by DeploymentManagerActor after HandleDeployArtifacts applies
  external-system changes. Static JsonSerializerOptions for method-list parsing.
- Inbound API: short-TTL ApiMethodCache fronts the per-request ApiMethod repository
  fetch; invalidated by name via the existing ScriptArtifactChangeSubscriber/
  IScriptArtifactChangeBus pipeline, self-healing via TTL for changes the bus
  doesn't cover (e.g. Management API edits).
- StoreAndForward: the cached-call audit-observer queue — the one unbounded channel
  left in the system — is now bounded (ObserverQueueCapacity, default 10,000) with
  DropOldest overflow and a dropped-notification counter.
- SiteStreamManager: alarm state changes now travel a dedicated publish
  source/broadcast hub, isolated from the (far higher-volume) attribute path, so an
  attribute storm can no longer evict a pending alarm transition; the alarm hand-off
  queue is bounded with a drop counter surfaced on the site health report
  (SiteStreamAlarmDropCount via the new SiteStreamAlarmDropReporter), and publishing
  is skipped entirely at zero subscribers on either path.
- CLI ManagementHttpClient: explicit 30s HttpClient.Timeout on the shared
  construction (was the 100s framework default), overridable via
  SCADABRIDGE_HTTP_TIMEOUT_SECONDS.

Deviation: the failback-probe heartbeat item is NOT included — its only viable
surface (CentralChannelProvider.cs / heartbeat consumers) lives entirely in the
Communication project, explicitly off-limits to this work package this phase.

Tests: SiteRuntime.Tests (550), InboundAPI.Tests (278), StoreAndForward.Tests (133),
CLI.Tests (390), HealthMonitoring.Tests (97) — all green after full solution build.
2026-08-14 20:59:43 -04:00
Joseph Doherty 8c0b36b2fa perf(ui): shared KPI cache, live-cache-backed alarm summary, coalesced debug renders 2026-08-14 20:53:38 -04:00
Joseph Doherty 1040dc0fcc docs(plans): DCL batch seam design — WP2.1a 2026-08-14 20:30:35 -04:00
Joseph Doherty 70cf718949 Merge branch 'worktree-agent-ac58bf5116b74ff84' into arch-review-remediation 2026-08-14 20:14:13 -04:00
Joseph Doherty 628a14c299 Merge branch 'worktree-agent-a44b52ad62ac16433' into arch-review-remediation 2026-08-14 20:14:13 -04:00
Joseph Doherty c6e26a1a6f Merge branch 'worktree-agent-a7e4ca4e5668d81b3' into arch-review-remediation 2026-08-14 20:14:13 -04:00
Joseph Doherty c7a89cbe88 Merge branch 'worktree-agent-a95eaaa8a3181ad4c' into arch-review-remediation 2026-08-14 20:14:13 -04:00
Joseph Doherty da62e01105 Merge branch 'worktree-agent-af8a44154c4e2dbba' into arch-review-remediation 2026-08-14 20:14:13 -04:00
Joseph Doherty 663ce2f14f Merge branch 'worktree-agent-a27e5c199270a6d63' into arch-review-remediation 2026-08-14 20:14:13 -04:00
Joseph Doherty 87e25c790c Merge branch 'worktree-agent-a143c0cc0b4d07e76' into arch-review-remediation 2026-08-14 20:14:13 -04:00
Joseph Doherty 2e4e41a8f7 fix(auditlog): site audit DB onto the data volume; required path + soft flush
Closes WP1.2 of the arch-review remediation plan (finding #2, High):
SqliteAuditWriterOptions.DatabasePath defaulted to CWD-relative "auditlog.db",
which on the docker rig resolves onto the container's ephemeral overlayfs
(not the mounted /app/data volume), silently discarding the pending audit
forward-state backlog on every recreate; nothing in docker/ or docker-env2/
overrode it; FlushIntervalMs was validated but never read by the writer loop
(one commit per event even at trickle rate); and no PRAGMA synchronous was
set (SQLite's FULL default fsyncs every commit).

- DatabasePath now has no default (mirrors ZB.MOM.WW.LocalDb's LocalDbOptions.Path)
  and is required pre-host for Site nodes only, via a new StartupValidator raw-config
  check (top-level "AuditLog:SiteWriter:DatabasePath", NOT nested under ScadaBridge:
  AddAuditLog binds that section off the configuration root). SqliteAuditWriterOptionsValidator
  deliberately does NOT check DatabasePath itself, because AddAuditLog runs its
  ValidateOnStart on both Central and Site composition roots but only Site nodes
  ever resolve the writer — checking it there would fail Central's boot too.
- All 8 site-node appsettings under docker/ and docker-env2/ now set
  AuditLog:SiteWriter:DatabasePath to /app/data/auditlog.db (mounted volume,
  survives container recreate, same convention as LocalDb:Path); the local-dev
  base appsettings.Site.json sets ./data/auditlog.db to match.
- The writer loop now honors FlushIntervalMs: after draining the immediately
  available burst, it keeps the transaction open (bounded by FlushIntervalMs
  from the first event) waiting for more trickle-rate events before committing,
  instead of flushing (and fsyncing) per event.
- PRAGMA synchronous = NORMAL on the write connection — audit is best-effort by
  design (CLAUDE.md: "Audit-write failure NEVER aborts the user-facing action"),
  so NORMAL's narrower power-loss window is an acceptable trade for far fewer
  fsyncs; WAL mode still guarantees no corruption.
- Tests: StartupValidator site-required/blank/central-exempt cases; writer
  trickle-load single-transaction coalescing + beyond-interval separate-transaction
  regression (new FlushCountForTests seam); options-validator doc updates reflecting
  the moved responsibility. Full suite runs green: AuditLog.Tests 368/368,
  Host.Tests 480/480.

One-time migration note: the existing container-local auditlog.db (wherever it
landed under CWD) is abandoned by this change, not migrated — already-forwarded
rows are safe centrally (AuditLog is the durable copy), and any still-Pending
rows on the abandoned path are lost once. This is the exact bug being fixed, not
a new loss: those rows were already living outside the mounted volume and would
not have survived the next container recreate regardless. Cross-reference
docs/known-issues/2026-07-20-cached-telemetry-drain-hot-loop.md, which this
placement bug caused.
2026-08-14 20:13:31 -04:00
Joseph Doherty 7ebdcd370a perf(host): install CDC capture only when replication is configured
SiteLocalDbSetup.OnReady registered all ten replicated tables
unconditionally, so a deliberately unreplicated site node (site-b and
site-c on the rig) carried the full 30-trigger CDC set forever. Every
write to those tables paid two extra INSERTs plus a json_object
serialization of the whole row, inside the caller's own transaction, and
appended to an oplog nothing ever drains. Arch-review finding #5 (High),
repo half; the library half — trigger cleanup API and O(1) backlog — is
WP3.3.

The ten RegisterReplicated calls are now behind a guard on whether the
node has LocalDb:Replication:PeerAddress OR LocalDb:Replication:ApiKey.
Either key counts, and the OR is load-bearing rather than defensive:
replication is one bidirectional stream that exactly one side dials, so
only the initiator sets PeerAddress. Verified against the rig — site-a
node-a has PeerAddress + ApiKey, site-a node-b (passive) has ApiKey
alone, site-b/site-c have no Replication section at all. Keying on
PeerAddress alone would have stripped capture from every passive node and
silently made each pair converge in one direction only.

The load-bearing ordering documented in the file is preserved: DDL still
precedes registration, and the legacy migrator still runs unconditionally
after it — an unreplicated node must still absorb its pre-Phase-1 files,
and it has no peer for those rows to be invisible to.

Known residual, documented in-file and in the topology guide: a database
file first created by an older build keeps its stale __localdb_* triggers.
The guard decides whether triggers are installed, not whether existing
ones are removed, and the library has no removal API until WP3.3. Moot on
the docker rig, where a schema-change redeploy recreates the volumes.
The inverse is also now documented: enabling replication on a site that
has run without it does not baseline existing rows, since CDC never
recorded them in __localdb_row_version and the snapshot resync streams
from that ledger.

Tests: new SiteLocalDbCdcRegistrationTests asserts trigger presence and
absence via sqlite_master across all four config shapes (none, ApiKey
only, PeerAddress + ApiKey, and the notification-table exclusion), plus
DDL-still-runs and migrator-still-runs on the unreplicated branch.
SiteLocalDbWiringTests and the integration site-pair harness now
configure an ApiKey — mirroring the rig's passive node — so their
registration and convergence assertions still describe a replicating
node. 483/483 Host.Tests pass; the 20 offline LocalDb convergence tests
still pass.
2026-08-14 19:59:53 -04:00
Joseph Doherty 125055d998 perf(runtime): O(1) attribute resolution, precomputed types, coalesced static writes, shared JSON options 2026-08-14 19:57:26 -04:00
Joseph Doherty 34a3f4bb69 fix(comms): reconnect on graceful stream completion — kills the 4h silent stream death 2026-08-14 19:57:08 -04:00
Joseph Doherty 600659d579 perf(sql): sweep/KPI covering indexes + sliced notification terminal purge 2026-08-14 19:55:48 -04:00
Joseph Doherty 2cfcd89052 perf(sitelog): batched event-log commits and sliced retention purge 2026-08-14 19:54:58 -04:00
Joseph Doherty c5e66ed4e4 fix(comms): fail known-dead sends immediately instead of burning Ask timeouts 2026-08-14 19:49:00 -04:00
Joseph Doherty 0b201e410c docs(plans): arch-review remediation plan — multi-phase, subagent-executable program for all 2026-08-14 review findings 2026-08-14 19:23:56 -04:00
Joseph Doherty ee193cd2bb test(centralui): pin the Administrator-only /admin/secrets nav link
The Secrets management UI (ZB.MOM.WW.Secrets Secrets.Ui, mounted at
/admin/secrets) has been linked from the NavMenu Admin section since the
Theme adoption, but no NavMenu test asserted it. Add bUnit coverage that
the item renders for an Administrator and is absent for a
Designer+Deployer principal, matching the existing role-gate test style.
2026-08-13 09:29:49 -04:00
Joseph Doherty b71fbae36e fix(docker+tests): restore rig LDAP login via redundant local GLAuth pair + FallbackServers
The rig pointed at the shared 10.100.0.35 GLAuth whose serviceaccount password
was rotated (SEC-36), so central login had been failing ('Authentication service
is misconfigured') and a TEMP DisableLogin workaround was pending. Central nodes
now point at the local redundant pair (scadaproj/infra/glauth-redundant,
host.docker.internal:3893 + FallbackServers :3894), where the dev bind password
is correct — live-gated on the redeployed rig: login OK, primary-kill failover,
sticky preference (bind-count proven), walk-back on backup-kill.

AuthFlowTests factory bound as cn=admin for search-then-bind, but the current
directory grants the search capability only to serviceaccount (admin searches
return 50 Insufficient access) — stale since the GLAuth config evolved; the test
had been skipping on the closed port and failed once anything answered :3893.
Now binds as serviceaccount; AuthFlowTests 5/5 against the pair.
2026-08-13 08:49:05 -04:00
Joseph Doherty d28824d967 chore(deps): bump ZB.MOM.WW.Auth to 0.2.1 — AD continuation-referral fix 2026-08-13 08:42:53 -04:00
Joseph Doherty d4bb4e17f2 docs(plans): AddTemplateScript OOM re-tested on wonder — no longer reproduces (both forms, throwaway template, <1s) 2026-08-12 18:28:05 -04:00
Joseph Doherty d6b1942932 docs(plans): record uptime-matched wonder working-set re-read — 4.5x/2.0x reduction confirmed flat 2026-08-12 18:25:00 -04:00