Classify transient S3 failures as retryable - #18
Open
ethanstoner wants to merge 1 commit into
Open
Conversation
The S3 backend turned every failure that was not a precondition failure
into `StoreError::Other`, so nothing on the S3 write path could ever
produce `StoreError::Retryable`. GCS classifies the same conditions —
`gcs::is_retryable` covers Unavailable/DeadlineExceeded/ResourceExhausted/
Internal/Aborted, HTTP 503/504/429/500, and connect/IO faults — which made
this a "GCS only" behaviour of the kind AGENTS.md rules out.
Two readers of `StoreError::is_retryable` were affected:
- `coord::cas_update`, the manifest CAS that is the only commit point.
On `Retryable` it sleeps with jittered backoff and re-reads; on
anything else it returns `CoordError::Store` and gives up. A throttled
manifest PUT therefore failed the push outright on S3. The SDK's own
retries (standard mode, three attempts) run underneath this and do not
replace it — what reaches walgit is what the SDK could not absorb.
- `smart::wal_err`, which maps retryable store errors to 503 and
everything else to 500. On S3 a transient bucket fault reached the git
client as a hard 500 instead of the 503 its comment describes.
Add `is_retryable` for `SdkError` — dispatch failures and timeouts, the
transient AWS error codes, and the transient HTTP statuses for
S3-compatible stores that do not use AWS codes — and route every
`SdkError` site through it: put, list, head, delete, and the multipart
create/upload/copy/complete/abort paths.
Tested with a fake S3 bound on an ephemeral port, driving real `SdkError`
values through the classifiers with SDK retries disabled: throttling, a
server fault, an unrecognised transient status, and an unreachable
endpoint are retryable; access denied is not; a failed precondition stays
a failed precondition.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
AGENTS.md§5 says S3 and GCS are both first class and that "GCS only" is a bug. This is one.What is wrong
classify_put_errormaps everything that is not a precondition failure toStoreError::Other, andclassify_list_erroris unconditionallyOther.head,deleteand the five multipart paths do the same. The result is that no S3 operation can produceStoreError::Retryable— the only exception is the presigned-GET path (s3.rs:195), which classifies 5xx/429 correctly because it reads the raw HTTP status.GCS does classify them, at
gcs.rs:1186: gRPCUnavailable/DeadlineExceeded/ResourceExhausted/Internal/Aborted, HTTP 503/504/429/500, plus connect/IO/transient faults.Why it matters
Two places read
StoreError::is_retryable:coord::cas_update— the manifest CAS, which per Principle II is the only commit point. OnRetryableit sleeps with jittered backoff and re-reads; on anything else it returnsCoordError::Storeand gives up. So a throttled manifest PUT fails the push outright on S3, while the same throttle on GCS is absorbed. S3 partitions by key prefix, so a hot manifest key under a monorepo push rate is exactly the shape that draws503 SlowDown.smart::wal_err— maps retryable store errors to503 ServiceUnavailableand everything else to500 Internal. Its comment reads "A store call that timed out / was throttled: fail fast, let the client retry (never hang the request on the bucket)" — on S3 that branch is unreachable, so the git client gets a hard 500 instead.The SDK's own retries sit underneath this, not in place of it. I confirmed against a fake service that the client makes three attempts and then surfaces
SdkError::ServiceErrorwithcode: "SlowDown"and raw status 503 — which walgit then discarded as permanent.The change
is_retryable(&SdkError<E>): dispatch failures and timeouts (no response at all), the transient AWS error codes, and the transient HTTP statuses — the status check matters because rustfs and other S3-compatible stores do not all use AWS error codes. EverySdkErrorsite routes through it: put, list, head, delete, and multipart create/upload/upload-copy/complete/abort.Precondition handling is untouched, and there is a regression test pinning it.
Tests
A fake S3 on an ephemeral port, driving real
SdkErrorvalues through the classifiers with SDK retries disabled so each test observes exactly the error the service produced. Hermetic, no bucket, no network, runs in 0.01s.SlowDown), server fault (500InternalError), unrecognised transient status (504), and an unreachable endpoint →RetryableOtherPreconditionFailed, unchangedReverting just the two classifier arms turns the five behavioural tests red, so they gate the fix rather than describing it.
Verification
cargo test -p walgit-storejust test-s3(contract vs rustfs in Docker)s3_contractpassescargo test -p walgit-server --test simcargo fmt --checkwalgit-storemain→ 39 on this branch; none new, oneneedless_pass_by_valueremovedThree gates fail identically on pristine
main(6d8fa54) in my environment, so I could not use them as signal and did not try to fix them here:just clippyfails atwalgit-server/build.rson twoclippy::expect_usedin the build script — the strict gate from Add a strict clippy gate to the justfile and CI #15 does not spare build scripts.walgit-proto's generated code adds 27 more under-D warnings.just test— threesetup::testsinstaller tests fail onmain.just e2e— 7 fail onmain, 6 on this branch (the difference isfetch_from_front_that_serves_the_base_remotely, whichAGENTS.mddocuments as ~1-in-3 flaky). My git is 2.43; the README asks for ≥2.46, which likely explains these.Happy to split the multipart sites out if you would rather keep the diff to the CAS path.