Skip to content

Normalise the strategy's wait time to int before jitter - #23

Open
noahott wants to merge 1 commit into
stechstudio:masterfrom
noahott:fix/float-strategy-int-conversion
Open

Normalise the strategy's wait time to int before jitter#23
noahott wants to merge 1 commit into
stechstudio:masterfrom
noahott:fix/float-strategy-int-conversion

Conversation

@noahott

@noahott noahott commented Aug 20, 2026

Copy link
Copy Markdown

Fixes #22.

A strategy may be any callable, and nothing requires it to return an int. cap() is type-preserving, so a float-returning strategy reaches mt_rand() unchanged — and mt_rand() takes int parameters.

Three symptoms, all from the same line:

Strategy returns Jitter Today
15.667 on Deprecated: Implicit conversion from float 15.667 to int loses precision (PHP 8.1+)
15.667 off returns float(15.667), contradicting getWaitTime()'s @return int
1e20 on TypeError: mt_rand(): Argument #2 ($max) must be of type int, float given

The last one is a hard crash, and it is reachable whenever no wait cap is set — cap() returns the strategy value untouched in that case, since is_int(null) is false.

The bundled strategies all cast to int already, which is why none of this shows up in normal use.

The change

-        return $this->jitter($this->cap($waitTime));
+        return $this->jitter((int) min(PHP_INT_MAX, $this->cap($waitTime)));

Two deliberate details:

  • After cap(), not before, so the negative-overflow guard added in Added protection against overflows in the wait time calculation. #20 still runs first. testWaitCapOverflow still passes.
  • min(PHP_INT_MAX, ...) rather than a bare cast. Casting a float above PHP_INT_MAX is undefined in PHP; clamping first makes the 1e20 case defined instead of platform-dependent. If you'd rather keep the diff minimal, (int) $this->cap($waitTime) fixes the deprecation and the TypeError too — it just leaves the out-of-range value undefined rather than clamped. Happy to switch.

Casting before jitter() rather than after also means the non-jittered path returns an int, matching the docblock.

Tests

Three added to BackoffTest, following the existing style (no attributes, $this->assertX()):

  • testFloatReturningStrategyProducesIntegerWaitTime — jitter off, asserts assertIsInt(). This is the one that needs no error-handler juggling, so it's the durable regression test.
  • testFloatReturningStrategyDoesNotTriggerDeprecationUnderJitter — installs a temporary E_DEPRECATED handler, restores it in a finally. Worth noting phpunit.xml sets neither failOnDeprecation nor failOnWarning, so without an explicit handler a test here cannot observe the deprecation at all.
  • testFloatStrategyBeyondIntMaxDoesNotThrow — the TypeError case.

Each fails on master and passes with the patch. On baab60f + tests only:

1) testFloatReturningStrategyProducesIntegerWaitTime
   getWaitTime() is documented @return int
   Failed asserting that 15.667 is of type int.

2) testFloatReturningStrategyDoesNotTriggerDeprecationUnderJitter
   Failed asserting that two arrays are identical.
   +    0 => 'Implicit conversion from float 15.667 to int loses precision'

3) testFloatStrategyBeyondIntMaxDoesNotThrow
   TypeError: mt_rand(): Argument #2 ($max) must be of type int, float given

With the patch:

PHPUnit 10.5.64 by Sebastian Bergmann and contributors.
Runtime:       PHP 8.3.31

OK (37 tests, 89 assertions)

Verified on PHP 8.3.31. Your CI matrix is PHP 8.2, where the deprecation also applies (8.1+).

A strategy may be any callable — buildStrategy() returns callables untouched
and testClosureStrategy covers that — but nothing requires one to return an
int. cap() is type-preserving, so a float-returning strategy reaches
mt_rand() unchanged, and mt_rand() takes int parameters.

Two symptoms, depending on magnitude:

- An in-range float deprecates on PHP 8.1+: "Implicit conversion from float
  15.667 to int loses precision". With jitter disabled the float is returned
  as-is, so getWaitTime() also contradicts its own @return int.
- A float beyond PHP_INT_MAX throws outright: "mt_rand(): Argument stechstudio#2 ($max)
  must be of type int, float given". Reachable whenever no wait cap is set,
  since cap() then returns the strategy value untouched.

The bundled strategies all cast to int already, which is why this has gone
unnoticed.

min(PHP_INT_MAX, ...) before the cast keeps the large-float case in range
rather than relying on an undefined float-to-int conversion, and casting
before jitter() means both the jittered and non-jittered paths honour the
documented int return.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Float-returning strategy reaches mt_rand() unconverted: deprecation on PHP 8.1+, TypeError beyond PHP_INT_MAX

1 participant