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.
This commit is contained in:
Joseph Doherty
2026-08-14 23:31:52 -04:00
parent b1de9dfdd4
commit 56c99c92c3
5 changed files with 210 additions and 26 deletions
+12 -8
View File
@@ -97,14 +97,18 @@
"ApiKey": "dev-site-a-localdb-sync-key",
// ---- Phase 2 sizing, from the Task 1 rig soak (not from the defaults) ----
//
// The MaxBatchSize = 16 pin is RETIRED as of LocalDb 0.2.0 (arch-review WP3.3).
// It existed only as a hand-computed proxy for a byte budget: batching was
// row-count-only, and 70 KB of production config_json x the 500 default is
// ~35 MB against gRPC's 4 MB receive limit. The library now bounds a batch by
// MaxBatchBytes (default 2 MB of summed serialized size), with the row count
// demoted to a secondary cap, so both are left at their defaults here - a
// deliberately unset MaxBatchBytes is the 2 MB default, and the widest row no
// longer has to be guessed at deploy time.
// The old MaxBatchSize = 16 pin (a hand-computed byte-budget proxy) was retired
// when LocalDb 0.2.0 added MaxBatchBytes (arch-review WP3.3) - but MaxBatchSize
// is NOT purely redundant with it. MaxBatchBytes bounds the WIRE message via a
// 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 the byte-budget split runs. At
// the unset 500 default, a reconnect drain of 70 KB worst-case config_json rows
// can transiently allocate ~35 MB per read even though every wire message stays
// under the 2 MB MaxBatchBytes default (arch-review adversarial finding F2). Pin
// it explicitly here to bound that transient allocation; MaxBatchBytes is left
// unset (its 2 MB default) to bound the wire message.
"MaxBatchSize": 64,
//
// Backlog caps bound the oplog while the peer is offline. Exceeding them is
// NOT data loss: the oplog is pruned to the ceiling and needs_snapshot is set,