From df40da7caab03d87cd935e708824c981f49729f4 Mon Sep 17 00:00:00 2001 From: Joel Wurtz Date: Sun, 31 Jan 2016 20:28:14 +0100 Subject: [PATCH 1/5] Add http client pool --- .../LeastUsedClientPoolSpec.php | 71 ++++++++++ .../RoundRobinClientPoolSpec.php | 71 ++++++++++ src/Exception/HttpClientNotFoundException.php | 14 ++ src/HttpClientPool.php | 57 ++++++++ src/HttpClientPool/LeastUsedClientPool.php | 45 ++++++ src/HttpClientPool/RoundRobinClientPool.php | 41 ++++++ src/HttpClientPoolItem.php | 131 ++++++++++++++++++ 7 files changed, 430 insertions(+) create mode 100644 spec/HttpClientPool/LeastUsedClientPoolSpec.php create mode 100644 spec/HttpClientPool/RoundRobinClientPoolSpec.php create mode 100644 src/Exception/HttpClientNotFoundException.php create mode 100644 src/HttpClientPool.php create mode 100644 src/HttpClientPool/LeastUsedClientPool.php create mode 100644 src/HttpClientPool/RoundRobinClientPool.php create mode 100644 src/HttpClientPoolItem.php diff --git a/spec/HttpClientPool/LeastUsedClientPoolSpec.php b/spec/HttpClientPool/LeastUsedClientPoolSpec.php new file mode 100644 index 0000000..24a7d8f --- /dev/null +++ b/spec/HttpClientPool/LeastUsedClientPoolSpec.php @@ -0,0 +1,71 @@ +shouldHaveType('Http\Client\Common\HttpClientPool\LeastUsedClientPool'); + } + + public function it_is_an_http_client() + { + $this->shouldImplement('Http\Client\HttpClient'); + } + + public function it_is_an_async_http_client() + { + $this->shouldImplement('Http\Client\HttpAsyncClient'); + } + + public function it_throw_exception_with_no_client(RequestInterface $request) + { + $this->shouldThrow('Http\Client\Common\Exception\HttpClientNotFoundException')->duringSendRequest($request); + $this->shouldThrow('Http\Client\Common\Exception\HttpClientNotFoundException')->duringSendAsyncRequest($request); + } + + public function it_sends_request(HttpClient $httpClient, RequestInterface $request, ResponseInterface $response) + { + $this->addHttpClient($httpClient); + $httpClient->sendRequest($request)->willReturn($response); + + $this->sendRequest($request)->shouldReturn($response); + } + + public function it_sends_async_request(HttpAsyncClient $httpAsyncClient, RequestInterface $request, Promise $promise) + { + $this->addHttpClient($httpAsyncClient); + $httpAsyncClient->sendAsyncRequest($request)->willReturn($promise); + $promise->then(Argument::type('callable'), Argument::type('callable'))->willReturn($promise); + + $this->sendAsyncRequest($request)->shouldReturn($promise); + } + + public function it_throw_exception_if_no_more_enable_client(HttpClient $client, RequestInterface $request) + { + $this->addHttpClient($client); + $client->sendRequest($request)->willThrow('Http\Client\Exception\HttpException'); + + $this->shouldThrow('Http\Client\Exception\HttpException')->duringSendRequest($request); + $this->shouldThrow('Http\Client\Common\Exception\HttpClientNotFoundException')->duringSendRequest($request); + } + + public function it_reenable_client(HttpClient $client, RequestInterface $request) + { + $this->addHttpClient(new HttpClientPoolItem($client->getWrappedObject(), 0)); + $client->sendRequest($request)->willThrow('Http\Client\Exception\HttpException'); + + $this->shouldThrow('Http\Client\Exception\HttpException')->duringSendRequest($request); + $this->shouldThrow('Http\Client\Exception\HttpException')->duringSendRequest($request); + } +} diff --git a/spec/HttpClientPool/RoundRobinClientPoolSpec.php b/spec/HttpClientPool/RoundRobinClientPoolSpec.php new file mode 100644 index 0000000..d84d568 --- /dev/null +++ b/spec/HttpClientPool/RoundRobinClientPoolSpec.php @@ -0,0 +1,71 @@ +shouldHaveType('Http\Client\Common\HttpClientPool\RoundRobinClientPool'); + } + + public function it_is_an_http_client() + { + $this->shouldImplement('Http\Client\HttpClient'); + } + + public function it_is_an_async_http_client() + { + $this->shouldImplement('Http\Client\HttpAsyncClient'); + } + + public function it_throw_exception_with_no_client(RequestInterface $request) + { + $this->shouldThrow('Http\Client\Common\Exception\HttpClientNotFoundException')->duringSendRequest($request); + $this->shouldThrow('Http\Client\Common\Exception\HttpClientNotFoundException')->duringSendAsyncRequest($request); + } + + public function it_sends_request(HttpClient $httpClient, RequestInterface $request, ResponseInterface $response) + { + $this->addHttpClient($httpClient); + $httpClient->sendRequest($request)->willReturn($response); + + $this->sendRequest($request)->shouldReturn($response); + } + + public function it_sends_async_request(HttpAsyncClient $httpAsyncClient, RequestInterface $request, Promise $promise) + { + $this->addHttpClient($httpAsyncClient); + $httpAsyncClient->sendAsyncRequest($request)->willReturn($promise); + $promise->then(Argument::type('callable'), Argument::type('callable'))->willReturn($promise); + + $this->sendAsyncRequest($request)->shouldReturn($promise); + } + + public function it_throw_exception_if_no_more_enable_client(HttpClient $client, RequestInterface $request) + { + $this->addHttpClient($client); + $client->sendRequest($request)->willThrow('Http\Client\Exception\HttpException'); + + $this->shouldThrow('Http\Client\Exception\HttpException')->duringSendRequest($request); + $this->shouldThrow('Http\Client\Common\Exception\HttpClientNotFoundException')->duringSendRequest($request); + } + + public function it_reenable_client(HttpClient $client, RequestInterface $request) + { + $this->addHttpClient(new HttpClientPoolItem($client->getWrappedObject(), 0)); + $client->sendRequest($request)->willThrow('Http\Client\Exception\HttpException'); + + $this->shouldThrow('Http\Client\Exception\HttpException')->duringSendRequest($request); + $this->shouldThrow('Http\Client\Exception\HttpException')->duringSendRequest($request); + } +} diff --git a/src/Exception/HttpClientNotFoundException.php b/src/Exception/HttpClientNotFoundException.php new file mode 100644 index 0000000..5d33f98 --- /dev/null +++ b/src/Exception/HttpClientNotFoundException.php @@ -0,0 +1,14 @@ + + */ +class HttpClientNotFoundException extends TransferException +{ +} diff --git a/src/HttpClientPool.php b/src/HttpClientPool.php new file mode 100644 index 0000000..00fab74 --- /dev/null +++ b/src/HttpClientPool.php @@ -0,0 +1,57 @@ +clientPool[] = $client; + } + + /** + * Return an http client given a specific strategy. + * + * @throws HttpClientNotFoundException When no http client has been found into the pool + * + * @return HttpClientPoolItem Return a http client that can do both sync or async + */ + abstract protected function chooseHttpClient(); + + /** + * {@inheritdoc} + */ + public function sendAsyncRequest(RequestInterface $request) + { + return $this->chooseHttpClient()->sendAsyncRequest($request); + } + + /** + * {@inheritdoc} + */ + public function sendRequest(RequestInterface $request) + { + return $this->chooseHttpClient()->sendRequest($request); + } +} diff --git a/src/HttpClientPool/LeastUsedClientPool.php b/src/HttpClientPool/LeastUsedClientPool.php new file mode 100644 index 0000000..084b5ae --- /dev/null +++ b/src/HttpClientPool/LeastUsedClientPool.php @@ -0,0 +1,45 @@ + + */ +class LeastUsedClientPool extends HttpClientPool +{ + /** + * {@inheritdoc} + */ + public function chooseHttpClient() + { + $clientPool = array_filter($this->clientPool, function (HttpClientPoolItem $clientPoolItem) { + return !$clientPoolItem->isDisabled(); + }); + + if (0 === count($clientPool)) { + throw new HttpClientNotFoundException('Cannot choose a http client as there is no one present in the pool'); + } + + usort($clientPool, function (HttpClientPoolItem $clientA, HttpClientPoolItem $clientB) { + if ($clientA->getSendingRequestCount() === $clientB->getSendingRequestCount()) { + return 0; + } + + if ($clientA->getSendingRequestCount() < $clientB->getSendingRequestCount()) { + return -1; + } + + return 1; + }); + + return reset($clientPool); + } +} diff --git a/src/HttpClientPool/RoundRobinClientPool.php b/src/HttpClientPool/RoundRobinClientPool.php new file mode 100644 index 0000000..99543d6 --- /dev/null +++ b/src/HttpClientPool/RoundRobinClientPool.php @@ -0,0 +1,41 @@ + + */ +class RoundRobinClientPool extends HttpClientPool +{ + /** + * {@inheritdoc} + */ + public function chooseHttpClient() + { + $last = current($this->clientPool); + + do { + $client = next($this->clientPool); + + if (false === $client) { + $client = reset($this->clientPool); + + if (false === $client) { + throw new HttpClientNotFoundException('Cannot choose a http client as there is no one present in the pool'); + } + } + + // Case when there is only one and the last one has been disabled + if ($last === $client && $client->isDisabled()) { + throw new HttpClientNotFoundException('Cannot choose a http client as there is no one enabled in the pool'); + } + } while ($client->isDisabled()); + + return $client; + } +} diff --git a/src/HttpClientPoolItem.php b/src/HttpClientPoolItem.php new file mode 100644 index 0000000..b93c3cd --- /dev/null +++ b/src/HttpClientPoolItem.php @@ -0,0 +1,131 @@ + + */ +class HttpClientPoolItem implements HttpClient, HttpAsyncClient +{ + /** @var int Number of request this client is currently sending */ + private $sendingRequestCount = 0; + + /** @var bool Status of the http client */ + private $disabled = false; + + /** @var \DateTime Time when this client has been disabled */ + private $disabledAt; + + /** @var int|null Number of seconds after this client is reenable, by default null: never reenable this client */ + private $reenableAfter; + + /** @var FlexibleHttpClient A http client responding to async and sync request */ + private $client; + + /** + * {@inheritdoc} + * + * @param null|int $reenableAfter Number of seconds after this client is reenable + */ + public function __construct($client, $reenableAfter = null) + { + $this->client = new FlexibleHttpClient($client); + $this->reenableAfter = $reenableAfter; + } + + /** + * {@inheritdoc} + */ + public function sendRequest(RequestInterface $request) + { + if ($this->isDisabled()) { + throw new Exception\RequestException('Cannot send the request as this client has been disabled', $request); + } + + try { + ++$this->sendingRequestCount; + $response = $this->client->sendRequest($request); + --$this->sendingRequestCount; + } catch (Exception $e) { + $this->disable(); + --$this->sendingRequestCount; + + throw $e; + } + + return $response; + } + + /** + * {@inheritdoc} + */ + public function sendAsyncRequest(RequestInterface $request) + { + if ($this->isDisabled()) { + throw new Exception\RequestException('Cannot send the request as this client has been disabled', $request); + } + + ++$this->sendingRequestCount; + + return $this->client->sendAsyncRequest($request)->then(function ($response) { + --$this->sendingRequestCount; + + return $response; + }, function ($exception) { + $this->disable(); + --$this->sendingRequestCount; + + throw $exception; + }); + } + + /** + * Get current number of request that is send by the underlying http client. + * + * @return int + */ + public function getSendingRequestCount() + { + return $this->sendingRequestCount; + } + + /** + * Disable the current client. + */ + protected function disable() + { + $this->disabled = true; + $this->disabledAt = new \DateTime('now'); + } + + /** + * Whether this client is disabled or not. + * + * Will also reactivate this client if possible + * + * @return bool + */ + public function isDisabled() + { + if ($this->disabled && null !== $this->reenableAfter) { + // Reenable after a certain time + $now = new \DateTime(); + + if (($now->getTimestamp() - $this->disabledAt->getTimestamp()) >= $this->reenableAfter) { + $this->disabled = false; + } + } + + return $this->disabled; + } +} From ec69827f882b3b9bb1d5823eb5c4a96b42cf5f9a Mon Sep 17 00:00:00 2001 From: Joel Wurtz Date: Tue, 2 Aug 2016 18:46:03 +0200 Subject: [PATCH 2/5] Add random client pool, better tests, add protected method for inheritance in client pool item --- .../LeastUsedClientPoolSpec.php | 17 ++++ spec/HttpClientPool/RandomClientPoolSpec.php | 71 ++++++++++++++ .../RoundRobinClientPoolSpec.php | 12 +++ src/HttpClientPool/RandomClientPool.php | 31 +++++++ src/HttpClientPoolItem.php | 92 +++++++++++++------ 5 files changed, 195 insertions(+), 28 deletions(-) create mode 100644 spec/HttpClientPool/RandomClientPoolSpec.php create mode 100644 src/HttpClientPool/RandomClientPool.php diff --git a/spec/HttpClientPool/LeastUsedClientPoolSpec.php b/spec/HttpClientPool/LeastUsedClientPoolSpec.php index 24a7d8f..8642c31 100644 --- a/spec/HttpClientPool/LeastUsedClientPoolSpec.php +++ b/spec/HttpClientPool/LeastUsedClientPoolSpec.php @@ -68,4 +68,21 @@ public function it_reenable_client(HttpClient $client, RequestInterface $request $this->shouldThrow('Http\Client\Exception\HttpException')->duringSendRequest($request); $this->shouldThrow('Http\Client\Exception\HttpException')->duringSendRequest($request); } + + public function it_uses_the_lowest_request_client(HttpClientPoolItem $client1, HttpClientPoolItem $client2, RequestInterface $request, ResponseInterface $response) + { + $this->addHttpClient($client1); + $this->addHttpClient($client2); + + $client1->getSendingRequestCount()->willReturn(10); + $client2->getSendingRequestCount()->willReturn(2); + + $client1->isDisabled()->willReturn(false); + $client2->isDisabled()->willReturn(false); + + $client1->sendRequest($request)->shouldNotBeCalled(); + $client2->sendRequest($request)->willReturn($response); + + $this->sendRequest($request)->shouldReturn($response); + } } diff --git a/spec/HttpClientPool/RandomClientPoolSpec.php b/spec/HttpClientPool/RandomClientPoolSpec.php new file mode 100644 index 0000000..4054d82 --- /dev/null +++ b/spec/HttpClientPool/RandomClientPoolSpec.php @@ -0,0 +1,71 @@ +shouldHaveType('Http\Client\Common\HttpClientPool\RandomClientPool'); + } + + public function it_is_an_http_client() + { + $this->shouldImplement('Http\Client\HttpClient'); + } + + public function it_is_an_async_http_client() + { + $this->shouldImplement('Http\Client\HttpAsyncClient'); + } + + public function it_throw_exception_with_no_client(RequestInterface $request) + { + $this->shouldThrow('Http\Client\Common\Exception\HttpClientNotFoundException')->duringSendRequest($request); + $this->shouldThrow('Http\Client\Common\Exception\HttpClientNotFoundException')->duringSendAsyncRequest($request); + } + + public function it_sends_request(HttpClient $httpClient, RequestInterface $request, ResponseInterface $response) + { + $this->addHttpClient($httpClient); + $httpClient->sendRequest($request)->willReturn($response); + + $this->sendRequest($request)->shouldReturn($response); + } + + public function it_sends_async_request(HttpAsyncClient $httpAsyncClient, RequestInterface $request, Promise $promise) + { + $this->addHttpClient($httpAsyncClient); + $httpAsyncClient->sendAsyncRequest($request)->willReturn($promise); + $promise->then(Argument::type('callable'), Argument::type('callable'))->willReturn($promise); + + $this->sendAsyncRequest($request)->shouldReturn($promise); + } + + public function it_throw_exception_if_no_more_enable_client(HttpClient $client, RequestInterface $request) + { + $this->addHttpClient($client); + $client->sendRequest($request)->willThrow('Http\Client\Exception\HttpException'); + + $this->shouldThrow('Http\Client\Exception\HttpException')->duringSendRequest($request); + $this->shouldThrow('Http\Client\Common\Exception\HttpClientNotFoundException')->duringSendRequest($request); + } + + public function it_reenable_client(HttpClient $client, RequestInterface $request) + { + $this->addHttpClient(new HttpClientPoolItem($client->getWrappedObject(), 0)); + $client->sendRequest($request)->willThrow('Http\Client\Exception\HttpException'); + + $this->shouldThrow('Http\Client\Exception\HttpException')->duringSendRequest($request); + $this->shouldThrow('Http\Client\Exception\HttpException')->duringSendRequest($request); + } +} diff --git a/spec/HttpClientPool/RoundRobinClientPoolSpec.php b/spec/HttpClientPool/RoundRobinClientPoolSpec.php index d84d568..48c2d01 100644 --- a/spec/HttpClientPool/RoundRobinClientPoolSpec.php +++ b/spec/HttpClientPool/RoundRobinClientPoolSpec.php @@ -68,4 +68,16 @@ public function it_reenable_client(HttpClient $client, RequestInterface $request $this->shouldThrow('Http\Client\Exception\HttpException')->duringSendRequest($request); $this->shouldThrow('Http\Client\Exception\HttpException')->duringSendRequest($request); } + + public function it_round_between_clients(HttpClient $client1, HttpClient $client2, RequestInterface $request, ResponseInterface $response) + { + $this->addHttpClient($client1); + $this->addHttpClient($client2); + + $client1->sendRequest($request)->willReturn($response); + $client2->sendRequest($request)->willReturn($response); + + $this->sendRequest($request)->shouldReturn($response); + $this->sendRequest($request)->shouldReturn($response); + } } diff --git a/src/HttpClientPool/RandomClientPool.php b/src/HttpClientPool/RandomClientPool.php new file mode 100644 index 0000000..25a3307 --- /dev/null +++ b/src/HttpClientPool/RandomClientPool.php @@ -0,0 +1,31 @@ + + */ +class RandomClientPool extends HttpClientPool +{ + /** + * {@inheritdoc} + */ + public function chooseHttpClient() + { + $clientPool = array_filter($this->clientPool, function (HttpClientPoolItem $clientPoolItem) { + return !$clientPoolItem->isDisabled(); + }); + + if (0 === count($clientPool)) { + throw new HttpClientNotFoundException('Cannot choose a http client as there is no one present in the pool'); + } + + return $clientPool[array_rand($clientPool)]; + } +} diff --git a/src/HttpClientPoolItem.php b/src/HttpClientPoolItem.php index b93c3cd..e88f43a 100644 --- a/src/HttpClientPoolItem.php +++ b/src/HttpClientPoolItem.php @@ -20,10 +20,7 @@ class HttpClientPoolItem implements HttpClient, HttpAsyncClient /** @var int Number of request this client is currently sending */ private $sendingRequestCount = 0; - /** @var bool Status of the http client */ - private $disabled = false; - - /** @var \DateTime Time when this client has been disabled */ + /** @var \DateTime|null Time when this client has been disabled or null if enable */ private $disabledAt; /** @var int|null Number of seconds after this client is reenable, by default null: never reenable this client */ @@ -53,12 +50,12 @@ public function sendRequest(RequestInterface $request) } try { - ++$this->sendingRequestCount; + $this->incrementRequestCount(); $response = $this->client->sendRequest($request); - --$this->sendingRequestCount; + $this->decrementRequestCount(); } catch (Exception $e) { $this->disable(); - --$this->sendingRequestCount; + $this->decrementRequestCount(); throw $e; } @@ -75,20 +72,47 @@ public function sendAsyncRequest(RequestInterface $request) throw new Exception\RequestException('Cannot send the request as this client has been disabled', $request); } - ++$this->sendingRequestCount; + $this->incrementRequestCount(); return $this->client->sendAsyncRequest($request)->then(function ($response) { - --$this->sendingRequestCount; + $this->decrementRequestCount(); return $response; }, function ($exception) { $this->disable(); - --$this->sendingRequestCount; + $this->decrementRequestCount(); throw $exception; }); } + /** + * Whether this client is disabled or not. + * + * Will also reactivate this client if possible + * + * @return bool + */ + public function isDisabled() + { + $disabledAt = $this->getDisabledAt(); + + if (null !== $this->reenableAfter && null !== $disabledAt) { + // Reenable after a certain time + $now = new \DateTime(); + + if (($now->getTimestamp() - $disabledAt->getTimestamp()) >= $this->reenableAfter) { + $this->enable(); + + return false; + } + + return true; + } + + return null !== $disabledAt; + } + /** * Get current number of request that is send by the underlying http client. * @@ -100,32 +124,44 @@ public function getSendingRequestCount() } /** - * Disable the current client. + * Return when this client has been disabled or null if it's enabled. + * + * @return \DateTime|null */ - protected function disable() + protected function getDisabledAt() { - $this->disabled = true; - $this->disabledAt = new \DateTime('now'); + return $this->disabledAt; } /** - * Whether this client is disabled or not. - * - * Will also reactivate this client if possible - * - * @return bool + * Increment the request count. */ - public function isDisabled() + protected function incrementRequestCount() { - if ($this->disabled && null !== $this->reenableAfter) { - // Reenable after a certain time - $now = new \DateTime(); + ++$this->sendingRequestCount; + } - if (($now->getTimestamp() - $this->disabledAt->getTimestamp()) >= $this->reenableAfter) { - $this->disabled = false; - } - } + /** + * Decrement the request count. + */ + protected function decrementRequestCount() + { + --$this->sendingRequestCount; + } + + /** + * Enable the current client. + */ + protected function enable() + { + $this->disabledAt = null; + } - return $this->disabled; + /** + * Disable the current client. + */ + protected function disable() + { + $this->disabledAt = new \DateTime('now'); } } From 133c0697993fa03b65efb2cceedcec6d1c597ca4 Mon Sep 17 00:00:00 2001 From: Joel Wurtz Date: Tue, 2 Aug 2016 19:08:38 +0200 Subject: [PATCH 3/5] Fix test when xdebug is enable --- spec/HttpClientPool/LeastUsedClientPoolSpec.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/spec/HttpClientPool/LeastUsedClientPoolSpec.php b/spec/HttpClientPool/LeastUsedClientPoolSpec.php index 8642c31..a976c31 100644 --- a/spec/HttpClientPool/LeastUsedClientPoolSpec.php +++ b/spec/HttpClientPool/LeastUsedClientPoolSpec.php @@ -6,6 +6,7 @@ use Http\Client\HttpAsyncClient; use Http\Client\HttpClient; use Http\Promise\Promise; +use PhpSpec\Exception\Example\SkippingException; use PhpSpec\ObjectBehavior; use Prophecy\Argument; use Psr\Http\Message\RequestInterface; @@ -71,6 +72,10 @@ public function it_reenable_client(HttpClient $client, RequestInterface $request public function it_uses_the_lowest_request_client(HttpClientPoolItem $client1, HttpClientPoolItem $client2, RequestInterface $request, ResponseInterface $response) { + if (extension_loaded('xdebug')) { + throw new SkippingException('This test fail when xdebug is enable on PHP < 7'); + } + $this->addHttpClient($client1); $this->addHttpClient($client2); From 291492890cafa61ad37d1081cdb1425667386773 Mon Sep 17 00:00:00 2001 From: Joel Wurtz Date: Wed, 3 Aug 2016 14:22:05 +0200 Subject: [PATCH 4/5] Make class final, add test for pool item, set protected method --- spec/HttpClientPoolItemSpec.php | 213 ++++++++++++++++++++ src/HttpClientPool/LeastUsedClientPool.php | 4 +- src/HttpClientPool/RandomClientPool.php | 4 +- src/HttpClientPool/RoundRobinClientPool.php | 4 +- src/HttpClientPoolItem.php | 10 +- 5 files changed, 224 insertions(+), 11 deletions(-) create mode 100644 spec/HttpClientPoolItemSpec.php diff --git a/spec/HttpClientPoolItemSpec.php b/spec/HttpClientPoolItemSpec.php new file mode 100644 index 0000000..17b1bc4 --- /dev/null +++ b/spec/HttpClientPoolItemSpec.php @@ -0,0 +1,213 @@ +beConstructedWith($httpClient); + } + + public function it_is_an_http_client() + { + $this->shouldImplement('Http\Client\HttpClient'); + } + + public function it_is_an_async_http_client() + { + $this->shouldImplement('Http\Client\HttpAsyncClient'); + } + + public function it_sends_request(HttpClient $httpClient, RequestInterface $request, ResponseInterface $response) + { + $httpClient->sendRequest($request)->willReturn($response); + + $this->sendRequest($request)->shouldReturn($response); + } + + public function it_sends_async_request(HttpAsyncClient $httpAsyncClient, RequestInterface $request, Promise $promise) + { + $this->beConstructedWith($httpAsyncClient); + + $httpAsyncClient->sendAsyncRequest($request)->willReturn($promise); + $promise->then(Argument::type('callable'), Argument::type('callable'))->willReturn($promise); + + $this->sendAsyncRequest($request)->shouldReturn($promise); + } + + public function it_disable_himself_on_send_request(HttpClient $httpClient, RequestInterface $request) + { + $exception = new TransferException(); + $httpClient->sendRequest($request)->willThrow($exception); + $this->shouldThrow($exception)->duringSendRequest($request); + $this->isDisabled()->shouldReturn(true); + $this->shouldThrow('Http\Client\Exception\RequestException')->duringSendRequest($request); + } + + public function it_disable_himself_on_send_async_request(HttpAsyncClient $httpAsyncClient, RequestInterface $request) + { + $this->beConstructedWith($httpAsyncClient); + + $promise = new RejectedPromise(new TransferException()); + $httpAsyncClient->sendAsyncRequest($request)->willReturn($promise); + + $this->sendAsyncRequest($request)->shouldReturnAnInstanceOf('Http\Promise\RejectedPromise'); + $this->isDisabled()->shouldReturn(true); + $this->shouldThrow('Http\Client\Exception\RequestException')->duringSendAsyncRequest($request); + } + + public function it_reactivate_himself_on_send_request(HttpClient $httpClient, RequestInterface $request) + { + $this->beConstructedWith($httpClient, 0); + + $exception = new TransferException(); + $httpClient->sendRequest($request)->willThrow($exception); + + $this->shouldThrow($exception)->duringSendRequest($request); + $this->isDisabled()->shouldReturn(false); + $this->shouldThrow($exception)->duringSendRequest($request); + } + + public function it_reactivate_himself_on_send_async_request(HttpAsyncClient $httpAsyncClient, RequestInterface $request) + { + $this->beConstructedWith($httpAsyncClient, 0); + + $promise = new RejectedPromise(new TransferException()); + $httpAsyncClient->sendAsyncRequest($request)->willReturn($promise); + + $this->sendAsyncRequest($request)->shouldReturnAnInstanceOf('Http\Promise\RejectedPromise'); + $this->isDisabled()->shouldReturn(false); + $this->sendAsyncRequest($request)->shouldReturnAnInstanceOf('Http\Promise\RejectedPromise'); + } + + public function it_increments_request_count(HttpAsyncClient $httpAsyncClient, RequestInterface $request, ResponseInterface $response) + { + $this->beConstructedWith($httpAsyncClient, 0); + + $promise = new NotResolvingPromise($response->getWrappedObject()); + $httpAsyncClient->sendAsyncRequest($request)->willReturn($promise); + + $this->getSendingRequestCount()->shouldReturn(0); + $this->sendAsyncRequest($request)->shouldReturn($promise); + $this->getSendingRequestCount()->shouldReturn(1); + $this->sendAsyncRequest($request)->shouldReturn($promise); + $this->getSendingRequestCount()->shouldReturn(2); + } + + public function it_decrements_request_count(HttpAsyncClient $httpAsyncClient, RequestInterface $request, ResponseInterface $response) + { + $this->beConstructedWith($httpAsyncClient, 0); + + $promise = new NotResolvingPromise($response->getWrappedObject()); + $httpAsyncClient->sendAsyncRequest($request)->willReturn($promise); + + $this->getSendingRequestCount()->shouldReturn(0); + $this->sendAsyncRequest($request)->shouldReturn($promise); + $this->getSendingRequestCount()->shouldReturn(1); + + $promise->wait(false); + + $this->getSendingRequestCount()->shouldReturn(0); + } +} + +class NotResolvingPromise implements Promise +{ + private $queue = []; + + private $state = Promise::PENDING; + + private $response; + + private $exception; + + public function __construct(ResponseInterface $response = null, Exception $exception = null) + { + $this->response = $response; + $this->exception = $exception; + } + + public function then(callable $onFulfilled = null, callable $onRejected = null) + { + $this->queue[] = [ + $onFulfilled, + $onRejected, + ]; + + return $this; + } + + public function getState() + { + return $this->state; + } + + public function wait($unwrap = true) + { + if ($this->state === Promise::FULFILLED) { + if (!$unwrap) { + return; + } + + return $this->response; + } + + if ($this->state === Promise::REJECTED) { + if (!$unwrap) { + return; + } + + throw $this->exception; + } + + while (count($this->queue) > 0) { + $callbacks = array_shift($this->queue); + + if ($this->response !== null) { + try { + $this->response = $callbacks[0]($this->response); + $this->exception = null; + } catch (Exception $exception) { + $this->response = null; + $this->exception = $exception; + } + } elseif ($this->exception !== null) { + try { + $this->response = $callbacks[1]($this->exception); + $this->exception = null; + } catch (Exception $exception) { + $this->response = null; + $this->exception = $exception; + } + } + } + + if ($this->response !== null) { + $this->state = Promise::FULFILLED; + + if ($unwrap) { + return $this->response; + } + } + + if ($this->exception !== null) { + $this->state = Promise::REJECTED; + + if ($unwrap) { + throw $this->exception; + } + } + } +} diff --git a/src/HttpClientPool/LeastUsedClientPool.php b/src/HttpClientPool/LeastUsedClientPool.php index 084b5ae..6299cce 100644 --- a/src/HttpClientPool/LeastUsedClientPool.php +++ b/src/HttpClientPool/LeastUsedClientPool.php @@ -13,12 +13,12 @@ * * @author Joel Wurtz */ -class LeastUsedClientPool extends HttpClientPool +final class LeastUsedClientPool extends HttpClientPool { /** * {@inheritdoc} */ - public function chooseHttpClient() + protected function chooseHttpClient() { $clientPool = array_filter($this->clientPool, function (HttpClientPoolItem $clientPoolItem) { return !$clientPoolItem->isDisabled(); diff --git a/src/HttpClientPool/RandomClientPool.php b/src/HttpClientPool/RandomClientPool.php index 25a3307..3255f86 100644 --- a/src/HttpClientPool/RandomClientPool.php +++ b/src/HttpClientPool/RandomClientPool.php @@ -11,12 +11,12 @@ * * @author Joel Wurtz */ -class RandomClientPool extends HttpClientPool +final class RandomClientPool extends HttpClientPool { /** * {@inheritdoc} */ - public function chooseHttpClient() + protected function chooseHttpClient() { $clientPool = array_filter($this->clientPool, function (HttpClientPoolItem $clientPoolItem) { return !$clientPoolItem->isDisabled(); diff --git a/src/HttpClientPool/RoundRobinClientPool.php b/src/HttpClientPool/RoundRobinClientPool.php index 99543d6..8d8e40a 100644 --- a/src/HttpClientPool/RoundRobinClientPool.php +++ b/src/HttpClientPool/RoundRobinClientPool.php @@ -10,12 +10,12 @@ * * @author Joel Wurtz */ -class RoundRobinClientPool extends HttpClientPool +final class RoundRobinClientPool extends HttpClientPool { /** * {@inheritdoc} */ - public function chooseHttpClient() + protected function chooseHttpClient() { $last = current($this->clientPool); diff --git a/src/HttpClientPoolItem.php b/src/HttpClientPoolItem.php index e88f43a..cd3c2db 100644 --- a/src/HttpClientPoolItem.php +++ b/src/HttpClientPoolItem.php @@ -128,7 +128,7 @@ public function getSendingRequestCount() * * @return \DateTime|null */ - protected function getDisabledAt() + private function getDisabledAt() { return $this->disabledAt; } @@ -136,7 +136,7 @@ protected function getDisabledAt() /** * Increment the request count. */ - protected function incrementRequestCount() + private function incrementRequestCount() { ++$this->sendingRequestCount; } @@ -144,7 +144,7 @@ protected function incrementRequestCount() /** * Decrement the request count. */ - protected function decrementRequestCount() + private function decrementRequestCount() { --$this->sendingRequestCount; } @@ -152,7 +152,7 @@ protected function decrementRequestCount() /** * Enable the current client. */ - protected function enable() + private function enable() { $this->disabledAt = null; } @@ -160,7 +160,7 @@ protected function enable() /** * Disable the current client. */ - protected function disable() + private function disable() { $this->disabledAt = new \DateTime('now'); } From e15422aaf4b33015837c322717b5a2471a9fc839 Mon Sep 17 00:00:00 2001 From: Joel Wurtz Date: Fri, 5 Aug 2016 12:51:53 +0200 Subject: [PATCH 5/5] Add changelog entry --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e4dc98..0365ac7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Change Log +## 1.3.0 - Unreleased + +- Add HttpClientPool client to leverage load balancing and fallback mechanism [see the documentation](http://docs.php-http.org/en/latest/components/client-common.html) for more details ## 1.2.1 - 2016-07-26