¿Las solicitudes asíncronas de Guzzle no son realmente asíncronas?

5 minutos de lectura

Avatar de usuario de Scalable
Escalable

Problema

Estamos tratando de hacer solicitudes asincrónicas simultáneas usando guzzle. Después de revisar algunos recursos, como este y este, se nos ocurrió un código que se comparte a continuación. Sin embargo, no está funcionando como se esperaba.

Parece que Guzzle está haciendo estas solicitudes de forma síncrona en lugar de asíncrona.

Expectativa

Solo para fines de prueba, estamos accediendo a una URL interna, que duerme durante 5 segundos. Con una concurrencia de 10, esperamos que las 10 solicitudes se pongan en cola inicialmente y se envíen al servidor. casi simultáneamente, donde esperarán durante 5 segundos, y luego casi todos terminarán casi al mismo tiempo. Lo que haría que el cliente guzzle recogiera 10 nuevas solicitudes del iterador y así sucesivamente.

Código

    $iterator = function() {
        $index = 0;
        while (true) {
            $client = new Client(['timeout'=>20]);
            $url="http://localhost/wait/5" . $index++;
            $request = new Request('GET',$url, []);
            echo "Queuing $url @ " . (new Carbon())->format('Y-m-d H:i:s') . PHP_EOL;
            yield $client
                ->sendAsync($request)
                ->then(function(Response $response) use ($request) {
                    return [$request, $response];
                });
        }
    };

    $promise = \GuzzleHttp\Promise\each_limit(
        $iterator(),
        10,  /// concurrency,
        function($result, $index) {
            /** GuzzleHttp\Psr7\Request $request */
            list($request, $response) = $result;
            echo (string) $request->getUri() . ' completed '.PHP_EOL;
        },
        function(RequestException $reason, $index) {
            // left empty for brevity
        }
    );
    $promise->wait();

Resultados actuales

Descubrimos que Guzzle nunca hizo una segunda solicitud hasta que finalizó la primera. etcétera.

Queuing http://localhost/wait/5/1 @ 2017-09-01 17:15:28
Queuing http://localhost/wait/5/2 @ 2017-09-01 17:15:28
Queuing http://localhost/wait/5/3 @ 2017-09-01 17:15:28
Queuing http://localhost/wait/5/4 @ 2017-09-01 17:15:28
Queuing http://localhost/wait/5/5 @ 2017-09-01 17:15:28
Queuing http://localhost/wait/5/6 @ 2017-09-01 17:15:28
Queuing http://localhost/wait/5/7 @ 2017-09-01 17:15:28
Queuing http://localhost/wait/5/8 @ 2017-09-01 17:15:28
Queuing http://localhost/wait/5/9 @ 2017-09-01 17:15:28
Queuing http://localhost/wait/5/10 @ 2017-09-01 17:15:28
http://localhost/wait/5/1 completed
Queuing http://localhost/wait/5/11 @ 2017-09-01 17:15:34
http://localhost/wait/5/2 completed
Queuing http://localhost/wait/5/12 @ 2017-09-01 17:15:39
http://localhost/wait/5/3 completed
Queuing http://localhost/wait/5/13 @ 2017-09-01 17:15:45
http://localhost/wait/5/4 completed
Queuing http://localhost/wait/5/14 @ 2017-09-01 17:15:50 

OS / información de la versión

  • ubuntu
  • PHP/7.1.3
  • GuzzleHttp/6.2.1
  • rizo/7.47.0

El problema podría ser \GuzzleHttp\Promise\each_limit .. que quizás no inicie o resuelva la promesa lo suficientemente rápido. Es posible que tengamos que engañar eso para ticking externamente.

En el código de ejemplo, está creando un nuevo GuzzleHttp\Client instancia para cada solicitud que desee realizar. Sin embargo, esto podría no parecer importante durante la creación de instancias de GuzzleHttp\Client establecerá un valor predeterminado manipulador si no se proporciona ninguno. (Este valor luego se transmite a cualquier solicitud que se envíe a través del Cliente, a menos que se anule).

Nota: Determina el mejor manejador a usar desde este función. Sin embargo, lo más probable es que termine por defecto en curl_mutli_exec.

¿Cuál es la importancia de esto? Es el controlador subyacente el responsable de rastrear y ejecutar varias solicitudes al mismo tiempo. Al crear un nuevo controlador cada vez, ninguna de sus solicitudes se agrupa y ejecuta correctamente. Para obtener más información sobre esto, eche un vistazo a la curl_multi_exec documentos.

Entonces, tienes dos formas de lidiar con esto:

Pase a través del cliente hasta el iterador:

$client = new GuzzleHttp\Client(['timeout' => 20]);

$iterator = function () use ($client) {
    $index = 0;
    while (true) {
        if ($index === 10) {
            break;
        }

        $url="http://localhost/wait/5/" . $index++;
        $request = new Request('GET', $url, []);

        echo "Queuing $url @ " . (new Carbon())->format('Y-m-d H:i:s') . PHP_EOL;

        yield $client
            ->sendAsync($request)
            ->then(function (Response $response) use ($request) {
                return [$request, $response];
            });

    }
};

$promise = \GuzzleHttp\Promise\each_limit(
    $iterator(),
    10,  /// concurrency,
    function ($result, $index) {
        /** @var GuzzleHttp\Psr7\Request $request */
        list($request, $response) = $result;
        echo (string)$request->getUri() . ' completed ' . PHP_EOL;
    }
);
$promise->wait();

o cree el controlador en otro lugar y páselo al cliente: (Aunque no estoy seguro de por qué haría esto, ¡pero está ahí!)

$handler = \GuzzleHttp\HandlerStack::create();

$iterator = function () use ($handler) {
    $index = 0;
    while (true) {
        if ($index === 10) {
            break;
        }

        $client = new Client(['timeout' => 20, 'handler' => $handler])
        $url="http://localhost/wait/5/" . $index++;
        $request = new Request('GET', $url, []);

        echo "Queuing $url @ " . (new Carbon())->format('Y-m-d H:i:s') . PHP_EOL;

        yield $client
            ->sendAsync($request)
            ->then(function (Response $response) use ($request) {
                return [$request, $response];
            });

    }
};

$promise = \GuzzleHttp\Promise\each_limit(
    $iterator(),
    10,  /// concurrency,
    function ($result, $index) {
        /** @var GuzzleHttp\Psr7\Request $request */
        list($request, $response) = $result;
        echo (string)$request->getUri() . ' completed ' . PHP_EOL;
    }
);
$promise->wait();

  • La razón por la que es importante crear un nuevo cliente es porque los requisitos dictan el uso del servidor proxy y otros parámetros específicos de la solicitud. Lo que significa que cada solicitud puede ser una solicitud completamente diferente, incluida la URL, el encabezado y el método. Si creo una única instancia de cliente, no sé qué compartirá con otras solicitudes. Con esa lógica, probablemente la segunda solución que propuso es preferible. Probaré las dos soluciones propuestas anteriormente y les informaré. Lástima que estoy en casa en este momento… y no tengo la configuración para probar lo anterior.

    – Escalable

    03/09/2017 a las 15:00


  • Debería poder configurar eso en su solicitud con el proxy opción, docs.guzzlephp.org/en/stable/request-options.html#proxy.

    – Adam Lavín

    3 de septiembre de 2017 a las 15:04

  • Al crear un nuevo controlador cada vez, ninguna de sus solicitudes se agrupa correctamente ¿Por qué es esto un problema? ¿Por qué no pueden correr juntos en grupos separados? ¿Significa eso que si solo hago una solicitud, siempre se ejecutará de forma sincrónica?

    – Draex_

    3 de abril de 2019 a las 7:26


  • Llegué aquí para descubrir que para ejecutar asíncrono, php-curl es imprescindible. No lo tenía instalado y estuve 2 días tratando de encontrar el problema.

    – STR

    10 de julio de 2019 a las 16:58

  • @AdamLavin proporcionar enlaces a archivos específicos (¡e incluso líneas!) en la rama maestra no es una buena idea; es casi seguro que se volverán obsoletos con el paso del tiempo. una mejor opción es usar el hash de revisión en lugar del nombre de la rama

    – por qué

    21 de enero de 2021 a las 17:55


¿Ha sido útil esta solución?