Timeouts in PHP

Timeouts are important for programs that connect to external resources or that otherwise need to bound execution time. Implementing timeouts in PHP is possible using non-blocking I/O and the stream_select() function.

<?php

// For our example, suppose we're executing an external
// call that returns its result after 2 seconds.
function longRunningTask() {
    sleep(2);
    return "result 1";
}

function runWithTimeout($func, $timeout) {
    // Create a stream pair
    $pair = stream_socket_pair(STREAM_PF_UNIX, STREAM_SOCK_STREAM, STREAM_IPPROTO_IP);
    
    // Fork the process
    $pid = pcntl_fork();
    if ($pid == -1) {
        die('Could not fork');
    } else if ($pid) {
        // Parent process
        fclose($pair[1]);
        $read = array($pair[0]);
        $write = $except = null;
        $result = stream_select($read, $write, $except, $timeout);
        if ($result === false) {
            die('stream_select failed');
        } else if ($result === 0) {
            // Timeout occurred
            posix_kill($pid, SIGTERM);
            pcntl_wait($status);
            return "timeout";
        } else {
            // Result received
            return stream_get_contents($pair[0]);
        }
    } else {
        // Child process
        fclose($pair[0]);
        $result = $func();
        fwrite($pair[1], $result);
        exit(0);
    }
}

// Run the long-running task with a 1-second timeout
$result = runWithTimeout('longRunningTask', 1);
echo $result . "\n";

// Run the long-running task with a 3-second timeout
$result = runWithTimeout('longRunningTask', 3);
echo $result . "\n";

Running this program shows the first operation timing out and the second succeeding.

$ php timeouts.php
timeout
result 1

In this PHP implementation:

  1. We define a longRunningTask() function that simulates a task taking 2 seconds to complete.

  2. The runWithTimeout() function takes a callback and a timeout duration as arguments. It uses pcntl_fork() to create a child process that runs the long-running task.

  3. In the parent process, we use stream_select() to wait for either the result from the child process or for the timeout to occur.

  4. If the timeout occurs before the result is received, we terminate the child process and return “timeout”.

  5. If the result is received before the timeout, we return the result.

  6. We run the long-running task twice: once with a 1-second timeout (which results in a timeout) and once with a 3-second timeout (which allows the task to complete).

Note that this implementation requires the pcntl extension to be enabled in PHP, which is typically available on Unix-like systems but not on Windows. For a more portable solution, you might consider using asynchronous libraries like ReactPHP or Amp.