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

4.5 KiB
Raw Blame History

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