Fix connection lifecycle: transfer-before-dispose reconnect, async connect teardown, truthful config load - #167
Conversation
…nnect teardown, truthful config load - TryRecreateSessionAsync now creates the new session and transfers subscriptions while the old session is still alive, then disposes it. Previously the old session was disposed first, so the client-side half of TransferSubscriptionsAsync always threw on the disposed session after the server-side transfer had already succeeded - every hard reconnect orphaned the transferred subscriptions on the server and fell back to creating a duplicate set. - ConnectAsync (wrapper and ConnectionManager) now awaits DisconnectAsync instead of calling the synchronous Disconnect(), which blocked the UI thread on the close round-trip - up to the 30s transport timeout when reconnecting over a dead server. - ConnectionManager.ConnectAsync no longer reports Connected when the monitoring subscription cannot be created: it raises ConnectionError, disconnects, and returns false instead of silently leaving a session that can never monitor anything. - LoadConfigurationAsync tears down the old session up front via the recording-aware DisconnectAsync (no more zombie REC indicator, no dead table rows), and on a failed connect or cancelled password prompt reverts the config service to untitled so Ctrl+S can no longer overwrite the newly selected file with stale session state. The failure dialog no longer claims the previous connection was preserved. https://claude.ai/code/session_012Vopnd9vWkzELveHRgZhie
Code ReviewThis PR addresses four real, interconnected connection-lifecycle bugs. The root-cause analysis in the description is accurate and the fixes are generally correct. A few items worth discussing: Transfer-before-dispose (
|
| Finding | Severity | File |
|---|---|---|
oldSession may leak if transfer throws (not covered by outer catch) |
Medium | OpcUaClientWrapper.cs |
| Double KeepAlive registration window during transfer | Low | OpcUaClientWrapper.cs |
| Event handlers wired on failed subscription init | Cosmetic | ConnectionManager.cs |
_configService.Reset() loses prior file reference on cancel |
Low/UX | MainWindow.cs |
The core logic of all four fixes is sound. The medium-severity leak is the only item I'd want resolved before merge — the rest are minor polish. Happy to look at a follow-up addressing the try/finally guard if the team agrees.
Once _session points at the new session, an exception from the subscription transfer would leave the old session undisposed - the outer catch has no reference to it. https://claude.ai/code/session_012Vopnd9vWkzELveHRgZhie
Code ReviewPR: Fix connection lifecycle: transfer-before-dispose reconnect, async connect teardown, truthful config load OverviewThis PR fixes four genuine bugs in the connection lifecycle, all well-described in the PR body. The changes are surgical and the motivations are clearly documented in both the description and inline comments. The PR description itself is exemplary. What's Good
Issues / Suggestions1. KeepAlive wired before In newSession.KeepAlive += Session_KeepAlive;
_session = newSession; // handler can fire here and see the old _sessionIf the keep-alive timer fires in that narrow window, _session = newSession;
newSession.KeepAlive += Session_KeepAlive;2. Transfer failure is indistinguishable from session-creation failure at the call site If 3. No new automated tests for the subscription-failure path The new branch in 4. In SummaryThe fixes are correct, well-motivated, and the inline documentation is high quality. Two actionable items:
The transfer-before-dispose fix in particular is a non-trivial correctness win that's easy to get wrong — the approach here gets it right. |
Summary
Fixes the three connection-lifecycle gate items from the v1.0 follow-up review (
docs/V1-REVIEW-FOLLOWUP.md, items 2–4, plus the related subscription-init swallow).TryRecreateSessionAsyncpreviously disposed the old session before transferring subscriptions. The client-side half ofTransferSubscriptionsAsyncmust detach each subscription from its previous session, which threw on the disposed session — after the server-side transfer had already succeeded — so the SDK reported failure and the app recreated a duplicate subscription set, orphaning the transferred ones on the server on every hard reconnect. The new session is now created and the transfer completed while the old session is still alive; the old session is disposed afterwards (still withoutCloseSession, so untransferred server-side subscriptions are not deleted). A failure during session creation now leaves existing state untouched for the next retry.ConnectionManager.ConnectAsyncandOpcUaClientWrapper.ConnectAsync(including its error paths) nowawait DisconnectAsync()instead of calling the synchronousDisconnect(), which blocked the calling (UI) thread on the OPC UA close round-trip — up to the 30 s transport timeout when connecting away from a dead server. The syncDisconnect()remains for back-compat callers.ConnectionManager.ConnectAsyncno longer ignores the result of subscription initialization. If the server refuses the monitoring subscription, it raisesConnectionError, disconnects, and returns false instead of reporting Connected on a session that can never monitor anything.MainWindow.LoadConfigurationAsyncnow tears down the old session up front via the recording-awareDisconnectAsync()(stops a live CSV recording — no more zombie REC indicator — and clears the views, so no dead rows survive). On a failed connect or a cancelled password prompt, the config service is reset to untitled so Ctrl+S can no longer silently overwrite the newly selected file with stale session state, and the failure dialog no longer claims "the previous connection and data have been preserved" when they weren't.Behavior changes
Test plan
dotnet build Opcilloscope.sln -c Release— 0 warnings, 0 errorsdotnet test -c Release— 613/613 passing, including the existingReconnectIntegrationTestswhich exercise the recreate pathPart of the v1 follow-up punch list (
docs/V1-REVIEW-FOLLOWUP.md, items 2–4).https://claude.ai/code/session_012Vopnd9vWkzELveHRgZhie
Generated by Claude Code