Skip to content

Do not report ECANCELED under a pending exception as a stream failure - #3

Merged
bukka merged 2 commits into
bukka:io_hooks_pocfrom
true-async:io-hooks-fixes
Oct 7, 2026
Merged

bukka merged 2 commits into
bukka:io_hooks_pocfrom
true-async:io-hooks-fixes

Conversation

@EdmondDantes

Copy link
Copy Markdown

Do not report ECANCELED under a pending exception as a stream failure

When an op is abandoned because of an exception (the provider's run() fails, or php_io_run_ex() does not start it because an exception is already pending), the wrappers get -1 with errno == ECANCELED. plain_wrapper.c (read and write) and xp_socket.c (read) treat that as a real failure:

  • a notice Read of 8192 bytes failed with errno=125 Operation canceled (or Write of …) on top of the exception;
  • eof set on a stream that is not at its end, so after catching the exception feof() is true and the next fgets() on the pipe returns false.

The socket write path already skips its warning under EG(exception), and xp_ssl treats ECANCELED as a retry, so the plain and socket read paths were the odd ones out.

Fix: skip the notice and eof only for ECANCELED with EG(exception) set. ECANCELED without an exception (a provider completing the op as Cancelled, a kernel -ECANCELED from the ring) stays an error. php_io_hooks.h now documents both meanings.

Tests: ext/standard/tests/streams/hooks/data-ops-cancel-socket-not-eof.phpt and data-ops-cancel-pipe-not-eof.phpt. Both fail on io_hooks_poc (608927e) and pass with the fix; ext/standard/tests/streams, file and ext/sockets pass.

A plain-wrapper read or write, or a socket read, returns ECANCELED when its
op was abandoned for an exception: the provider's run() failed, or the op
never started because the exception was already pending. The wrappers took
that for a failure: a notice on top of the exception, and eof set on a
stream that is not at its end. The socket write path and the TLS wrapper
already stay quiet in that case. ECANCELED without a pending exception, a
provider completing the op as Cancelled, stays an error.
@EdmondDantes
EdmondDantes requested a review from bukka as a code owner October 6, 2026 08:53
Once a parked accept armed the multishot accept of a listener, the ring
moved new connections from the kernel into the record's buffer, and
stream_select() and socket_select() polled the kernel only: a select and
a non-blocking accept loop waited its whole timeout with connections
pending. A Poll for reading on such a listener is now answered from the
buffer, or parks on the record until the multishot buffers a connection
or ends.

A non-blocking accept with an empty buffer no longer arms the multishot:
the direct one completes Unsupported so the core calls accept(), which
also makes it work on the threads backend, where a chain whose deadline
is due never starts.
@bukka
bukka merged commit c43e1d5 into bukka:io_hooks_poc Oct 7, 2026
1 check passed
@bukka

bukka commented Oct 7, 2026

Copy link
Copy Markdown
Owner

Thanks this is now integrated with the more generic fix for the select / multishot bit in 67af13e

@EdmondDantes
EdmondDantes deleted the io-hooks-fixes branch October 7, 2026 16:10
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.

2 participants