-
Notifications
You must be signed in to change notification settings - Fork 11.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[8.x] Handle concurrent asynchronous requests in the HTTP client #36948
Conversation
Thanks for your work on this - renamed |
Awesome work I long awaited, but I was wondering why the pool doesn't take advantage of Guzzle's pool and others utils? In terms of DX, I find it easier not to depend on the Pool object and use the intuitive array's "key=>value" notation instead of the pool Here is a working equivalent example of the actual use Illuminate\Support\Facades\Http;
use GuzzleHttp\Promise\Utils;
$responses = Utils::all([
'foo' => Http::async()->get('https://httpbin.org/delay/3')->then(/* ... */),
'bar' => Http::async()->get('https://httpbin.org/delay/3')->then(/* ... */),
'baz' => Http::async()->get('https://httpbin.org/delay/3')->then(/* ... */),
])->wait();
$responses['foo']->ok();
$responses['bar']->successful();
$connectionFailed = $responses['baz'] instanceof GuzzleHttp\Exception\ConnectException; I would happily PR something like That way we can use "preconfigured" instances like I usually do: $http = Http::baseUrl('https://httpbin.org/')->acceptJson()->timeout(10)->withToken($token);
$responses = Http::all([
'foo' => $http->async()->get('/delay/3'),
'bar' => $http->async()->get('/delay/3'),
'baz' => $http->async()->get('/delay/3'),
]); |
Thanks for your contribution, @louisgab :) Originally the Pool class was meant to collect requests while making sure they:
However you are right, this looks like a limit to use cases like yours. Adding another method to accomplish the same end result may represent an overhead, an alternative may be altering the /**
* Send a pool of asynchronous requests concurrently.
*
* @param iterable|callable $requests
* @return array
*/
public function pool($requests)
{
$results = [];
$requests = is_iterable($requests) ? $requests : tap(new Pool($this->factory), $requests)->getRequests();
foreach ($requests as $key => $item) {
$results[$key] = $item instanceof static ? $item->getPromise()->wait() : $item->wait();
}
ksort($results);
return $results;
} This way the Pool class is skipped if we provide an iterable (array, iterator, generator, etc.) What do you think? |
Good enough for me, but we could go further! I would prefer the Also, I digged inside Guzzle, and found out that And it's awesome since it enables us to do things like that : $promise = Utils:all([
// Requests
])
->then(/* ... */)
->otherwise(/* ... */);
if($someCondition){
$promise->cancel();
}
$responses = $promise-wait(); It means we can write logic like "throw if any of the requests went wrong" instead of doing it manually one by one on each response. It would be nice to hear more opinions, maybe @driesvints or @GrahamCampbell ? |
This PR aims to bring concurrency while sending asynchronous requests with the Laravel HTTP client.
Laravel Octane is not required to make this feature work, however Octane should be compatible with this feature as the latest version of Swoole supports curl_multi operations.
The PR introduces:
Asynchronous requests can be created with the following syntax:
The result is an instance of
GuzzleHttp\Promise\Promise
.Based on whether a promise is fulfilled or rejected, the
then()
method of the promise receives:Illuminate\Http\Client\Response
if the promise is fulfilledIlluminate\Http\Client\Response
if the promise is rejected but a response was provided (4xx, 5xx errors)GuzzleHttp\Exception\TransferException
if the promise is rejected with no response (e.g. timeout)The pool collects asynchronous requests and send them concurrently, so that the slowest request determines the maximum waiting time for all promises to be fulfilled.
Asynchronous requests can also be added to the pool with a key:
Responses are instances of
Illuminate\Http\Client\Response
if requests were responded (2xx, 3xx, 4xx, 5xx). Otherwise if no response was received, the exception that provoked the promise rejection is returned.All requests added to the pool are asynchronous by default, so there is no need to chain the
async()
method. It may also be worth mentioning that all the fluent methods ofPendingRequest
are available to the requests in the pool:The pool leverages
Illuminate\Http\Client\Factory
under the hood, so we can take advantage of all its testing functionalities in our unit tests: