SNOW-3745081: fix retry in GCS storage client#2688
Merged
sfc-gh-dszmolka merged 5 commits intoJul 7, 2026
Conversation
sfc-gh-rkowalski
requested changes
Jul 7, 2026
Contributor
Author
E2E Tests Performed After Review-Comment ChangesAll tests ran against live Snowflake account located in GCS (London, europer-west2) using the fixed JAR built from branch Retry / mitmproxy tests —
|
| # | Strategy | Scenario | Outcome |
|---|---|---|---|
| 1 | GCS Default | mitmproxy: connection refused on every attempt | 5 retries exhausted, error surfaced to caller |
| 2 | GCS Default | mitmproxy: 503 on attempts 1–3, pass on attempt 4 | Retried successfully, upload completed on 4th attempt |
| 3 | GCS AWS SDK | mitmproxy: connection refused on every attempt | Initially failed after 1 attempt (exposed findFirstCauseOfType bug in AWS handler); fixed and re-run → 5 retries exhausted, error surfaced |
| 4 | GCS AWS SDK | mitmproxy: 503 on attempts 1–3, pass on attempt 4 | Fixed and re-run → retried successfully, upload completed on 4th attempt |
OVERWRITE tests — PUT @~, direct connection (no proxy)
| # | Strategy | Scenario | Outcome |
|---|---|---|---|
| 5 | GCS AWS SDK | OVERWRITE=TRUE, new file |
Uploaded successfully to stage |
| 6 | GCS AWS SDK | OVERWRITE=FALSE, file already on stage |
Upload skipped by filterExistingFiles |
| 7 | GCS AWS SDK | OVERWRITE=FALSE, file not on stage |
Uploaded successfully |
| 8 | GCS Default | OVERWRITE=TRUE, new file |
Uploaded successfully to stage |
| 9 | GCS Default | OVERWRITE=FALSE, file already on stage |
Upload skipped by filterExistingFiles |
| 10 | GCS Default | OVERWRITE=FALSE, file not on stage |
Uploaded successfully |
Download tests — GET, direct connection (no proxy)
| # | Strategy | Scenario | Outcome |
|---|---|---|---|
| 11 | GCS Default | GET existing file | Downloaded successfully, 0 retries |
| 12 | GCS Default | GET non-existing file | Server returned empty file list; no GCS call made, statement completed cleanly |
| 13 | GCS Default | GET existing file, local copy already present | MD5 from listObjects response matched local file; download marked SKIPPED |
| 14 | GCS AWS SDK | GET existing file | Downloaded successfully, 0 retries |
| 15 | GCS AWS SDK | GET non-existing file | Server returned empty file list; no GCS call made, statement completed cleanly |
| 16 | GCS AWS SDK | GET existing file, local copy already present | HEAD request returned 200 OK; digest compared, file re-downloaded |
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 subscribe to this conversation on GitHub.
Already have an account?
Sign in.
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.
Problem
GCS uploads silently gave up after a single failure despite
putgetmaxretriesbeing configured(default: 25). Customers saw the driver throw immediately on the first HTTP 503 ("We encountered
an internal error. Please try again.") with no retries.
The bug affects both GCS access strategies:
GCSDefaultAccessStrategy— the GCS Java SDK path, which is the default when using internalGCS stages with an access token.
GCSAccessStrategyAwsSdk— the S3-compatible path, used whenSNOWFLAKE_GCS_FORCE_VIRTUAL_STYLE_DOMAINS=true.Root cause
SnowflakeGCSClient.upload()delegates the actual cloud SDK call to an inner private method,uploadWithDownScopedToken(). When the SDK raises an exception (e.g. on HTTP 503), the innermethod catches it and always re-throws it wrapped inside a
SnowflakeSQLLoggedExceptionbefore control returns to the outer retry loop.
The outer retry loop calls the strategy's
handleStorageException()with this wrapped exception.Both handlers only recognised the SDK exception when it appeared as the direct argument, not
when it was buried inside a wrapper:
The exception chains reaching the outer handler are:
A similar bug was fixed for Azure in a previous commit; this change applies the same pattern
to both GCS strategies.
Changes
1.
SnowflakeUtil.java— shared cause-chain helperAdded a generic
findFirstCauseOfType(Throwable ex, Class<T> type)utility method that walksthe full cause chain and returns the first exception of the given type, or
nullif none isfound. Both GCS strategies use this instead of hand-written traversal loops, keeping them
consistent with each other and with the Azure fix pattern.
2.
GCSDefaultAccessStrategy.java— bug fix + defensive improvementsWhy this was needed:
handleStorageExceptioncheckedex instanceof StorageExceptionto decide whether to retry.Because
uploadWithDownScopedTokenalways wraps the SDK exception inSnowflakeSQLLoggedException,the
instanceofcheck always failed and the method returnedfalse— no retry was everattempted, regardless of
putgetmaxretries.What changed:
SnowflakeUtil.findFirstCauseOfType(ex, StorageException.class)so wrapped exceptions are detected at any nesting depth.UPLOAD/DOWNLOADthrow immediately (mirrorsS3ErrorHandler.isClientException400Or404); scoped to upload/download only to preserve retry behaviour forcompareRemoteFiles404s (SNOW-14521 race condition).StorageExceptionin chain) are now re-thrown instead of silently returningfalse;InterruptedExceptionandSocketTimeoutExceptionstill returnfalseas before.logger.errorcalls passexas the final argument (SLF4J convention), removing the previous duplicate stack-trace log line.3.
GCSDefaultAccessStrategyTest.java(new file) — regression tests for the fixThree unit tests covering
handleStorageException:testHandleStorageExceptionDirect503BelowMaxRetriesStorageException(503), below max retriestestHandleStorageExceptionWrapped503BelowMaxRetriesRuntimeException→StorageException(503), below max retriestestHandleStorageExceptionWrapped503OverMaxRetriesRuntimeException→StorageException(503), over max retriesSnowflakeSQLException4.
GCSAccessStrategyAwsSdk.java— same wrapping bug fixed + SDK retry alignmentWhy this was needed:
GCSAccessStrategyAwsSdk.handleStorageExceptiononly walked one level deep(
ex.getCause() instanceof SdkException). BecauseuploadWithDownScopedTokenwraps the SDKexception inside
CompletionExceptionwhich is in turn wrapped inSnowflakeSQLLoggedException,the one-level check missed it and returned
false— no retry was attempted.Additionally, the AWS SDK was running with its own hardcoded internal retry policy (standard
mode: 3 retries with SDK-internal backoff), independently of the JDBC outer loop. This created
a double retry loop: the SDK would retry 3 times internally, and if it eventually threw,
the JDBC outer loop would retry the entire thing again — multiplying the actual attempts.
What changed:
SnowflakeUtil.findFirstCauseOfType(ex, SdkException.class), consistent with the GCS default strategy fix.RetryPolicy.defaultRetryPolicy().toBuilder().numRetries(0): built from the default policy to preserve the AWS circuit breaker and throttling conditions;numRetries(0)hands full retry control to the JDBC outer loop.S3ErrorHandler.isClientException400Or404, consistent withSnowflakeS3Client.About why no new unit test was added:
The
handleStorageExceptionchange inGCSAccessStrategyAwsSdkfollows the same pattern asthe default strategy fix (cause-chain walking). Constructing a meaningful mock test requires
either a real
SdkExceptionhierarchy or a functional AWS SDK mock — both are integration-testterritory. The existing
SnowflakeGCSClientTesttests cover construction ofGCSAccessStrategyAwsSdkthroughSnowflakeGCSClient.createSnowflakeGCSClient()and continueto pass unchanged. The behaviour is verified by the E2E tests described below.