docs(plans): issue batch #667-#673 design

This commit is contained in:
Joseph Doherty
2026-10-11 08:14:38 -04:00
parent ee67b6370a
commit db6a53036b
@@ -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).