cocoindex-io/cocoindex · error · ValueError
retry_transient requires a positive timeout
Error message
retry_transient requires a positive timeout
What it means
retry_transient requires its timeout keyword, when provided, to be strictly positive; a zero or negative datetime.timedelta raises this ValueError at call time. timeout=None (no time limit) is the only non-positive-friendly form. A zero timeout would expire before the first attempt could ever start, so the library rejects it as a configuration bug.
Source
Thrown at python/cocoindex/_internal/deadline.py:170
``coco.timeout(...)`` scope, merging with any ambient deadline by
min-nesting. Expiry raises ``DeadlineExceededError``.
Deadline enforcement is best-effort-or-better: no attempt starts past
the deadline, no result is accepted past it (checked after each attempt
completes), backoff sleeps never exceed the remaining time — and with
``bound_attempt=True``, an in-flight attempt is additionally cancelled
at the effective deadline via ``asyncio.wait_for`` and surfaces as
``DeadlineExceededError``.
With neither ``max_attempts`` nor a deadline, retries are unbounded;
this helper is internal and every call site sets at least one limit.
Cancellation, ``KeyboardInterrupt``, and ``SystemExit`` always propagate
untouched.
"""
if max_attempts is not None and max_attempts < 1:
raise ValueError("retry_transient requires max_attempts >= 1")
if timeout is not None and timeout <= timedelta(0):
raise ValueError("retry_transient requires a positive timeout")
if backoff is None:
backoff = exponential_backoff()
scope = _timeout_scope(timeout) if timeout is not None else contextlib.nullcontext()
with scope:
# Exception, not BaseException: doubles as a type-level guard — if
# the except clause below ever widens back to BaseException, this
# assignment becomes a mypy error.
last_error: Exception | None = None
attempt_index = 0
while True:
# Never start an attempt past the deadline. A remaining time of
# exactly zero counts as expired, so a sleep clipped to the
# deadline cannot spin at the boundary.
check_cancellation()
remaining = remaining_seconds()
if remaining is not None and remaining <= 0:
raise DeadlineExceededError("CocoIndex timeout deadline exceeded")View on GitHub (pinned to e84aa99b32)
Solutions
- Pass a positive timedelta, e.g. timedelta(seconds=30), or pass None to have no timeout.
- Default missing config to None, not 0: timeout=cfg.get('timeout') and convert non-None values to timedelta.
- Guard the value before the call: if timeout is not None and timeout <= timedelta(0): skip retrying or raise a domain-specific error.
- If the budget may legitimately be exhausted before the loop starts, let DeadlineExceededError surface from inside a coco.timeout() scope rather than passing a zero timeout.
Example fix
// before
timeout_s = cfg.get('timeout', 0)
await retry_transient(fn, retry_on=(IOError,), timeout=timedelta(seconds=timeout_s)) # ValueError when 0
// after
timeout_s = cfg.get('timeout')
timeout = timedelta(seconds=timeout_s) if timeout_s else None
await retry_transient(fn, retry_on=(IOError,), timeout=timeout) Defensive patterns
Strategy: validation
Validate before calling
from datetime import timedelta
def positive_timeout(td):
if td is None:
return None
if td <= timedelta(0):
raise ValueError(f"timeout must be positive, got {td}")
return td Type guard
def is_positive_timeout(v) -> bool:
from datetime import timedelta
return v is None or (isinstance(v, timedelta) and v > timedelta(0)) Try / catch
try:
result = await retry_transient(fn, retry_on=retry_on, timeout=td)
except ValueError as e:
if 'positive timeout' in str(e):
result = await retry_transient(fn, retry_on=retry_on, timeout=None) # no limit
else:
raise Prevention
- Default timeout config to None (no limit), never 0
- Guard for elapsed budgets: if timeout <= timedelta(0), don't start the retry loop
- Convert all user/config durations through one helper that rejects non-positive values
When it happens
Trigger: Calling retry_transient(fn, retry_on=..., timeout=timedelta(0)) or timeout=timedelta(seconds=-5); computing the timeout as end_time - now after the deadline already passed, or reading 0 from an unset config field.
Common situations: Config-driven timeouts where a missing value defaults to 0 instead of None (cfg.get('timeout', 0)); computing remaining time budgets that have already elapsed before the retry loop starts; tests passing timedelta(0) expecting an instant-expiry behavior instead of DeadlineExceededError.
Understand the failure class
Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.
- Timeouts: ETIMEDOUT, deadlines, and hung requests — what actually expires when a request times out.
Related errors
- retry_transient requires max_attempts >= 1
- CocoIndex timeout deadline exceeded
- expected None{loc}, got {type(value).__name__}
- expected {tp}{loc}, got {type(value).__name__}: {value!r}
- expected tuple{loc}, got {type(value).__name__}
AI-assisted analysis of cocoindex-io/cocoindex@e84aa99b32 (2026-09-08).
Data as JSON: /api/errors/fd27ebf06dba769a.
Report an issue: GitHub.