1
0
mirror of https://github.com/guzzle/guzzle.git synced 2025-02-24 10:03:27 +01:00

Adding retry tests

This commit is contained in:
Michael Dowling 2014-02-10 23:40:10 -08:00
parent 1f08b4f91f
commit 45e2733bd1
2 changed files with 207 additions and 25 deletions

View File

@ -27,6 +27,9 @@ class RetrySubscriber implements EventSubscriberInterface
/** @var int */
private $maxRetries;
/** @var callable */
private $sleepFn;
public static function getSubscribedEvents()
{
return [
@ -36,45 +39,48 @@ class RetrySubscriber implements EventSubscriberInterface
}
/**
* @param callable $filter Filter used to determine whether or not to retry a request. The filter must be a
* callable that accepts the current number of retries and an AbstractTransferStatsEvent
* object. The filter must return true or false to denote if the request must be retried.
* @param callable $delayFunc Callable that accepts the number of retries and an AbstractTransferStatsEvent and
* returns the amount of of time in seconds to delay.
* @param int $maxRetries Maximum number of retries
* @param callable $filter Filter used to determine whether or not to retry a request. The filter must be a
* callable that accepts the current number of retries and an AbstractTransferStatsEvent
* object. The filter must return true or false to denote if the request must be retried
* @param callable $delayFunc Callable that accepts the number of retries and an AbstractTransferStatsEvent and
* returns the amount of of time in seconds to delay.
* @param int $maxRetries Maximum number of retries
* @param callable $sleepFn Function invoked when the subscriber needs to sleep. Accepts a float containing the
* amount of time in seconds to sleep and an AbstractTransferStatsEvent.
*/
public function __construct(
callable $filter,
callable $delayFunc,
$maxRetries = 3
$maxRetries = 5,
callable $sleepFn = null
) {
$this->filter = $filter;
$this->delayFunc = $delayFunc;
$this->maxRetries = $maxRetries;
$this->sleepFn = $sleepFn ?: function($time) { usleep($time * 1000); };
}
/**
* Retrieve a basic truncated exponential backoff plugin that will retry HTTP errors and cURL errors
*
* @param int $maxRetries Maximum number of retries
* @param array $httpCodes HTTP response codes to retry
* @param array $curlCodes cURL error codes to retry
* @param LoggerInterface|bool $logger Pass a logger instance to log each retry or pass true for STDOUT
* @param string|MessageFormatter $formatter Pass a message formatter if logging to customize log messages
* @param array $config Exponential backoff configuration
* - max_retries: Maximum number of retries (overiddes the default of 5)
* - http_codes: HTTP response codes to retry (overrides the default)
* - curl_codes: cURL error codes to retry (overrides the default)
* - logger: Pass a logger instance to log each retry or pass true for STDOUT
* - formatter: Pass a message formatter if logging to customize log messages
* - sleep_fn: Pass a callable to override how to sleep.
*
* @return self
* @throws \InvalidArgumentException if logger is not a boolean or LoggerInterface
*/
public static function getExponentialBackoff(
$maxRetries = 3,
array $httpCodes = null,
array $curlCodes = null,
$logger = null,
$formatter = null
) {
public static function getExponentialBackoff(array $config = [])
{
$httpCodes = isset($config['http_codes']) ? $config['http_codes'] : null;
if (!extension_loaded('curl')) {
$filter = self::createStatusFilter($httpCodes);
} else {
$curlCodes = isset($config['curl_codes']) ? $config['curl_codes'] : null;
$filter = self::createChainFilter([
self::createStatusFilter($httpCodes),
self::createCurlFilter($curlCodes)
@ -83,16 +89,18 @@ class RetrySubscriber implements EventSubscriberInterface
$delay = [__CLASS__, 'exponentialDelay'];
if ($logger) {
if ($logger === true) {
$logger = new SimpleLogger(STDOUT);
} elseif (!($logger instanceof LoggerInterface)) {
if (isset($config['logger'])) {
$logger = $config['logger'];
if (!($logger instanceof LoggerInterface)) {
throw new \InvalidArgumentException('$logger must be true, false, or a LoggerInterface');
}
$formatter = isset($config['formatter']) ? $config['formatter'] : null;
$delay = self::createLoggingDelay($delay, $logger, $formatter);
}
return new self($filter, $delay, $maxRetries);
$maxRetries = isset($config['max_retries']) ? $config['max_retries'] : 5;
return new self($filter, $delay, $maxRetries, isset($config['sleep_fn']) ? $config['sleep_fn'] : null);
}
public function onRequestSent(AbstractTransferStatsEvent $event)
@ -104,7 +112,8 @@ class RetrySubscriber implements EventSubscriberInterface
$filterFn = $this->filter;
if ($filterFn($retries, $event)) {
$delayFn = $this->delayFunc;
sleep($delayFn($retries, $event));
$sleepFn = $this->sleepFn;
$sleepFn($delayFn($retries, $event), $event);
$request->getConfig()->set('retries', ++$retries);
$event->intercept($event->getClient()->send($request));
}

View File

@ -0,0 +1,173 @@
<?php
namespace Guzzle\Tests\Subscriber\RetrySubscriber;
use Guzzle\Http\Adapter\Transaction;
use Guzzle\Http\Client;
use Guzzle\Http\Event\RequestAfterSendEvent;
use Guzzle\Http\Message\Request;
use Guzzle\Http\Message\RequestInterface;
use Guzzle\Http\Message\Response;
use Guzzle\Http\Message\ResponseInterface;
use Guzzle\Http\Subscriber\History;
use Guzzle\Http\Subscriber\Mock;
use Guzzle\Subscriber\Log\SimpleLogger;
use Guzzle\Subscriber\Retry\RetrySubscriber;
class RetrySubscriberTest extends \PHPUnit_Framework_TestCase
{
public function testCreatesDefaultStatusFilter()
{
$f = RetrySubscriber::createStatusFilter();
$e = $this->createEvent(new Response(500));
$this->assertTrue($f(1, $e));
$e = $this->createEvent(new Response(503));
$this->assertTrue($f(0, $e));
$e = $this->createEvent(new Response(200));
$this->assertFalse($f(1, $e));
}
public function testCreatesCustomStatusFilter()
{
$f = RetrySubscriber::createStatusFilter([202, 304]);
$e = $this->createEvent(new Response(500));
$this->assertFalse($f(1, $e));
$e = $this->createEvent(new Response(503));
$this->assertFalse($f(0, $e));
$e = $this->createEvent(new Response(202));
$this->assertTrue($f(1, $e));
$e = $this->createEvent();
$this->assertFalse($f(1, $e));
}
public function testCreatesDefaultCurlFilter()
{
$f = RetrySubscriber::createCurlFilter();
$e = $this->createEvent(null, null, null, ['curl_result' => CURLE_RECV_ERROR]);
$this->assertTrue($f(1, $e));
$e = $this->createEvent(null, null, null, ['curl_result' => CURLE_OK]);
$this->assertFalse($f(0, $e));
}
public function testCreatesCustomCurlFilter()
{
$f = RetrySubscriber::createCurlFilter([CURLE_OK]);
$e = $this->createEvent(null, null, null, ['curl_result' => CURLE_RECV_ERROR]);
$this->assertFalse($f(1, $e));
$e = $this->createEvent(null, null, null, ['curl_result' => CURLE_OK]);
$this->assertTrue($f(0, $e));
}
public function testCreatesChainFilter()
{
$e = $this->createEvent(new Response(500));
$f = RetrySubscriber::createChainFilter([
function () { return false; },
function () { return true; },
]);
$this->assertTrue($f(1, $e));
$f = RetrySubscriber::createChainFilter([function () { return false; }]);
$this->assertFalse($f(1, $e));
$f = RetrySubscriber::createChainFilter([function () { return true; }]);
$this->assertTrue($f(1, $e));
}
public function testCreateLoggingDelayFilter()
{
$str = fopen('php://temp', 'r+');
$l = new SimpleLogger($str);
$e = $this->createEvent(new Response(500));
$f = RetrySubscriber::createLoggingDelay(function () {
return true;
}, $l);
$this->assertTrue($f(2, $e));
rewind($str);
$this->assertContains('500 Internal Server Error - Retries: 3, Delay: 1', stream_get_contents($str));
}
public function testCreateLoggingDelayFilterWithCustomFormat()
{
$str = fopen('php://temp', 'r+');
$l = new SimpleLogger($str);
$e = $this->createEvent(new Response(500));
$f = RetrySubscriber::createLoggingDelay(function () {
return true;
}, $l, 'Foo');
$this->assertTrue($f(2, $e));
rewind($str);
$this->assertContains('Foo', stream_get_contents($str));
}
public function testCalculatesExponentialDelay()
{
$e = $this->createEvent(new Response(500));
$this->assertEquals(0, RetrySubscriber::exponentialDelay(0, $e));
$this->assertEquals(1, RetrySubscriber::exponentialDelay(1, $e));
$this->assertEquals(2, RetrySubscriber::exponentialDelay(2, $e));
$this->assertEquals(4, RetrySubscriber::exponentialDelay(3, $e));
$this->assertEquals(8, RetrySubscriber::exponentialDelay(4, $e));
}
public function testCreatesExponentialPlugin()
{
$t = null;
$str = fopen('php://temp', 'r+');
$l = new SimpleLogger($str);
$s = RetrySubscriber::getExponentialBackoff([
'max_retries' => 3,
'http_codes' => [500, 503],
'curl_codes' => [CURLE_OPERATION_TIMEOUTED],
'logger' => $l,
'sleep_fn' => function ($time) use (&$t) {
$t += $time;
}
]);
$c = new Client();
$c->getEmitter()->addSubscriber($s);
$h = new History();
$c->getEmitter()->addSubscriber($h);
$c->getEmitter()->addSubscriber(new Mock([
new Response(500),
new Response(503),
new Response(200)
]));
$this->assertEquals(200, $c->get('/')->getStatusCode());
rewind($str);
$this->assertNotEmpty(stream_get_contents($str));
$this->assertEquals(3, count($h));
$this->assertEquals(1, $t);
}
private function createEvent(
ResponseInterface $response = null,
RequestInterface $request = null,
\Exception $exception = null,
array $transferInfo = [],
$type = 'Guzzle\Http\Event\AbstractTransferStatsEvent'
) {
if (!$request) {
$request = new Request('GET', 'http://www.foo.com');
}
$e = $this->getMockBuilder($type)
->setMethods(['getResponse', 'getTransferInfo', 'getRequest', 'getException'])
->disableOriginalConstructor()
->getMockForAbstractClass();
$e->expects($this->any())
->method('getRequest')
->will($this->returnValue($request));
$e->expects($this->any())
->method('getResponse')
->will($this->returnValue($response));
$e->expects($this->any())
->method('getException')
->will($this->returnValue($exception));
$e->expects($this->any())
->method('getTransferInfo')
->will($this->returnCallback(function ($arg) use ($transferInfo) {
return $arg ? (isset($transferInfo[$arg]) ? $transferInfo[$arg] : null) : $transferInfo;
}));
return $e;
}
}