Files
lmxopcua/docs/plans/2026-10-11-issue-batch-667-673-design.md
T
2026-10-11 08:14:38 -04:00

71 lines
4.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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).