io: keep a cancelled read's bytes, and release a device a failed setup claimed - #1201
Open
BioCam wants to merge 2 commits into
Open
io: keep a cancelled read's bytes, and release a device a failed setup claimed#1201BioCam wants to merge 2 commits into
io: keep a cancelled read's bytes, and release a device a failed setup claimed#1201BioCam wants to merge 2 commits into
Conversation
…ure needs to replay Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Builds on #1199, based on that branch, so this is the two commits on top.
Moving the blocking calls onto the executor makes them cancellable for the first time.
asyncio.wait_foraround a read onio.ftdiused to be a no-op, because the call never suspended and so always ran to completion; now the timeout fires. Cancelling the task does not stop the worker thread though: the transfer finishes, its bytes land in a future nobody is waiting on, and they are dropped. Those bytes are already out of the chip's buffer, so the nextreadpicks up after them, part-way through the response the driver was assembling. Nothing raises where the loss happens, so it surfaces later as a checksum failure or a reply matched to the wrong command.stop_shaking()inAgilentBioTekPlateReaderBasealready has this shape: it cancels_shaking_task, which may be insidesend_command.Separately,
USB.setup's new failure path clearsself.devafter_setup_syncmay already have claimed the device, andstop()returns early ondev is None, so nothing ever disposes it.FTDI.readshields the executor call and keeps what it produced in a small buffer that the nextreadis served from. Areadissued while one is in flight waits for that one instead of starting a second.FTDI.writeshields too, so a cancelled write reaches the device in full rather than leaving it holding a partial command, or none, with no way for the caller to tell which.FTDI.usb_purge_rx_buffer,FTDI.usb_resetandFTDI.stopdrop the buffer and anything in flight, since all three mean the caller has declared that data stale.FTDICommandassignsdataand takesmodule, likeSerialCommandandUSBCommandalready do. It declareddatawithout assigning it, so everyFTDICommandwas written out without its bytes, and rebuilding one from a capture file raised onmodule.FTDIValidator.readalso compared the recorded hex string's length against a byte count, so no recorded read could match.FTDIValidatorcan now replay a recorded capture; wiring thevalidate()entry point back up, and wideningFTDIValidator.__init__to accept whatFTDI.serialize()returns, are separate and not touched here.FTDI.readlineis assembled from single-byte reads, with aterminatorand atimeout.pylibftdi's own raisesTypeErrorunless the device was opened in text mode andFTDI.setupopens it in byte mode, so the inherited one could only ever raise. No backend onio.ftdicalls it; the fifteenreadlinecallers in the tree are all onio.serialorio.socket.USB.setupdisposes the device before clearing it.Behaviour: no change to a
readorwritethat runs to completion, which is every caller in the tree today. A cancelledreadstill raises, but no longer costs the bytes already in flight; a cancelledwritenow always completes.FTDI.readlinegains two optional arguments and returns an assembled line instead of raising. Captures fromio.ftdigain thedatafield they always declared; no existingio.ftdicapture file was replayable, so nothing regresses.io.serial,io.hidandio.usbreads have the same cancellation shape, pre-dating #1199 since they were already offloaded, and are left alone here.Tests:
FTDICancellationTests(a cancelledread's bytes reach the nextreadand are captured once, a cancelledwritestill reaches the device, a purge discards held bytes, a text-mode device still reads, a second concurrent reader backs off),FTDICaptureTests(a capture replays throughFTDIValidator),FTDIReadlineTests(terminator,timeout, emptyterminator), andUSBSetupFailureTests(a failed setup disposes the claimed device). All twelve fail against #1199 alone.ruff format,ruff check --select I,ruff checkandmypy pylabrobot/io --check-untyped-defsare clean; the full suite passes (2144 passed, 2 skipped, 162 subtests).🤖 Generated with Claude Code