4.5 KiB
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).