diff --git a/docs/plans/2026-08-17-perf-followups-r3.md b/docs/plans/2026-08-17-perf-followups-r3.md index 7bc9fc9a..72fb449f 100644 --- a/docs/plans/2026-08-17-perf-followups-r3.md +++ b/docs/plans/2026-08-17-perf-followups-r3.md @@ -413,6 +413,83 @@ because it exercises the real authoring path. --- +### Task 9: Roslyn evaluator racing test — remove the timing assumption + +**Classification:** standard +**Estimated implement time:** ~5 min +**Parallelizable with:** Task 10 (disjoint files); independent of the rig tasks + +**Origin:** ScadaBridge deferred-work register row 38, relayed cross-session 2026-08-18 +(coordinated by the ScadaBridge session on Joe's behalf). + +**Files:** +- Modify: `tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests/RoslynVirtualTagEvaluatorTests.cs` + (`Evaluate_racing_ClearCompiledScripts_never_fails_with_disposed`, ~:391) + +**Spec.** The test is load-fragile: it fails under full-solution parallel test load but +passes 3/3 in isolation. Its design assumes at most ONE `ClearCompiledScripts` lands +inside a single evaluation (so the product's one-shot retry suffices); under contention +two can land and the retry legitimately exhausts. Test-design issue, NOT a product bug. +Fix by removing the timing assumption while keeping the original intent (no +disposed-script failure ever ESCAPES the retry): either bound the clear cadence relative +to observed evaluation latency, or assert on retry-exhaustion behaviour instead of zero +failures — implementer picks whichever preserves intent with less machinery, documents +the choice in the test doc-comment, and demonstrates the test survives a +full-parallel-load run (run the containing suite alongside another heavy suite, or +repeat-run the suite ≥5×; report what was actually run). + +--- + +### Task 10: LocalDb conditional registration + baseline + deregister self-heal + +**Classification:** high-risk +**Estimated implement time:** ~8 min +**Parallelizable with:** Task 9 (disjoint files) + +**Origin:** ScadaBridge deferred-work register row 39, relayed cross-session 2026-08-18. +ScadaBridge closed the same class with conditional registration + boot-time self-heal +(`Host/SiteLocalDbSetup.cs`, arch-review WP1.3/WP3.3); port that pattern here. + +**Files:** +- Modify: `src/Server/ZB.MOM.WW.OtOpcUa.Host/Configuration/LocalDbSetup.cs` (`OnReady`, + registration at :63-66) +- Read for the predicate: `src/Server/ZB.MOM.WW.OtOpcUa.Host/Configuration/LocalDbRegistration.cs` + (`LocalDb:SyncListenPort`, `LocalDb:Replication:*` keys) +- Modify: `tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests/LocalDbSetupTests.cs` + (the registered-table-set + ordering pins — update DELIBERATELY, say so in the diff) +- Modify: `CLAUDE.md` LocalDb section — the two bullets recording unconditional + registration as deliberate ("DeregisterReplicated exists and this repo deliberately + does not call it" / "Consequently, baselineExistingRows is not used either") must be + rewritten to record the new conditional shape and why it changed. + +**Spec.** +- "Replication configured" = `PeerAddress` OR `ApiKey` present (OR, because only the + dialling half sets PeerAddress while the passive half carries just the key + listen + port — verify the exact key names against `LocalDbRegistration`). +- Configured ⇒ `RegisterReplicated(table, baselineExistingRows: true)` per table, in the + existing DDL→register order (the ordering is load-bearing and pinned) — seeds + row-versions at the LWW floor and flags snapshot resync, so late-enabled replication + on an established node converges on EXISTING rows, not just subsequent writes. +- NOT configured ⇒ `DeregisterReplicated` on every table at boot — idempotent self-heal: + drops stale triggers, prunes oplog/row-version rows, logs once at Information when it + actually cleaned something (and stays silent when there was nothing to clean). +- Registration stays per-table in a loop (0.3.0 made the snapshot flag per-table; the + loop preserves that granularity if the pin ever moves). +- **Pin note:** this repo is on `ZB.MOM.WW.LocalDb 0.2.1`, which has both + `baselineExistingRows` and `DeregisterReplicated`. Do NOT bump the pin unless a needed + API is genuinely absent at 0.2.1 — if it is, stop and report before bumping. +- **Traps (both hit by ScadaBridge):** the registered-table set is pinned by + `LocalDbSetupTests` — update the pins deliberately with the behaviour change named in + the test doc-comment; and deregistration must be pair-symmetric — the sync handshake + compares registered-table digests fail-closed, so flipping registration is a + both-nodes-together change. Record the pair-symmetry consequence in CLAUDE.md and in + the rollout note (docker-dev site-a has replication ON and was enabled from fresh, so + current impact is low; the change is correctness-before-someone-enables-late). +- Tests: pin the new predicate both ways (configured ⇒ registered with baseline; + unconfigured ⇒ deregistered/self-healed) and keep the DDL→register ordering pin green. + +--- + ### Task 8: Close-out — sweep + Outcomes + register annotations **Classification:** close-out (blockedBy: all) diff --git a/docs/plans/2026-08-17-perf-followups-r3.md.tasks.json b/docs/plans/2026-08-17-perf-followups-r3.md.tasks.json index 630b1012..f4c749d3 100644 --- a/docs/plans/2026-08-17-perf-followups-r3.md.tasks.json +++ b/docs/plans/2026-08-17-perf-followups-r3.md.tasks.json @@ -3,12 +3,14 @@ "tasks": [ {"id": 1, "subject": "Task 1: ACL InvalidateIfStale — monotonic-max CAS loop", "status": "pending"}, {"id": 2, "subject": "Task 2: S7 read-pass health — worst-state-wins aggregation", "status": "pending"}, - {"id": 3, "subject": "Task 3: Fix stale OTOPCUA_CONFIG_MODE docs", "status": "pending"}, + {"id": 3, "subject": "Task 3: Fix stale OTOPCUA_CONFIG_MODE docs", "status": "completed", "note": "e9ba1741; docs/Configuration.md:253 + CLAUDE.md:554 rewritten (compose facts verified first); historical plan docs deliberately untouched; trivial => no review"}, {"id": 4, "subject": "Task 4: Live gate — first-POST-404 catch + no-body POST binding", "status": "pending", "blockedBy": [1, 2, 3]}, {"id": 5, "subject": "Task 5: Live gate — FetchAndCache legs", "status": "pending", "blockedBy": [4]}, {"id": 6, "subject": "Task 6: Live gate — alarm-bearing surgical ratchet", "status": "pending", "blockedBy": [5]}, {"id": 7, "subject": "Task 7: Live gate — ACL-enabled enforcement on site-a", "status": "pending", "blockedBy": [6]}, - {"id": 8, "subject": "Task 8: Close-out — sweep + Outcomes + register annotations", "status": "pending", "blockedBy": [1, 2, 3, 4, 5, 6, 7]} + {"id": 8, "subject": "Task 8: Close-out — sweep + Outcomes + register annotations", "status": "pending", "blockedBy": [1, 2, 3, 4, 5, 6, 7, 9, 10]}, + {"id": 9, "subject": "Task 9: Roslyn evaluator racing test — remove timing assumption (ScadaBridge row 38)", "status": "pending"}, + {"id": 10, "subject": "Task 10: LocalDb conditional registration + baseline + deregister self-heal (ScadaBridge row 39)", "status": "pending"} ], "lastUpdated": "2026-08-17T19:05:00Z" }