Skip to content

[FEATURE] Bounded exponential retry with jitter for external calls #111

Description

@koydas

🎯 Goal

Add a shared retry utility with bounded exponential backoff + jitter to absorb transient external failures without destabilizing a run.

📍 Context

  • Repo: autonomous-dev-loop
  • Domain: Transient failure tolerance
  • Component: scripts/lib/anthropic_client.mjs, scripts/lib/groq_client.mjs, scripts/auto_fix_pr.mjs (ghFetch)

🚀 Description

Currently, external call resilience is inconsistent:

  • anthropic_client.mjs: single raw fetch, zero retry, zero timeout — any transient failure fails the entire run immediately.
  • groq_client.mjs: retries on HTTP 429 only (good pattern), but HTTP 5xx throws immediately with no retry, and there is no per-call timeout.
  • ghFetch in auto_fix_pr.mjs: no retry, no timeout — a single network error on any GitHub API call (diff fetch, label fetch, comment post) fails the entire run.

The fix is a single shared retryWithBackoff utility consumed by all three call sites.

🧩 Scope

In:

  • Create scripts/lib/retry.mjs exporting retryWithBackoff(fn, options).
  • Default configuration:
    • maxAttempts: 4
    • baseDelayMs: 200
    • maxDelayMs: 8000
    • timeoutMs: 15000 (per individual attempt)
    • jitter: true (±20% random spread on each delay)
  • Retryable status codes: 429, 500, 502, 503, 504.
  • Non-retryable status codes: 400, 401, 403, 404, 422 — throw immediately.
  • If the response includes a Retry-After header (or body hint), use that wait time instead of computed backoff (preserving groq_client.mjs existing behaviour).
  • Integrate into anthropic_client.mjs (wrap the existing fetch call).
  • Extend groq_client.mjs to pass 5xx through retryWithBackoff (the existing 429 loop can be simplified to use the same utility).
  • Apply to ghFetch in auto_fix_pr.mjs.

Out:

  • Changes to retry budgets for non-external operations.
  • Global pipeline performance optimisation.
  • Persistent cross-run state.

🧪 Acceptance criteria

  • Functional

    • HTTP 500, 502, 503, 504 responses are retried up to maxAttempts - 1 times before throwing.
    • HTTP 429 responses are retried using Retry-After wait time when present, or computed backoff otherwise.
    • HTTP 400, 401, 403, 404, 422 responses throw immediately without any retry attempt.
    • Network-level errors (connection reset, DNS failure) are treated as retryable and follow the same backoff schedule.
  • Edge cases

    • After maxAttempts (default: 4) total attempts, the last error is thrown — no further retries.
    • A single attempt that exceeds timeoutMs (default: 15 000 ms) is aborted and counted as a retryable failure.
    • Jitter ensures no two concurrent retries produce identical wait times.
    • A 429 with Retry-After: 60 waits exactly 60 seconds before the next attempt (regardless of backoff schedule).
  • Tests

    • Unit: retryWithBackoff retries exactly 3 times on 503 before throwing (4 total attempts).
    • Unit: retryWithBackoff throws immediately on 401 (1 total attempt, no wait).
    • Unit: backoff sequence for attempts 1–3 is approximately [200, 400, 800] ms before jitter.
    • Unit: timeoutMs triggers an AbortError that is counted as a retryable attempt.
    • Unit: Retry-After: 2 causes a ~2 000 ms wait on 429.
    • Regression: llm_client.mjs provider fallback is NOT triggered when anthropic_client.mjs throws on 401 (permanent error must propagate, not be absorbed).

⚙️ Constraints

  • No new runtime dependencies (use setTimeout + AbortController, both available in Node 20).
  • The utility must be synchronously importable (no top-level await).
  • All retry attempts must emit a structured log line via logger.mjs ({ msg: 'retry', attempt, statusCode, waitMs }).
  • Keep API minimal: retryWithBackoff(fn, options?) where fn is an async function returning Response.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestready-for-devIssue validated and ready for automated implementation

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions