Last active
July 21, 2023 10:10
-
-
Save orottier/3ac79378dd38b91ac6953c8618708eb4 to your computer and use it in GitHub Desktop.
Retry function for PHP with exponential backoff
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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
You are right! Sorry it took me three years to update this