ExponentialBackoff overflows timedelta above attempt 46
Nobody has claimed this yet.
Assessment
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Newbie friendliness
- 78/100
Research direction
Start at threadmill.retry.ExponentialBackoff.call and inspect how its delay is calculated before min() is applied. Reproduce the issue with the provided attempt loop, then check Executor.retry_delay to confirm the callback no longer fails at high attempts. Done means attempts through max_retries remain capped at max_delay instead of ending at the overflow.
Written by the indexing model from the issue text.
Description
ExponentialBackoff.__call__ computes
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)
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:
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.
- Dominant language
- Python
- Stars
- 12
- Forks
- 1
- Avg merge
- 1d 1h
- Merged PRs (30d)
- 10
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from codingjoe/threadmill
-
Difficulty 2/5 Half a day Newbie friendliness 84/100
codingjoe/threadmill#52 ·
-
WorkerProcess passes None poll intervals to the backend, breaking acquire in the consumer thread Open
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
codingjoe/threadmill#53 ·
-
Difficulty 1/5 Under an hour Newbie friendliness 90/100
codingjoe/threadmill#51 ·
-
Difficulty 3/5 1-2 days Newbie friendliness 68/100
codingjoe/threadmill#55 · 1 comment ·
-
Difficulty 3/5 1-2 days Newbie friendliness 72/100
codingjoe/threadmill#54 ·
All issues in codingjoe/threadmill
Similar issues
-
triage/confirmed
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
agentscope-ai/agentscope#2775 ·
-
comp/desktop P3 type/bug
Difficulty 1/5 Under an hour Newbie friendliness 92/100
NousResearch/hermes-agent#118866 ·
-
bug
Difficulty 1/5 Under an hour Newbie friendliness 90/100
apache/cloudstack#14222 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 82/100