Skip to content

Keep the bytes a cancelled read took from the descriptor - #289

Merged
EdmondDantes merged 1 commit into
mainfrom
288-cancelled-read-swallows-bytes
Sep 9, 2026
Merged

EdmondDantes merged 1 commit into
mainfrom
288-cancelled-read-swallows-bytes

Conversation

@EdmondDantes

Copy link
Copy Markdown
Contributor

Closes #288.

A file read is submitted with offset = -1, so read(2) moves the descriptor offset. A cancelled or timed-out coroutine returned -1 and disposed the request, and the bytes the worker had already taken went to nobody: the next reader on that handle carried on past them, and ftell() answered a number unrelated to the descriptor. On a file whose every 16 bytes carry their own record number, after a cancelled 64 KB read the next read returned the record at byte 65536 while ftell() said 32.

Three parts, two of them here:

  • A read abandoned mid-flight seeks the descriptor back by what it took. The rewind waits for the thread pool to be empty: a write, an fsync or a sendfile on the same handle moves that offset too.
  • File reads are serialized per handle the way file writes already are, so nothing reads from the advanced offset before the rewind. Without the queue the test is red 6 runs in 10; with it, 0 in 10. The two queues share one unlink helper — the copy it replaces cleared q_next while looking in the other queue, which cut the list in two.
  • php-src side, true-async c9932d7d878: a read that finished before the exception arrived hands its count back instead of -1, since the bytes are already in the caller's buffer. A stream with a read filter keeps the old answer — the filter call under a pending exception returns without running, and the filter layer reads that as a fatal error the stream never recovers from.

A submit that fails now completes the request with its exception instead of throwing, which is what the write path already does: fread() reports the error as -1 with a notice rather than as an exception.

Verification

  • tests/io/100-cancel_keeps_the_position.phpt: red 4 runs in 5 before, green 10 in 10 after.
  • Debug: ext/async/tests plus php-src file, streams, filters, general_functions, zlib, phar, session — 3831 tests, 3 failures, all three the local symlink and network artefacts (curl 063, curl 064, io 082).
  • ASAN: ext/async/tests plus php-src file, streams, filters — 1937 tests, 1 failure, the local network artefact.
  • Cost: one read at a time per handle is what the stream's read-side lock already enforces, so the queue is only reached when a cancelled read is still in flight. 256 MB in 8 KB chunks on a release ZTS build, medians of five runs: 2226.3 ms before, 2171.6 ms after.

A file read is submitted with offset -1, so read(2) moves the descriptor
offset. A cancelled coroutine returned -1 and disposed the request, and the
bytes the worker had already taken went to nobody: the next reader carried on
past them, and ftell() answered a number unrelated to the descriptor.

A read abandoned mid-flight now seeks the descriptor back by what it took, and
file reads are serialized per handle the way file writes already are, so
nothing reads from the advanced offset before that rewind. The two queues share
one unlink helper: the copy this replaces cleared q_next while looking in the
other queue, which cut the list in two.

A submit that fails now completes the request with its exception instead of
throwing, which is what the write path already does: fread() reports the error
as -1 with a notice rather than as an exception.

Evidence: tests/io/100-cancel_keeps_the_position.phpt, red 4 runs in 5 before
and green 10 in 10 after. The queue costs nothing measurable — 256 MB in 8 KB
chunks on a release build, medians of five runs, 2226.3 ms before, 2171.6 ms
after.
@codecov

codecov Bot commented Sep 9, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@EdmondDantes
EdmondDantes merged commit 46aae49 into main Sep 9, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A cancelled read swallows the bytes the worker already took

1 participant