Xavi Montero
Xavi Montero

Reputation: 10725

Make ReactPhp to return "202 Accepted" and continue processing after the response has been sent

I'm willing to create a handler on ReactPHP that is able to return a "202 Accepted" immediately after the request has been made, but keeps doing things on the background.

For this example, no matter if it's 202 or its merely 200. The problem is to push back the heavy load to the loop and return before executing the load.

To test I created a blank dir and required ReactPhp via composer. Then I created a file named server.php with this:

<?php

declare( strict_types = 1 );

namespace App;

require __DIR__ . '/vendor/autoload.php';

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use React\EventLoop\Loop;
use React\Http\HttpServer;
use React\Http\Message\Response;
use React\Socket\SocketServer;

function respond( ServerRequestInterface $request ) : ResponseInterface
{
    // This creates only a "humanized id" to play with multiple concurrent
    // browsers and watching the matches between the echoes in the console
    // and the results on the browser window
    $path = $request->getUri()->getPath();
    $callId = mt_rand( 1000, 9999 );
    $id = json_encode( [ 'path' => $path, 'callId' => $callId ] );

    // I simulate a heavy-load that lasts 10 seconds. I print the
    // numbers 0 to 9 waiting 1 second between each other.
    // I was expecting that pushing this into the Loop with a callback
    // would not block the request, as what I do here (I think) is
    // just call Loop::futureTick( [callback] ) to set the callback but
    // not to execute it.
    Loop::futureTick( function() use ( $id ) {
        $first = true;
        for( $i = 0; $i < 10; $i++ )
        {
            if( ! $first )
            {
                sleep( 1 );
            }
            echo( $i . ' - ' . $id . PHP_EOL );
            $first = false;
        }
    } );

    // I was expecting to be here *after* setting the callback
    // and *before* executing the callback.
    // Nevertheless the browser does not display the result
    // until after 10 seconds, instead of immediately.
    return Response::plaintext( 'Hello World! - ' . $id . PHP_EOL );
}

$server = new HttpServer( 'App\\respond' );

$url = '0.0.0.0:22900';
$socket = new SocketServer( $url );
$server->listen( $socket );

echo "Server running at http://" . $url . PHP_EOL;

When I run the server in the shell I see it running.

Essentially it creates a server, it sets the function respond as the handler and, overall, it works.

Except that I was expecting the return Response::plaintext(...) to be responded immediately because I thought that the callback inside Loop::futureTick() would be called after returning and therefore after the response was sent.

Instead, I see the loop counting from 0 to 9 during 10 seconds in the shell and only after that the browser displays the response.

Question

How can I detach the heavy load from the response hook and push it into the Loop so it's executed later, way after the response was sent to the browser?

Upvotes: 0

Views: 141

Answers (1)

Xavi Montero
Xavi Montero

Reputation: 10725

I solved it by using the Async library from ReactPhp here https://reactphp.org/async/

First I required it as it seems it does not come with the default ReactPhp requirement, by doing this:

composer require react/async

Then I changed 3 pieces of code:

  • I wrapped the callback with an async() call.
  • I wrapped the the sleep with an await() call.
  • I replaced the sleep() by an asynchronous sleep from ReactPhp.

I did all by importing the functions into the namespace so the code is easier to read than setting the full namespace there:

In the use section:

use function React\Async\async;
use function React\Async\await;
use function React\Promise\Timer\sleep;

The full handler:

function respond( ServerRequestInterface $request ) : ResponseInterface
{
    $path = $request->getUri()->getPath();
    $callId = mt_rand( 1000, 9999 );
    $id = json_encode( [ 'path' => $path, 'callId' => $callId ] );

    // CHANGE 1: Note the async() here wrapping all the callback
    Loop::futureTick( async( function() use ( $id ) {
        $first = true;
        for( $i = 0; $i < 10; $i++ )
        {
            if( ! $first )
            {
                // CHANGE 2: Note the await() wrapping the sleep().
                // CHANGE 3: The sleep() has been replaced thanks to the use statement.
                await( sleep( 1 ) );
            }
            echo( $i . ' - ' . $id . PHP_EOL );
            $first = false;
        }
    } ) );

    // Added this to check in the CLI when the response is sent.
    echo( 'About to respond! - ' . $id . PHP_EOL );
    return Response::plaintext( 'Hello World! - ' . $id . PHP_EOL );
}

Now the browser displays the response immediately, and way after the response has been printed on the browser the console shows the loops.

Even doing that in multiple browsers, each loop is run "in parallel" to the other.

This screenshot shows:

  1. loading browser A
  2. then waiting for 3 seconds
  3. then loading browser B
  4. then waiting for 3 more seconds
  5. and then capturing the screen

We can see IDs already printed in the browser window and still the CLI in the middle of the loop (second 6 for ID A and second 3 for ID B):

Response workers for browsers A and B

Upvotes: 0

Related Questions