codingjoe / codingjoe/threadmill

ExponentialBackoff overflows timedelta above attempt 46

Offen Anfängerfreundlich
#47 0 Kommentare 0 Reaktionen 0 zugewiesene Personen Auf GitHub ansehen
Vorherrschende Sprache
Python
Sterne
12
Forks
1
Ø Merge
1 T. 1 Std.
Gemergte PRs (30 T.)
10

Beschreibung

`ExponentialBackoff.__call__` computes

```python
delay = min(self.base_delay * (self.factor**context.attempt), self.max_delay)
```

so the product is evaluated before `min()` clamps it. With the default shape (base delay 1 s, factor 2.0, max delay 1 h) the product exceeds the `timedelta` limit at attempt 47:

```
OverflowError: days=1628906115; must have magnitude <= 999999999
```

### Repro (threadmill 0.7.1)

```python
import datetime
from types import SimpleNamespace
from threadmill.retry import ExponentialBackoff

policy = ExponentialBackoff(
base_delay=datetime.timedelta(seconds=1),
max_delay=datetime.timedelta(hours=1),
factor=2.0,
max_retries=720,
)

def context(attempt):
error = SimpleNamespace(exception_class=ValueError)
return SimpleNamespace(attempt=attempt, task_result=SimpleNamespace(errors=[error]))

for attempt in range(1, 60):
try:
print(attempt, policy(context(attempt)))
except Exception as exc:
print(attempt, type(exc).__name__, exc)
break
```

Attempts 1 to 46 return a delay (2 s doubling to the 1 h cap at attempt 12), attempt 47 raises.

### Why it matters

`Executor.retry_delay` catches the exception, logs `Retry callback failed`, and returns `None`, so the backend acknowledges the result and the retry chain ends. The task looks like it exhausted its policy, but `max_retries` was never reached: a policy of 720 attempts really stops after 46 retries.

### Suggested fix

Clamp in seconds before building the `timedelta`, and derive the capped attempt count from `max_delay` so the exponential is never evaluated past the cap:

```python
seconds = min(
self.base_delay.total_seconds() * self.factor**context.attempt,
self.max_delay.total_seconds(),
)
return datetime.timedelta(seconds=seconds)
```

A plain `min()` on the two `timedelta` values does not help on its own, because the product still overflows before the comparison.

Found while bounding the spam scan retry budget in codingjoe/relay#230.

Beitragsleitfaden

Beitragsleitfaden öffnen

Rechercherichtung

Beginne bei threadmill.retry.ExponentialBackoff.__call__ und untersuche, wie seine Verzögerung berechnet wird, bevor min() angewendet wird. Reproduziere das Problem mit der bereitgestellten Versuchsschleife und prüfe anschließend Executor.retry_delay, um zu bestätigen, dass der Callback bei hohen Versuchsanzahlen nicht mehr fehlschlägt. Erledigt ist die Aufgabe, wenn Versuche bis einschließlich max_retries bei max_delay gedeckelt bleiben, anstatt mit dem Overflow zu enden.

Vom Indexierungsmodell aus dem Issue-Text verfasst.

Bewertung

Tech-Stack
python
Bereich
backend
Issue-Typ
Bug
Schwierigkeit
2/5
Geschätzter Aufwand
1-3 Stunden
Aktivitätsstatus
Aktiv
Klarheit
Klar beschrieben
Anfängerfreundlichkeit
78/100

Neue Issues direkt in Ihr Postfach

Eine kurze Übersicht über anfängerfreundliche GitHub-Issues.