Skip to content

Report Io\Poll level watchers with buffered stream data - #24017

Open
bukka wants to merge 1 commit into
php:PHP-8.6from
bukka:poll_buffered_data
Open

bukka wants to merge 1 commit into
php:PHP-8.6from
bukka:poll_buffered_data

Conversation

@bukka

@bukka bukka commented Sep 30, 2026

Copy link
Copy Markdown
Member

A stream can hold unread data in its buffer while its descriptor is idle. stream_select() treats such a stream as readable but a level watcher looked at the descriptor only, so a loop waiting for readability never woke up (GH-23948).

Context::wait() now checks level watchers with Read interest for buffered data first and reports them with Read triggered. The backend is then asked without waiting so descriptor events are merged in and a watcher readable both ways is returned once. Edge watchers are not affected. A one-shot reported from its buffer is disarmed like one fired by the backend and modifyEvents() re-arms it. The check reads the stream buffer positions only and does no IO.

The check is a handle operation so other handle types can provide it. It is registered separately as php_poll_handle_ext_ops because the handle ops struct cannot grow without an ABI change. This is temporary and the operation moves to the ops struct in master.

@bukka
bukka marked this pull request as ready for review September 30, 2026 13:33
A stream can hold unread data in its buffer while its descriptor is idle.
stream_select() treats such a stream as readable but a level watcher
looked at the descriptor only, so a loop waiting for readability never
woke up (phpGH-23948).

Context::wait() now checks level watchers with Read interest for buffered
data first and reports them with Read triggered. The backend is then asked
without waiting so descriptor events are merged in and a watcher readable
both ways is returned once. Edge watchers are not affected. A one-shot
reported from its buffer is disarmed like one fired by the backend and
modifyEvents() re-arms it. The check reads the stream buffer positions
only and does no IO.

The check is a handle operation so other handle types can provide it. It
is registered separately as php_poll_handle_ext_ops because the handle ops
struct cannot grow without an ABI change. This is temporary and the
operation moves to the ops struct in master.
Comment thread main/php_poll.h
/* Extra handle ops registered per ops pointer (temporary, ABI) */
typedef struct php_poll_handle_ext_ops {
/* Data above the descriptor that a read returns without waiting */
bool (*has_buffered_data)(php_poll_handle_object *handle);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would make sense to add this operation on php_poll_handle_ops directly. We can make it optional (function can be null).

This pattern is adding complexity but the benefits are not obvious to me. This doesn't appear to be for decoupling or separation of concerns purposes. It saves one pointer per php_poll_handle_ops (or more if we add other extra ops), but we lose that in php_poll_handle_ext_ops_registry, plus the lookup.

Comment thread ext/standard/io_poll.c
Comment on lines +991 to +994
if (watcher->wait_gen == gen) {
/* Already reported from its buffer */
watcher->triggered_events |= events[i].revents;
continue;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The watcher may end up being incorrectly skipped in case the gen overflows.

Does watcher->wait_gen need to survive? If not this could be a boolean if we reset it after this loop.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants