Guzzle async requests not really async? - php

Problem
We are trying to do concurrent asynchronous requests using guzzle. After going through a few resources, like this and this, we came up with some code that is shared below. However it is not working as expected.
It looks like Guzzle is doing these request synchronously rather than async.
Expectation
Just for test purposes, we are hitting an internal url, which does a 5 second sleep. With a concurrency of 10 we expect that all 10 requests will initially be queued and send to the server almost simultaneously, where they will wait for 5 seconds, and will then almost all of those will finish nearly at the same time. Which would make the guzzle client to pick up 10 new requests from iterator and so on.
Code
$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();
Actual Results
We find that that Guzzle never made a second request until the first one is finished. and so on.
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 / Version information
Ubuntu
PHP/7.1.3
GuzzleHttp/6.2.1
curl/7.47.0
The issue could be with \GuzzleHttp\Promise\each_limit .. which perhaps does not initiates or resolves the promise fast enough. It may be possible that we have to trick that into ticking externally.

In the example code, you're creating a new GuzzleHttp\Client instance for every request you want to make. This might not seem important, however, during instantiation of GuzzleHttp\Client it will set a default handler if none is provided. (This value is then passed down to any request being sent through the Client, unless it is overridden.)
Note: It determines the best handler to use from this function. Though, it'll most likely end up defaulting to curl_mutli_exec.
What's the importance of this? It's the underlying handler that is responsible for tracking and executing multiple requests at the same time. By creating a new handler every time, none of your requests are properly being grouped up and ran together. For some more insight into this take a gander into the curl_multi_exec docs.
So, you kind of have two ways of dealing with this:
Pass through the client through to the iterator:
$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();
or create the handler elsewhere and pass it to the client: (Though I'm not sure why you'd do this, but it's there!)
$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();

Related

How to use guzzle to simulate event loop HTTP pool

test.php
<?php
//simulate different blocking times
if(isset($_GET['appid'])) {
sleep(rand(3, 10));
echo $_GET['appid'];
exit;
}
client.php
<?php
require './vendor/autoload.php';
use Psr\Http\Message\ResponseInterface;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\Client;
$client = new Client();
$promise = null;
$loop = function ($appId) use($client, &$loop, &$promise) {
$promise = $client->getAsync("http://127.0.0.1/test.php?appid={$appId}");
$promise->then(
function (ResponseInterface $res) use(&$loop, $appId) {
echo $res->getBody();
//After completing the current request, initiate the request again to simulate EventLoop
$loop($appId);
},
function (RequestException $e) use(&$loop, $appId) {
//Ignore the error and continue to simulate EventLoop
$loop($appId);
}
);
};
foreach(range(1, 10) as $appId) {
$loop($appId);
}
$promise->wait();
I try to use promise to simulate EventLoop. As shown in my example code, I have 10 application IDs. I want to launch 10 requests concurrently. At the same time, the response time of each request is different. I want to launch the request of the current application ID immediately after each request is completed to simulate EventLoop.
But I find it doesn't seem to work. How can I simulate EventLoop

Long polling telegram with ReactPHP async

I'm trying to do long polling with reactphp.
I have a function getNotifications that stay in long polling waiting a response from Telegram.
This telegram api can hold the request open until timeout or
can send a response before the end of the 50 seconds if there is a notification.
How I can recall getNotifications after a response from Telegram?
Basically I want that getNotifications is called again when there is a response.
This is my code, thank you all
<?php
require "vendor/autoload.php";
use Clue\React\Buzz\Browser;
use Psr\Http\Message\ResponseInterface;
use React\EventLoop\LoopInterface;
$loop = React\EventLoop\Factory::create();
$browser = new Browser($loop);
$method = "getUpdates";
$timeout = 50;
$params = [
"offset" => 550495530,
"limit" => 1000,
"timeout" => $timeout
];
$bot_key = "your bot token";
$query = http_build_query($params);
$url = "https://api.telegram.org/bot" . $bot_key . "/" . $method . "?" . $query;
$browser = $browser->withOptions(array(
'timeout' => $timeout
));
function callback(){
echo "done";
}
function getNotifications($url, $browser, LoopInterface $loop){
$browser->get($url)->then(function (ResponseInterface $response) {
// response received within 50 seconds. Telegram longpolling
var_dump((string)$response->getBody());
callback();
});
}
function timer($start, LoopInterface $loop)
{
//Timer only used to count seconds
$loop->addPeriodicTimer(1.0, function ($timer) use (&$start, $loop) {
echo "tick ". $start++ . "\n";
});
}
timer(0, $loop);
getNotifications($url, $browser, $loop);
$loop->run();
Ok I find the solution. To pass parameter to the anonymous function I need to use the use keyword. Then inside the function I can access correctly the variables.
function getNotifications($url, $browser, LoopInterface $loop){
$browser->get($url)->then(function (ResponseInterface $response) use ($url, $browser,$loop) {
// response received within 50 seconds. Telegram longpolling
var_dump((string)$response->getBody());
callback();
getNotifications($url, $browser,$loop);
});
}

php - how to send multiple xml request (web-services)

I didn't find any example of how to make several web-service request at the same time (simultaneous).
For instant, this is request I send in order to get hotel details:
$s = new soapclient("http://www.wb-service-address.com",array('wsdl'));
$HotelInfo = new stdClass;
<HotelInfo softwareID="123" SessionId="153" Lang="EN" InfoType="">
<Hotel ID="103" />
</HotelInfo>
$HotelInfo->xmlRequest = $paramsStr;
$result = $s->__call("SubmitXmlString",array($HotelInfo));
$obj_pros = get_object_vars($result);
$hotel_full_xml = $obj_pros['SubmitXmlStringResult'];
$hotel_full_xml = simplexml_load_string($hotel_full_xml);
(The XML "HotelInfo" is the request)
I would like to send the same request to number of systems (urls) at the same time.
I'll appreciate your help
php by nature does not do that but you can using another library like GuzzleHttp
Exemple:
public function index()
{
$promises = call_user_func(function () {
foreach ($this->usernames as $username) {
(yield $this->client->requestAsync('GET', 'https://api.github.com/users/' . $username));
}
});
// Wait till all the requests are finished.
\GuzzleHttp\Promise\all($promises)->then(function (array $responses) {
$this->profiles = array_map(function ($response) {
return json_decode($response->getBody(), true);
}, $responses);
})->wait();
// Return JSON response
$response = new Response();
// StreamInterface objects are not immutable!
$response->getBody()->write($this->html());
return $response->withHeader('Content-type', 'text/html');
}
Also you can using this solution: https://github.com/amphp/amp
another solution with python: ThreadPoolExecutor
see that in python documentation: Concurrent Execution

How to send multiple requests at once ReactPHP?

I am using guzzle to send some requests:
$response = $this->client->request(new SomeObject());
using the class below
...
public function request(Request $request)
{
return $this->requestAsync($request)->wait();
}
// Here I'm using Guzzle Async, but I can remove that is needed
public function requestAsync(Request $request)
{
$promise = $this->http->requestAsync($request->getMethod(), $request->getUri());
$promise = $promise->then(
function (ResponseInterface $response) use ($request) {
return $response;
}
);
return $promise;
}
...
I would like to use ReactPHP to send multiple requests at once in a foreach loop:
$requests = [];
foreach ($data as $value) {
$requests[] = $this->client->request(new SomeObject());
}
// pass $requests to ReactPHP here and wait on the response
Any ideas?
First of all, you don't need ReactPHP to use parallel HTTP requests with Guzzle. Guzzle itself provides this feature (if you use cURL handler, which is the default).
For example:
$promises = [];
foreach ($data as $value) {
$promises[] = $guzzleClient->getAsync(/* some URL */);
}
// Combine all promises
$combinedPromise = \GuzzleHttp\Promise\all($promises)
// And wait for them to finish (all requests are executed in parallel)
$responses = $combinedPromise->wait();
If you still want to use Guzzle with ReactPHP event loop, then, unfortunately, there are no straightforward solution. You cat take a look at https://github.com/productsupcom/guzzle-react-bridge (I'm the developer, so feel free to ask questions).

How to check if endpoint is working when using GuzzleHTTP

So I am working with guzzleHttp and I can get the responses that I am after and catch errors.
The only problem I am having is that if the base URI is wrong, the whole script fails... how can I do some sort of checking to make sure that the endpoint is actually up?
$client = new GuzzleHttp\Client(['base_uri' => $url]);
You might have many issues with your query, not only that the endpoint is down. Network interface on your server can go down right at the moment of query, DNS can go down, a route to the host might not be available, a connection timeout, etc.
So you definitely should be ready for many issues. I usually catch a general RequestException and do something (logging, app specific handling), plus catch specific exceptions if I should handle them differently.
Also, there are many existing patterns (and solutions) for error handling. For example, it's usual to retry a query is an endpoint is unavailable.
$stack = HandlerStack::create();
$stack->push(Middleware::retry(
function (
$retries,
RequestInterface $request,
ResponseInterface $response = null,
RequestException $exception = null
) {
// Don't retry if we have run out of retries.
if ($retries >= 5) {
return false;
}
$shouldRetry = false;
// Retry connection exceptions.
if ($exception instanceof ConnectException) {
$shouldRetry = true;
}
if ($response) {
// Retry on server errors.
if ($response->getStatusCode() >= 500) {
$shouldRetry = true;
}
}
// Log if we are retrying.
if ($shouldRetry) {
$this->logger->debug(
sprintf(
'Retrying %s %s %s/5, %s',
$request->getMethod(),
$request->getUri(),
$retries + 1,
$response ? 'status code: ' . $response->getStatusCode() :
$exception->getMessage()
)
);
}
return $shouldRetry;
}
));
$client = new Client([
'handler' => $stack,
'connect_timeout' => 60.0, // Seconds.
'timeout' => 1800.0, // Seconds.
]);

Categories