Repository navigation
fix(device): drop a blank line that arrives while the command is still queued - #602
Conversation
…l queued A text exchange marks its stale-line boundary before the setup action runs, and #591 added a second one the instant that action returns. The setup action returns when the command has been handed to the message producer, though, not when the producer has written it — so a leftover blank from an earlier exchange could still arrive in between and be counted as this exchange's terminator. The boundary itself cannot be moved later to cover that: #593 records the bench evidence that draining the outbound queue first puts a real Nq1's genuine SYSTem:LOG? terminator on the wrong side of it, and the diagnostics layer then reports a healthy device as silent, 10/10 runs. Instead each collected line now carries a sample of the outbound writer taken as the line arrived — how many writes it had begun, and whether it still had work outstanding. A blank that arrived while a command sat queued behind a writer that had not started anything for this exchange cannot be answering it, and is dropped; anything from the moment the write begins onwards is kept, so a fast device's reply is never at risk. The wait loop asks the same question, so it no longer treats such a blank as proof the device started replying. Both halves of the sample matter: a write count that has not moved is not on its own evidence that nothing was asked, because a setup action that sends by another route leaves it just as still. Closes #593. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
/agentic_review |
PR Summary by QodoFix stale blank lines arriving before queued command is written
AI Description
Diagram
High-Level Assessment
Files changed (9)
|
Code Review by Qodo
1.
|
…s writes Qodo, round 1. The double's remarks said writes "pass straight through" while Write discarded them, and one test's premise — that the command really was queued and not lost — was never actually checked. Writes are now counted (the bytes still go nowhere, like every other stream double in this file, and the remarks say so), the queued command is asserted to reach the wire once the writer is released, and the stream's wait handles are disposed after the release that wakes anyone parked on them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
/agentic_review |
|
Code review by qodo was updated up to the latest commit a9f1822 |
…ndles Qodo, round 2. Disposing the ManualResetEventSlim pair races the writer thread that may still be parked on it, which is how a teardown fault gets into a test run. A monitor has nothing to dispose, so both the race and the leak it was meant to fix stop existing. Applied to both doubles that hold a write open. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
/agentic_review |
|
Code review by qodo was updated up to the latest commit 013475c |
Qodo, round 3. The bounded waits in both write-holding doubles derived their remaining time from DateTime.UtcNow, which is wall-clock: an NTP step or a VM resuming moves it, and a bounded wait that reads it can cut short or stretch by however far the clock jumped. That is a flaky test, and a slow one is just as bad as an early one here because the writer parks inside the wait. Stopwatch is monotonic, so the remaining time now only counts elapsed time. Applied to all four waits across the two doubles. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
/agentic_review |
|
Code review by qodo was updated up to the latest commit b5a560d |
Qodo, round 4. The bound on a held write existed so a test that never releases fails on its own assertion instead of wedging the producer thread for the rest of the run. But it returned exactly like a release, so an expiry could let the window a test was holding open close underneath it and leave the test reading a world it no longer controlled. Recorded rather than thrown. This runs on the producer's background thread, where an exception is not a test failure: MessageProducer catches it, raises SendFailed and counts it toward the consecutive-failure run, so throwing would push the test through production error handling and change the behaviour under test. A flag the test asserts on reaches the test instead. Both doubles expose HoldExpired, and all four tests that hold a write open now assert it is false once released. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
/agentic_review |
|
Code review by qodo was updated up to the latest commit a28b81e |
|
Qodo-clean, CI green — ready for review. |
The subscription-wait loop introduced in 3958ddb gated its timeout on DateTime.UtcNow. Wall-clock time is not monotonic: an NTP step during the wait either cuts it short (losing the leading frames the loop exists to protect) or stretches it past the intended bound. Stopwatch is the clock every other wait loop in this suite uses, and PR #602 replaced a UtcNow deadline for exactly this reason. Test-only; no production code touched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
What was wrong
Ask a DAQiFi device for its system log and, very occasionally, you would get an answer that was not yours.
SYSTem:LOG?ends its dump with a blank line, and that blank is the whole signal the library uses to tell "the device answered and its log is empty" apart from "the device never answered at all". A blank left over from an earlier command that gave up could still arrive in the moment between the library queueing your command and actually writing it to the wire — and it would be counted as your answer. The visible result is an empty log reported for a device that had not yet been asked anything, and, worse, a diagnostics read that stops waiting early because it thinks the reply has already started coming in.PR #591 closed most of this window. What remained is the gap between "the command has been handed to the writer" and "the writer has put it on the wire", which is where this fix lands.
How it was fixed
Every line the exchange collects now carries a snapshot of the outbound writer taken at the moment the line arrived: how many writes the writer had begun, and whether it still had something waiting. A blank that arrived while a command sat queued behind a writer that had not begun writing anything for this exchange cannot be answering that exchange, so it is dropped. Anything from the instant the write begins onward is kept.
The obvious alternative — waiting for the outbound queue to drain before deciding — is what issue #593 exists to rule out: on a real Nq1 that breaks
SYSTem:LOG?outright, 10/10 runs, because the device answers faster than the drain's own poll tick and its genuine terminator lands on the wrong side of the boundary. Nothing here waits for anything; the writer is only ever sampled.Two things a reviewer may want to push back on, both deliberate:
SYSTem:LOG?framing test regressed exactly the way the drain does.Verification
/dev/cu.usbmodem1101), example CLI and a read-only probe built against this branch:SYSTem:LOG?10/10 clean at ~1056 ms per call — the same figure Residual capture-to-write window in TextExchangeEngine's blank-line boundary, and why draining the outbound queue must not be used to close it #593 records for the healthy baseline — with a populated log on the first read and the blank-terminated empty log on the rest. Connect/initialize,--show-status,--lan-chip-infoand a short 10 Hz stream all behaved normally. Nothing destructive was run.closes #593
Not merging — for review.