Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Handling exceptions on a Pool of Requests

Tags:

php

guzzle

I'm using Guzzle to send a number of requests to an API endpoint, using the Pool functionality to send these asynchronously and concurrently.

The script looks like this:

use GuzzleHttp\Client;
use GuzzleHttp\Promise;
use GuzzleHttp\Pool;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Exception\RequestException;

/* Configure logger */
Logger::configure("config/logger.xml");
$logger = Logger::getLogger("console");

/* Configure Guzzle Client */
$client = new Client([
    'base_uri' => 'http://my.api/',
    'timeout' => 2.0,
    'allow_redirects' => false,
]);

/* Anonymous function (closure) to 'yield' X number of Requests */
$requests = function ($num_requests) {
    for ($i = 0; $i < $num_requests; $i++) {
        yield new Request('GET', "/MY_UNIQUE_IDENTIFIER/");
    }
};

/* Create a Pool for the above requests */
$pool = new Pool($client, $requests(20), [
    'concurrency' => 5,  // Determine how many requests to send concurrently
    'fulfilled' => function ($response, $index) {
        $logger->info('$index: ' . $index . ', $response: ' . $response);
    },
    'rejected' => function ($reason, $index) {
        try {
            echo $reason;
        } catch (\Exception $e) {
            trigger_error($e->getMessage(), E_USER_WARNING);
        }
    },
]);

/* Initiate transfers/create a promise */
$promise = $pool->promise();

/* Force the pool of requests to complete */
$promise->wait();

Basically, send 20 requests (5 at a time) to http://my.api/MY_UNIQUE_IDENTIFIER.

The Pool appears to work. If I add an echo to the rejected requests, I get output like:

#0 /Users/me/guzzle-POC/vendor/guzzlehttp/guzzle/src/Handler/CurlFactory.php(149): GuzzleHttp\Handler\CurlFactory::createRejection(Object(GuzzleHttp\Handler\EasyHandle), Array)
#1 /Users/me/guzzle-POC/vendor/guzzlehttp/guzzle/src/Handler/CurlFactory.php(102): GuzzleHttp\Handler\CurlFactory::finishError(Object(GuzzleHttp\Handler\CurlMultiHandler), Object(GuzzleHttp\Handler\EasyHandle), Object(GuzzleHttp\Handler\CurlFactory))
#2 /Users/me/guzzle-POC/vendor/guzzlehttp/guzzle/src/Handler/CurlMultiHandler.php(181): GuzzleHttp\Handler\CurlFactory::finish(Object(GuzzleHttp\Handler\CurlMultiHandler), Object(GuzzleHttp\Handler\EasyHandle), Object(GuzzleHttp\Handler\CurlFactory))
#3 /Users/me/guzzle-POC/vendor/guzzlehttp/guzzle/src/Handler/CurlMultiHandler.php(110): GuzzleHttp\Handler\CurlMultiHandler->processMessages()
#4 /Users/me/guzzle-POC/vendor/guzzlehttp/guzzle/src/Handler/CurlMultiHandler.php(125): GuzzleHttp\Handler\CurlMultiHandler->tick()
#5 /Users/me/guzzle-POC/vendor/guzzlehttp/promises/src/Promise.php(246): GuzzleHttp\Handler\CurlMultiHandler->execute(true)
#6 /Users/me/guzzle-POC/vendor/guzzlehttp/promises/src/Promise.php(223): GuzzleHttp\Promise\Promise->invokeWaitFn()
#7 /Users/me/guzzle-POC/vendor/guzzlehttp/promises/src/Promise.php(267): GuzzleHttp\Promise\Promise->waitIfPending()
#8 /Users/me/guzzle-POC/vendor/guzzlehttp/promises/src/Promise.php(225): GuzzleHttp\Promise\Promise->invokeWaitList()
#9 /Users/me/guzzle-POC/vendor/guzzlehttp/promises/src/Promise.php(62): GuzzleHttp\Promise\Promise->waitIfPending()
#10 /Users/me/guzzle-POC/vendor/guzzlehttp/promises/src/EachPromise.php(101): GuzzleHttp\Promise\Promise->wait()
#11 /Users/me/guzzle-POC/vendor/guzzlehttp/promises/src/Promise.php(246): GuzzleHttp\Promise\EachPromise->GuzzleHttp\Promise\{closure}(true)
#12 /Users/me/guzzle-POC/vendor/guzzlehttp/promises/src/Promise.php(223): GuzzleHttp\Promise\Promise->invokeWaitFn()
#13 /Users/me/guzzle-POC/vendor/guzzlehttp/promises/src/Promise.php(62): GuzzleHttp\Promise\Promise->waitIfPending()
#14 /Users/me/guzzle-POC/poc.php(50): GuzzleHttp\Promise\Promise->wait()
#15 {main}GuzzleHttp\Exception\ConnectException: cURL error 6: Could not resolve host: my.api (see http://curl.haxx.se/libcurl/c/libcurl-errors.html) in /Users/me/guzzle-POC/vendor/guzzlehttp/guzzle/src/Handler/CurlFactory.php:185

The main problem here being #15, Could not resolve host: my.api. This is expected behaviour, but I want to catch this exception.

The try/catch that I've used just doesn't work at all - it catches nothing.

Presumably this is because of the Pool's asynchronous nature, but is it possible to catch these exceptions in any way?

All I want to achieve basically is, if can't resolve; log error and continue with other requests-type approach.

like image 338
turbonerd Avatar asked May 17 '19 09:05

turbonerd


Video Answer


1 Answers

I had a similar problem, I would share a solution which worked for me.

Taking example from your question,

This is the picture which gives the tree heirarchy of guzzle exception(source Guzzle Docs.) Source: https://docs.guzzlephp.org/en/latest/quickstart.html#exceptions

Also GuzzleHttp\Exception\ConnectException exception is thrown in the event of a networking error and also ConnectException does not have an associated response, so therefore there is no 400 or 500 error. So it should give false for hasResponse()


$client = new Client([
    'base_uri' => 'http://my.api/',
    'timeout' => 2.0,
    'allow_redirects' => false,
]);

/* Anonymous function (closure) to 'yield' X number of Requests */
$requests = function ($num_requests) {
    for ($i = 0; $i < $num_requests; $i++) {
        yield new Request('GET', "/MY_UNIQUE_IDENTIFIER/");
    }
};

/* Create a Pool for the above requests */
$pool = new Pool($client, $requests(20), [
    'concurrency' => 5,  // Determine how many requests to send concurrently
    'fulfilled' => function ($response, $index) {
        $logger->info('$index: ' . $index . ', $response: ' . $response);
    },
    'rejected' => function (\GuzzleHttp\Exception\TransferException $reason, $index) {
            if ($reason->hasResponse()){
            // this will mainly catch RequestException(Exception with statuscode and responses)
                    if ($reason->getResponse()->getStatusCode() == '400') {
                        // log your exception
                    } elseif($reason->getResponse()->getStatusCode() == '403'){
                        //log your unauthorised code exception 
                    }else{
                        //similarly log your other status code exception 
                    }
            } else{ 
                // ConnectException should come here, you can log it, will not have any responses as the request was never sent.
            }
    },
]);

/* Initiate transfers/create a promise */
$promise = $pool->promise();

/* Force the pool of requests to complete */
$promise->wait();

Demerits

You need to know the exact status code of your exceptions.


Also if someone is specific to only catch ServerException or ClientException, then they can replace TransferException with other exception down the heirarchy tree.

'rejected' => function (\GuzzleHttp\Exception\RequestException $reason, $index) {
like image 71
bhucho Avatar answered Nov 01 '22 16:11

bhucho