Skip to content

Commit

Permalink
Rewrite and moving to org (#6)
Browse files Browse the repository at this point in the history
* 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
jenky and jenky committed Sep 26, 2023
1 parent baa1731 commit f16ed73
Show file tree
Hide file tree
Showing 42 changed files with 1,169 additions and 603 deletions.
178 changes: 158 additions & 20 deletions README.md
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

Expand All @@ -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

Expand All @@ -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

26 changes: 15 additions & 11 deletions composer.json
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
{
"name": "jenky/atlas-pool",
"description": "Send concurrent requests for Atlas",
"name": "fansipan/peak",
"description": "A simple and efficient solution for concurrently sending HTTP requests using PSR-18 client implementations.",
"keywords": [
"jenky",
"atlas",
"concurrently",
"pool",
"http",
"request",
"response",
"concurrent-requests",
"parallel-requests",
"async",
"await"
],
"homepage": "https://github.com/jenky/atlas-pool",
"homepage": "https://github.com/phanxipang/peak",
"license": "MIT",
"authors": [
{
Expand All @@ -21,29 +23,31 @@
],
"require": {
"php": "^8.1",
"jenky/atlas": "^0.5",
"jenky/concurrency": "^1.0"
"psr/http-client": "^1.0",
"psr/http-factory": "^1.0"
},
"require-dev": {
"azjezz/psl": "^2.7",
"clue/mq-react": "^1.6",
"fansipan/mock-client": "^1.0",
"friendsofphp/php-cs-fixer": "^3.15",
"guzzlehttp/guzzle": "^7.5",
"jenky/atlas-mock-client": "^1.0",
"jenky/atlas": "^0.5",
"phpstan/phpstan": "^1.10",
"phpunit/phpunit": "^9.0",
"react/async": "^4.1",
"react/http": "^1.9",
"symfony/http-client": "^6.3"
"symfony/http-client": "^6.3",
"symfony/var-dumper": "^6.3"
},
"autoload": {
"psr-4": {
"Jenky\\Atlas\\Pool\\": "src"
"Fansipan\\Peak\\": "src"
}
},
"autoload-dev": {
"psr-4": {
"Jenky\\Atlas\\Pool\\Tests\\": "tests"
"Fansipan\\Peak\\Tests\\": "tests"
}
},
"scripts": {
Expand Down
102 changes: 102 additions & 0 deletions src/Client/AsyncClientFactory.php
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
}
}
16 changes: 16 additions & 0 deletions src/Client/AsyncClientInterface.php
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;
}
Loading

0 comments on commit f16ed73

Please sign in to comment.