Skip to content

Commit e709366

Browse files
committed
Add http client pool
1 parent 10891ee commit e709366

8 files changed

+431
-1
lines changed
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
<?php
2+
3+
namespace spec\Http\Client\Common\HttpClientPool;
4+
5+
use Http\Client\Common\HttpClientPoolItem;
6+
use Http\Client\HttpAsyncClient;
7+
use Http\Client\HttpClient;
8+
use Http\Promise\Promise;
9+
use PhpSpec\ObjectBehavior;
10+
use Prophecy\Argument;
11+
use Psr\Http\Message\RequestInterface;
12+
use Psr\Http\Message\ResponseInterface;
13+
14+
class LeastUsedClientPoolSpec extends ObjectBehavior
15+
{
16+
public function it_is_initializable()
17+
{
18+
$this->shouldHaveType('Http\Client\Common\HttpClientPool\LeastUsedClientPool');
19+
}
20+
21+
public function it_is_an_http_client()
22+
{
23+
$this->shouldImplement('Http\Client\HttpClient');
24+
}
25+
26+
public function it_is_an_async_http_client()
27+
{
28+
$this->shouldImplement('Http\Client\HttpAsyncClient');
29+
}
30+
31+
public function it_throw_exception_with_no_client(RequestInterface $request)
32+
{
33+
$this->shouldThrow('Http\Client\Common\Exception\NotFoundHttpClientException')->duringSendRequest($request);
34+
$this->shouldThrow('Http\Client\Common\Exception\NotFoundHttpClientException')->duringSendAsyncRequest($request);
35+
}
36+
37+
public function it_sends_request(HttpClient $httpClient, RequestInterface $request, ResponseInterface $response)
38+
{
39+
$this->addHttpClient($httpClient);
40+
$httpClient->sendRequest($request)->willReturn($response);
41+
42+
$this->sendRequest($request)->shouldReturn($response);
43+
}
44+
45+
public function it_sends_async_request(HttpAsyncClient $httpAsyncClient, RequestInterface $request, Promise $promise)
46+
{
47+
$this->addHttpClient($httpAsyncClient);
48+
$httpAsyncClient->sendAsyncRequest($request)->willReturn($promise);
49+
$promise->then(Argument::type('callable'), Argument::type('callable'))->willReturn($promise);
50+
51+
$this->sendAsyncRequest($request)->shouldReturn($promise);
52+
}
53+
54+
public function it_throw_exception_if_no_more_enable_client(HttpClient $client, RequestInterface $request)
55+
{
56+
$this->addHttpClient($client);
57+
$client->sendRequest($request)->willThrow('Http\Client\Exception\HttpException');
58+
59+
$this->shouldThrow('Http\Client\Exception\HttpException')->duringSendRequest($request);
60+
$this->shouldThrow('Http\Client\Common\Exception\NotFoundHttpClientException')->duringSendRequest($request);
61+
}
62+
63+
public function it_reenable_client(HttpClient $client, RequestInterface $request)
64+
{
65+
$this->addHttpClient(new HttpClientPoolItem($client->getWrappedObject(), 0));
66+
$client->sendRequest($request)->willThrow('Http\Client\Exception\HttpException');
67+
68+
$this->shouldThrow('Http\Client\Exception\HttpException')->duringSendRequest($request);
69+
$this->shouldThrow('Http\Client\Exception\HttpException')->duringSendRequest($request);
70+
}
71+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
<?php
2+
3+
namespace spec\Http\Client\Common\HttpClientPool;
4+
5+
use Http\Client\Common\HttpClientPoolItem;
6+
use Http\Client\HttpAsyncClient;
7+
use Http\Client\HttpClient;
8+
use Http\Promise\Promise;
9+
use PhpSpec\ObjectBehavior;
10+
use Prophecy\Argument;
11+
use Psr\Http\Message\RequestInterface;
12+
use Psr\Http\Message\ResponseInterface;
13+
14+
class RoundRobinClientPoolSpec extends ObjectBehavior
15+
{
16+
public function it_is_initializable()
17+
{
18+
$this->shouldHaveType('Http\Client\Common\HttpClientPool\RoundRobinClientPool');
19+
}
20+
21+
public function it_is_an_http_client()
22+
{
23+
$this->shouldImplement('Http\Client\HttpClient');
24+
}
25+
26+
public function it_is_an_async_http_client()
27+
{
28+
$this->shouldImplement('Http\Client\HttpAsyncClient');
29+
}
30+
31+
public function it_throw_exception_with_no_client(RequestInterface $request)
32+
{
33+
$this->shouldThrow('Http\Client\Common\Exception\NotFoundHttpClientException')->duringSendRequest($request);
34+
$this->shouldThrow('Http\Client\Common\Exception\NotFoundHttpClientException')->duringSendAsyncRequest($request);
35+
}
36+
37+
public function it_sends_request(HttpClient $httpClient, RequestInterface $request, ResponseInterface $response)
38+
{
39+
$this->addHttpClient($httpClient);
40+
$httpClient->sendRequest($request)->willReturn($response);
41+
42+
$this->sendRequest($request)->shouldReturn($response);
43+
}
44+
45+
public function it_sends_async_request(HttpAsyncClient $httpAsyncClient, RequestInterface $request, Promise $promise)
46+
{
47+
$this->addHttpClient($httpAsyncClient);
48+
$httpAsyncClient->sendAsyncRequest($request)->willReturn($promise);
49+
$promise->then(Argument::type('callable'), Argument::type('callable'))->willReturn($promise);
50+
51+
$this->sendAsyncRequest($request)->shouldReturn($promise);
52+
}
53+
54+
public function it_throw_exception_if_no_more_enable_client(HttpClient $client, RequestInterface $request)
55+
{
56+
$this->addHttpClient($client);
57+
$client->sendRequest($request)->willThrow('Http\Client\Exception\HttpException');
58+
59+
$this->shouldThrow('Http\Client\Exception\HttpException')->duringSendRequest($request);
60+
$this->shouldThrow('Http\Client\Common\Exception\NotFoundHttpClientException')->duringSendRequest($request);
61+
}
62+
63+
public function it_reenable_client(HttpClient $client, RequestInterface $request)
64+
{
65+
$this->addHttpClient(new HttpClientPoolItem($client->getWrappedObject(), 0));
66+
$client->sendRequest($request)->willThrow('Http\Client\Exception\HttpException');
67+
68+
$this->shouldThrow('Http\Client\Exception\HttpException')->duringSendRequest($request);
69+
$this->shouldThrow('Http\Client\Exception\HttpException')->duringSendRequest($request);
70+
}
71+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
3+
namespace Http\Client\Common\Exception;
4+
5+
use Http\Client\Exception\TransferException;
6+
7+
/**
8+
* Thrown when a http client cannot be chosen in a pool.
9+
*
10+
* @author Joel Wurtz <[email protected]>
11+
*/
12+
class HttpClientNotFoundException extends TransferException
13+
{
14+
}

src/FlexibleHttpClient.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
*
1212
* @author Joel Wurtz <[email protected]>
1313
*/
14-
final class FlexibleHttpClient implements HttpClient, HttpAsyncClient
14+
class FlexibleHttpClient implements HttpClient, HttpAsyncClient
1515
{
1616
use HttpClientDecorator;
1717
use HttpAsyncClientDecorator;

src/HttpClientPool.php

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
<?php
2+
3+
namespace Http\Client\Common;
4+
5+
use Http\Client\Common\Exception\HttpClientNotFoundException;
6+
use Http\Client\HttpAsyncClient;
7+
use Http\Client\HttpClient;
8+
use Psr\Http\Message\RequestInterface;
9+
10+
/**
11+
* A http client pool allows to send requests on a pool of different http client using a specific strategy (least used,
12+
* round robin, ...).
13+
*/
14+
abstract class HttpClientPool implements HttpAsyncClient, HttpClient
15+
{
16+
/** @var HttpClientPoolItem[] */
17+
protected $clientPool = [];
18+
19+
/**
20+
* Add a client to the pool.
21+
*
22+
* @param HttpClient|HttpAsyncClient $client
23+
*/
24+
public function addHttpClient($client)
25+
{
26+
if (!$client instanceof HttpClientPoolItem) {
27+
$client = new HttpClientPoolItem($client);
28+
}
29+
30+
$this->clientPool[] = $client;
31+
}
32+
33+
/**
34+
* Return an http client given a specific strategy.
35+
*
36+
* @throws HttpClientNotFoundException When no http client has been found into the pool
37+
*
38+
* @return HttpClientPoolItem Return a http client that can do both sync or async
39+
*/
40+
abstract protected function chooseHttpClient();
41+
42+
/**
43+
* {@inheritdoc}
44+
*/
45+
public function sendAsyncRequest(RequestInterface $request)
46+
{
47+
return $this->chooseHttpClient()->sendAsyncRequest($request);
48+
}
49+
50+
/**
51+
* {@inheritdoc}
52+
*/
53+
public function sendRequest(RequestInterface $request)
54+
{
55+
return $this->chooseHttpClient()->sendRequest($request);
56+
}
57+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php
2+
3+
namespace Http\Client\Common\HttpClientPool;
4+
5+
use Http\Client\Common\Exception\HttpClientNotFoundException;
6+
use Http\Client\Common\HttpClientPool;
7+
use Http\Client\Common\HttpClientPoolItem;
8+
9+
/**
10+
* LeastUsedClientPool will choose the client with the less current request in the pool.
11+
*
12+
* This strategy is only useful when doing async request
13+
*
14+
* @author Joel Wurtz <[email protected]>
15+
*/
16+
class LeastUsedClientPool extends HttpClientPool
17+
{
18+
/**
19+
* {@inheritdoc}
20+
*/
21+
public function chooseHttpClient()
22+
{
23+
$clientPool = array_filter($this->clientPool, function (HttpClientPoolItem $clientPoolItem) {
24+
return !$clientPoolItem->isDisabled();
25+
});
26+
27+
if (0 === count($clientPool)) {
28+
throw new HttpClientNotFoundException('Cannot choose a http client as there is no one present in the pool');
29+
}
30+
31+
usort($clientPool, function (HttpClientPoolItem $clientA, HttpClientPoolItem $clientB) {
32+
if ($clientA->getSendingRequestCount() === $clientB->getSendingRequestCount()) {
33+
return 0;
34+
}
35+
36+
if ($clientA->getSendingRequestCount() < $clientB->getSendingRequestCount()) {
37+
return -1;
38+
}
39+
40+
return 1;
41+
});
42+
43+
return reset($clientPool);
44+
}
45+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
3+
namespace Http\Client\Common\HttpClientPool;
4+
5+
use Http\Client\Common\Exception\HttpClientNotFoundException;
6+
use Http\Client\Common\HttpClientPool;
7+
8+
/**
9+
* RoundRobinClientPool will choose the next client in the pool.
10+
*
11+
* @author Joel Wurtz <[email protected]>
12+
*/
13+
class RoundRobinClientPool extends HttpClientPool
14+
{
15+
/**
16+
* {@inheritdoc}
17+
*/
18+
public function chooseHttpClient()
19+
{
20+
$last = current($this->clientPool);
21+
22+
do {
23+
$client = next($this->clientPool);
24+
25+
if (false === $client) {
26+
$client = reset($this->clientPool);
27+
28+
if (false === $client) {
29+
throw new HttpClientNotFoundException('Cannot choose a http client as there is no one present in the pool');
30+
}
31+
}
32+
33+
// Case when there is only one and the last one has been disabled
34+
if ($last === $client && $client->isDisabled()) {
35+
throw new HttpClientNotFoundException('Cannot choose a http client as there is no one enabled in the pool');
36+
}
37+
} while ($client->isDisabled());
38+
39+
return $client;
40+
}
41+
}

0 commit comments

Comments
 (0)