Skip to content

Instantly share code, notes, and snippets.

@orottier
Last active July 21, 2023 10:10
Show Gist options
  • Save orottier/3ac79378dd38b91ac6953c8618708eb4 to your computer and use it in GitHub Desktop.
Save orottier/3ac79378dd38b91ac6953c8618708eb4 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
);
@razvanioan
Copy link

It's not needed as it's an optional parameter (it has a default value)

@orottier
Copy link
Author

hey @orottier dig this (using a variation of it now), but I think you wanna pass in the $exponent to the retry call on on line #36 of retry.php

You are right! Sorry it took me three years to update this

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment