This is an automated email from the ASF dual-hosted git repository.
chamikaramj pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/beam.git
The following commit(s) were added to refs/heads/master by this push:
new 932dbe68f7f [Java][BigQueryIO] Fix BigQueryIO streaming insert
silently dropping transient network errors during response body read (#40420)
932dbe68f7f is described below
commit 932dbe68f7f7961ca0bb9221815b8f42040e87c0
Author: Danish Amjad <[email protected]>
AuthorDate: Fri Oct 23 02:30:13 2026 +0200
[Java][BigQueryIO] Fix BigQueryIO streaming insert silently dropping
transient network errors during response body read (#40420)
---
.../sdk/io/gcp/bigquery/BigQueryServicesImpl.java | 50 ++++++++
.../io/gcp/bigquery/BigQueryServicesImplTest.java | 141 +++++++++++++++++++++
2 files changed, 191 insertions(+)
diff --git
a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/BigQueryServicesImpl.java
b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/BigQueryServicesImpl.java
index a921be3700e..81ba6ce3ca7 100644
---
a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/BigQueryServicesImpl.java
+++
b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/BigQueryServicesImpl.java
@@ -99,6 +99,8 @@ import io.grpc.Status;
import io.grpc.Status.Code;
import io.grpc.protobuf.ProtoUtils;
import java.io.IOException;
+import java.net.SocketException;
+import java.net.SocketTimeoutException;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
@@ -1081,6 +1083,27 @@ public class BigQueryServicesImpl implements
BigQueryServices {
} catch (IOException e) {
GoogleJsonError.ErrorInfo errorInfo = getErrorInfo(e);
if (errorInfo == null) {
+ String networkErrorReason = transientNetworkErrorReason(e);
+ if (networkErrorReason != null) {
+ // Route all rows in this batch through InsertRetryPolicy so
the outer insertAll
+ // loop handles retries consistently via the pipeline's
configured policy.
+ serviceCallMetric.call(networkErrorReason);
+ result.updateFailedRpcMetrics(start, start,
BigQuerySinkMetrics.UNKNOWN);
+ LOG.warn(
+ "BigQuery insertAll transient network error, routing rows
to retry policy", e);
+ List<TableDataInsertAllResponse.InsertErrors> syntheticErrors
= new ArrayList<>();
+ for (int i = 0; i < rows.size(); i++) {
+ syntheticErrors.add(
+ new TableDataInsertAllResponse.InsertErrors()
+ .setIndex((long) i)
+ .setErrors(
+ Collections.singletonList(
+ new ErrorProto()
+ .setReason(networkErrorReason)
+
.setMessage(Strings.nullToEmpty(e.getMessage())))));
+ }
+ return syntheticErrors;
+ }
serviceCallMetric.call(ServiceCallMetric.CANONICAL_STATUS_UNKNOWN);
result.updateFailedRpcMetrics(start, start,
BigQuerySinkMetrics.UNKNOWN);
throw e;
@@ -1481,6 +1504,33 @@ public class BigQueryServicesImpl implements
BigQueryServices {
successfulRows);
}
+ /**
+ * Returns a BigQuery-style error reason string for transient network
errors that occur outside
+ * the scope of {@link com.google.api.client.http.HttpRequest#execute}
(e.g. during chunked
+ * response body reading), or {@code null} if the exception is not a
transient network error.
+ */
+ static @Nullable String transientNetworkErrorReason(IOException e) {
+ Throwable cause = e;
+ while (cause != null) {
+ if (cause instanceof SocketTimeoutException) {
+ return "socketTimeout";
+ }
+ if (cause instanceof SocketException) {
+ String msg = cause.getMessage();
+ if (msg != null) {
+ if (msg.contains("Connection reset")) {
+ return "connectionReset";
+ }
+ if (msg.contains("Broken pipe")) {
+ return "brokenPipe";
+ }
+ }
+ }
+ cause = cause.getCause();
+ }
+ return null;
+ }
+
protected static GoogleJsonError.@Nullable ErrorInfo
getErrorInfo(IOException e) {
if (!(e instanceof GoogleJsonResponseException)) {
return null;
diff --git
a/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/bigquery/BigQueryServicesImplTest.java
b/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/bigquery/BigQueryServicesImplTest.java
index 642d35b4390..f9ca23fe227 100644
---
a/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/bigquery/BigQueryServicesImplTest.java
+++
b/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/bigquery/BigQueryServicesImplTest.java
@@ -84,6 +84,8 @@ import io.grpc.Status;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
+import java.net.SocketException;
+import java.net.SocketTimeoutException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
@@ -822,6 +824,145 @@ public class BigQueryServicesImplTest {
verifyWriteMetricWasSet("project", "dataset", "table", "quotaexceeded", 1);
}
+ @Test
+ public void testTransientNetworkErrorReason_socketTimeout() {
+ assertEquals(
+ "socketTimeout",
+ DatasetServiceImpl.transientNetworkErrorReason(
+ new SocketTimeoutException("Read timed out")));
+ }
+
+ @Test
+ public void testTransientNetworkErrorReason_wrappedSocketTimeout() {
+ assertEquals(
+ "socketTimeout",
+ DatasetServiceImpl.transientNetworkErrorReason(
+ new IOException("wrapper", new SocketTimeoutException("Read timed
out"))));
+ }
+
+ @Test
+ public void testTransientNetworkErrorReason_connectionReset() {
+ assertEquals(
+ "connectionReset",
+ DatasetServiceImpl.transientNetworkErrorReason(new
SocketException("Connection reset")));
+ }
+
+ @Test
+ public void testTransientNetworkErrorReason_brokenPipe() {
+ assertEquals(
+ "brokenPipe",
+ DatasetServiceImpl.transientNetworkErrorReason(new
SocketException("Broken pipe")));
+ }
+
+ @Test
+ public void testTransientNetworkErrorReason_nonTransient() {
+ assertNull(DatasetServiceImpl.transientNetworkErrorReason(new
IOException("Some other error")));
+ }
+
+ /**
+ * Tests that {@link DatasetServiceImpl#insertAll} retries when a
SocketTimeoutException occurs
+ * during response body reading (outside RetryHttpRequestInitializer scope).
+ */
+ @Test
+ public void testInsertSocketTimeoutRetry() throws Exception {
+ TableReference ref =
+ new
TableReference().setProjectId("project").setDatasetId("dataset").setTableId("table");
+ List<FailsafeValueInSingleWindow<TableRow, TableRow>> rows = new
ArrayList<>();
+ rows.add(wrapValue(new TableRow()));
+
+ // First response: 200 OK but content stream throws SocketTimeoutException
during body read,
+ // simulating a timeout mid-chunked-response. Second response succeeds.
+ setupMockResponses(
+ response -> {
+ when(response.getStatusCode()).thenReturn(200);
+ when(response.getContentType()).thenReturn(Json.MEDIA_TYPE);
+ when(response.getContent())
+ .thenReturn(
+ new InputStream() {
+ @Override
+ public int read() throws IOException {
+ throw new SocketTimeoutException("Read timed out");
+ }
+ });
+ },
+ response -> {
+ when(response.getStatusCode()).thenReturn(200);
+ when(response.getContentType()).thenReturn(Json.MEDIA_TYPE);
+ when(response.getContent()).thenReturn(toStream(new
TableDataInsertAllResponse()));
+ });
+
+ DatasetServiceImpl dataService =
+ new DatasetServiceImpl(bigquery, PipelineOptionsFactory.create());
+ dataService.insertAll(
+ ref,
+ rows,
+ null,
+ BackOffAdapter.toGcpBackOff(TEST_BACKOFF.backoff()),
+ TEST_BACKOFF,
+ new MockSleeper(),
+ InsertRetryPolicy.alwaysRetry(),
+ null,
+ null,
+ false,
+ false,
+ false,
+ null);
+
+ verifyAllResponsesAreRead();
+ expectedLogs.verifyWarn(
+ "BigQuery insertAll transient network error, routing rows to retry
policy");
+ }
+
+ /**
+ * Tests that {@link DatasetServiceImpl#insertAll} throws IOException when
SocketTimeoutException
+ * retries are exhausted via the outer insertAll backoff.
+ */
+ @Test
+ public void testInsertSocketTimeoutExhausted() throws Exception {
+ TableReference ref =
+ new
TableReference().setProjectId("project").setDatasetId("dataset").setTableId("table");
+ List<FailsafeValueInSingleWindow<TableRow, TableRow>> rows = new
ArrayList<>();
+ rows.add(wrapValue(new TableRow()));
+
+ // Single response that times out during body read. With maxRetries(0) on
the outer backoff
+ // there is no retry; the synthetic timeout errors are collected and
IOException is thrown.
+ setupMockResponses(
+ response -> {
+ when(response.getStatusCode()).thenReturn(200);
+ when(response.getContentType()).thenReturn(Json.MEDIA_TYPE);
+ when(response.getContent())
+ .thenReturn(
+ new InputStream() {
+ @Override
+ public int read() throws IOException {
+ throw new SocketTimeoutException("Read timed out");
+ }
+ });
+ });
+
+ DatasetServiceImpl dataService =
+ new DatasetServiceImpl(bigquery, PipelineOptionsFactory.create());
+ // Rows are routed through InsertRetryPolicy as synthetic "timeout"
errors; when the outer
+ // backoff is exhausted insertAll throws IOException("Insert failed: ...").
+ assertThrows(
+ IOException.class,
+ () ->
+ dataService.insertAll(
+ ref,
+ rows,
+ null,
+
BackOffAdapter.toGcpBackOff(FluentBackoff.DEFAULT.withMaxRetries(0).backoff()),
+ FluentBackoff.DEFAULT.withMaxRetries(0),
+ new MockSleeper(),
+ InsertRetryPolicy.alwaysRetry(),
+ null,
+ null,
+ false,
+ false,
+ false,
+ null));
+ }
+
/** Tests that {@link DatasetServiceImpl#insertAll} can stop quotaExceeded
retry attempts. */
@Test
public void testInsertStoppedRetry() throws Exception {