-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Rewrite new version * Refactor * Rename * Tweak * Rename property * Update composer.json * Rename package * Update readme * Update composer.json * Update readme * Use static closure * Delay for async client * Use real address to test ReactClient * Minor changes * Applies CS fixes * Add callback operation * Driver discovery test * Update readme * Update driver * Ignore coverage * Rename * Add docblock * Update readme * Rename * Change generic type * Update readme * Improve delay * Delayable * Update readme --------- Co-authored-by: jenky <[email protected]>
- Loading branch information
Showing
42 changed files
with
1,169 additions
and
603 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,25 +1,169 @@ | ||
|
||
# Atlas Pool | ||
# Peak | ||
|
||
[![Latest Version on Packagist][ico-version]][link-packagist] | ||
[![Github Actions][ico-gh-actions]][link-gh-actions] | ||
[![Codecov][ico-codecov]][link-codecov] | ||
[![Total Downloads][ico-downloads]][link-downloads] | ||
[![Software License][ico-license]](LICENSE.md) | ||
|
||
A powerful tool that allows you to send multiple HTTP requests simultaneously. | ||
A simple and efficient solution for concurrently sending HTTP requests using PSR-18 client implementations. | ||
|
||
Peak is a library that enables concurrent request sending using a request pool. It leverages the event loop of [ReactPHP](https://github.com/reactphp) or [PSL](https://github.com/azjezz/psl) to handle and manage the requests concurrently. | ||
|
||
## Requirements | ||
|
||
- PHP 8.1 or higher. | ||
- A package that supports non-block I/O using Fibers under the hood (now refer as **driver**). | ||
|
||
## Installation | ||
|
||
You can install the package via composer: | ||
|
||
```bash | ||
composer require jenky/atlas-pool | ||
composer require fansipan/peak | ||
``` | ||
|
||
Additionally, depending on your choice of driver, these packages may also need to be installed. | ||
|
||
### PSL | ||
|
||
```bash | ||
composer require azjezz/psl | ||
``` | ||
|
||
### ReactPHP | ||
|
||
```bash | ||
composer require clue/mq-react react/async | ||
``` | ||
|
||
## Usage | ||
|
||
See the [documentation](https://jenky.github.io/atlas) for detailed installation and usage instructions. | ||
### Create Request Pool | ||
|
||
Typical applications would use the `PoolFactory` class to create a pool. | ||
|
||
```php | ||
use Fansipan\Peak\PoolFactory; | ||
|
||
/** @var \Psr\Http\Client\ClientInterface $client */ | ||
$pool = PoolFactory::createForClient($client); | ||
``` | ||
|
||
It will attempt to create async version of the client using `AsyncClientFactory`. The supported clients are [Guzzle](https://github.com/guzzle/guzzle) and [Symfony HTTPClient](https://github.com/symfony/http-client) ([`Psr18Client`](https://symfony.com/doc/current/http_client.html#psr-18-and-psr-17)) except for [ReactPHP driver](#reactphp). | ||
|
||
> You can use any PSR-18 client implementations with ReactPHP driver. If an unsupported client is used, it will be replaced with the [`Browser`](https://github.com/reactphp/http#browser) HTTP client. | ||
The `Fansipan\Peak\PoolFactory` provides a configured request pool based on the installed packages, which is suitable for most cases. However, if desired, you can specify a particular implementation if it is available on your platform and/or in your application. | ||
|
||
First, you need to create your desired driver: | ||
|
||
```php | ||
use Fansipan\Peak\Concurrency\PslDeferred; | ||
use Fansipan\Peak\Concurrency\ReactDeferred; | ||
|
||
// PSL | ||
$defer = new PslDeferred(); | ||
|
||
// ReactPHP | ||
$defer = new ReactDeferred(); | ||
``` | ||
|
||
Then create an asynchronous client, which is essentially a decorator for the PSR-18 client: | ||
|
||
```php | ||
use Fansipan\Peak\Client\GuzzleClient; | ||
use Fansipan\Peak\Client\SymfonyClient; | ||
use Fansipan\Peak\ClientPool; | ||
|
||
// Guzzle | ||
|
||
$asyncClient = new GuzzleClient($defer); | ||
// or using existing Guzzle client | ||
/** @var \GuzzleHttp\ClientInterface $client */ | ||
$asyncClient = new GuzzleClient($defer, $client); | ||
|
||
// Symfony HTTP Client | ||
|
||
$asyncClient = new SymfonyClient($defer); | ||
// or using existing Symfony client | ||
/** @var \Symfony\Contracts\HttpClient\HttpClientInterface $client */ | ||
$asyncClient = new SymfonyClient($defer, $client); | ||
|
||
|
||
$pool = new ClientPool($asyncClient); | ||
``` | ||
|
||
### Sending Requests | ||
|
||
The `send` method accepts an iterator of PSR-7 requests or closures/invokable class which receive an `Psr\Http\Client\ClientInterface` instance. | ||
|
||
```php | ||
use Psr\Http\Client\ClientInterface; | ||
use Psr\Http\Message\RequestInterface; | ||
use Psr\Http\Message\ResponseInterface; | ||
|
||
// Using array | ||
$responses = $pool->send([ | ||
$psr7Request, | ||
fn (ClientInterface $client): ResponseInterface => $client->sendRequest($psr7Request), | ||
]); | ||
|
||
var_dump($responses[0]); | ||
var_dump($responses[1]); | ||
|
||
// Using generator when you have an indeterminate amount of requests you wish to send | ||
$requests = static function (int $total) { | ||
for ($i = 0; $i < $total; $i++) { | ||
yield $psr7Request; | ||
} | ||
} | ||
$responses = $pool->send($requests(100)); | ||
``` | ||
|
||
### Retrieving Responses | ||
|
||
As you can see from the example above, each response instance can be accessed using an index. However, the response order is not guaranteed. If you wish, you can assign names to the requests to easily track the specific requests that have been sent. This allows you to access the corresponding responses by their assigned names. | ||
|
||
```php | ||
use Psr\Http\Client\ClientInterface; | ||
use Psr\Http\Message\RequestInterface; | ||
use Psr\Http\Message\ResponseInterface; | ||
|
||
$responses = $pool->send([ | ||
'first' => $psr7Request, | ||
'second' => fn (ClientInterface $client): ResponseInterface => $client->sendRequest($psr7Request), | ||
]); | ||
|
||
// Or using generator | ||
|
||
$requests = function (): \Generator { | ||
yield 'first' => $psr7Request; | ||
yield 'second' => fn (ClientInterface $client): ResponseInterface => $client->sendRequest($psr7Request); | ||
}; | ||
|
||
$responses = $pool->send($requests()); | ||
|
||
var_dump($responses['first']); | ||
var_dump($responses['second']); | ||
``` | ||
|
||
### Concurrency Limit | ||
|
||
Sending an excessive number of requests may either take up all resources on your side or it may even get you banned by the remote side if it sees an unreasonable number of requests from your side. | ||
|
||
As a consequence, it's usually recommended to limit concurrency on the sending side to a reasonable value. It's common to use a rather small limit, as doing more than a dozen of things at once may easily overwhelm the receiving side. | ||
|
||
You can use `concurrent` method to set the maximum number of requests to send concurrently. The default value is `25`. | ||
|
||
```php | ||
$response = $pool | ||
->concurrent(10) // Process up to 10 requests concurrently | ||
->send($requests); | ||
``` | ||
|
||
Additional requests that exceed the concurrency limit will automatically be enqueued until one of the pending requests completes. | ||
|
||
## Testing | ||
|
||
|
@@ -37,7 +181,7 @@ Please see [CONTRIBUTING](CONTRIBUTING.md) and [CODE_OF_CONDUCT](CODE_OF_CONDUCT | |
|
||
## Security | ||
|
||
If you discover any security related issues, please email [email protected] instead of using the issue tracker. | ||
If you discover any security related issues, please email [email protected] instead of using the issue tracker. | ||
|
||
## Credits | ||
|
||
|
@@ -48,20 +192,14 @@ If you discover any security related issues, please email [email protected] in | |
|
||
The MIT License (MIT). Please see [License File](LICENSE.md) for more information. | ||
|
||
[ico-version]: https://img.shields.io/packagist/v/jenky/atlas-pool.svg?style=for-the-badge | ||
[ico-version]: https://img.shields.io/packagist/v/fansipan/peak.svg?style=for-the-badge | ||
[ico-license]: https://img.shields.io/badge/license-MIT-brightgreen.svg?style=for-the-badge | ||
[ico-travis]: https://img.shields.io/travis/jenky/atlas-pool/master.svg?style=for-the-badge | ||
[ico-scrutinizer]: https://img.shields.io/scrutinizer/coverage/g/jenky/atlas-pool.svg?style=for-the-badge | ||
[ico-code-quality]: https://img.shields.io/scrutinizer/g/jenky/atlas-pool.svg?style=for-the-badge | ||
[ico-gh-actions]: https://img.shields.io/github/actions/workflow/status/jenky/atlas-pool/testing.yml?branch=main&label=actions&logo=github&style=for-the-badge | ||
[ico-codecov]: https://img.shields.io/codecov/c/github/jenky/atlas-pool?logo=codecov&style=for-the-badge | ||
[ico-downloads]: https://img.shields.io/packagist/dt/jenky/atlas-pool.svg?style=for-the-badge | ||
|
||
[link-packagist]: https://packagist.org/packages/jenky/atlas-pool | ||
[link-travis]: https://travis-ci.org/jenky/atlas-pool | ||
[link-scrutinizer]: https://scrutinizer-ci.com/g/jenky/atlas-pool/code-structure | ||
[link-code-quality]: https://scrutinizer-ci.com/g/jenky/atlas-pool | ||
[link-gh-actions]: https://github.com/jenky/atlas-pool | ||
[link-codecov]: https://codecov.io/gh/jenky/atlas-pool | ||
[link-downloads]: https://packagist.org/packages/jenky/atlas-pool | ||
[ico-gh-actions]: https://img.shields.io/github/actions/workflow/status/phanxipang/peak/testing.yml?branch=main&label=actions&logo=github&style=for-the-badge | ||
[ico-codecov]: https://img.shields.io/codecov/c/github/phanxipang/peak?logo=codecov&style=for-the-badge | ||
[ico-downloads]: https://img.shields.io/packagist/dt/fansipan/peak.svg?style=for-the-badge | ||
|
||
[link-packagist]: https://packagist.org/packages/phanxipang/peak | ||
[link-gh-actions]: https://github.com/phanxipang/peak | ||
[link-codecov]: https://codecov.io/gh/phanxipang/peak | ||
[link-downloads]: https://packagist.org/packages/fansipan/peak | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Fansipan\Peak\Client; | ||
|
||
use Fansipan\Peak\Concurrency\Driver; | ||
use Fansipan\Peak\Concurrency\DriverDiscovery; | ||
use Fansipan\Peak\Concurrency\PslDeferred; | ||
use Fansipan\Peak\Concurrency\ReactDeferred; | ||
use Fansipan\Peak\Exception\UnsupportedClientException; | ||
use Fansipan\Peak\Exception\UnsupportedFeatureException; | ||
use GuzzleHttp\ClientInterface as GuzzleClientInterface; | ||
use Http\Discovery\Psr18ClientDiscovery; | ||
use Psr\Http\Client\ClientInterface; | ||
use Symfony\Component\HttpClient\Psr18Client; | ||
use Symfony\Contracts\HttpClient\HttpClientInterface; | ||
|
||
class AsyncClientFactory | ||
{ | ||
/** | ||
* Create new async version of the given client. | ||
* | ||
* @throws \Fansipan\Peak\Exception\UnsupportedClientException | ||
* @throws \Fansipan\Peak\Exception\UnsupportedFeatureException | ||
*/ | ||
public static function create(?ClientInterface $client = null): AsyncClientInterface | ||
{ | ||
if ($client === null) { | ||
$client = self::createClient(); | ||
} | ||
|
||
if ($client instanceof AsyncClientInterface) { | ||
return $client; | ||
} | ||
|
||
$driver = DriverDiscovery::find(); | ||
|
||
if ($driver === Driver::PSL) { | ||
if ($client instanceof GuzzleClientInterface) { | ||
return new GuzzleClient(new PslDeferred(), $client); | ||
} | ||
|
||
if ($client instanceof Psr18Client) { | ||
return new SymfonyClient(new PslDeferred(), self::getUnderlyingSymfonyHttpClient($client)); | ||
} | ||
|
||
throw new UnsupportedClientException(\sprintf( | ||
'The client %s is not supported. The PSL Pool only supports "guzzlehttp/guzzle" and "symfony/http-client".', | ||
\get_debug_type($client) | ||
)); | ||
} | ||
|
||
if ($driver === Driver::REACT) { | ||
if ($client instanceof GuzzleClientInterface) { | ||
return new GuzzleClient(new ReactDeferred(), $client); | ||
} | ||
|
||
if ($client instanceof Psr18Client) { | ||
return new SymfonyClient(new ReactDeferred(), self::getUnderlyingSymfonyHttpClient($client)); | ||
} | ||
|
||
if (\class_exists(Browser::class)) { | ||
return new ReactClient(); | ||
} | ||
|
||
throw new UnsupportedClientException(\sprintf( | ||
'The concurrent requests feature cannot be used as the client %s is not supported. To utilize this feature, please install package "react/http".', | ||
\get_debug_type($client) | ||
)); | ||
} | ||
|
||
// @codeCoverageIgnoreStart | ||
throw new UnsupportedFeatureException('You cannot use the concurrent request pool feature as the required packages are not installed.'); // @phpstan-ignore-line | ||
// @codeCoverageIgnoreEnd | ||
} | ||
|
||
private static function createClient(): ClientInterface | ||
{ | ||
if (! class_exists(Psr18ClientDiscovery::class)) { | ||
// @codeCoverageIgnoreStart | ||
throw new \RuntimeException('Unable to create PSR-18 client as the "php-http/discovery" package is not installed. Try running "composer require php-http/discovery".'); | ||
// @codeCoverageIgnoreEnd | ||
} | ||
|
||
return Psr18ClientDiscovery::find(); | ||
} | ||
|
||
private static function getUnderlyingSymfonyHttpClient(Psr18Client $client): ?HttpClientInterface | ||
{ | ||
try { | ||
$reflectionProperty = new \ReflectionProperty($client, 'client'); | ||
$reflectionProperty->setAccessible(true); | ||
|
||
return $reflectionProperty->getValue($client); | ||
// @codeCoverageIgnoreStart | ||
} catch (\Throwable) { | ||
return null; | ||
} | ||
// @codeCoverageIgnoreEnd | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Fansipan\Peak\Client; | ||
|
||
use Fansipan\Peak\Concurrency\Driver; | ||
use Psr\Http\Client\ClientInterface; | ||
|
||
interface AsyncClientInterface extends ClientInterface | ||
{ | ||
/** | ||
* Get the underlying async driver type. | ||
*/ | ||
public function driver(): ?Driver; | ||
} |
Oops, something went wrong.