Skip to content

feat(load): exit 75 and report retryable when a rerun may fix every failure - #183

Merged
kvlonge merged 2 commits into
mainfrom
feat/retryable-exit-codes
Nov 12, 2026
Merged

kvlonge merged 2 commits into
mainfrom
feat/retryable-exit-codes

Conversation

@kvlonge

@kvlonge kvlonge commented Nov 12, 2026

Copy link
Copy Markdown
Contributor

plan, apply, load and copy now tell a scheduler whether to retry. They exit 75 (EX_TEMPFAIL) instead of 1 when every failure is one a rerun may get past. Under --json, retryable says the same thing on the run, on each table, in plan and in the {"error": …} document. retryable is true exactly when the exit code is 75. Everything else is unchanged: 0 for success, 1 for any lasting failure, 2 for usage and 130 for a forced stop.

What already worked before this change (CHI-1241 confirmation):

  • sl.http paging, auth and retries: six paginators, bearer, API-key, basic and OAuth2 auth (client credentials and refresh token, with renewal on expiry or a 401), and retries on transport errors, 429 and 5xx that honour Retry-After. packages/sqlload/tests/test_http_jobs.py covers these (retries-429-and-503, the paginator cases and the OAuth renewal tests).
  • load --json report: a stable document versioned by schema_version: 1, documented on the load page, with ok matching exit 0.
  • Safe reruns: batched loads keep what they committed and resume from the destination's cursor (test_batched_loads.rs). Logged tables resume from their last checkpoint (test_logged_state.py). replace swaps in only at the end.

What was missing: a scheduler couldn't tell a dropped connection from a broken configuration, because both exited 1.

Classification (LoaderError::is_retryable):

  • Retryable:
    • A dropped, refused or timed-out connection, to a target or to a source while planning or loading.
    • A 429, 502, 503 or 504, or a 500 with no code or a transient ClickHouse or BigQuery code, from a warehouse.
    • An sl.http job whose retries ran out on 429, 5xx or transport errors.
    • The Postgres SQLSTATEs 08000, 08003, 08006, 53200, 53300, 57P01–57P03, 40001, 40P01 and 55P03. 08P01 is excluded because PgBouncer also sends it for an unknown database.
    • A table another run is loading.
    • A Python table that raised ConnectionError or TimeoutError.
  • Not retryable: configuration problems, plan errors (other than an unreachable source), credentials, permissions, data errors, 4xx responses, requests that can't be built, and answers that aren't the JSON expected.
  • Object stores count only for a connection that dropped or timed out. object_store 0.14 doesn't export the error that carries a status after its own retries.
  • A target failure while a source streams keeps its classification, because read_source remembers the publisher's error. CDC loads without --follow report it too.

Tests:

  • Rust: a table of 22 errors classified retryable or not, and exit 75 versus 1 through the command path: a refused target, an unreachable source on its own (75) and beside a lasting plan error (1), an invalid configuration (1).
  • Python, end to end:
    • A logged, checkpointed table and a cursor table both fail on the network: exit 75, retryable: true. A rerun loads every row exactly once.
    • A network failure beside a ValueError: exit 1, and each table says which kind it is.
    • An sl.http 500 after its retries: exit 75. A 404: exit 1.

Docs: an exit code table entry and a "Retrying failed runs" section in cli/index.mdx, plus the new fields in cli/load.mdx, marked "From 0.34.0 (not released yet)".

Checks: make check (fensu 0 faults), make test-unit, and make test-python (340 passed). Reviewed once, plus a follow-up check of the fixes.

…ailure

plan, apply, load and copy exit 75 (EX_TEMPFAIL) instead of 1 when every
failure is one a rerun may get past: a dropped, refused or timed-out
connection, throttling or server errors that outlasted their retries, a
transient Postgres SQLSTATE, a table another run is loading, or a Python
table that raised ConnectionError or TimeoutError. --json reports say so
with retryable on the run, each table, the plan and the error document.
…object stores

Target failures while a source streams keep their classification; CDC
loads report it; Postgres retries only exact transient SQLSTATEs (not
08P01, which PgBouncer sends for an unknown database); unbuildable
requests and answers that are not the JSON expected are protocol errors;
object store connect and timeout errors count; logged-state keeps exit 1.
@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying sqlload with  Cloudflare Pages  Cloudflare Pages

Latest commit: 6098824
Status: ✅  Deploy successful!
Preview URL: https://427ee4ae.sqlload.pages.dev
Branch Preview URL: https://feat-retryable-exit-codes.sqlload.pages.dev

View logs

@kvlonge
kvlonge merged commit 74d1abe into main Nov 12, 2026
13 checks passed
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.

1 participant