Repository navigation
Conversation
83eb1cf to
b3e1cac
Compare
|
Ran into this again in prod — a transient 503 on a The default Rebased on main. PTAL at your earliest convenience — happy to split the |
| })) | ||
| defer srv.Close() | ||
|
|
||
| t.Setenv("STORAGE_EMULATOR_HOST", srv.Listener.Addr().String()) |
There was a problem hiding this comment.
What does this STORAGE_EMULATOR_HOST mean? Maybe there's a constant for it defined somewhere in some module that provides more context?
There was a problem hiding this comment.
It's the GCS SDK's documented emulator override: https://cloud.google.com/storage/docs/emulator. It makes it so that the GCS client library points at a local endpoint instead of the real API. It's already used in other places in this file. There's no constant for it today, happy to add a constant but didn't want to touch more lines in this PR.
b366179 to
74f0d2c
Compare
The default RetryIdempotent policy doesn't cover Delete without an IfGenerationMatch precondition, so a single 503 from GCS bubbles up to the caller. Switch Delete to RetryAlways. Also apply MaxRetries (default 3 when unset) to all operations so the cap is no longer opt-in. See https://docs.cloud.google.com/storage/docs/retry-strategy. Signed-off-by: Luis Larco <langelo91@gmail.com>
Signed-off-by: Luis Larco <langelo91@gmail.com>
- Drop the hardcoded default from the MaxRetries doc comment and the CHANGELOG entry; both duplicated DefaultConfig and would drift from it. - t.Cleanup over defer, and t.Context() over context.Background(), in the new retry test. Signed-off-by: Luis Larco <langelo91@gmail.com>
74f0d2c to
29beab5
Compare
| // https://docs.cloud.google.com/storage/docs/retry-strategy for what | ||
| // counts as transient and how the SDK handles backoff. | ||
| maxAttempts := gc.MaxRetries | ||
| if maxAttempts == 0 { |
There was a problem hiding this comment.
If maxAttempts can only be 0 or above then maybe it could be a uint?
There was a problem hiding this comment.
I think the idiomatic/standard convention in golang is to use int for this kind of variables while keeping uint and other variations of ints for specific operations (eg. bitwise operations, bitmasking, etc) and let the code logic enforce/validate the the value explicitly.
Also, golang does not have implicit type casting, so if for whatever reason we wanted to compare this value to another one line for i := 0; i < maxAttempts; i++, we would actually have to typecast it: for i := 0; i < int(maxAttempts); i++.
I found an interested thread about this topic: https://stackoverflow.com/questions/63105394/in-go-when-should-you-use-uint-vs-int
And also this quote "When you need an integer value you should use int unless you have a specific reason to use a sized or unsigned integer type" from https://go.dev/tour/basics/11.
I looked at the rest of the repo, and there are many cases where int is being used for non-negative numbers (eg. counters) rather than unit, so I figured I'd stick to majority of the code style across the repo.
Either way, if you want me to switch it to an uint, let me know and I'll make the change
| // Delete removes the object with the given name. | ||
| // Delete removes the object with the given name. RetryAlways overrides the | ||
| // default RetryIdempotent policy, which would otherwise exclude Delete because | ||
| // we don't pass IfGenerationMatch. |
There was a problem hiding this comment.
IfGenerationMatch is GCS's way of handling race conditions for operations that would otherwise require atomicity (see https://docs.cloud.google.com/storage/docs/request-preconditions#precondition_criteria). Since we are not passing such configuration, the default policy is to not retry 5xx errors for delete operations.
I've updated the comment to explain this inline rather than just naming it.
The previous wording named IfGenerationMatch without saying what it is, so the reason RetryAlways is needed wasn't self-contained. Signed-off-by: Luis Larco <langelo91@gmail.com>
The default
RetryIdempotentpolicy doesn't cover Delete without anIfGenerationMatchprecondition, so a single 503 from GCS bubbles up to the caller. SwitchDeletetoRetryAlways. Also applyMaxRetries(default 3 when unset) to all operations so the cap is no longer opt-in.See https://docs.cloud.google.com/storage/docs/retry-strategy.
Changes
Deletenow callsbkt.Object(name).Retryer(storage.WithPolicy(storage.RetryAlways)).Delete(ctx). WithoutRetryAlways, the SDK skips retries entirely for plain Delete because the defaultRetryIdempotentpolicy treats unconditional Delete as non-idempotent.newBucketnow always setsWithMaxAttempts(MaxRetries)on the bucket handle. Previously this was only applied when the user setmax_retries > 0. The ObjectHandle inherits the bucket's retry config, so the new Delete retry inherits the cap.DefaultConfig.MaxRetries = 3sets the new default.newBucketfalls back to this whengc.MaxRetries == 0, covering both the YAML config path (via DefaultConfig) and directNewBucketWithConfigcallers.Config.MaxRetriesdoc updated.MaxRetries: 1to disable retries still works.Verification
Added
TestBucket_Delete_RetriesOnTransient5xxinproviders/gcs/gcs_test.go. Stands up anhttptest.NewServerthat returns 503 on the first 2 DELETE requests and 204 on the third. Assertsbkt.Deletereturns nil and the server saw exactly 3 attempts.Context: this surfaced as recurring crashes in the Thanos compactor's
BlocksCleaner.DeleteMarkedBlockswhen GCS returned a transient 503 on a single delete during the cleanup pass. Discussion: thanos-io/thanos#8838 (now closed in favor of fixing the issue here in objstore).