Skip to content

Commit 69b2fa5

Browse files
addressing Driver team review comments
1 parent 62ddf1a commit 69b2fa5

4 files changed

Lines changed: 112 additions & 63 deletions

File tree

src/main/java/net/snowflake/client/internal/jdbc/SnowflakeUtil.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -649,6 +649,22 @@ public static Throwable getRootCause(Exception ex) {
649649
return cause;
650650
}
651651

652+
/**
653+
* Walks the exception cause chain and returns the first {@link Throwable} that is an instance of
654+
* {@code type}, or {@code null} if none is found. Unlike {@link #getRootCause(Exception)}, which
655+
* always returns the deepest cause, this method stops at the first match of the requested type.
656+
*/
657+
public static <T extends Throwable> T findFirstCauseOfType(Throwable ex, Class<T> type) {
658+
Throwable cause = ex;
659+
while (cause != null) {
660+
if (type.isInstance(cause)) {
661+
return type.cast(cause);
662+
}
663+
cause = cause.getCause();
664+
}
665+
return null;
666+
}
667+
652668
public static boolean isBlank(String input) {
653669
if ("".equals(input) || input == null) {
654670
return true;

src/main/java/net/snowflake/client/internal/jdbc/cloud/storage/GCSAccessStrategyAwsSdk.java

Lines changed: 26 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
import java.io.InputStream;
1010
import java.net.URI;
1111
import java.net.URISyntaxException;
12-
import java.time.Duration;
1312
import java.util.List;
1413
import java.util.Map;
1514
import java.util.Optional;
@@ -34,7 +33,6 @@
3433
import software.amazon.awssdk.core.client.config.SdkAdvancedClientOption;
3534
import software.amazon.awssdk.core.exception.SdkException;
3635
import software.amazon.awssdk.core.retry.RetryPolicy;
37-
import software.amazon.awssdk.core.retry.backoff.FullJitterBackoffStrategy;
3836
import software.amazon.awssdk.http.nio.netty.NettyNioAsyncHttpClient;
3937
import software.amazon.awssdk.http.nio.netty.ProxyConfiguration;
4038
import software.amazon.awssdk.regions.Region;
@@ -57,13 +55,7 @@ class GCSAccessStrategyAwsSdk implements GCSAccessStrategy {
5755
private static final SFLogger logger = SFLoggerFactory.getLogger(GCSAccessStrategyAwsSdk.class);
5856
private final S3AsyncClient amazonClient;
5957

60-
GCSAccessStrategyAwsSdk(
61-
StageInfo stage,
62-
SFBaseSession session,
63-
int maxRetries,
64-
int retryBackoffMin,
65-
int retryBackoffMaxExponent)
66-
throws SnowflakeSQLException {
58+
GCSAccessStrategyAwsSdk(StageInfo stage, SFBaseSession session) throws SnowflakeSQLException {
6759
String accessToken = (String) stage.getCredentials().get("GCS_ACCESS_TOKEN");
6860

6961
Optional<String> oEndpoint = stage.gcsCustomEndpoint();
@@ -94,20 +86,12 @@ class GCSAccessStrategyAwsSdk implements GCSAccessStrategy {
9486
overrideConfiguration.putAdvancedOption(
9587
SdkAdvancedClientOption.SIGNER, new AwsSdkGCPSigner(accessToken));
9688

97-
// Mirror the JDBC driver's retry settings so putGetMaxRetries and backoff parameters
98-
// apply consistently to this strategy. maxBackoffMs = retryBackoffMin <<
99-
// retryBackoffMaxExponent
100-
// (e.g. 1000ms << 4 = 16 000ms), matching S3ErrorHandler.retryRequestWithExponentialBackoff.
101-
long maxBackoffMs = (long) retryBackoffMin << retryBackoffMaxExponent;
89+
// Disable the SDK's own internal retries so the outer JDBC retry loop in
90+
// SnowflakeGCSClient.upload() is the sole controller of retry count and backoff.
91+
// Starting from the default policy (rather than an empty builder) preserves the AWS
92+
// token-bucket circuit breaker and throttling-aware retry conditions.
10293
overrideConfiguration.retryPolicy(
103-
RetryPolicy.builder()
104-
.numRetries(maxRetries)
105-
.backoffStrategy(
106-
FullJitterBackoffStrategy.builder()
107-
.baseDelay(Duration.ofMillis(retryBackoffMin))
108-
.maxBackoffTime(Duration.ofMillis(maxBackoffMs))
109-
.build())
110-
.build());
94+
RetryPolicy.defaultRetryPolicy().toBuilder().numRetries(0).build());
11195

11296
ProxyConfiguration proxyConfiguration;
11397
if (session != null) {
@@ -285,17 +269,31 @@ public boolean handleStorageException(
285269
String queryId,
286270
SnowflakeGCSClient gcsClient)
287271
throws SnowflakeSQLException {
288-
Throwable cause = ex.getCause();
289-
if (cause instanceof SdkException) {
290-
logger.debug("GCSAccessStrategyAwsSdk: " + cause.getMessage());
272+
// Walk the full cause chain to find an SdkException. The exception may arrive wrapped
273+
// (e.g. inside SnowflakeSQLLoggedException → CompletionException) when it originates from the
274+
// inner uploadWithDownScopedToken wrapper. Walking the chain ensures that transient errors
275+
// (503, connection refused, etc.) are always treated as retryable regardless of wrapping depth,
276+
// consistent with how GCSDefaultAccessStrategy handles StorageException.
277+
SdkException sdkEx = SnowflakeUtil.findFirstCauseOfType(ex, SdkException.class);
278+
if (sdkEx != null) {
279+
if (sdkEx != ex) {
280+
logger.debug(
281+
"GCSAccessStrategyAwsSdk: found SdkException ({}) wrapped inside {} during {};"
282+
+ " treating as retryable",
283+
sdkEx.getMessage(),
284+
ex.getClass().getSimpleName(),
285+
operation);
286+
} else {
287+
logger.debug("GCSAccessStrategyAwsSdk: {}", sdkEx.getMessage());
288+
}
291289

292290
if (retryCount > gcsClient.getMaxRetries()
293-
|| S3ErrorHandler.isClientException400Or404(cause)) {
291+
|| S3ErrorHandler.isClientException400Or404(sdkEx)) {
294292
throwIfClientExceptionOrMaxRetryReached(
295-
operation, session, command, queryId, gcsClient, cause);
293+
operation, session, command, queryId, gcsClient, sdkEx);
296294
} else {
297295
retryRequestWithExponentialBackoff(
298-
ex, retryCount, operation, session, command, gcsClient, queryId, cause);
296+
ex, retryCount, operation, session, command, gcsClient, queryId, sdkEx);
299297
}
300298
return true;
301299
} else {

src/main/java/net/snowflake/client/internal/jdbc/cloud/storage/GCSDefaultAccessStrategy.java

Lines changed: 69 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import com.google.cloud.storage.StorageOptions;
1919
import java.io.File;
2020
import java.io.InputStream;
21+
import java.net.SocketTimeoutException;
2122
import java.nio.channels.Channels;
2223
import java.util.Map;
2324
import net.snowflake.client.api.exception.SnowflakeSQLException;
@@ -29,6 +30,7 @@
2930
import net.snowflake.client.internal.log.SFLoggerFactory;
3031
import net.snowflake.client.internal.util.SFPair;
3132
import net.snowflake.common.core.SqlState;
33+
import org.apache.http.HttpStatus;
3234

3335
class GCSDefaultAccessStrategy implements GCSAccessStrategy {
3436
private static final SFLogger logger = SFLoggerFactory.getLogger(GCSDefaultAccessStrategy.class);
@@ -209,32 +211,55 @@ public boolean handleStorageException(
209211
// re-wraps it before re-throwing. Walking the chain ensures that a transient 503 or similar
210212
// GCS error is always treated as retryable regardless of how many layers of wrapping were
211213
// added.
212-
StorageException se = null;
213-
if (ex instanceof StorageException) {
214-
se = (StorageException) ex;
215-
} else {
216-
Throwable cause = ex.getCause();
217-
while (cause != null) {
218-
if (cause instanceof StorageException) {
219-
se = (StorageException) cause;
220-
break;
221-
}
222-
cause = cause.getCause();
223-
}
224-
if (se != null) {
225-
logger.debug(
226-
"GCSDefaultAccessStrategy: found StorageException (HTTP {}) wrapped inside {} during {};"
227-
+ " treating as retryable",
228-
se.getCode(),
229-
ex.getClass().getSimpleName(),
230-
operation);
231-
}
214+
StorageException se = SnowflakeUtil.findFirstCauseOfType(ex, StorageException.class);
215+
if (se != null && se != ex) {
216+
logger.debug(
217+
"GCSDefaultAccessStrategy: found StorageException (HTTP {}) wrapped inside {} during {};"
218+
+ " treating as retryable",
219+
se.getCode(),
220+
ex.getClass().getSimpleName(),
221+
operation);
232222
}
233223

234224
if (se != null) {
235225
// NOTE: this code path only handles Access token based operations.
236226
// Presigned URL operations do not raise StorageException.
237227

228+
// Non-transient client errors must not be retried during upload/download, mirroring
229+
// S3ErrorHandler.isClientException400Or404 (400 and 404 only).
230+
// 401 is excluded here because it is handled below via token refresh.
231+
//
232+
// WHY THE SCOPE IS LIMITED TO UPLOAD AND DOWNLOAD:
233+
// When OVERWRITE=FALSE, SnowflakeFileTransferAgent.filterExistingFiles() calls
234+
// storageClient.listObjects() (returns an empty collection on miss — never a 404) and
235+
// then storageClient.getObjectMetadata() on each listed object to compare digests.
236+
// If the object disappears between the list and the metadata fetch (a race condition),
237+
// GCSDefaultAccessStrategy.getObjectMetadata() throws StorageProviderException wrapping a
238+
// synthetic StorageException(404). The SnowflakeFileTransferAgent.compareAndSkipRemoteFiles()
239+
// SNOW-14521 handler attempts to recognise this 404 via isServiceException404(), but that
240+
// method only checks SdkServiceException (AWS SDK); it does not recognise the GCS-native
241+
// StorageException, so the exception is re-thrown and eventually reaches
242+
// handleStorageException() with operation="compareRemoteFiles". Applying the
243+
// immediate-throw rule for 404 here would break that retry path — the correct behaviour is
244+
// to retry the list/compare cycle via the normal max-retries logic.
245+
// NOTE: for the upload path itself, OVERWRITE=FALSE produces only a LIST (GET ?prefix=...)
246+
// call and, if the file is found, skips the upload before getObjectMetadata() is ever
247+
// reached. No 404 is generated by the upload-path existence check.
248+
if ((StorageHelper.UPLOAD.equals(operation) || StorageHelper.DOWNLOAD.equals(operation))
249+
&& (se.getCode() == HttpStatus.SC_BAD_REQUEST
250+
|| se.getCode() == HttpStatus.SC_NOT_FOUND)) {
251+
throw new SnowflakeSQLLoggedException(
252+
queryId,
253+
session,
254+
SqlState.SYSTEM_ERROR,
255+
StorageHelper.getOperationException(operation).getMessageCode(),
256+
se,
257+
operation,
258+
se.getCode(),
259+
se.getMessage(),
260+
se.getReason());
261+
}
262+
238263
// If we have exceeded the max number of retries, propagate the error.
239264
if (retryCount > gcsClient.getMaxRetries()) {
240265
logger.error(
@@ -243,8 +268,8 @@ public boolean handleStorageException(
243268
gcsClient.getMaxRetries(),
244269
se.getCode(),
245270
se.getMessage(),
246-
operation);
247-
logger.error("Stack trace: ", ex);
271+
operation,
272+
ex);
248273
throw new SnowflakeSQLLoggedException(
249274
queryId,
250275
session,
@@ -260,8 +285,8 @@ public boolean handleStorageException(
260285
"Encountered exception ({}) during {}, retry count: {}",
261286
se.getMessage(),
262287
operation,
263-
retryCount);
264-
logger.debug("Stack trace: ", ex);
288+
retryCount,
289+
ex);
265290

266291
// exponential backoff up to a limit
267292
int backoffInMillis = gcsClient.getRetryBackoffMin();
@@ -294,13 +319,29 @@ public boolean handleStorageException(
294319
}
295320
return true;
296321
} else {
322+
// InterruptedException and SocketTimeoutException are handled by the outer retry loop in
323+
// SnowflakeGCSClient.handleStorageException (retry within max retries, throw when exhausted).
324+
// Return false so that logic is preserved rather than bypassed.
325+
if (ex instanceof InterruptedException
326+
|| SnowflakeUtil.getRootCause(ex) instanceof SocketTimeoutException) {
327+
return false;
328+
}
297329
logger.error(
298-
"GCSDefaultAccessStrategy: unhandled exception type {} during {}, not retrying: {}",
330+
"GCSDefaultAccessStrategy: unhandled exception type {} during {}, not retrying",
299331
ex.getClass().getName(),
300332
operation,
301-
ex.getMessage());
302-
logger.error("Stack trace: ", ex);
303-
return false;
333+
ex);
334+
// Re-throw as-is to avoid double-wrapping an already well-formed SQL exception.
335+
if (ex instanceof SnowflakeSQLException) {
336+
throw (SnowflakeSQLException) ex;
337+
}
338+
throw new SnowflakeSQLLoggedException(
339+
queryId,
340+
session,
341+
SqlState.SYSTEM_ERROR,
342+
StorageHelper.getOperationException(operation).getMessageCode(),
343+
ex,
344+
"Encountered exception during " + operation + ": " + ex.getMessage());
304345
}
305346
}
306347

src/main/java/net/snowflake/client/internal/jdbc/cloud/storage/SnowflakeGCSClient.java

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1244,13 +1244,7 @@ private void setupGCSClient(
12441244
boolean overrideAwsAccessStrategy =
12451245
Boolean.valueOf(systemGetEnv("SNOWFLAKE_GCS_FORCE_VIRTUAL_STYLE_DOMAINS"));
12461246
if (stage.getUseVirtualUrl() || overrideAwsAccessStrategy) {
1247-
this.gcsAccessStrategy =
1248-
new GCSAccessStrategyAwsSdk(
1249-
stage,
1250-
session,
1251-
this.getMaxRetries(),
1252-
this.getRetryBackoffMin(),
1253-
this.getRetryBackoffMaxExponent());
1247+
this.gcsAccessStrategy = new GCSAccessStrategyAwsSdk(stage, session);
12541248
} else {
12551249
this.gcsAccessStrategy = new GCSDefaultAccessStrategy(stage, session);
12561250
}

0 commit comments

Comments
 (0)