4.0.0
Composer users: require
4.0.1instead. Packagist published 4.0.0 at an earlier commit, before the
timeout-to-delay rename landed, and a stable version there cannot be republished at a different reference, so
4.0.0 is not installable through Composer. 4.0.1 is this same code under a number Packagist accepts.
The list below is long, but nearly all of it is on API surface you had to opt into. An ordinary 3.x caller has a line or
two to change, often none. Where you stand:
Nothing to change if you are on PHP 8.1 or above, retry with one of the shipped conditions —
CrowdStar\Backoff\EmptyValueCondition, ExceptionBasedCondition, NullCondition — and never called ::setType(),
never passed the second constructor argument, and never called ::getTimeoutSeconds(), ::getTimeoutMicroseconds(),
::getCurrentAttempts() or ExceptionBasedCondition::setException()/::getException(). Your code runs unchanged.
One behavior change reaches you anyway, and it needs no code: a delay is now randomized over its whole length rather
than being lengthened by up to 10%, so the same run finishes sooner — 1.795 s against 0.465 s, measured with everything
left at its default. Pass Jitter::None to wait exactly as long as calculated, which is as close to 3.x as this gets.
A line or two if you used one of the settings that moved. Every one has a direct equivalent; the step numbers are
the ones under "Migration from 3.x" below.
| If you called | Use instead | Step |
|---|---|---|
| ::setType(TYPE_SECONDS) | ::setInitialDelay(1_000_000) | 3 |
| ::setType(TYPE_MICROSECONDS) | nothing — it was, and is, the default | 3 |
| new ExponentialBackoff($condition, 1) | new ExponentialBackoff($condition, Mode::Blocking) | 4 |
| new ExponentialBackoff($condition, 2) | new ExponentialBackoff($condition, Mode::Swoole), or nothing at all | 4 |
| ::getTimeoutMicroseconds() | ::getDelayMicroseconds() | 3 |
| ::getTimeoutSeconds() | intdiv(::getDelayMicroseconds(…), 1_000_000) | 3 |
| ::getCurrentAttempts() | nothing; drop the call | 6 |
| ExceptionBasedCondition::setException() | ::setExceptions(), which takes one or more types | 5 |
Actual work, and only in these two cases: a retry condition of your own has to rename met() to ::shouldRetry()
and return the opposite (step 2), and a subclass that overrides ::run() has to declare mixed ...$params and a
mixed return type. Both fail loudly rather than quietly — a leftover met() is a fatal error naming the method, and a
mismatched ::run() is rejected at declaration — so neither can slip into production unnoticed.
Two further behavior changes, neither of them needing a code change: a single delay is capped at 30 seconds, which only
affects runs that would have waited longer than that — with the default initial delay, nine attempts or more; and PHP
8.0 and below are no longer supported, so stay on 3.x there. Against that, the Fixed entries below are two silent
correctness bugs: an instance built outside a coroutine used to block forever inside one, and two callers sharing an
instance used to cut each other's runs short while reporting success.
Changed
-
BREAKING PHP 8.1 or above is now required. PHP 8.0 and below are no longer supported; use version 3.x there.
-
BREAKING The unit of a delay is no longer a choice: delays are always expressed in microseconds, and how long to
wait before the first retry is set with ExponentialBackoff::setInitialDelay() rather than picked from two hardcoded
values. This is what the removed setType() was standing in for, and unlike it, any delay can now be expressed — a
tenth of a second, or a second and a half. -
BREAKING The second parameter of _ExponentialBackoff::_construct() is now
?CrowdStar\Backoff\Modeinstead of
an integer, and defaults to NULL (autodetect) instead of 0. -
BREAKING Method AbstractRetryCondition::met() is now ::shouldRetry(), and it answers the opposite question:
return TRUE to try the call again, where met() returned TRUE to stop. Every other retry library phrases this the way
around shouldRetry() does, and met() conflated "this succeeded" with "give up on this", which is why the shipped
conditions read as double negatives. The method also declares its first parameter asmixednow.Renaming and inverting together is deliberate: because met() is gone, a condition that still implements it fails to
declare itself at all — a fatal error naming the method — rather than quietly retrying whenever it used to stop. -
BREAKING A single delay is now capped at 30 seconds by default, configurable with
ExponentialBackoff::setMaxDelay(). Delays double until they reach the cap and stay there, where before they doubled
without limit. With the default of 4 attempts the longest delay is 1 second, so the default behavior is unchanged;
runs configured with more attempts than that now wait considerably less. -
BREAKING Method ExponentialBackoff::getTimeoutMicroseconds() is now ::getDelayMicroseconds(), it takes a
maximum delay as its third parameter and a CrowdStar\Backoff\Jitter case as its fourth, and its second and third
parameters are named$initialDelayand$maxDelay, which matters only where they are passed as named arguments.
Pass the third and fourth explicitly to opt out of the default cap and of the default randomness. -
BREAKING A delay is now randomized over its whole length instead of being lengthened by up to 10%, and the amount
of randomness is configurable with ExponentialBackoff::setJitter(). Waits are therefore shorter on average and no
longer predictable: a delay has become the longest a wait may take rather than the shortest. Clients that failed
together are what this spreads out; the measurements behind it are in the AWS article linked from enum
CrowdStar\Backoff\Jitter. Pass Jitter::None for the previous predictability, or Jitter::Equal to keep at least
half of every delay. -
BREAKING Method ExponentialBackoff::run() now declares
mixed ...$paramsand amixedreturn type. Calling it
is unchanged; a subclass that overrides ::run() has to declare both, or PHP rejects the declaration outright. -
The randomness is no longer rounded away for delays of about a second. Where a seconds-mode delay used to be rounded
down to whole seconds after being randomized — which for a one-second delay discarded the randomness entirely, leaving
every client to retry in lockstep — nothing rounds any more.
Added
- Enum CrowdStar\Backoff\Mode with cases Blocking, Swoole and Sleeper. The first two can be passed as the second
constructor parameter instead of having the mode worked out per wait, and only Mode::Blocking changes anything: it
opts out of non-blocking waits altogether, where Mode::Swoole and NULL both mean "wait non-blockingly where a
coroutine is running". Mode::Sleeper is only ever reported by ::getMode(), for when a callback set with
::setSleeper() is doing the waiting; the constructor rejects it, since no case can stand in for a callback. - Methods ExponentialBackoff::setInitialDelay(), ::getInitialDelay(), ::setMaxDelay() and ::getMaxDelay(), plus
constants ExponentialBackoff::DEFAULT_INITIAL_DELAY and ::DEFAULT_MAX_DELAY. A delay is what one wait between two
attempts lasts, and ::setMaxDelay() caps a single one of them; the wall-clock budget for a whole run is a separate
setting, ::setMaxElapsedTime(). - Method ExponentialBackoff::getMode(), telling which mode a wait would happen in right now.
- Enum CrowdStar\Backoff\Jitter with cases None, Full and Equal, along with methods
ExponentialBackoff::setJitter() and ::getJitter() and constant ExponentialBackoff::DEFAULT_JITTER. - Class CrowdStar\Backoff\CallbackCondition and method ExponentialBackoff::when(), for deciding whether to retry
with a closure instead of a condition class of your own:
ExponentialBackoff::when(fn (mixed $result): bool => empty($result))->run($c). The closure receives what the call
returned and what it threw; a second argument to ::when() says whether an exception the last attempt was left with
should be thrown out, and a third takes the same Mode case the constructor does. - Methods ExceptionBasedCondition::setIgnoredExceptions() and ::getIgnoredExceptions(), listing types that are
never retried. Ignored types take priority over the types being retried on, so "retry every HttpException except
HttpBadRequestException" no longer means enumerating every sibling of the one exception to be left alone. An
ignored exception ends the run at once and is thrown out, the same as one that was never covered. - Method ExponentialBackoff::setSleeper(), handing the waiting over to a callback that receives the wait in
microseconds. It takes precedence over both other modes, which ::getMode() reports as Mode::Sleeper, and is for
waiting on an event loop this library knows nothing about — ReactPHP, Amp, Revolt, a Fiber of your own — or for tests,
where a callback that records and returns makes a retrying test instant and lets it assert the delays that would have
been waited for. - Methods ExponentialBackoff::setMaxElapsedTime() and ::getMaxElapsedTime(), giving a whole run a wall-clock budget
in microseconds on top of its maximum number of attempts. Once the next wait would not finish inside the budget it is
not started at all, and the run hands back whatever the last attempt produced. Worth having because attempts say
nothing about how long they take, and because PHP's ownmax_execution_timedoes not count time spent inusleep()
on Unix — a runaway backoff is otherwise killed mid-wait by PHP-FPM or a proxy, with no error to log.
Fixed
- Whether to wait in non-blocking mode is now decided per wait instead of once at construction. An instance built
outside a coroutine — a service put together during bootstrap, say — used to block forever afterwards, even when used
by coroutines, which is the way it is normally wired up in a Swoole application. Passing Mode::Swoole where no
coroutine is running no longer raises the Swoole\Error it would produce either; the wait falls back to blocking. - One instance of ExponentialBackoff can now be used by several callers at once. The attempt counter was kept on the
object and reset at the start of every ::run(), so a closure that called ::run() again on the same instance reset
the count of the run it was part of, and that run then gave up after a single attempt while returning as if it had
succeeded. The same went for concurrent Swoole coroutines sharing an instance, which is how a service tends to be
wired up there — and which the 3.0.11 note about reusing an instance did not warn about, being true only for runs
happening one after another. - Delays no longer overflow. Doubling an uncapped delay left the integer range from the 46th attempt on, where
ExponentialBackoff::getDelayMicroseconds() threw a TypeError, and from the 65th attempt on the bit shift it used
returned 0, silently disabling the backoff altogether. Both were reachable through ::setMaxAttempts(), and neither
could be caught by ::run(), which handles exceptions rather than errors. Delays now stop at the maximum instead of
growing past what an integer holds, and a non-positive iteration is treated as the first one rather than raising an
ArithmeticError.
Removed
- BREAKING Constants ExponentialBackoff::TYPE_MICROSECONDS and ExponentialBackoff::TYPE_SECONDS along with
methods ::setType() and ::getType(). Use ::setInitialDelay() instead. - BREAKING Method ExponentialBackoff::getTimeoutSeconds(). It rounded delays down to whole seconds, which
discarded the randomness of anything under ten seconds, and it existed only to serve the removed seconds mode. Divide
the result of ::getDelayMicroseconds() by 1000000 where seconds are wanted. - BREAKING Method ExponentialBackoff::getCurrentAttempts(), deprecated since 3.x. There is no replacement: the
attempt counter belongs to a single run and is no longer kept on the object. - BREAKING Methods ExceptionBasedCondition::getException() and ExceptionBasedCondition::setException(),
deprecated since 3.0.10. Use ::getExceptions() and ::setExceptions() instead, which handle one or more types. - The exception previously thrown for an invalid backoff type. The backoff type is gone entirely, so there is nothing
left to reject. _ExponentialBackoff::_construct() still rejects one second argument — Mode::Sleeper, which
::getMode() answers but nobody asks for — where before it rejected any integer outside the two SAPI constants.
Migration from 3.x
- Require PHP 8.1 or above.
- Rename met() to shouldRetry() in every condition of your own, and negate what it returns:
Conditions written around exceptions usually get shorter:
public function met(mixed $result, ?Exception $e): bool // 3.x { return !empty($result); // TRUE meant "stop, this worked" } public function shouldRetry(mixed $result, ?Exception $e): bool // 4.0 { return empty($result); // TRUE means "try again" }
return (empty($e) || (!($e instanceof Exception)));
becomesreturn ($e instanceof Exception);. A condition left implementing met() raises a fatal error saying
shouldRetry() is not implemented, so nothing silently starts retrying where it used to stop. - Replace the type constants with an initial delay in microseconds:
Any other delay works as well now:
$backoff->setType(ExponentialBackoff::TYPE_SECONDS); // 3.x $backoff->setInitialDelay(1_000_000); // 4.0 $backoff->setType(ExponentialBackoff::TYPE_MICROSECONDS); // 3.x — this was the default $backoff->setInitialDelay(250_000); // 4.0 — still the default, so drop the call
setInitialDelay(100_000)waits up to about a tenth of a second before the
first retry. Method getTimeoutSeconds() is gone, and getTimeoutMicroseconds() is now getDelayMicroseconds():
divide its result by 1000000 where seconds are wanted, and pass Jitter::None to get the whole delay the way the old
method gave it — with the default randomness left on, a delay of a second divides down to 0 more often than to 1:ExponentialBackoff::getTimeoutSeconds($i, 1); // 3.x intdiv( ExponentialBackoff::getDelayMicroseconds($i, 1_000_000, jitter: \CrowdStar\Backoff\Jitter::None), 1_000_000 ); // 4.0
- If you passed the second constructor parameter, pass an enum case instead of an integer:
Pass NULL, or nothing at all, to keep autodetecting Swoole coroutines — which is also all Mode::Swoole does, since
new ExponentialBackoff($condition, 1); // 3.x, SAPI_DEFAULT new ExponentialBackoff($condition, \CrowdStar\Backoff\Mode::Blocking); // 4.0 new ExponentialBackoff($condition, 2); // 3.x, SAPI_SWOOLE new ExponentialBackoff($condition, \CrowdStar\Backoff\Mode::Swoole); // 4.0
a non-blocking wait needs a running coroutine either way. - Replace the singular exception accessors on ExceptionBasedCondition with the plural ones:
Method getExceptions() returns a
$condition->setException(Exception::class); // 3.x $condition->setExceptions(Exception::class); // 4.0, accepts one or more types
string[]where getException() returned a single class name. - Drop any call to ExponentialBackoff::getCurrentAttempts(). There is no replacement; the attempt counter is
internal.