Skip to content

Cap Retry-After delay and stop pooling broken upload URLs after hard failures - #32

Open
b2-jared wants to merge 3 commits into
Backblaze:masterfrom
b2-jared:fix/blazer-retry-hang-followups
Open

Cap Retry-After delay and stop pooling broken upload URLs after hard failures#32
b2-jared wants to merge 3 commits into
Backblaze:masterfrom
b2-jared:fix/blazer-retry-hang-followups

Conversation

@b2-jared

Copy link
Copy Markdown

Fixes #31.

Two small, related follow-ups to #24's retry-count fix, found while root-causing a customer-reported ~80 hour hang in a restic B2 backup (small locks/ object upload).

1. Cap the server-supplied Retry-After delay

base.Backoff() converted the Retry-After header straight into a time.Duration with no upper bound. #24 bounded the retry count, but a single retry-after-derived sleep could still be arbitrarily long. This clamps it to the same 30s ceiling internal/retry.Backoff already uses for its own exponential-backoff fallback.

2. Don't return a broken upload URL to the shared pool after a hard failure

Writer.simpleWriteFile()'s defer unconditionally returned the upload URL/token to the bucket's shared urlPool, even when the function was about to return a hard, non-retryable error. Since the pool has no liveness check, a subsequent unrelated Writer on the same bucket could pull that broken URL back out via getUploadURL() and immediately hit the same failure. Now it's only pooled on success.

Testing

  • New base/base_test.go: table-driven TestBackoff covering pass-through, at-cap, and over-cap Retry-After values.
  • New tests in b2/b2_test.go: TestFailedUploadDoesNotPoolURL (hard failure → pool stays empty) and TestSuccessfulUploadPoolsURL (happy path still pools normally).
  • go build ./..., go vet ./base/... ./b2/..., and go test ./base/... ./b2/... -short all pass (integration tests skip as expected without B2 credentials).

b2-jared and others added 3 commits July 29, 2026 19:06
Writer.simpleWriteFile unconditionally returned its upload URL/token to
the bucket's urlPool on exit via a bare defer, even when retry.Do had
exhausted retries and returned a hard error. Since urlPool.get()/put()
do no liveness check on stored entries, an unrelated subsequent Writer
on the same bucket could pull that broken upload URL back out of the
pool and immediately fail the same way (e.g. a small lock-file write
right after a large upload's non-retryable failure on the same
bucket).

Guard the pooling with a boolean success flag set only right before
simpleWriteFile's final nil return, so a failed upload's last-used URL
(reassigned to a fresh one on each retry.OnRetry) is discarded instead
of pooled.

Adds TestFailedUploadDoesNotPoolURL, which forces a hard failure via
the existing testError{reupload:true, maxReuploads:0} fake and asserts
urlPool.get() returns nil afterward, and
TestSuccessfulUploadPoolsURL, which confirms the happy path still
pools the URL as before.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
PR Backblaze#24 (internal/retry package) bounded the retry loop's iteration count,
but base.Backoff() still converts the B2 server's Retry-After header
straight into a time.Duration with no upper limit. A large or malformed
Retry-After value (from B2 or a misbehaving proxy) could still cause a
single very long sleep, undermining that bounded-retry-count fix.

Clamp the result to 30 seconds, matching the ceiling internal/retry.Backoff
already uses for its own exponential-backoff fallback.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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.

Retry fix (#18/#24) merged but never released — v0.7.2 still fully exposed; two related gaps remain

1 participant