Skip to content

fix(dr): retry transient R2 5xx on backup S3 client - #1090

Merged
kody-bot merged 3 commits into
mainfrom
cursor/sentry-triage-kody-cloudflare-7643617296-5f24
Jul 31, 2026
Merged

kody-bot merged 3 commits into
mainfrom
cursor/sentry-triage-kody-cloudflare-7643617296-5f24

Conversation

@kentcdodds

@kentcdodds kentcdodds commented Jul 31, 2026 •

Copy link
Copy Markdown
Owner

Summary

Sentry KODY-CLOUDFLARE issue 7643617296: DR backup PUT failed for staging/…/exporter/progress.json: HTTP 500.

In the failing production tick, two conditional progress.json PUTs succeeded (HTTP 200), then the next PUT in the same exportArtifactsPhase got an R2 S3 API HTTP 500. That aborted the entire dr_export scheduled lane. Progress already written is durable, and the next cron tick resumes — but the tick wastes its remaining budget and pages Sentry for a platform blip.

Fix: createDrBackupS3Client now retries transient HTTP statuses (429, 500, 502, 503, 504) and transport errors with exponential backoff (same shape as D1 lock retry). 412 precondition failures stay non-retryable so overlapping progress ownership still skips cleanly.

Merge gate: touches backup-control-plane / disaster-recovery surface — leaving open for review; do not auto-merge.

Test plan

  • npx vitest run packages/worker/src/dr/backup-s3.node.test.ts
  • CI npm run validate equivalent (Validate aggregate green on prior head; re-running after getBytes test)
System recap — extends existing primitives (medium risk)

Mode: recap · Base: main @ 6d0dc439 · Head: 66b13836

Classification: extends — changes backup-control-plane R2 S3 client behavior to retry transient platform failures before failing the nightly exporter tick.

Primitives touched

Primitive Group Impact
backup-control-plane storage extends — retry 429/5xx + transport blips on DR backup S3 HEAD/GET/PUT

System map

Nightly dr_export cron persists progress.json via the signed R2 S3 client; transient R2 500s are retried inside that client instead of failing the scheduled lane.

Legend: green = composes (wiring only) · amber = extended by this PR · red = new primitive · gray = context.

flowchart LR
	scheduled["Worker scheduled<br/>dr_export lane"]:::untouched
	exporter["runDrExportTick<br/>persistProgress"]:::untouched
	s3Client["DrBackupS3Client<br/>signedFetchWithRetry"]:::extended
	r2["R2 S3 API<br/>progress.json"]:::untouched
	scheduled --> exporter --> s3Client --> r2
	classDef extended fill:#9a6700,color:#fff
	classDef untouched fill:#57606a,color:#fff
Loading

Before / after

Before: a single R2 PutObject HTTP 500 during persistProgress threw, aborted the dr_export lane, and opened a Sentry issue even when prior puts in the same tick succeeded.

After: the S3 client retries 429/5xx and transport errors with short backoff; exhausted failures still throw (and still alert). Conditional 412 conflicts remain immediate non-retries.

Risk

Medium — disaster-recovery write path. Retries are idempotent for conditional puts (phantom success → next attempt 412 → existing skip path). Left open for human review; not auto-merged.

Test gaps

No live R2 integration test; coverage is unit-level with an injected fetch stub (HEAD/GET text/GET bytes/PUT, plus non-retry of 412).

Open in Web Open in Cursor 

Summary by CodeRabbit

  • Bug Fixes

    • Improved reliability of S3/R2 backup operations by automatically retrying transient failures.
    • Added exponential backoff for retry attempts across upload, metadata, and download requests.
    • Preserved immediate handling for non-retryable precondition failures.
    • Improved handling of failed responses and exhausted retry attempts.
  • Tests

    • Added comprehensive coverage for retry behavior, transport failures, response metadata, and required configuration.

Nightly exporter ticks were aborting on a single R2 PutObject HTTP 500
while persisting progress.json. Retry 429/5xx and transport blips with
backoff so conditional progress writes can complete within the tick.

Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
@coderabbitai

coderabbitai Bot commented Jul 31, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The DR S3 client now retries transient HTTP and transport failures with configurable exponential backoff. HEAD, GET, and PUT operations use the retry path. Tests cover status classification, retries, exhausted failures, response data, conditional headers, and configuration parsing.

Changes

DR S3 retry handling

Layer / File(s) Summary
Retry policy and execution
packages/worker/src/dr/backup-s3.ts
The client classifies transient statuses, drains retryable response bodies, retries transport failures, and applies configurable exponential backoff.
Operation integration and validation
packages/worker/src/dr/backup-s3.ts, packages/worker/src/dr/backup-s3.node.test.ts
HEAD, text GET, byte GET, and PUT use the retry path. Tests cover retry behavior, exhausted failures, precondition handling, response data, headers, and configuration parsing.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant DRBackupS3Client
  participant SignedFetch
  participant S3R2
  DRBackupS3Client->>SignedFetch: Send signed HEAD, GET, or PUT request
  SignedFetch->>S3R2: Execute request
  S3R2-->>SignedFetch: Return transient response or transport failure
  SignedFetch->>SignedFetch: Drain response and wait with exponential backoff
  SignedFetch->>S3R2: Retry request
  S3R2-->>SignedFetch: Return final response
  SignedFetch-->>DRBackupS3Client: Return response or throw after exhaustion
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: adding retries for transient R2 failures in the backup S3 client.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch cursor/sentry-triage-kody-cloudflare-7643617296-5f24

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@kody-bot
kody-bot marked this pull request as ready for review July 31, 2026 02:40
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/worker/src/dr/backup-s3.node.test.ts (1)

90-118: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a getBytes retry test.

getBytes now uses signedFetchWithRetry, but this test covers only head and getText. Add a transient-response case that asserts the retry count and the returned byte sequence.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/worker/src/dr/backup-s3.node.test.ts` around lines 90 - 118, Add a
getBytes retry scenario alongside the existing head and getText cases, using a
mocked transient 503 followed by a successful response; assert the fetch mock is
called twice and verify getBytes returns the expected byte sequence.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/worker/src/dr/backup-s3.ts`:
- Around line 105-108: Namespace every backup-s3 read and write operation by a
validated userId before signing requests: update the client or operation
boundary around signedFetch and its HEAD, GET, and PUT callers to derive
user-scoped object keys rather than signing the raw key. In
packages/worker/src/dr/backup-s3.ts lines 105-108, apply this requirement to
signedFetch and all paths using it; in
packages/worker/src/dr/backup-s3.node.test.ts lines 9-14, update the fixture and
assert distinct user IDs generate isolated object keys.

---

Nitpick comments:
In `@packages/worker/src/dr/backup-s3.node.test.ts`:
- Around line 90-118: Add a getBytes retry scenario alongside the existing head
and getText cases, using a mocked transient 503 followed by a successful
response; assert the fetch mock is called twice and verify getBytes returns the
expected byte sequence.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 492a8303-b081-4cf2-8454-6a32a4e72956

📥 Commits

Reviewing files that changed from the base of the PR and between 6d0dc43 and 8074740.

📒 Files selected for processing (2)
  • packages/worker/src/dr/backup-s3.node.test.ts
  • packages/worker/src/dr/backup-s3.ts

Comment on lines 105 to 108
async function signedFetch(key: string, init?: RequestInit) {
const request = await aws.sign(objectUrl(config, key), init)
return fetchImpl(request)
}

@coderabbitai coderabbitai Bot Jul 31, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

Namespace DR backup object access by userId.

The client signs the caller-provided key directly, and its API has no userId. HEAD, GET, and PUT therefore access one shared bucket keyspace without tenant isolation.

  • packages/worker/src/dr/backup-s3.ts#L105-L108: require a validated userId at the client or operation boundary and derive the object key from that namespace before signing.
  • packages/worker/src/dr/backup-s3.node.test.ts#L9-L14: update the fixture and add assertions that distinct user IDs produce isolated object keys.

As per coding guidelines, “every read and write path must be scoped by userId.”

📍 Affects 2 files
  • packages/worker/src/dr/backup-s3.ts#L105-L108 (this comment)
  • packages/worker/src/dr/backup-s3.node.test.ts#L9-L14
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/worker/src/dr/backup-s3.ts` around lines 105 - 108, Namespace every
backup-s3 read and write operation by a validated userId before signing
requests: update the client or operation boundary around signedFetch and its
HEAD, GET, and PUT callers to derive user-scoped object keys rather than signing
the raw key. In packages/worker/src/dr/backup-s3.ts lines 105-108, apply this
requirement to signedFetch and all paths using it; in
packages/worker/src/dr/backup-s3.node.test.ts lines 9-14, update the fixture and
assert distinct user IDs generate isolated object keys.

Source: Coding guidelines

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not applicable here: this client is the platform disaster-recovery backup path (backup-control-plane), which intentionally writes a shared staging/sealed backup keyspace for the whole deployment. Per-user isolation applies to user-owned storage/MCP paths, not this operator backup bucket. Adding userId namespacing would break the exporter/restore contract.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Skipped: comment is from another GitHub bot.

@github-actions

github-actions Bot commented Jul 31, 2026 •

Copy link
Copy Markdown
Contributor

🔎 Preview deployed: https://kody-pr-1090.kody-a99.workers.dev

Worker: kody-pr-1090
D1: kody-pr-1090-db
KV: kody-pr-1090-oauth-kv

Mocks:

Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
@kody-bot
kody-bot merged commit 30b51c3 into main Jul 31, 2026
10 checks passed
@kody-bot
kody-bot deleted the cursor/sentry-triage-kody-cloudflare-7643617296-5f24 branch July 31, 2026 03:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants