refactor(device): extract connect/disconnect serialization into a collaborator (part of #344) - #437
Conversation
…laborator (part of #344) `DaqifiDevice` is now the largest file in the repo at 4,193 lines. This takes the smallest self-contained bite out of it: the issue #379 lifecycle gate that serializes connect against disconnect, which has four call sites, owns no device state and raises no events. `LifecycleGate` (internal, `Device/Internal/`) holds the semaphore, the AsyncLocal re-entry flag, and the two contention policies. `DaqifiDevice` keeps `LifecycleLockTimeout`/`TeardownLockTimeout` — they are the `internal virtual` seam tests override — and hands them to the gate as delegates rather than values, because a test subclass assigns its override through an `init` property that runs after the base constructor builds the gate. Pure refactor: no public API change and no behavior change. Verified by a normalized statement multiset diff in both directions; the device lost exactly the region body and gained exactly the field, three constructor wirings and four call-site renames. Zero edits to existing tests. `_lifecycleLock` is deliberately still never disposed, matching current teardown; the ObjectDisposedException handlers stay defensive. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
/agentic_review |
PR Summary by QodoExtract lifecycle connect/disconnect gate into internal collaborator
AI Description
Diagram
High-Level Assessment
Files changed (4)
|
Code Review by Qodo
1.
|
…344) The constructor null-checks all four injected delegates, but `Run`/`RunAsync` invoked the caller's operation without validating it, so internal misuse would have surfaced as a NullReferenceException from one of three invocation sites rather than as an ArgumentNullException naming the parameter. The guard runs before the re-entry branch, not after: that branch invokes the delegate without acquiring anything, so a guard placed later would leave exactly that path throwing NRE. Placing it first also means a null delegate can never take the gate on its way to failing. Matches the entry-point convention already established by the sibling collaborator `ChannelControlOperations` (#432). +4 tests, all four of which fail if either guard is removed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
/agentic_review |
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
/agentic_review |
|
Code review by qodo was updated up to the latest commit 22d060b |
|
Ready for review: Qodo review came back clean on the latest commit ( |
|
Bench re-validation (real Nq1, fw 3.7.2, USB, non-destructive) — this time through the shipped example CLI built against this branch (
Pushed (Edited to drop the device serial number, per the Qodo finding on the same content.) |
|
/agentic_review |
|
Code review by qodo was updated up to the latest commit eda3157 |
|
/agentic_review |
|
Code review by qodo was updated up to the latest commit a0f60eb |
|
Ready for review (supersedes the earlier note, which pinned Both commits since One note on CI history for this branch: the two intermediate red runs were net10-only timing flakes on docs-only commits — Not merging — awaiting your review. |
Problem
With the SD-card, diagnostics, network, LAN-info, channel-control and frame-decode blocks now extracted,
DaqifiDevice.csat 4,193 lines is the largest file in the repo — larger than either class #344 originally named. That is item 4 of the issue's own status comment, raised there as an open scope question. This answers it by taking the smallest safe bite rather than re-scoping the issue.What moved
The issue #379 lifecycle gate: the semaphore that serializes connect against disconnect so the device never drives its transport from two threads at once. It was the best candidate on the file by a wide margin — 4 call sites, no events, no device state, where the next largest region (automatic reconnection, ~500 lines) owns three events and two
protected virtualhooks thatDaqifiStreamingDeviceoverrides.Device/Internal/LifecycleGate.csnow holds the semaphore, theAsyncLocalre-entry flag, theLifecycleContentionpolicy enum and the twoRun/RunAsyncentry points.DaqifiDevice4,193 → 3,947 lines.LifecycleLockTimeoutandTeardownLockTimeoutdeliberately stay on the device — they are theinternal virtualseamDeviceReconnectTestsoverrides — and reach the gate asFunc<TimeSpan>.The one design point worth reading
The timeouts are delegates rather than values, and that is load-bearing rather than stylistic. The test subclass assigns its override through an
initproperty, which runs after the base constructor that builds the gate. A gate that read them at construction would have captured the 10 s / 30 s defaults and silently ignored every override — tests would still pass, just slowly and for the wrong reason. Pinned byContentionWait_ReadsTheTimeoutWhenContentionHappens_NotAtConstruction.Behavior deliberately left alone
_lifecycleLockis never disposed today —ReleaseResourcesdisposes only_textExchangeLock— so theObjectDisposedExceptionhandlers are defensive rather than reachable. Adding disposal would be a behavior change smuggled into a pure refactor, so it is documented in the collaborator instead of "fixed" here.SafeLogis a per-class private copy, matching the established convention (MessageProducer,DaqifiDevice, andStatusChannelPopulatorfrom #433 each carry their own) rather than introducing a new shared helper.Verification
Pure move, verified mechanically in both directions — normalized statement multiset diff (comments/usings stripped, mechanical renames canonicalized), the same method used by #419/#422/#432/#433:
SafeLogcopy.Tests: zero edits to existing tests. That is the evidence that matters —
DeviceReconnectTestsstill races a real reconnect loop against a caller's connect over a scripted transport, through the new seam. +21 new cases against the collaborator directly, which the extraction newly makes possible (contention is now constructed rather than raced into): both policies across sync and async, re-entry including across anawait, the re-entry flag not leaking to a later caller, release-on-throw, the lazy-timeout seam, and the #341 cancelled-teardown contract.Daqifi.Mcp.Tests23.TreatWarningsAsErrors+GenerateDocumentationFileturns a stale<see cref>into an error).Bench (real Nq1, fw 3.7.2, USB, non-destructive)
Connect / disconnect / status only — no stream start, no NVM write, no reboot.
ConnectAsync/DisconnectAsynccyclesDisconnectAsyncstill tears down (#341)StatusDisconnect()re-entered from inside a liveStatusChangedhandlerThe last two are the ones a unit test cannot establish: a mocked transport cannot show an OS-level handle release, and a simulated handler cannot show that re-entry survives a real blocking
SerialPortteardown.Part of #344.
Not merging — opened for your review.
🤖 Generated with Claude Code