====== PHP RFC: Polling API ======
  * Version: 1.0
  * Date: 2025-10-30
  * Author: Jakub Zelenka, bukka@php.net
  * Status: Under Discussion
===== Introduction =====
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 (typically 1024)
  * Poor performance with large numbers of file descriptors
  * No access to modern, efficient polling mechanisms like epoll (Linux), or kqueue (BSD/macOS)
  * No support for advanced features like edge-triggered notifications
Modern applications, particularly those handling many concurrent connections (web servers, message
queues, WebSocket servers), would benefit significantly from access to platform-specific,
high-performance polling mechanisms.
===== 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, with an option to
explicitly choose a specific backend for testing or compatibility purposes.
==== PollHandle Abstraction ====
A key innovation in this API is the PollHandle 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 future implementations can add:
  * **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 and constants:
==== Event Constants ====
Event constants can be combined using bitwise OR operator (|).
=== Input Events ===
These events can be specified when adding or modifying watchers to indicate which conditions to monitor:
  * **POLL_EVENT_READ** - Monitor for readability - the file descriptor has data available to read
  * **POLL_EVENT_WRITE** - Monitor for writability - the file descriptor is ready to accept data
  * **POLL_EVENT_RDHUP** - Monitor for peer shutdown of write half of connection (Linux epoll only, must be explicitly requested)
  * **POLL_EVENT_ONESHOT** - One-shot mode - automatically remove the watcher after it triggers once
  * **POLL_EVENT_ET** - 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:
  * **POLL_EVENT_READ** - The file descriptor is ready for reading
  * **POLL_EVENT_WRITE** - The file descriptor is ready for writing
  * **POLL_EVENT_ERROR** - An error condition occurred on the file descriptor (automatically monitored)
  * **POLL_EVENT_HUP** - Hang up occurred - connection closed by peer (automatically monitored)
  * **POLL_EVENT_RDHUP** - Peer closed connection or shut down writing half of connection (Linux epoll only)
Note that POLL_EVENT_READ, POLL_EVENT_WRITE, and POLL_EVENT_RDHUP can appear in both input (to request monitoring) and output (when conditions occur). POLL_EVENT_ERROR and POLL_EVENT_HUP are automatically monitored by all backends and only appear in output events.
==== Usage Examples ====
=== TCP Server Example ===
add($serverHandle, POLL_EVENT_READ, ['type' => 'server']);
$clients = [];
echo "Server listening on port 8080\n";
while (true) {
    // Wait for events (timeout in milliseconds, -1 = infinite)
    $events = $poll->wait(1000);
    
    foreach ($events as $watcher) {
        $data = $watcher->getData();
        
        if ($data['type'] === 'server' && $watcher->hasTriggered(POLL_EVENT_READ)) {
            // Accept new client connection
            $handle = $watcher->getHandle();
            $server = $handle->getStream();
            $client = stream_socket_accept($server, 0);
            if ($client) {
                stream_set_blocking($client, false);
                $clientHandle = new StreamPollHandle($client);
                $clientWatcher = $poll->add($clientHandle, POLL_EVENT_READ, ['type' => 'client']);
                $clients[] = $clientWatcher;
                echo "New client connected\n";
            }
        } elseif ($data['type'] === 'client') {
            $handle = $watcher->getHandle();
            $stream = $handle->getStream();
            
            if ($watcher->hasTriggered(POLL_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(POLL_EVENT_HUP | POLL_EVENT_ERROR)) {
                echo "Client connection error or hangup\n";
                $watcher->remove();
                fclose($stream);
            }
        }
    }
}
?>
=== TCP Client Example ===
wait($timeout);
    
    if (empty($events)) {
        echo "Timeout waiting for response\n";
        return false;
    }
    
    foreach ($events as $watcher) {
        if ($watcher->hasTriggered(POLL_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(POLL_EVENT_ERROR | POLL_EVENT_HUP)) {
            echo "Connection error\n";
            return false;
        }
    }
    
    return true;
}
// Create a poll context
$poll = new PollContext();
// 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, POLL_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 PollContext();
    }
    
    /**
     * Add a stream with callback handlers
     * 
     * @param resource $stream The stream to monitor
     * @param int $events Events to watch for
     * @param callable $callback Callback to invoke when events occur
     * @return PollWatcher The created watcher
     */
    public function addStream($stream, int $events, callable $callback): PollWatcher
    {
        $handle = new StreamPollHandle($stream);
        return $this->poll->add($handle, $events, ['callback' => $callback]);
    }
    
    /**
     * Run the event loop
     */
    public function run(): void
    {
        while (true) {
            $events = $this->poll->wait();
            
            foreach ($events as $watcher) {
                $data = $watcher->getData();
                $callback = $data['callback'];
                
                // Get stream from the handle
                $handle = $watcher->getHandle();
                $stream = $handle->getStream();
                
                // Invoke the callback with the watcher and stream
                $callback($watcher, $stream);
            }
        }
    }
    
    public function getContext(): PollContext
    {
        return $this->poll;
    }
}
// Usage example - Simple echo server using callbacks
$loop = new EventLoop();
$server = stream_socket_server('tcp://127.0.0.1:9090');
stream_set_blocking($server, false);
$loop->addStream($server, POLL_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, POLL_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, POLL_EVENT_READ, 'some data');
// Later, modify to watch for write events instead
$watcher->modifyEvents(POLL_EVENT_WRITE);
// Or modify both events and data
$watcher->modify(POLL_EVENT_READ | POLL_EVENT_WRITE, 'updated data');
// Just modify the associated data
$watcher->modifyData('new data');
// Remove the watcher when done
$watcher->remove();
?>
=== Edge-Triggered Mode ===
add($handle, POLL_EVENT_READ | POLL_EVENT_ET);
while (true) {
    $events = $poll->wait();
    
    foreach ($events as $watcher) {
        if ($watcher->hasTriggered(POLL_EVENT_READ)) {
            // In ET mode, read until EAGAIN
            while (($data = fread($stream, 8192)) !== false && $data !== '') {
                process_data($data);
            }
        }
    }
}
?>
=== Explicit Backend Selection ===
getMessage() . "\n";
    exit(1);
}
// Check which backend is actually being used
echo "Active backend: " . $poll->getBackend()->value . "\n";
?>
==== Internal API ====
In addition to the userspace API, this RFC introduces a new internal polling API that will be
available to PHP core and extensions. This internal API provides:
  * **Unified polling interface** - Extensions can use a 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 for extensions that need it
The internal API is designed to eventually replace the various ad-hoc polling implementations
currently scattered throughout the codebase. The API is exposed through the php_poll.h header
and provides the following key functions:
  * **php_poll_create()** - Create a new poll context with specified backend
  * **php_poll_add()** - Add a file descriptor to monitor
  * **php_poll_modify()** - Modify watched events for a file descriptor
  * **php_poll_remove()** - Remove a file descriptor from monitoring
  * **php_poll_wait()** - Wait for events with timeout
Extensions can also provide custom handle types by implementing the php_poll_handle_ops structure,
which allows them to define how their resources map to file descriptors and how validity is checked.
===== 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
===== 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
Modern polling mechanisms like epoll and kqueue are specifically designed for high-performance
applications and can scale to tens of thousands of concurrent connections where select() becomes
impractical.
===== 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
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 [repository URL].
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.0 - Initial version