Repository navigation
refactor(core): extract shared SafeLog/SafeTrace helper - #763
Conversation
Collapse the copy-pasted try/catch swallows and “mirrors DaqifiDevice.SafeLog” remark walls into one internal DiagnosticGuard. Transport drop-path traces still compose the message inside the guard so a throwing Exception.ToString cannot escape the catch. Co-authored-by: Tyler Kron <tylerkron@gmail.com>
Review follow-up on #763. No behaviour change. The twelve deleted helpers carried the reasoning for the containment, not just the containment itself, and some of it had no home in the shared type: - Both transports now say why the trace line is composed inside a lambda. Without that, `SafeTrace(() => $"...")` reads as idle indirection and the obvious "simplification" back to `SafeTrace($"...")` silently reopens #494 -- a subscriber exception whose ToString() throws escaping the catch that was containing it. - DiagnosticGuard's remarks state the contract the empty catch depends on: purely diagnostic work only, nothing the device's correctness rests on, and no logging from inside the catch (#98). SafeTrace_WritesWithoutThrowing only asserted that nothing was thrown, which is equally true of a guard that quietly writes nothing. Replaced with two tests that capture the trace and assert the line actually lands, through the parallel-safe synchronized-listener pattern DropPathSubscriberIsolationTests established. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
/agentic_review |
Code Review by Qodo
1.
|
The contract added in fdaddbe said the guarded work must be purely diagnostic -- a logger call or a Trace write, nothing else. That is wrong about this codebase: SafeLog also guards best-effort event raises (MessageProducer's SendFailed, DaqifiDevice's SendFailed and ErrorOccurred), where the point is that a throwing subscriber must not take down the producer's background thread or the reader loop. A maintainer following the too-narrow wording could have removed that isolation as out of contract. Restated as what actually holds: what may be passed is a best-effort notification OUT to consumer code -- logger, TraceListener, or a best-effort event raise -- where the only thing lost when it throws is that one notification. What may not be passed is anything the device's own state or correctness depends on. Documentation only; no code change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
/agentic_review |
|
Code review by qodo was updated up to the latest commit 79429cc |
PR Summary by QodoCentralize SafeLog and SafeTrace diagnostic guards
AI Description
Diagram
High-Level Assessment
Files changed (14)
|
|
Code review by qodo was updated up to the latest commit 79429cc |
The branch had drifted behind main far enough to conflict, which also meant GitHub never created a CI run for the head (no merge ref to build). One conflict, in MessageProducer: main added SignalIdleIfQuiet immediately after the private SafeLog twin this branch deletes. Resolved by keeping main's new method and dropping the twin, which is what both sides intended. Re-checked that main introduced no new SafeLog/SafeTrace twins in the meantime -- none remain anywhere in Daqifi.Core. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
/agentic_review |
|
Reviewed (Claude): verified genuinely behaviour-free — twelve identical bare-catch helpers collapse to one, every catch body was already empty, and the two transports' message composition still happens inside the guard. Qodo-clean on 3bc7ef4, CI green — ready for review. What I checked, since "pure refactor" is the claim doing the work here:
Changes made under review:
No public API change ( |
The remarks called the twelve helpers it replaced byte-identical. They were not: four shapes (Action, string, Exception, and an instance Action<ILogger>), and the transports composed their own line. Replace the change-log sentence with the guidance a future reader needs, and widen the catch comment from "logger" to consumer code, matching the documented contract (event raises too). Comment-only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
/agentic_review |
|
Code review by qodo was updated up to the latest commit fc5785f |
|
Qodo-clean, CI green — ready for review |
What was wrong
Nothing a library user can see. This is internal tidying, and it is deliberately behaviour-free.
Twelve types in Core each carried their own private copy of the same small helper: run a logging or
Tracecall, and swallow anything a consumer-supplied logger orTraceListenerthrows, so a misbehaving one can't take down a connect, a reconnect, a drop path or the frame pipeline. Most copies came with a paragraph explaining that they mirrored the copy in the class next door. One guarantee, twelve implementations, and twelve places to forget when the guarantee changes.How it was fixed
One internal
DiagnosticGuardinDaqifi.Core.Internal:SafeLog(Action): swallows a throwing logger (or a throwing best-effort event subscriber)SafeTrace(string): swallows a throwingTraceListenerSafeTrace(Func<string>): composes the line inside the guard, so an exception whoseToString()throws can't escape the drop path (bug(transport): a throwing StatusChanged subscriber on the drop path leaks the port handle and silently cancels auto-reconnect #494)The twelve private copies are deleted. Call sites keep the familiar
SafeLog/SafeTracespelling viausing static. Call sites changed inDaqifiDevice,DaqifiStreamingDevice,LifecycleGate,StatusChannelPopulator,OperationSerializer,ReconnectSupervisor,TextExchangeEngine,StreamingSessionController,MessageProducer,AllTransportsDeviceFinder,TcpStreamTransport,SerialStreamTransport.Why this is behaviour-free
Each of the twelve deleted helpers was checked against the guard:
catch { }(all exceptions, not just non-fatal ones), and so does the guard. Nothing that used to propagate is now swallowed, and nothing that was swallowed now escapes. None had extra behaviour (a fallback trace, a rethrow, a counter) that the shared helper drops.SafeLog(Action)copies ran the whole lambda inside thetry, and still do. The threeSafeTrace(string)copies took a message the caller had already built, and still do. The two transports'SafeTrace(Exception)copies built the line inside thetry, which theFunc<string>overload preserves with the same text.TextExchangeEngine.Log(Action<ILogger>)read_host.Loggerinside thetry, and the one-line delegation still does.Silent catches are the settled convention here (#180), and the guard adds no logging inside a catch (#98). The type-level remarks state the contract the empty catch relies on: best-effort notifications out to consumer code only (logger calls, trace writes, best-effort event raises such as
SendFailed/ErrorOccurred), never anything the device's correctness depends on.DropPathSubscriberIsolationTestsis the real regression suite for this change. It installs a real listener, asserts the traced text, covers the unrenderable-exception case from #494, and passes unchanged.Notes for reviewers
SafeTrace(() => $"...")at the two transports is load-bearing, not decoration. Collapsing it toSafeTrace($"...")would reopen bug(transport): a throwing StatusChanged subscriber on the drop path leaks the port handle and silently cancels auto-reconnect #494. Both call sites now say so in a comment, because a future reader would otherwise be right to think it was noise.DaqifiStreamingDevice(theStreamFrameDiscarded/GapDetectedsubscriber catches) andAllTransportsDeviceFinder(RaiseIsolatedand bothSafeDiscoverAsynccatches) interpolate{ex}before callingSafeTrace(string), exactly as they did onmain. The bug(transport): a throwing StatusChanged subscriber on the drop path leaks the port handle and silently cancels auto-reconnect #494 hazard at those five sites is pre-existing. Moving them to theFunc<string>overload would change behaviour, so it belongs in a follow-up, not in this refactor.DeviceFinderBase.RaiseIsolated,ContinuousDeviceFinder.SafeRaise/SafeScanErrorandStreamMessageConsumer'sSafe*helpers are left alone. They are event or health-sink isolators with different shapes (catchExceptionthen trace, reroute toScanError, no closure on a hot path), not copies of this helper.Tests
DiagnosticGuardTestspins the swallow, the factory-throw isolation, and that the line actually reaches a listener, since "it didn't throw" is equally true of a guard that quietly writes nothing. It uses the parallel-safe synchronized-listener patternDropPathSubscriberIsolationTestsestablished. Mutation-checked: makingSafeTrace(string)a no-op, or composing theFunc<string>line outside the guard, turnsDiagnosticGuardTestsand the two #494DropPathSubscriberIsolationTestscases red.Full suite green locally on net9.0 and net10.0 (Core: 4347 passed, 3 skipped, 0 failed each; Mcp: 269 passed each). Build: 0 warnings, 0 errors.
DiagnosticGuardisinternal, so there is no public API change (PublicAPI.*.txtuntouched). No hardware board attached, so no bench validation was run.🤖 Generated with Claude Code