From a11da9f4014b83a006cad732d2da272638f0b9c3 Mon Sep 17 00:00:00 2001 From: Goutam Adwant Date: Sat, 6 Jun 2026 13:52:43 -0700 Subject: [PATCH 1/2] fix(bigquery): retry getTable server errors Translate HTTP response exceptions thrown by getTable before retry evaluation so existing BigQueryException retryability handles transient server errors. Keep the change scoped to getTable and leave raw network IOExceptions on the existing retry path. Fixes #12885 --- .../google/cloud/bigquery/BigQueryImpl.java | 15 +++++--- .../cloud/bigquery/BigQueryImplTest.java | 35 +++++++++++++++++++ 2 files changed, 45 insertions(+), 5 deletions(-) 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..b02083d81231 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 @@ -20,6 +20,7 @@ import static com.google.common.base.Preconditions.checkArgument; import static java.net.HttpURLConnection.HTTP_NOT_FOUND; +import com.google.api.client.http.HttpResponseException; import com.google.api.core.BetaApi; import com.google.api.core.InternalApi; import com.google.api.gax.paging.Page; @@ -1123,11 +1124,15 @@ && getOptions().getOpenTelemetryTracer() != null) { new Callable() { @Override public com.google.api.services.bigquery.model.Table call() throws IOException { - return bigQueryRpc.getTableSkipExceptionTranslation( - completeTableId.getProject(), - completeTableId.getDataset(), - completeTableId.getTable(), - optionsMap); + try { + return bigQueryRpc.getTableSkipExceptionTranslation( + completeTableId.getProject(), + completeTableId.getDataset(), + completeTableId.getTable(), + optionsMap); + } catch (HttpResponseException e) { + throw new BigQueryException(e); + } } }, getOptions().getRetrySettings(), 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( From b11388b99d9001768021efb959e6a77ac62fa135 Mon Sep 17 00:00:00 2001 From: Goutam Adwant <8672451+goutamadwant@users.noreply.github.com> Date: Wed, 10 Jun 2026 21:12:58 -0700 Subject: [PATCH 2/2] fix(bigquery): preserve custom getTable retry handling --- .../google/cloud/bigquery/BigQueryImpl.java | 61 ++++++++++++++--- .../cloud/bigquery/BigQueryImplTest.java | 66 +++++++++++++++++-- 2 files changed, 110 insertions(+), 17 deletions(-) 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 b02083d81231..55cd7e4e6177 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 @@ -18,12 +18,18 @@ import static com.google.cloud.bigquery.PolicyHelper.convertFromApiPolicy; import static com.google.cloud.bigquery.PolicyHelper.convertToApiPolicy; import static com.google.common.base.Preconditions.checkArgument; +import static java.net.HttpURLConnection.HTTP_BAD_GATEWAY; +import static java.net.HttpURLConnection.HTTP_GATEWAY_TIMEOUT; +import static java.net.HttpURLConnection.HTTP_INTERNAL_ERROR; import static java.net.HttpURLConnection.HTTP_NOT_FOUND; +import static java.net.HttpURLConnection.HTTP_UNAVAILABLE; import com.google.api.client.http.HttpResponseException; 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.gax.retrying.TimedAttemptSettings; import com.google.api.services.bigquery.model.ErrorProto; import com.google.api.services.bigquery.model.GetQueryResultsResponse; import com.google.api.services.bigquery.model.QueryRequest; @@ -66,6 +72,26 @@ final class BigQueryImpl extends BaseService implements BigQuery { + private static final ResultRetryAlgorithm DEFAULT_GET_TABLE_RETRY_ALGORITHM = + new ResultRetryAlgorithm() { + @Override + public TimedAttemptSettings createNextAttempt( + Throwable previousThrowable, + Object previousResponse, + TimedAttemptSettings previousSettings) { + return null; + } + + @Override + public boolean shouldRetry(Throwable previousThrowable, Object previousResponse) { + if (isRetryableHttpResponseException(previousThrowable)) { + return true; + } + return BigQueryBaseService.DEFAULT_BIGQUERY_EXCEPTION_HANDLER.shouldRetry( + previousThrowable, previousResponse); + } + }; + private static class DatasetPageFetcher implements NextPageFetcher { private static final long serialVersionUID = -3057564042439021278L; @@ -1124,19 +1150,15 @@ && getOptions().getOpenTelemetryTracer() != null) { new Callable() { @Override public com.google.api.services.bigquery.model.Table call() throws IOException { - try { - return bigQueryRpc.getTableSkipExceptionTranslation( - completeTableId.getProject(), - completeTableId.getDataset(), - completeTableId.getTable(), - optionsMap); - } catch (HttpResponseException e) { - throw new BigQueryException(e); - } + return bigQueryRpc.getTableSkipExceptionTranslation( + completeTableId.getProject(), + completeTableId.getDataset(), + completeTableId.getTable(), + optionsMap); } }, getOptions().getRetrySettings(), - getOptions().getResultRetryAlgorithm(), + getTableRetryAlgorithm(), getOptions().getClock(), EMPTY_RETRY_CONFIG, getOptions().isOpenTelemetryTracingEnabled(), @@ -1157,6 +1179,25 @@ public com.google.api.services.bigquery.model.Table call() throws IOException { } } + private ResultRetryAlgorithm getTableRetryAlgorithm() { + ResultRetryAlgorithm configuredAlgorithm = getOptions().getResultRetryAlgorithm(); + if (configuredAlgorithm != BigQueryBaseService.DEFAULT_BIGQUERY_EXCEPTION_HANDLER) { + return configuredAlgorithm; + } + return DEFAULT_GET_TABLE_RETRY_ALGORITHM; + } + + private static boolean isRetryableHttpResponseException(Throwable previousThrowable) { + if (!(previousThrowable instanceof HttpResponseException)) { + return false; + } + int statusCode = ((HttpResponseException) previousThrowable).getStatusCode(); + return statusCode == HTTP_INTERNAL_ERROR + || statusCode == HTTP_BAD_GATEWAY + || statusCode == HTTP_UNAVAILABLE + || statusCode == HTTP_GATEWAY_TIMEOUT; + } + @Override public Model getModel(String datasetId, String modelId, ModelOption... options) { return getModel(ModelId.of(datasetId, modelId), options); 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 7fea041b4025..8a908e2e8011 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 @@ -41,6 +41,8 @@ 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.gax.retrying.ResultRetryAlgorithm; +import com.google.api.gax.retrying.TimedAttemptSettings; import com.google.api.services.bigquery.model.ErrorProto; import com.google.api.services.bigquery.model.GetQueryResultsResponse; import com.google.api.services.bigquery.model.JobConfigurationQuery; @@ -76,6 +78,7 @@ import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -941,31 +944,80 @@ void testGetTable() throws IOException { @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(serviceUnavailableException()) + .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); + } + + @Test + void testGetTableFailureUsesCustomRetryAlgorithm() throws IOException { + AtomicReference retryThrowable = new AtomicReference<>(); + ResultRetryAlgorithm retryAlgorithm = + new ResultRetryAlgorithm() { + @Override + public TimedAttemptSettings createNextAttempt( + Throwable previousThrowable, + Object previousResponse, + TimedAttemptSettings previousSettings) { + if (previousThrowable != null) { + retryThrowable.set(previousThrowable); + } + return null; + } + + @Override + public boolean shouldRetry(Throwable previousThrowable, Object previousResponse) { + if (previousThrowable != null) { + retryThrowable.set(previousThrowable); + } + return previousThrowable instanceof HttpResponseException; + } + }; when(bigqueryRpcMock.getTableSkipExceptionTranslation( PROJECT, DATASET, TABLE, EMPTY_RPC_OPTIONS)) - .thenThrow(new GoogleJsonResponseException(serverErrorResponse(), error)) + .thenThrow(serviceUnavailableException()) .thenReturn(TABLE_INFO_WITH_PROJECT.toPb()); bigquery = options.toBuilder() .setRetrySettings(ServiceOptions.getDefaultRetrySettings()) + .setResultRetryAlgorithm(retryAlgorithm) .build() .getService(); + assertSame(retryAlgorithm, bigquery.getOptions().getResultRetryAlgorithm()); Table table = bigquery.getTable(DATASET, TABLE); assertEquals(new Table(bigquery, new TableInfo.BuilderImpl(TABLE_INFO_WITH_PROJECT)), table); + assertThat(retryThrowable.get()).isInstanceOf(HttpResponseException.class); verify(bigqueryRpcMock, times(2)) .getTableSkipExceptionTranslation(PROJECT, DATASET, TABLE, EMPTY_RPC_OPTIONS); } + private static GoogleJsonResponseException serviceUnavailableException() { + 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)); + return new GoogleJsonResponseException(serverErrorResponse(), error); + } + private static HttpResponseException.Builder serverErrorResponse() { return new HttpResponseException.Builder(503, "Service Unavailable", new HttpHeaders()); }