From db6a53036ba8e8897cbfd43d88a3401635c6980c Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Sun, 11 Oct 2026 08:14:38 -0400 Subject: [PATCH] docs(plans): issue batch #667-#673 design --- .../2026-10-11-issue-batch-667-673-design.md | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 docs/plans/2026-10-11-issue-batch-667-673-design.md diff --git a/docs/plans/2026-10-11-issue-batch-667-673-design.md b/docs/plans/2026-10-11-issue-batch-667-673-design.md new file mode 100644 index 00000000..ccac2eaa --- /dev/null +++ b/docs/plans/2026-10-11-issue-batch-667-673-design.md @@ -0,0 +1,70 @@ +# Issue batch #667–#673 — design (2026-10-11) + +All seven open issues ≥ #667 are testable with available infra (docker-dev rig, SQL Server, opc-plc fixture on +10.100.0.35). Base: master `ee67b637`. Recommended choices taken throughout (user-approved). + +## Lanes + +| Lane | Issue | Model | Wave | +|---|---|---|---| +| L1 | #673 FOCAS flake | sonnet | 1 | +| L2 | #670 OpcUaClient session loss | opus | 1 | +| L3 | #672 crash-failover capture gap | opus | 1 | +| L4 | #669 event HistoryRead continuation points | sonnet | 1 | +| L5 | #667 SqlServer retention/purge | sonnet | 1 | +| L6 | #671 time-weighted Part 13 aggregates | opus | 1 | +| L7 | #668 Part 11 modified values | opus | 2 (after L4 + L5 merge) | + +Each lane: isolated worktree reset to master, TDD, independent opus review, merge, full suite, docker-dev live gate. + +## #673 +Root cause: `FocasDriver.FixedTreeLoopAsync` publishes `LastStatus` before `FixedTreeCache` / `LastFixedPollUtc`; +the test waits on `LastStatus` only, and an authored `FIXED:Status/*` read returns `BadCommunicationError` while +`LastFixedPollUtc` is null. Fix: test waits on all three; driver writes `LastStatus` last via `Volatile.Write`. + +## #670 +Root causes: bad keep-alive never touches `_health` nor publishes; `LastSuccessfulRead` never advances on +notifications/keep-alives; the SDK `SessionReconnectHandler` retries forever (exhausted path unreachable) and on a +session recreate clones subscriptions the driver's dictionaries no longer track. +Fix (Option B): bad keep-alive ⇒ `Degraded` (stamp preserved) + one `BadNoCommunication` publish per subscribed ref +per outage (value retained) + quality-only alarm update; good keep-alive / notifications advance +`LastSuccessfulRead`; a self-healed short outage re-reads and republishes Good (beats the (value,status) dedup); +sustained loss is recovered by the #527 watchdog (shutdown + init + resubscribe). Delete `SessionReconnectHandler` +wiring, `OnReconnectComplete`, `TryRearmReconnect`, the `reconnect_exhausted` counter. Wire SDK telemetry logging +at Warning. Correct `docs/DriverLifecycle.md` survey. + +## #672 +Standby opens the historization capture gate when its recording Primary is cluster-UNREACHABLE (local +`ClusterEvent.ReachabilityEvent`, recomputed from the cached redundancy snapshot in `DriverHostActor`). Alarm drain +gate and data-plane Primary gate unchanged (falsification tests). Knob +`ContinuousHistorization:CaptureWhilePrimaryUnreachable` (default true). No backend capability flag. Gap shrinks to +the failure-detector window (~10–12 s). Documented: drivers without a source timestamp (Modbus) duplicate rows in +the overlap window. + +## #669 +Event HistoryRead issues/resumes continuation points: cursor = last event time + trailing tie count; oversized tie +cluster over-fetch via `[T, T+resolution)` bounded by `MaxTieClusterOverfetch`; generic `SliceTieCluster`; store +state widened to carry an event cursor; unknown CP ⇒ `BadContinuationPointInvalid`. Reverse event reads out of +scope. Ties exact on SqlServer/QuestDB, best-effort on Gateway (documented). Adds an over-the-wire event HistoryRead +test. + +## #667 +`ServerHistorian:SqlServer:{RetentionDays (null=forever), PurgeInterval=1h, PurgeBatchSize=4000}`; optional +`IHistorianBackendProvider.CreateRetention`; `HistorianRetentionService` gated on `IHistorizationRoleView` +plus `sp_getapplock` (timeout 0); per-tag batched `DELETE TOP(n)` on the clustered key, events by time; cutoff on +source time; metric `otopcua.historian.retention.purged{table}`. QuestDB: no change (TTL). Partition switching +deferred/documented. + +## #671 +New time-weighted family: TimeAverage, TimeAverage2, Interpolative, Total (now the Part 13 time-weighted integral, +matching Gateway — client-visible, release-noted), Total2. Bounding values outside the window + `LEAD()` in-bucket +areas in SQL; C# splits bucket-crossing segments. Sloped for numeric, stepped for Boolean. Server-default +AggregateConfiguration (client's ignored). QuestDB mirrored after a lag/lead support spike. Hand-computed literals +in the shared conformance suite. + +## #668 +`hist.SampleModified` (schema v2), prior values via `OUTPUT deleted.* INTO` in the same statement; +`HistoryUpdateContext(Principal, ApiKeyId)` threaded into updater methods; Insert operations logged (empty prior); +retention purges never logged but #667 purges `SampleModified`; `ReadModifiedAsync` serves `IsReadModified=true` +with continuation points, authorized by the HistoryRead gate. QuestDB: append-only `sample_modified` written before +the change (orphan-audit-row-on-crash, documented deviation).