diff --git a/java-bigquery/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/BigQueryImpl.java b/java-bigquery/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/BigQueryImpl.java index 74c9ce60e84f..a36136419824 100644 --- a/java-bigquery/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/BigQueryImpl.java +++ b/java-bigquery/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/BigQueryImpl.java @@ -23,6 +23,7 @@ import com.google.api.core.BetaApi; import com.google.api.core.InternalApi; import com.google.api.gax.paging.Page; +import com.google.api.gax.retrying.ResultRetryAlgorithm; import com.google.api.services.bigquery.model.ErrorProto; import com.google.api.services.bigquery.model.GetQueryResultsResponse; import com.google.api.services.bigquery.model.QueryRequest; @@ -396,7 +397,7 @@ public com.google.api.services.bigquery.model.Routine call() throws IOException } }, getOptions().getRetrySettings(), - getOptions().getResultRetryAlgorithm(), + getRetryAlgorithmWithHttpRetry(), getOptions().getClock(), EMPTY_RETRY_CONFIG, getOptions().isOpenTelemetryTracingEnabled(), @@ -592,7 +593,7 @@ public com.google.api.services.bigquery.model.Dataset call() throws IOException } }, getOptions().getRetrySettings(), - getOptions().getResultRetryAlgorithm(), + getRetryAlgorithmWithHttpRetry(), getOptions().getClock(), EMPTY_RETRY_CONFIG, getOptions().isOpenTelemetryTracingEnabled(), @@ -658,7 +659,7 @@ private static Page listDatasets( } }, serviceOptions.getRetrySettings(), - serviceOptions.getResultRetryAlgorithm(), + BigQueryRetryHelper.maybeWrapForHttpRetry(serviceOptions.getResultRetryAlgorithm()), serviceOptions.getClock(), EMPTY_RETRY_CONFIG, serviceOptions.isOpenTelemetryTracingEnabled(), @@ -1131,7 +1132,7 @@ public com.google.api.services.bigquery.model.Table call() throws IOException { } }, getOptions().getRetrySettings(), - getOptions().getResultRetryAlgorithm(), + getRetryAlgorithmWithHttpRetry(), getOptions().getClock(), EMPTY_RETRY_CONFIG, getOptions().isOpenTelemetryTracingEnabled(), @@ -1190,7 +1191,7 @@ public com.google.api.services.bigquery.model.Model call() throws IOException { } }, getOptions().getRetrySettings(), - getOptions().getResultRetryAlgorithm(), + getRetryAlgorithmWithHttpRetry(), getOptions().getClock(), EMPTY_RETRY_CONFIG, getOptions().isOpenTelemetryTracingEnabled(), @@ -1249,7 +1250,7 @@ public com.google.api.services.bigquery.model.Routine call() throws IOException } }, getOptions().getRetrySettings(), - getOptions().getResultRetryAlgorithm(), + getRetryAlgorithmWithHttpRetry(), getOptions().getClock(), EMPTY_RETRY_CONFIG, getOptions().isOpenTelemetryTracingEnabled(), @@ -1467,7 +1468,7 @@ public Tuple> cal } }, serviceOptions.getRetrySettings(), - serviceOptions.getResultRetryAlgorithm(), + BigQueryRetryHelper.maybeWrapForHttpRetry(serviceOptions.getResultRetryAlgorithm()), serviceOptions.getClock(), EMPTY_RETRY_CONFIG, serviceOptions.isOpenTelemetryTracingEnabled(), @@ -1508,7 +1509,7 @@ public Tuple> cal } }, serviceOptions.getRetrySettings(), - serviceOptions.getResultRetryAlgorithm(), + BigQueryRetryHelper.maybeWrapForHttpRetry(serviceOptions.getResultRetryAlgorithm()), serviceOptions.getClock(), EMPTY_RETRY_CONFIG, serviceOptions.isOpenTelemetryTracingEnabled(), @@ -1549,7 +1550,7 @@ private static Page listRoutines( } }, serviceOptions.getRetrySettings(), - serviceOptions.getResultRetryAlgorithm(), + BigQueryRetryHelper.maybeWrapForHttpRetry(serviceOptions.getResultRetryAlgorithm()), serviceOptions.getClock(), EMPTY_RETRY_CONFIG, serviceOptions.isOpenTelemetryTracingEnabled(), @@ -1725,7 +1726,7 @@ public TableDataList call() throws IOException { } }, serviceOptions.getRetrySettings(), - serviceOptions.getResultRetryAlgorithm(), + BigQueryRetryHelper.maybeWrapForHttpRetry(serviceOptions.getResultRetryAlgorithm()), serviceOptions.getClock(), EMPTY_RETRY_CONFIG, serviceOptions.isOpenTelemetryTracingEnabled(), @@ -1802,7 +1803,7 @@ public com.google.api.services.bigquery.model.Job call() throws IOException { } }, getOptions().getRetrySettings(), - getOptions().getResultRetryAlgorithm(), + getRetryAlgorithmWithHttpRetry(), getOptions().getClock(), EMPTY_RETRY_CONFIG, getOptions().isOpenTelemetryTracingEnabled(), @@ -1859,7 +1860,7 @@ public Tuple> call( } }, serviceOptions.getRetrySettings(), - serviceOptions.getResultRetryAlgorithm(), + BigQueryRetryHelper.maybeWrapForHttpRetry(serviceOptions.getResultRetryAlgorithm()), serviceOptions.getClock(), EMPTY_RETRY_CONFIG, serviceOptions.isOpenTelemetryTracingEnabled(), @@ -1914,7 +1915,7 @@ public Boolean call() throws IOException { } }, getOptions().getRetrySettings(), - getOptions().getResultRetryAlgorithm(), + getRetryAlgorithmWithHttpRetry(), getOptions().getClock(), EMPTY_RETRY_CONFIG, getOptions().isOpenTelemetryTracingEnabled(), @@ -2169,7 +2170,7 @@ public GetQueryResultsResponse call() throws IOException { } }, serviceOptions.getRetrySettings(), - serviceOptions.getResultRetryAlgorithm(), + BigQueryRetryHelper.maybeWrapForHttpRetry(serviceOptions.getResultRetryAlgorithm()), serviceOptions.getClock(), DEFAULT_RETRY_CONFIG, serviceOptions.isOpenTelemetryTracingEnabled(), @@ -2240,7 +2241,7 @@ public com.google.api.services.bigquery.model.Policy call() throws IOException { } }, getOptions().getRetrySettings(), - getOptions().getResultRetryAlgorithm(), + getRetryAlgorithmWithHttpRetry(), getOptions().getClock(), EMPTY_RETRY_CONFIG, getOptions().isOpenTelemetryTracingEnabled(), @@ -2334,7 +2335,7 @@ public com.google.api.services.bigquery.model.TestIamPermissionsResponse call() } }, getOptions().getRetrySettings(), - getOptions().getResultRetryAlgorithm(), + getRetryAlgorithmWithHttpRetry(), getOptions().getClock(), EMPTY_RETRY_CONFIG, getOptions().isOpenTelemetryTracingEnabled(), @@ -2411,4 +2412,16 @@ private static boolean isRetryErrorCodeHttpNotFound(BigQueryRetryHelperException } return false; } + + /** + * Helper to retrieve the retry algorithm wrapped for HTTP error retries. + * + *

This delegates to {@link BigQueryRetryHelper#maybeWrapForHttpRetry} to ensure safe + * conditional wrapping of the default algorithm while leaving custom user algorithms untouched. + */ + @SuppressWarnings("unchecked") + private ResultRetryAlgorithm getRetryAlgorithmWithHttpRetry() { + return BigQueryRetryHelper.maybeWrapForHttpRetry( + (ResultRetryAlgorithm) getOptions().getResultRetryAlgorithm()); + } } diff --git a/java-bigquery/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/BigQueryRetryHelper.java b/java-bigquery/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/BigQueryRetryHelper.java index 98adb0b273e1..089e7cd78c33 100644 --- a/java-bigquery/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/BigQueryRetryHelper.java +++ b/java-bigquery/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/BigQueryRetryHelper.java @@ -15,6 +15,7 @@ */ package com.google.cloud.bigquery; +import com.google.api.client.http.HttpResponseException; import com.google.api.core.ApiClock; import com.google.api.gax.retrying.DirectRetryingExecutor; import com.google.api.gax.retrying.ExponentialRetryAlgorithm; @@ -23,6 +24,7 @@ import com.google.api.gax.retrying.RetrySettings; import com.google.api.gax.retrying.RetryingExecutor; import com.google.api.gax.retrying.RetryingFuture; +import com.google.api.gax.retrying.TimedAttemptSettings; import com.google.api.gax.retrying.TimedRetryAlgorithm; import com.google.cloud.RetryHelper; import io.opentelemetry.api.trace.Span; @@ -119,6 +121,49 @@ private static V run( return retryingFuture.get(); } + /** + * Conditionally wraps the provided retry algorithm with a wrapper that retries on transient HTTP + * errors. + * + *

Wrapping only occurs if the provided algorithm is the default {@link + * BigQueryBaseService#DEFAULT_BIGQUERY_EXCEPTION_HANDLER}. Custom user-defined retry algorithms + * are returned unmodified to preserve custom retry policies. + */ + static ResultRetryAlgorithm maybeWrapForHttpRetry(ResultRetryAlgorithm algorithm) { + if (algorithm == BigQueryBaseService.DEFAULT_BIGQUERY_EXCEPTION_HANDLER) { + return wrapDefaultAlgorithm(algorithm); + } + return algorithm; + } + + /** + * Wraps the default retry algorithm to additionally retry on transient HTTP status codes 500, + * 502, 503, and 504. Other retry decisions and timing logic are delegated back to the default + * algorithm. + */ + private static ResultRetryAlgorithm wrapDefaultAlgorithm( + ResultRetryAlgorithm defaultAlgorithm) { + return new ResultRetryAlgorithm() { + @Override + public TimedAttemptSettings createNextAttempt( + Throwable previousThrowable, V previousResponse, TimedAttemptSettings previousSettings) { + return defaultAlgorithm.createNextAttempt( + previousThrowable, previousResponse, previousSettings); + } + + @Override + public boolean shouldRetry(Throwable previousThrowable, V previousResponse) { + if (previousThrowable instanceof HttpResponseException) { + int statusCode = ((HttpResponseException) previousThrowable).getStatusCode(); + if (statusCode == 500 || statusCode == 502 || statusCode == 503 || statusCode == 504) { + return true; + } + } + return defaultAlgorithm.shouldRetry(previousThrowable, previousResponse); + } + }; + } + public static class BigQueryRetryHelperException extends RuntimeException { private static final long serialVersionUID = -8519852520090965314L; diff --git a/java-bigquery/google-cloud-bigquery/src/test/java/com/google/cloud/bigquery/BigQueryImplTest.java b/java-bigquery/google-cloud-bigquery/src/test/java/com/google/cloud/bigquery/BigQueryImplTest.java index 20a6ef679e89..7fea041b4025 100644 --- a/java-bigquery/google-cloud-bigquery/src/test/java/com/google/cloud/bigquery/BigQueryImplTest.java +++ b/java-bigquery/google-cloud-bigquery/src/test/java/com/google/cloud/bigquery/BigQueryImplTest.java @@ -36,6 +36,10 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import com.google.api.client.googleapis.json.GoogleJsonError; +import com.google.api.client.googleapis.json.GoogleJsonResponseException; +import com.google.api.client.http.HttpHeaders; +import com.google.api.client.http.HttpResponseException; import com.google.api.gax.paging.Page; import com.google.api.services.bigquery.model.ErrorProto; import com.google.api.services.bigquery.model.GetQueryResultsResponse; @@ -935,6 +939,37 @@ void testGetTable() throws IOException { .getTableSkipExceptionTranslation(PROJECT, DATASET, TABLE, EMPTY_RPC_OPTIONS); } + @Test + void testGetTableFailureShouldRetryServerErrors() throws IOException { + GoogleJsonError error = new GoogleJsonError(); + error.setMessage("Visibility check was unavailable. Please retry the request"); + error.setCode(503); + GoogleJsonError.ErrorInfo errorInfo = new GoogleJsonError.ErrorInfo(); + errorInfo.setReason("backendError"); + error.setErrors(ImmutableList.of(errorInfo)); + + when(bigqueryRpcMock.getTableSkipExceptionTranslation( + PROJECT, DATASET, TABLE, EMPTY_RPC_OPTIONS)) + .thenThrow(new GoogleJsonResponseException(serverErrorResponse(), error)) + .thenReturn(TABLE_INFO_WITH_PROJECT.toPb()); + + bigquery = + options.toBuilder() + .setRetrySettings(ServiceOptions.getDefaultRetrySettings()) + .build() + .getService(); + + Table table = bigquery.getTable(DATASET, TABLE); + + assertEquals(new Table(bigquery, new TableInfo.BuilderImpl(TABLE_INFO_WITH_PROJECT)), table); + verify(bigqueryRpcMock, times(2)) + .getTableSkipExceptionTranslation(PROJECT, DATASET, TABLE, EMPTY_RPC_OPTIONS); + } + + private static HttpResponseException.Builder serverErrorResponse() { + return new HttpResponseException.Builder(503, "Service Unavailable", new HttpHeaders()); + } + @Test void testGetModel() throws IOException { when(bigqueryRpcMock.getModelSkipExceptionTranslation(