Skip to content

Commit

Permalink
Merge pull request #340 from zendesk/kcasas/MI-1724-retry-handling
Browse files Browse the repository at this point in the history
[MI-1724] retry ssl issues by default
  • Loading branch information
kcasas authored Jul 14, 2017
2 parents e5e398f + 6e62057 commit 7685d8c
Show file tree
Hide file tree
Showing 5 changed files with 390 additions and 6 deletions.
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,23 @@ The allowed options are
* page
* sort_order

### Retrying Requests

Add the `RetryHandler` middleware on the `HandlerStack` of your `GuzzleHttp\Client` instance. By default `Zendesk\Api\HttpClient`
retries:
* timeout requests
* those that throw `Psr\Http\Message\RequestInterface\ConnectException:class`
* and those that throw `Psr\Http\Message\RequestInterface\RequestException:class` that are identified as ssl issue.

#### Available options
Options are passed on `RetryHandler` as an array of values.

* max = 2 _limit of retries_
* interval = 300 _base delay between retries in milliseconds_
* max_interval = 20000 _maximum delay value_
* backoff_factor = 1 _backoff factor_
* exceptions = [ConnectException::class] _Exceptions to retry without checking retry_if_
* retry_if = null _callable function that can decide whether to retry the request or not_

## Copyright and license

Expand Down
9 changes: 8 additions & 1 deletion src/Zendesk/API/HttpClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@
* spl_autoload_register(function($c){@include 'src/'.preg_replace('#\\\|_(?!.+\\\)#','/',$c).'.php';});
*/

use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\HandlerStack;
use Zendesk\API\Exceptions\AuthException;
use Zendesk\API\Middleware\RetryHandler;
use Zendesk\API\Resources\Core\Activities;
use Zendesk\API\Resources\Core\AppInstallations;
use Zendesk\API\Resources\Core\Apps;
Expand Down Expand Up @@ -186,7 +189,11 @@ public function __construct(
$guzzle = null
) {
if (is_null($guzzle)) {
$this->guzzle = new \GuzzleHttp\Client();
$handler = HandlerStack::create();
$handler->push(new RetryHandler(['retry_if' => function ($retries, $request, $response, $e) {
return $e instanceof RequestException && strpos($e->getMessage(), 'ssl') !== false;
}]), 'retry_handler');
$this->guzzle = new \GuzzleHttp\Client(compact('handler'));
} else {
$this->guzzle = $guzzle;
}
Expand Down
105 changes: 105 additions & 0 deletions src/Zendesk/API/Middleware/RetryHandler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
<?php

namespace Zendesk\API\Middleware;

use GuzzleHttp\Exception\ConnectException;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\RetryMiddleware;

class RetryHandler
{
/**
* @var array $timeoutCodes list of timeout status codes: Request Timeout, Authentication Timeout, Gateway Timeout
*/
private $timeoutCodes = [408, 419, 504];

private $options = [
'max' => 2, // limit of retries
'interval' => 300, // base delay between retries, unit is in milliseconds
'max_interval' => 20000, // maximum delay value
'backoff_factor' => 1, // backoff factor
'exceptions' => [ConnectException::class], // Exceptions to retry without checking retry_if
'retry_if' => null, // callable function that can decide whether to retry the request or not
];

/**
* RetryHandler constructor.
*
* @param array $config
*/
public function __construct(array $config = [])
{
$this->options = array_merge($this->options, $config);
}

/**
* Returns the function that will decide whether to retry the request or not.
*
* @return callable
*/
public function shouldRetryRequest()
{
return function ($retries, Request $request, $response, $exception) {
if ($retries >= $this->options['max']) {
return false;
} elseif ($this->isRetryableException($exception)) {
return true;
} elseif (is_callable($this->options['retry_if'])) {
return call_user_func($this->options['retry_if'], $retries, $request, $response, $exception);
}

return $response && in_array($response->getStatusCode(), $this->timeoutCodes);
};
}

/**
* Returns the function that computes the delay before the next retry
*
* @return callable
*/
public function delay()
{
return function ($retries) {
$current_interval = $this->options['interval'] * pow($this->options['backoff_factor'], $retries);
$current_interval = min([$current_interval, $this->options['max_interval']]);

return $current_interval;
};
}

/**
* Called when the middleware is handled by the client.
*
* @param callable $handler
*
* @return RetryMiddleware
*/
public function __invoke(callable $handler)
{
$retryMiddleware = new RetryMiddleware($this->shouldRetryRequest(), $handler, $this->delay());

return $retryMiddleware;
}

/**
* Checks if the exception thrown warrants a retry
*
* @param $exception
*
* @return bool
*/
private function isRetryableException($exception)
{
if (!$this->options['exceptions']) {
return true;
}

foreach ($this->options['exceptions'] as $expectedException) {
if ($exception instanceof $expectedException) {
return true;
}
}

return false;
}
}
17 changes: 12 additions & 5 deletions tests/Zendesk/API/UnitTests/BasicTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,10 @@ protected function setUp()
*
* @param array $responses
* An array of GuzzleHttp\Psr7\Response objects
* @param array $config
* config for the GuzzleHttp\Client
*/
protected function mockApiResponses($responses = [])
protected function mockApiResponses($responses = [], array $config = [])
{
if (empty($responses)) {
return;
Expand All @@ -96,11 +98,16 @@ protected function mockApiResponses($responses = [])

$history = Middleware::history($this->mockedTransactionsContainer);
$mock = new MockHandler($responses);
$handler = HandlerStack::create($mock);
$handler->push($history);

$this->client->guzzle = new Client(['handler' => $handler]);
$handlerStack = HandlerStack::create($mock);
$handlerStack->push($history);
if (isset($config['handlers'])) {
foreach ($config['handlers'] as $handler) {
$handlerStack->push($handler);
}
}
$config['handler'] = $handlerStack;

return $this->client->guzzle = new Client($config);
}

/**
Expand Down
Loading

0 comments on commit 7685d8c

Please sign in to comment.