refactor(fs-retry): consolidate file-retry loops behind withRetry - #526
Conversation
…hRetry Add generic withRetry/withRetrySync helpers to lib/fs-retry.ts (per-call maxAttempts, fixed or per-attempt backoff schedules, optional jitter, per-site retryable code sets defaulting to FILE_RETRY_CODES, onRetry hook; zero-delay retries schedule no timer) and migrate 14 hand-rolled retry loops onto them with per-site attempt counts, backoff schedules, retryable code sets, and error propagation preserved exactly: - lib/fs-retry.ts: withFileOperationRetry now delegates to withRetry - lib/config.ts: readFileSyncWithConfigRetry, getConfigFileMtimeMs, writeJsonFileAtomicWithRetry rename loop, unlinkConfigLockWithRetry, readConfigRecordForSave (the dead post-loop generic-message block was unreachable and is dropped) - lib/storage.ts: copyFileWithRetry, renameFileWithRetry - lib/quota-cache.ts: readCacheFileWithRetry, saveQuotaCache rename loop (local RETRYABLE_FS_CODES duplicate of FILE_RETRY_CODES removed) - lib/recovery/storage.ts: renameSyncWithRetry, safeUnlinkWithRetry (via withRetrySync busy-wait) - lib/codex-manager/commands/uninstall.ts: local withFileOperationRetry duplicate replaced by the shared lib/fs-retry.ts export (identical schedule and code set) Skipped (semantics that withRetry cannot express without contortion): - lib/config.ts withConfigFileLock acquisition loop: deadline-based wait with interleaved stale-lock takeover, not an attempt-bounded retry - lib/config.ts savePluginConfig ESTALE CAS loop: only the write is guarded; the re-read/re-merge runs between attempts outside the retryable region - lib/storage.ts renameTempToPath: sleeps after the final failed attempt before rethrowing (10..160ms), a schedule withRetry deliberately does not reproduce New unit coverage for withRetry/withRetrySync/withFileOperationRetry in test/fs-retry.test.ts (18 tests). No attempt counts or timings tuned; audit roadmap section 4.2. https://claude.ai/code/session_01XNtnkLbBiXZxfQQYLMpucB
|
Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits. |
|
Warning Review limit reached
More reviews will be available in 5 minutes and 57 seconds. Learn how PR review limits work. Your organization has run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (7)
✨ Finishing Touches🧪 Generate unit tests (beta)
✨ Simplify code
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
…Retry Review follow-up: the migration to withRetrySync collapsed the explicit ENOENT guard into a generic catch. Behavior was identical, but the already-gone and exhausted-retries cases are worth keeping separable as the seam for future telemetry. https://claude.ai/code/session_01XNtnkLbBiXZxfQQYLMpucB
Summary
Consolidates the divergent hand-rolled file-retry loops behind a single
withRetryhelper inlib/fs-retry.ts(the existing home ofFILE_RETRY_CODES), per audit roadmap §4.2 (docs/audits/AUDIT_2026-06-10.md, PR #522). Mechanical migration only — every call site keeps its exact attempt count, backoff schedule, jitter, retryable code set, and error propagation. Tuning, if any, belongs in a follow-up PR.Changes
New API in
lib/fs-retry.ts:backoffMsaccepts a number or a per-attempt function (covers the existing linear/exponential schedules);retryableCodesdefaults toFILE_RETRY_CODES; non-retryable errors rethrow immediately; exhaustion rethrows the final error unchanged.withFileOperationRetryis now a thin delegate.14 loops migrated across
lib/config.ts(×5),lib/storage.ts(×2),lib/quota-cache.ts(×2, including removing a local duplicate ofFILE_RETRY_CODES),lib/recovery/storage.ts(×2, sync),lib/codex-manager/commands/uninstall.ts(localwithFileOperationRetryduplicate deleted in favor of the shared one), andlib/fs-retry.tsitself.3 loops deliberately skipped (semantics don't fit attempt-bounded retry; forcing them would change behavior):
withConfigFileLockacquisition — deadline-based wait with interleaved stale-lock takeoversavePluginConfigESTALE CAS loop — re-read/re-merge runs between attempts outside the retryable regionstorage.tsrenameTempToPath— sleeps after the final failed attempt before rethrowing, a schedulewithRetryintentionally cannot expressNew tests:
test/fs-retry.test.ts(18 cases) — first-try success, retry-then-success, exhaustion rethrow, non-retryable immediate throw, fake-timer backoff schedules (including jitter and the shared 25/50/100/200/400ms ladder), zero-delay paths,onRetry, and the sync variant.Validation
npm run typechecknpx eslint <all touched files> --max-warnings=0test/storage.test.tsandtest/storage-recovery-paths.test.ts: failure lists diffed before (clean base) vs after — identical; all failures are the known container sandbox-EACCES environment issues documented indocs/audits/evidence/test-baseline-2026-06-10.txtRisk / Rollback
Single-commit mechanical migration; revert to roll back. The three skipped sites are unchanged on purpose and listed above for the next pass.
https://claude.ai/code/session_01XNtnkLbBiXZxfQQYLMpucB
Generated by Claude Code
note: greptile review for oc-chatgpt-multi-auth. cite files like
lib/foo.ts:123. confirm regression tests + windows concurrency/token redaction coverage.Greptile Summary
this pr consolidates 14 hand-rolled file-retry loops across 6 modules into a single
withRetry/withRetrySynchelper inlib/fs-retry.ts. the migration is mechanical: every call site's attempt count, backoff formula, retryable code set, and error-propagation contract is preserved.withRetry,withRetrySync,RetryOptions) replaces per-module duplicates;withFileOperationRetryis now a thin delegate with identical behavior.10 * 2 ** attempt(0-based) ↔10 * 2 ** (attempt - 1)(1-based), verified across every migrated site.getConfigFileMtimeMsand the best-effort-swallow pattern inunlinkConfigLockWithRetry.Confidence Score: 5/5
mechanical migration only — every backoff schedule, attempt count, retryable code set, and error-propagation contract has been faithfully preserved across all 14 migrated sites; no new logic is introduced.
all 0-based-to-1-based backoff conversions are verified correct; ENOENT and non-retryable errors still escape immediately at every call site; the three skipped sites are unchanged and explicitly documented; 375/375 migrated-module tests pass; the only open items were raised in prior review cycles and are all non-blocking.
no files require special attention; lib/fs-retry.ts is the new shared owner of retry semantics and is well covered by the new test suite.
Important Files Changed
Flowchart
%%{init: {'theme': 'neutral'}}%% flowchart TD A["caller: operation()"] --> B["withRetry / withRetrySync"] B --> C{"attempt\nsucceeds?"} C -- yes --> D["return result"] C -- no --> E{"isRetryableError?\n(code in retryableCodes)"} E -- no --> F["rethrow immediately"] E -- yes --> G{"attempt >=\nmaxAttempts?"} G -- yes --> F G -- no --> H["onRetry?.(error, attempt)"] H --> I["computeDelayMs\n(backoffMs + jitter)"] I --> J{"delayMs > 0?"} J -- no --> B J -- yes --> K["async: await sleep(ms)\nsync: busy-wait loop"] K --> B subgraph callers["migrated call sites"] L["lib/config.ts ×4"] M["lib/storage.ts ×2"] N["lib/quota-cache.ts ×2"] O["lib/recovery/storage.ts ×2"] P["lib/codex-manager/uninstall.ts"] end callers --> BComments Outside Diff (2)
test/fs-retry.test.ts, line 929-993 (link)withRetrySyncbusy-wait path untestedthe
withRetrySyncsuite only exercisesbackoffMs: 0andbackoffMs: 1; neither path enters thewhile (Date.now() < waitUntil)busy-wait. on windows,Date.now()resolution can be ~15ms, so a test that passesbackoffMs: 10with a spy onDate.now()(or a real-elapsed assertion) would give confidence that the spin loop terminates correctly and actually waits the expected budget. the production callers inlib/recovery/storage.tsuse 10–40ms schedules, which makes this the highest-risk uncovered path in the new helper.Prompt To Fix With AI
lib/config.ts, line 270-310 (link)the PR description claims "every call site keeps its exact … error propagation." in
readConfigRecordForSave, when all 5 attempts on a retryable code (EBUSY/EPERM/EAGAIN) are exhausted and the old loop fell through,logConfigWarnOncereceived"Failed to read config from ${configPath}."(no original error detail). the new catch block formats it as"Failed to read config from ${configPath}: ${error.message}". this is a strictly more useful message, but it is a behavioral delta from the "mechanical only" claim and could surprise snapshot tests or log parsers that expect the old format.Prompt To Fix With AI
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Reviews (2): Last reviewed commit: "refactor(recovery): keep the ENOENT bran..." | Re-trigger Greptile