-
Notifications
You must be signed in to change notification settings - Fork 53
Add http client pool #25
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
Changes from 1 commit
df40da7
ec69827
133c069
2914928
e15422a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
<?php | ||
|
||
namespace spec\Http\Client\Common\HttpClientPool; | ||
|
||
use Http\Client\Common\HttpClientPoolItem; | ||
use Http\Client\HttpAsyncClient; | ||
use Http\Client\HttpClient; | ||
use Http\Promise\Promise; | ||
use PhpSpec\ObjectBehavior; | ||
use Prophecy\Argument; | ||
use Psr\Http\Message\RequestInterface; | ||
use Psr\Http\Message\ResponseInterface; | ||
|
||
class LeastUsedClientPoolSpec extends ObjectBehavior | ||
{ | ||
public function it_is_initializable() | ||
{ | ||
$this->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); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
<?php | ||
|
||
namespace spec\Http\Client\Common\HttpClientPool; | ||
|
||
use Http\Client\Common\HttpClientPoolItem; | ||
use Http\Client\HttpAsyncClient; | ||
use Http\Client\HttpClient; | ||
use Http\Promise\Promise; | ||
use PhpSpec\ObjectBehavior; | ||
use Prophecy\Argument; | ||
use Psr\Http\Message\RequestInterface; | ||
use Psr\Http\Message\ResponseInterface; | ||
|
||
class RoundRobinClientPoolSpec extends ObjectBehavior | ||
{ | ||
public function it_is_initializable() | ||
{ | ||
$this->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); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
<?php | ||
|
||
namespace Http\Client\Common\Exception; | ||
|
||
use Http\Client\Exception\TransferException; | ||
|
||
/** | ||
* Thrown when a http client cannot be chosen in a pool. | ||
* | ||
* @author Joel Wurtz <joel.wurtz@gmail.com> | ||
*/ | ||
class HttpClientNotFoundException extends TransferException | ||
{ | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
<?php | ||
|
||
namespace Http\Client\Common; | ||
|
||
use Http\Client\Common\Exception\HttpClientNotFoundException; | ||
use Http\Client\HttpAsyncClient; | ||
use Http\Client\HttpClient; | ||
use Psr\Http\Message\RequestInterface; | ||
|
||
/** | ||
* A http client pool allows to send requests on a pool of different http client using a specific strategy (least used, | ||
* round robin, ...). | ||
*/ | ||
abstract class HttpClientPool implements HttpAsyncClient, HttpClient | ||
{ | ||
/** @var HttpClientPoolItem[] */ | ||
protected $clientPool = []; | ||
|
||
/** | ||
* Add a client to the pool. | ||
* | ||
* @param HttpClient|HttpAsyncClient $client | ||
*/ | ||
public function addHttpClient($client) | ||
{ | ||
if (!$client instanceof HttpClientPoolItem) { | ||
$client = new HttpClientPoolItem($client); | ||
} | ||
|
||
$this->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); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
<?php | ||
|
||
namespace Http\Client\Common\HttpClientPool; | ||
|
||
use Http\Client\Common\Exception\HttpClientNotFoundException; | ||
use Http\Client\Common\HttpClientPool; | ||
use Http\Client\Common\HttpClientPoolItem; | ||
|
||
/** | ||
* LeastUsedClientPool will choose the client with the less current request in the pool. | ||
* | ||
* This strategy is only useful when doing async request | ||
* | ||
* @author Joel Wurtz <joel.wurtz@gmail.com> | ||
*/ | ||
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'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should we just force-use a disabled client in this case rather than do a hard failure? it would reduce the time we have problems if the backend went away for a short time. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This can be done by setting 0 on the http client pool item |
||
} | ||
|
||
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); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
<?php | ||
|
||
namespace Http\Client\Common\HttpClientPool; | ||
|
||
use Http\Client\Common\Exception\HttpClientNotFoundException; | ||
use Http\Client\Common\HttpClientPool; | ||
|
||
/** | ||
* RoundRobinClientPool will choose the next client in the pool. | ||
* | ||
* @author Joel Wurtz <joel.wurtz@gmail.com> | ||
*/ | ||
class RoundRobinClientPool extends HttpClientPool | ||
{ | ||
/** | ||
* {@inheritdoc} | ||
*/ | ||
public function chooseHttpClient() | ||
{ | ||
$last = current($this->clientPool); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is there a way how we could randomize with which client to start, maybe in the constructor? that would result in better load balancing of web requests, if we only do one request with each client. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Isn't the least used client exactly for that? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. both are only within the same php process. when using a client in web requests, there hopefully is only one request to some other service, not several, and in that case this only makes sense if we randomize There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can maybe add a third strategy which randomly choose a client between the one that are available ? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. that would make sense i think, and should be easy: shuffle the pool before doing what we do here. we might even extend this class and do shuffle, then call parent. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done see |
||
|
||
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; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All the chooseHttpClient should be protected, right?