Skip to content

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

Merged
merged 5 commits into from
Aug 5, 2016
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions spec/HttpClientPool/LeastUsedClientPoolSpec.php
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);
}
}
71 changes: 71 additions & 0 deletions spec/HttpClientPool/RoundRobinClientPoolSpec.php
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);
}
}
14 changes: 14 additions & 0 deletions src/Exception/HttpClientNotFoundException.php
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
{
}
57 changes: 57 additions & 0 deletions src/HttpClientPool.php
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);
}
}
45 changes: 45 additions & 0 deletions src/HttpClientPool/LeastUsedClientPool.php
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()
Copy link
Member

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?

{
$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');
Copy link
Contributor

Choose a reason for hiding this comment

The 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.

Copy link
Member Author

Choose a reason for hiding this comment

The 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);
}
}
41 changes: 41 additions & 0 deletions src/HttpClientPool/RoundRobinClientPool.php
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);
Copy link
Contributor

Choose a reason for hiding this comment

The 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.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't the least used client exactly for that?

Copy link
Contributor

Choose a reason for hiding this comment

The 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

Copy link
Member Author

Choose a reason for hiding this comment

The 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 ?

Copy link
Contributor

Choose a reason for hiding this comment

The 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.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done see RandomClientPool also added in the PR docs


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;
}
}
Loading