esroyo / backoff-async
Non-blocking retry functionality with multiple backoff strategies and jitter support
Requires
- php: >=5.4.0
- react/promise: ^2.7.1
- stechstudio/backoff: ^1.0
Requires (Dev)
- clue/block-react: ^1.3
- esroyo/block-react-settle: ^1.0
- phpunit/phpunit: ^7.1 || ^6.4
This package is auto-updated.
Last update: 2024-10-27 23:54:14 UTC
README
Easily wrap your code with retry functionality in a non-blocking way.
This library extends stechstudio/backoff thus inheriting options and strategies. We are highlighting in this document only the advantages and key differences. Please check the PHP Backoff README to get the general usage ideas.
Motivation
PHP Backoff is great, until you need to run some instances at the same time. On that situation only one Backoff can run at the same time because of the PHP's synchronous nature. 6 Backoffs that take 10s each means 60s to run complete. What if we could make the most out of the process by running a Backoff while another sleeps? For that we need an event loop like ReactPHP's EventLoop.
Installation
composer require esroyo/backoff-async
Differences with synchronous Backoff use
First, there is only one key difference in parameters: along with the usual callable
you need to pass in a companion \React\EventLoop\LoopInterface
instance. Easy to remember: always follow the callable with the loop instance.
When using Backoff as a class:
use \Esroyo\BackoffAsync\Backoff;
$backoff = new Backoff(10, 'exponential', 10000, true);
$promise = $backoff->run(function () {
return doSomeWorkThatMightFail();
}, $loop);
When using the helper function:
use \Esroyo\BackoffAsync;
$promise = BackoffAsync\backoff(function () {
return doSomeWorkThatMightFail();
}, $loop);
An instance of LoopInterface
is created automatically when the $loop
parameter is not present. That instance can be obtained with the \Esroyo\BackoffAsync\loop()
helper function.
A second difference: the returned value either from the run($callable, $loop)
method or the backoff($callable, $loop, ...)
helper function is always a promise. You must use then()
to attach actions that will be executed once the backoff succeeds or definitely fails:
$promise = BackoffAsync\backoff(function () {
return doSomeWorkThatMightFail();
}, $loop);
$promise->then(function ($result) {
// doSomeWorkThatMightFail() has
// finally returned some result
echo "Success: $result \n";
}, function ($reason) {
// doSomeWorkThatMightFail() failed
// usually $reason will be an Exception
echo "Failure: {$reason->getMessage()} \n";
});
Usage within an evented application
When you are already working with an event loop the usage of this package will fit naturally. Usually you won't need to worry on when an how to run the event loop because that will be already implicit in your application flow.
Before instantiating any backoff object you may pass in the event loop to the loop()
helper, so you can omit the $loop
parameter later on:
use \Esroyo\BackoffAsync;
use \Esroyo\BackoffAsync\Backoff;
$myLoop = React\EventLoop\Factory::create();
// Here you pass in the event loop instance once
BackoffAsync\loop($myLoop);
...
// When using as a class the `$loop` parameter can be
// completely ommitted and the Backoff will use $myLoop
$backoff = new Backoff();
$promise = $backoff->run(function () {
...
});
// When working with the helper function you will still
// need to specify the $loop parameter as null
$promise = BackoffAsync\backoff(function () {
...
}, null, 10, 'constant');
// Unless you use the defaults
$promise = BackoffAsync\backoff(function () {
...
});
// At some point in you application the loop will run
$myLoop->run();
Usage within a synchronous application
A traditional, synchronous (blocking) application can still take advantage of the asynchronous resolution of several backoffs. For instance when doing/retrying some work on a MVC framework controller you absolutely need the work to finish before returning an HTTP response. One very easy way to do that is with the await
functionality provided by clue/block-react:
use \Esroyo\BackoffAsync;
use \Clue\React\Block;
function blockingExample()
{
// Store the promises returned by all Backoffs
$promises = [];
$promises[] = BackoffAsync\backoff(function () {
return doSomeWorkThatMightFail();
);
$promises[] = BackoffAsync\backoff(function () {
return doAnotherWorkThatMightFail();
);
try {
// Keep the loop running (i.e. block) until all promises resolve
$allResults = Block\awaitAll($promises, BackoffAsync\loop());
// Work with $allResults, each item contains the result of each work
...
} catch (\Exception $e) {
// One of the works failed ...
}
}
Functions Clue\Block\awaitAny(array $promises, LoopInterface $loop, $timeout = null)
and Clue\Block\awaitAll(array $promises, LoopInterface $loop, $timeout = null)
are a good fit for the use case. The former can be used to wait for ANY of the given promises to resolve, and the later to wait for ALL of the given promises to resolve. However in both cases if a work fails thus rejecting the promise, they will try to cancel()
all remaining promises and throw an Exception
.
That approach is inconvenient if you want all the works to finish (successfully or not) to later inspect the results. For that you'll need a similar blocking settle
functionality provided by esroyo/block-react-settle:
use \Esroyo\BackoffAsync;
use \Esroyo\React\Block;
function blockingExample()
{
// Store the promises returned by all Backoffs
$promises = [];
$promises[] = BackoffAsync\backoff(function () {
return doSomeWorkThatMightFail();
);
$promises[] = BackoffAsync\backoff(function () {
return doAnotherWorkThatMightFail();
);
// Keep the loop running (i.e. block) until all promises resolve
// this will never throw an exception
$allResults = Block\settle($promises, BackoffAsync\loop());
// $allResults, each item contains the result of work or the rejection reason
echo $allResults[0]['state'];
// > "fulfilled"
echo $allResults[0]['value'];
// > "This is the result of doSomeWorkThatMightFail()"
echo $allResults[1]['state'];
// > "rejected"
echo $allResults[1]['reason'];
// > "This is the reason why doAnotherWorkThatMightFail() failed (probably an Exception)"
}
Jitter
If you have lots of Backoffs starting at the same time with same timing/strategy it is a good idea to add randomness. You'll get more performance from the event loop by avoiding concentration of tasks on the same time points. See here for a good explanation:
https://www.awsarchitectureblog.com/2015/03/backoff.html
You can enable jitter by passing true
in as the sixth argument to the backoff
helper function, or by using the enableJitter()
method on the Backoff class.