Skip to content

Instantly share code, notes, and snippets.

@sidigi
Forked from orottier/RetryTest.php
Created October 18, 2022 15:40
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save sidigi/421051687b7cf89278f147fd4f8095e9 to your computer and use it in GitHub Desktop.
Save sidigi/421051687b7cf89278f147fd4f8095e9 to your computer and use it in GitHub Desktop.
Retry function for PHP with exponential backoff
<?php
/*
* Retry function for e.g. external API calls
*
* Will try the risky API call, and retries with an ever increasing delay if it fails
* Throws the latest error if $maxRetries is reached,
* otherwise it will return the value returned from the closure.
*
* You specify the exceptions that you expect to occur.
* If another exception is thrown, the script aborts
*
*/
function retry(callable $callable, $expectedErrors, $maxRetries = 5, $initialWait = 1.0, $exponent = 2)
{
if (!is_array($expectedErrors)) {
$expectedErrors = [$expectedErrors];
}
try {
return call_user_func($callable);
} catch (Exception $e) {
// get whole inheritance chain
$errors = class_parents($e);
array_push($errors, get_class($e));
// if unexpected, re-throw
if (!array_intersect($errors, $expectedErrors)) {
throw $e;
}
// exponential backoff
if ($maxRetries > 0) {
usleep($initialWait * 1E6);
return retry($callable, $expectedErrors, $maxRetries - 1, $initialWait * $exponent, $exponent);
}
// max retries reached
throw $e;
}
}
<?php
class RetryTest extends TestCase
{
public function setUp()
{
parent::setUp();
// abuse superglobal to keep track of state
$_GET['a'] = 0;
}
protected static function failOnce($exception)
{
$_GET['a']++;
if ($_GET['a'] == 1) {
throw $exception;
}
return $_GET['a'];
}
public function testSucceed()
{
$callable = function() { return 'hello'; };
$return = retry($callable, Exception::class);
$this->assertSame($return, 'hello');
}
public function testFailOnce()
{
$callable = function() {
return self::failOnce(new Exception('Fail once'));
};
$return = retry($callable, Exception::class, 3, 0);
$this->assertSame(2, $return);
}
public function testFailOnceInherited()
{
$callable = function() {
return self::failOnce(new UnexpectedValueException('Fail once'));
};
$return = retry($callable, Exception::class, 3, 0);
$this->assertSame(2, $return);
}
/**
* @expectedException Exception
*/
public function testFailAfterMax()
{
$callable = function() { throw new Exception('Error'); };
$return = retry($callable, Exception::class, 3, 0);
}
public function testFailCount()
{
$callable = function() {
$_GET['a']++;
throw new Exception('FailCount');
};
$retries = 5;
try {
$return = retry($callable, Exception::class, $retries, 0);
} catch(Exception $e) {
$this->assertSame($e->getMessage(), 'FailCount');
}
$this->assertSame($_GET['a'], $retries + 1);
}
/**
* @expectedException Exception
*/
public function testFailUnexpected()
{
$callable = function() { throw new Exception('Error'); };
$return = retry($callable, UnexpectedValueException::class);
}
}
<?php
$expectedErrors = 'Google_Service_Exception';
$maxRetries = 5; // means we try max 6 times
$initialWait = 1.0; // seconds
$exponent = 2; // double the waiting time each try
/* your application stuff */
$service = ...
$calendarAddress = ...
$vEvent = ...
$returnValue = retry(
function() use ($service, $calendarAddress, $vEvent) {
return $service->events->insert($calendarAddress, $vEvent);
},
$expectedErrors, $maxRetries, $initialWait, $exponent
);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment