====== PHP RFC: Polling API ====== * Version: 1.1 * Date: 2025-10-30 * Author: Jakub Zelenka, bukka@php.net * Status: Under Discussion ===== Introduction ===== ==== What is I/O Polling? ==== I/O polling (also called I/O multiplexing) is a mechanism that allows a program to efficiently monitor multiple I/O resources (like network sockets, pipes, or files) simultaneously and be notified when any of them are ready for reading, writing, or have encountered an error condition. Instead of checking each resource repeatedly or blocking on a single resource, polling lets you wait for events on many resources at once. Think of it like a receptionist monitoring multiple phone lines: rather than picking up each phone individually to check if someone is calling, the receptionist can see at a glance which lines have incoming calls and handle them as needed. Common polling mechanisms include: * **select()** and **poll()** - Traditional POSIX mechanisms, work everywhere but have performance limitations * **epoll** (Linux) - High-performance, scalable to thousands of connections * **kqueue** (BSD/macOS) - High-performance, generalized event notification * **event ports** (Solaris/illumos) - High-performance kernel event delivery * **WSAPoll** (Windows) - Windows equivalent of poll() ==== Current Situation in PHP ==== PHP currently provides only the stream_select() function for I/O multiplexing, which is based on the select() system call. This approach has several limitations: * **Limited scalability** - select() has a maximum file descriptor limit in the current implementation (typically 1024 on many systems) * **Poor performance** - O(n) complexity with large numbers of file descriptors * **No access to modern mechanisms** - Cannot use epoll (Linux), kqueue (BSD/macOS), or event ports (Solaris) * **No advanced features** - No support for edge-triggered notifications or one-shot mode * **No internal polling API** - No standardized polling mechanism that PHP core and extensions can share ==== Primary Motivation: Internal API ==== The **primary motivation** for this RFC is to provide a unified internal polling API that PHP core and extensions can use. Currently, there are several areas in PHP that would benefit from better polling capabilities: * **Signal handling in ZTS** - Safe, efficient signal handling in threaded environments, which is important for FrankenPHP's goroutine-based TSRM mode * **PHP-FPM improvements** - More flexible event handling in FPM workers before accept(), replacing current ad-hoc implementations * **Timer support** - Cross-platform timer implementation, particularly important on macOS where current implementations have issues * **Extension needs** - Extensions like sockets, curl, and others can use a standardized polling interface By creating this internal API, we establish a foundation that PHP's internals can rely on for efficient I/O operations without depending on external libraries like libuv or libevent. ==== Secondary Benefit: Userspace API ==== Once we have a robust internal API, exposing it to userspace provides significant benefits: * **stream_select() replacement** - Modern, scalable alternative for stream polling * **Asynchronous framework support** - Projects like AMPHP, ReactPHP, and Revolt can use a single, efficient backend instead of maintaining multiple backend implementations * **High-performance applications** - Users building servers, message queues, or WebSocket servers get access to platform-specific optimizations * **Lower barrier to entry** - No need to install external extensions for basic async I/O This userspace API is designed to be **minimal and focused** - it provides polling capabilities, not a complete event loop. Applications that need full event loop abstractions with timers, signals, and child process management can use libraries like AMPHP or ReactPHP, which can then utilize this API as their polling backend. ==== Why Not Bundle libuv or libevent? ==== A common question is why not make ext-uv (libuv) or ext-event (libevent) built-in extensions. There are several reasons: * **Dependencies** - Both are external libraries, which cannot be enabled by default unless bundled * **Bloat** - libuv in particular is quite large and includes many features beyond basic polling * **Overhead** - For internal use cases (like signal handling in ZTS), we need minimal overhead * **Control** - Having the implementation in PHP core gives us full control over the API and behavior * **Integration** - A native implementation integrates better with PHP's existing stream infrastructure The polling API provides exactly what we need - efficient, cross-platform polling - without the complexity of a full event loop library. ===== Proposal ===== The proposal is to add a new polling API to PHP core that provides a unified interface to platform-specific polling mechanisms. This API will automatically select the best available backend for the current platform while providing a consistent interface across all systems. ==== Supported Backends ==== The implementation will support the following polling backends: * **epoll** - Linux * **kqueue** - BSD, macOS * **event ports** - Solaris, illumos * **WSAPoll** - Windows * **poll** - Fallback for other POSIX systems The backend will be selected automatically based on platform availability. Users generally should not need to specify a backend explicitly - the Auto backend selection will always choose the most appropriate one for the platform. ==== Handle Abstraction ==== A key innovation in this API is the Handle abstraction. This abstract class represents any pollable resource and can be extended to support various resource types. The initial implementation provides StreamPollHandle for stream resources, but the design allows for future implementations: * **StreamPollHandle** - For stream resources (initial implementation) * **SocketPollHandle** - For socket extension resources (future scope) * **CurlPollHandle** - For libcurl file descriptors (future scope) This design allows the polling API to evolve and support additional resource types without breaking backward compatibility. ===== API ===== The proposal introduces several new classes organized under the ''Io\Poll'' namespace: ==== Context Class ==== ==== Watcher Class ==== ==== Handle Classes ==== ==== Backend Enum ==== ==== Event Enum ==== ==== Exception Hierarchy ==== ==== Event Enum Cases ==== Event enum cases can be combined in arrays passed to ''Context::add()'' and ''Watcher::modify()'' methods. === Input Events === These events can be specified when adding or modifying watchers to indicate which conditions to monitor: * **Event::Read** - Monitor for readability - the file descriptor has data available to read * **Event::Write** - Monitor for writability - the file descriptor is ready to accept data * **Event::ReadHangUp** - Monitor for peer shutdown of write half of connection (Linux epoll only, must be explicitly requested) * **Event::OneShot** - One-shot mode - automatically remove the watcher after it triggers once * **Event::EdgeTriggered** - Edge-triggered mode - only report state changes rather than state (epoll and kqueue only) === Output Events === These events are returned by the polling backend to indicate conditions that occurred: * **Event::Read** - The file descriptor is ready for reading * **Event::Write** - The file descriptor is ready for writing * **Event::Error** - An error condition occurred on the file descriptor (automatically monitored) * **Event::HangUp** - Hang up occurred - connection closed by peer (automatically monitored) * **Event::ReadHangUp** - Peer closed connection or shut down writing half of connection (Linux epoll only) Note that ''Event::Read'', ''Event::Write'', and ''Event::ReadHangUp'' can appear in both input (to request monitoring) and output (when conditions occur). ''Event::Error'' and ''Event::HangUp'' are automatically monitored by all backends and only appear in output events. ===== Usage Examples ===== ==== Basic TCP Server ==== add($serverHandle, [Event::Read], ['type' => 'server']); echo "Server listening on port 8080\n"; while (true) { // Wait for events (timeout in milliseconds, -1 = infinite) // Returns array of Watcher instances that have events $watchers = $poll->wait(1000); foreach ($watchers as $watcher) { $data = $watcher->getData(); if ($data['type'] === 'server' && $watcher->hasTriggered(Event::Read)) { // Accept new client connection $handle = $watcher->getHandle(); if ($handle instanceof StreamPollHandle) { $server = $handle->getStream(); $client = stream_socket_accept($server, 0); if ($client) { stream_set_blocking($client, false); $clientHandle = new StreamPollHandle($client); $poll->add($clientHandle, [Event::Read], ['type' => 'client']); echo "New client connected\n"; } } } elseif ($data['type'] === 'client') { $handle = $watcher->getHandle(); if ($handle instanceof StreamPollHandle) { $stream = $handle->getStream(); if ($watcher->hasTriggered(Event::Read)) { // Read data from client $buffer = fread($stream, 8192); if ($buffer === false || $buffer === '') { echo "Client disconnected\n"; $watcher->remove(); fclose($stream); } else { echo "Received: $buffer"; // Echo back to client fwrite($stream, "Echo: $buffer"); } } if ($watcher->hasTriggered(Event::HangUp) || $watcher->hasTriggered(Event::Error)) { echo "Client connection error or hangup\n"; $watcher->remove(); fclose($stream); } } } } } ==== TCP Client ==== wait($timeout); if (empty($watchers)) { echo "Timeout waiting for response\n"; return false; } foreach ($watchers as $watcher) { if ($watcher->hasTriggered(Event::Read)) { $data = fread($client, 8192); if ($data === false || $data === '') { echo "Server closed connection\n"; return false; } echo "Received: $data"; return true; } if ($watcher->hasTriggered(Event::Error) || $watcher->hasTriggered(Event::HangUp)) { echo "Connection error\n"; return false; } } return true; } // Create a poll context $poll = new Context(); // Connect to the echo server $client = stream_socket_client('tcp://127.0.0.1:8080', $errno, $errstr, 30); if (!$client) { die("Failed to connect: $errstr\n"); } stream_set_blocking($client, false); // Create handle and watch for readable data $handle = new StreamPollHandle($client); $watcher = $poll->add($handle, [Event::Read]); // Send messages to the server $messages = ["Hello, Server!\n", "How are you?\n", "Goodbye!\n"]; foreach ($messages as $message) { if (!sendAndReceive($poll, $watcher, $client, $message)) { break; } // Small delay between messages usleep(100000); // 100ms } fclose($client); echo "Client finished\n"; ==== Callback-based Event Handler ==== poll = new Context(); } /** * Add a stream with callback handlers * * @param resource $stream The stream to monitor * @param array $events Array of Event enums to watch for * @param callable $callback Callback to invoke when events occur * @return \Io\Poll\Watcher The created watcher */ public function addStream($stream, array $events, callable $callback): \Io\Poll\Watcher { $handle = new StreamPollHandle($stream); return $this->poll->add($handle, $events, ['callback' => $callback]); } /** * Run the event loop */ public function run(): void { while (true) { $watchers = $this->poll->wait(); foreach ($watchers as $watcher) { $data = $watcher->getData(); $callback = $data['callback']; // Get stream from the handle $handle = $watcher->getHandle(); if ($handle instanceof \StreamPollHandle) { $stream = $handle->getStream(); // Invoke the callback with the watcher and stream $callback($watcher, $stream); } } } } public function getContext(): Context { return $this->poll; } } addStream($server, [Event::Read], function($watcher, $stream) use ($loop) { // Accept new client $client = stream_socket_accept($stream, 0); if ($client) { stream_set_blocking($client, false); echo "Client connected\n"; // Add client with its own callback $loop->addStream($client, [Event::Read], function($watcher, $stream) { $data = fread($stream, 8192); if ($data === false || $data === '') { echo "Client disconnected\n"; $watcher->remove(); fclose($stream); return; } echo "Received: $data"; // Echo back to client fwrite($stream, "Echo: $data"); }); } }); echo "Callback-based server listening on port 9090\n"; $loop->run(); ==== Modifying Watchers ==== add($handle, [Event::Read], 'some data'); // Later, modify to watch for write events instead $watcher->modifyEvents([Event::Write]); // Or modify both events and data $watcher->modify([Event::Read, Event::Write], 'updated data'); // Just modify the associated data $watcher->modifyData('new data'); // Remove the watcher when done $watcher->remove(); ==== Edge-Triggered Mode ==== getBackend(); if (!$backend->supportsEdgeTriggering()) { die("Edge-triggered mode not supported on this platform\n"); } $stream = stream_socket_client('tcp://example.com:80'); stream_set_blocking($stream, false); $handle = new StreamPollHandle($stream); // Use edge-triggered mode // In ET mode, you must read/write until EAGAIN to avoid missing events $watcher = $poll->add($handle, [Event::Read, Event::EdgeTriggered]); while (true) { $watchers = $poll->wait(); foreach ($watchers as $watcher) { if ($watcher->hasTriggered(Event::Read)) { // In ET mode, read until EAGAIN while (($data = fread($stream, 8192)) !== false && $data !== '') { process_data($data); } } } } ==== Backend Information ==== name}"; if ($backend->supportsEdgeTriggering()) { echo " (supports edge-triggering)"; } echo "\n"; } // Use automatic backend selection (recommended) $poll = new Context(); echo "Selected backend: {$poll->getBackend()->name}\n"; // Explicit backend selection (mainly for testing) // This will throw an exception if the backend is not available try { $poll = new Context(Backend::Epoll); echo "Using epoll backend\n"; } catch (\Io\Poll\FailedContextInitializationException $e) { echo "Epoll backend not available: " . $e->getMessage() . "\n"; } ===== Internal API ===== In addition to the userspace API, this RFC introduces a new internal polling API available to PHP core and extensions through the ''php_poll.h'' header. ==== Key Features ==== * **Unified polling interface** - Single API regardless of platform * **Extension handle support** - Extensions can register custom handle types through ''php_poll_handle_ops'' * **Direct file descriptor access** - Low-level access when needed * **Consistent error handling** - Unified error codes across all backends ==== Core Functions ==== /* Create a new poll context with specified backend */ php_poll_ctx *php_poll_create(php_poll_backend_type backend, uint32_t flags); /* Initialize the context (must be called before use) */ zend_result php_poll_init(php_poll_ctx *ctx); /* Add a file descriptor to monitor */ zend_result php_poll_add(php_poll_ctx *ctx, int fd, uint32_t events, void *data); /* Modify watched events for a file descriptor */ zend_result php_poll_modify(php_poll_ctx *ctx, int fd, uint32_t events, void *data); /* Remove a file descriptor from monitoring */ zend_result php_poll_remove(php_poll_ctx *ctx, int fd); /* Wait for events with timeout */ int php_poll_wait(php_poll_ctx *ctx, php_poll_event *events, int max_events, int timeout); /* Destroy the context and free resources */ void php_poll_destroy(php_poll_ctx *ctx); /* Get error information */ php_poll_error php_poll_get_error(php_poll_ctx *ctx); ==== Custom Handle Types ==== Extensions can provide custom handle types by implementing the ''php_poll_handle_ops'' structure: struct php_poll_handle_ops { /* Get file descriptor for this handle */ php_socket_t (*get_fd)(php_poll_handle_object *handle); /* Check if handle is still valid */ int (*is_valid)(php_poll_handle_object *handle); /* Cleanup handle-specific data */ void (*cleanup)(php_poll_handle_object *handle); }; This allows extensions to integrate their resources seamlessly with the polling API. ===== Backward Incompatible Changes ===== None. This RFC only adds new functionality and does not modify existing APIs. ===== Proposed PHP Version(s) ===== PHP 8.6 ===== Future Scope ===== This proposal lays the groundwork for several future enhancements: * **SocketPollHandle** - Native support for socket extension resources * **CurlPollHandle** - Integration with libcurl's multi interface for efficient async HTTP * **TimerHandle** - Timer-based events for scheduling and timeouts * **SignalHandle** - Signal notification support (SIGUSR1, SIGTERM, etc.) * **ZTS signal handling improvements** - Use the internal API to properly handle signals in threaded environments * **FPM event loop optimization** - Migrate PHP-FPM to use the internal polling API for improved performance * **Internal polling consolidation** - Replace various ad-hoc polling implementations throughout the codebase ===== Performance Considerations ===== The polling API provides significant performance benefits over stream_select(): * **Scalability** - epoll/kqueue can handle thousands of file descriptors efficiently * **O(1) complexity** - Modern backends operate in constant time regardless of descriptor count * **Reduced system calls** - Persistent event registration vs. per-call registration in select() * **Edge-triggered mode** - Further reduces system calls by notifying only on state changes Benchmarks show that epoll and kqueue maintain consistent performance with 10,000+ concurrent connections, whereas select() degrades significantly beyond a few hundred connections. ===== Proposed Voting Choices ===== As per the voting RFC, a yes/no vote with a 2/3 majority is needed for this proposal to be accepted. * Yes * No * Abstain The vote started on 2025-XX-XX at XX:XX UTC and ends on 2025-XX-XX at XX:XX UTC. ===== Patches and Tests ===== A working implementation is under development and will be available at https://github.com/php/php-src/pull/19572. The implementation includes a complete test suite covering all backends and common use cases. The tests are designed to run on all supported platforms and automatically skip backend-specific tests when the backend is not available. ===== Implementation ===== After the project is implemented, this section will contain: - the version(s) it was merged to - a link to the git commit(s) ===== References ===== * Linux epoll: https://man7.org/linux/man-pages/man7/epoll.7.html * BSD kqueue: https://man.freebsd.org/cgi/man.cgi?kqueue * Solaris Event Ports: https://docs.oracle.com/cd/E88353_01/html/E37843/port-create-3c.html * Windows WSAPoll: https://learn.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-wsapoll ===== Changelog ===== * 1.1 - Updated with better motivation explanation, polling overview, target audience clarification, fixed examples * 1.0 - Initial version