← Back to PRs

#21514: fix(retry): make retryAsync abort-aware during backoff sleep

by amabito open 2026-02-20 01:35 View on GitHub →
size: S
## Summary `retryAsync` currently ignores `AbortSignal` entirely: when a signal fires mid-backoff the sleep runs to completion, then the next attempt starts (and succeeds or fails on its own). This means aborting a high-level operation does not promptly propagate through the retry loop. This PR wires `AbortSignal` into `retryAsync` with three guard points: 1. **Pre-attempt check** – throws immediately if the signal is already aborted before the call to `fn()`. 2. **isAbortError short-circuit** – if `fn()` itself throws an abort error the loop breaks without further retries. 3. **Abort-aware sleep** – replaces the bare `sleep(delay)` with the existing `sleepWithAbort(delay, signal)` primitive so an in-flight backoff sleep is cancelled as soon as the signal fires. ### Why this matters Without this change a cancelled operation can trigger up to `maxAttempts - 1` additional LLM round-trips (three by default) before the abort is observed. In a three-layer retry stack (per-call × per-step × auth-rotation) that multiplies quickly. ### API The change is fully backwards-compatible: `signal` is an optional field on the existing `RetryOptions` type, defaulting to `undefined` (current behaviour unchanged). ```ts // existing callers — no change required const result = await retryAsync(fn, { label: "my-op" }); // new: pass an AbortSignal to stop retrying on cancellation const result = await retryAsync(fn, { label: "my-op", signal: controller.signal }); ``` ### Implementation notes - Reuses `sleepWithAbort` from `src/infra/backoff.ts` (already in tree, already tested) – no new primitives. - Reuses `isAbortError` from `src/infra/unhandled-rejections.ts` – consistent with the rest of the codebase. - Three new tests cover: abort during sleep, pre-aborted signal, and normal retry path unchanged. ## Test plan - [x] `pnpm format:check` — 0 issues - [x] `pnpm tsgo` — 0 errors - [x] New tests: abort during sleep, pre-aborted signal, normal retry path - [x] All 11 tests in `src/infra/retry.test.ts` pass <!-- greptile_comment --> <h3>Greptile Summary</h3> Added `AbortSignal` support to `retryAsync` with three guard points: pre-attempt check, abort error short-circuit, and abort-aware sleep using the existing `sleepWithAbort` primitive. - Wires optional `signal` field into `RetryOptions` (fully backwards-compatible) - Checks `signal?.aborted` before each attempt and throws `AbortError` immediately - Short-circuits retry loop when `fn()` throws an abort error via `isAbortError` check - Replaces bare `sleep(delay)` with `sleepWithAbort(delay, signal)` to cancel in-flight backoff - Reuses existing `sleepWithAbort` from `src/infra/backoff.ts` and `isAbortError` from `src/infra/unhandled-rejections.ts` - Three new tests cover abort during sleep, pre-aborted signal, and normal retry unchanged - Implementation is clean and consistent with codebase patterns <h3>Confidence Score: 5/5</h3> - This PR is safe to merge with minimal risk - The implementation is well-designed with three clear guard points for abort handling. It reuses battle-tested primitives (`sleepWithAbort`, `isAbortError`) already in the codebase, maintains full backward compatibility (optional `signal` field, legacy number-based API unchanged), and includes comprehensive tests covering all three abort scenarios. The change is isolated to retry logic with no ripple effects. - No files require special attention <sub>Last reviewed commit: 0333880</sub> <!-- greptile_other_comments_section --> <!-- /greptile_comment -->

Most Similar PRs