avtonom/delay-exponential-backoff-bundle
Installation:
composer require avtonom/exponential-backoff-bundle
Add to config/bundles.php (Symfony 4+):
return [
// ...
Avtonom\ExponentialBackoffBundle\AvtonomExponentialBackoffBundle::class => ['all' => true],
];
Configure (optional) in config/packages/avtonom_exponential_backoff.yaml:
avtonom_exponential_backoff:
cap: 1000000000 # 1 second in microseconds
max_attempts: 5
First Use Case: Inject the service in a controller/service and use it for retries:
use Avtonom\ExponentialBackoffBundle\Service\ExponentialBackoffService;
class MyService {
public function __construct(private ExponentialBackoffService $backoff) {}
public function retryOperation() {
$attempt = 1;
while (true) {
try {
$this->callExternalService();
break;
} catch (Exception $e) {
if ($attempt >= $this->backoff->getMaxAttempts()) {
throw $e;
}
$this->backoff->delay($attempt);
$attempt++;
}
}
}
}
Retry Logic in Services:
public function fetchDataWithRetry() {
$attempt = 1;
while (true) {
try {
return $this->fetchData();
} catch (RateLimitException $e) {
if ($attempt >= $this->backoff->getMaxAttempts()) {
throw new RetryFailedException('Max retries exceeded', 0, $e);
}
$this->backoff->equalJitter($attempt); // Add jitter for randomness
$attempt++;
}
}
}
Command Bus Integration: Use with Symfony Messenger or similar for async retries:
$message = new MyMessage();
$this->bus->dispatch($message);
// In handler:
public function __invoke(MyMessage $message) {
$attempt = 1;
while (true) {
try {
$this->process($message);
break;
} catch (Exception $e) {
if ($attempt >= $this->backoff->getMaxAttempts()) {
throw $e;
}
$this->backoff->fullJitter($attempt);
$attempt++;
$this->bus->dispatch($message); // Requeue
}
}
}
Middleware for HTTP Clients: Wrap Guzzle/HTTP client calls:
$client = new Client([
'handler' => HandlerStack::create([
new RetryMiddleware($this->backoff),
GuzzleHttp\HandlerStack::create(),
]),
]);
ExponentialBackoffService.config/packages/ for environment-specific tuning.$this->backoff->shouldReceive('equalJitter')->with(1)->andReturn(100000); // 0.1s
Microseconds vs Seconds:
sleep():
sleep($this->backoff->exponential($attempt) / 1_000_000);
usleep() directly:
usleep($this->backoff->halfDelay($attempt));
Max Attempts = 0:
max_attempts: 0 means no limit. Set explicitly (e.g., max_attempts: 3) to avoid infinite loops.Cap Overrides:
cap is too low, delays may truncate unexpectedly. Test edge cases:
$this->backoff->exponential(10); // May return capped value if cap=1000000
Thread Safety:
$delay = $this->backoff->fullJitter($attempt);
$this->logger->debug('Retry delay', ['attempt' => $attempt, 'microseconds' => $delay]);
php bin/console exponential-backoff to verify calculations before integrating.Custom Strategies: Extend the service or create decorators:
class CustomBackoffService extends ExponentialBackoffService {
public function customDelay($attempt) {
return min(parent::exponential($attempt) * 1.5, $this->getCap());
}
}
Event-Based Retries: Dispatch events on retry attempts for observability:
$this->eventDispatcher->dispatch(
new RetryAttemptEvent($attempt, $delay),
RetryAttemptEvent::NAME
);
Dynamic Configuration:
Override getDefaultOptions() in a custom service to fetch values from a database or API.
How can I help you explore Laravel packages today?