This is an automated email from the ASF dual-hosted git repository.
scwhittle 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 f33feafb2b2 [Dataflow Java Streaming] Add a timeout to how long
commits will retry to the service. (#39085)
f33feafb2b2 is described below
commit f33feafb2b2ab46dd73b198a29ea182a1c3c2a00
Author: Sam Whittle <[email protected]>
AuthorDate: Wed Jun 24 15:30:10 2026 +0200
[Dataflow Java Streaming] Add a timeout to how long commits will retry to
the service. (#39085)
* [Dataflow Java Streaming] Add a timeout to how long commits will retry to
the service. In cases where the service is unavailable this prevents build-up
of commits on workers that are no longer relevant. This defaults to 30 minutes
but can be disabled via an experiment or by setting the option.
---
.../options/DataflowStreamingPipelineOptions.java | 17 ++++
.../dataflow/worker/StreamingDataflowWorker.java | 2 +
.../windmill/client/grpc/GrpcCommitWorkStream.java | 94 +++++++++++++++++-----
.../client/grpc/GrpcWindmillStreamFactory.java | 10 +++
.../client/grpc/GrpcCommitWorkStreamTest.java | 82 +++++++++++++++++++
5 files changed, 183 insertions(+), 22 deletions(-)
diff --git
a/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/options/DataflowStreamingPipelineOptions.java
b/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/options/DataflowStreamingPipelineOptions.java
index 0897e526382..cca261fff62 100644
---
a/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/options/DataflowStreamingPipelineOptions.java
+++
b/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/options/DataflowStreamingPipelineOptions.java
@@ -196,6 +196,13 @@ public interface DataflowStreamingPipelineOptions extends
PipelineOptions {
void setStuckCommitDurationMillis(int value);
+ @Description(
+ "Retry commits on stream errors until this much time has elapsed since
the commit was scheduled. If zero, retry forever.")
+ @Default.InstanceFactory(CommitWorkStreamRetryTimeoutMillisFactory.class)
+ long getCommitWorkStreamRetryTimeoutMillis();
+
+ void setCommitWorkStreamRetryTimeoutMillis(long value);
+
@Description(
"Period for sending 'global get config' requests to the service. The
duration is "
+ "specified as seconds in 'PTx.yS' format, e.g. 'PT5.125S'."
@@ -343,4 +350,14 @@ public interface DataflowStreamingPipelineOptions extends
PipelineOptions {
return ExperimentalOptions.hasExperiment(options,
"enable_windmill_service_direct_path");
}
}
+
+ class CommitWorkStreamRetryTimeoutMillisFactory implements
DefaultValueFactory<Long> {
+ @Override
+ public Long create(PipelineOptions options) {
+ if (ExperimentalOptions.hasExperiment(options,
"disable_commit_retry_timeout")) {
+ return 0L;
+ }
+ return Duration.standardMinutes(30).getMillis();
+ }
+ }
}
diff --git
a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/StreamingDataflowWorker.java
b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/StreamingDataflowWorker.java
index 9e82343474c..9f063d39370 100644
---
a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/StreamingDataflowWorker.java
+++
b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/StreamingDataflowWorker.java
@@ -756,6 +756,8 @@ public final class StreamingDataflowWorker {
new
WorkHeartbeatResponseProcessor(computationStateCache::get))
.setHealthCheckIntervalMillis(
options.getWindmillServiceStreamingRpcHealthCheckPeriodMs())
+ .setCommitWorkStreamRetryTimeout(
+
java.time.Duration.ofMillis(options.getCommitWorkStreamRetryTimeoutMillis()))
.build();
return ConfigFetcherComputationStateCacheAndWindmillClient.builder()
.setWindmillDispatcherClient(dispatcherClient)
diff --git
a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/client/grpc/GrpcCommitWorkStream.java
b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/client/grpc/GrpcCommitWorkStream.java
index d24676652fd..160b0cce013 100644
---
a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/client/grpc/GrpcCommitWorkStream.java
+++
b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/client/grpc/GrpcCommitWorkStream.java
@@ -20,7 +20,6 @@ package
org.apache.beam.runners.dataflow.worker.windmill.client.grpc;
import static
org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkNotNull;
import static
org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkState;
-import com.google.auto.value.AutoValue;
import java.io.PrintWriter;
import java.time.Duration;
import java.util.HashMap;
@@ -77,6 +76,7 @@ final class GrpcCommitWorkStream
private final JobHeader jobHeader;
private final int streamingRpcBatchLimit;
private volatile boolean logMissingResponse = true;
+ private final Duration maxRetryDuration;
private GrpcCommitWorkStream(
String backendWorkerToken,
@@ -90,6 +90,7 @@ final class GrpcCommitWorkStream
AtomicLong idGenerator,
int streamingRpcBatchLimit,
Duration halfClosePhysicalStreamAfter,
+ Duration maxRetryDuration,
ScheduledExecutorService executor) {
super(
LOG,
@@ -104,6 +105,7 @@ final class GrpcCommitWorkStream
this.idGenerator = idGenerator;
this.jobHeader = jobHeader;
this.streamingRpcBatchLimit = streamingRpcBatchLimit;
+ this.maxRetryDuration = maxRetryDuration;
}
static GrpcCommitWorkStream create(
@@ -118,6 +120,7 @@ final class GrpcCommitWorkStream
AtomicLong idGenerator,
int streamingRpcBatchLimit,
Duration halfClosePhysicalStreamAfter,
+ Duration maxRetryDuration,
ScheduledExecutorService executor) {
return new GrpcCommitWorkStream(
backendWorkerToken,
@@ -130,6 +133,7 @@ final class GrpcCommitWorkStream
idGenerator,
streamingRpcBatchLimit,
halfClosePhysicalStreamAfter,
+ maxRetryDuration,
executor);
}
@@ -224,14 +228,48 @@ final class GrpcCommitWorkStream
failureHandler.throwIfNonEmpty();
}
- @Override
@SuppressWarnings("ReferenceEquality")
+ private boolean belongsToThisHandler(StreamAndRequest streamAndRequest) {
+ return streamAndRequest.handler == this;
+ }
+
+ @Override
public boolean hasPendingRequests() {
- return pending.entrySet().stream().anyMatch(e -> e.getValue().handler ==
this);
+ return pending.entrySet().stream().anyMatch(e ->
belongsToThisHandler(e.getValue()));
}
@Override
+ @SuppressWarnings("ReferenceEquality")
public void onDone(Status status) {
+ if (maxRetryDuration.compareTo(Duration.ZERO) > 0) {
+ // Remove the requests that have exceeded the retry time so they are
not retried.
+ long nowNanos = System.nanoTime();
+ long maxRetryDurationNanos = maxRetryDuration.toNanos();
+ Iterator<Map.Entry<Long, StreamAndRequest>> iterator =
pending.entrySet().iterator();
+ int keptRequests = 0, removedRequests = 0;
+ while (iterator.hasNext()) {
+ StreamAndRequest streamAndRequest =
checkNotNull(iterator.next().getValue());
+ PendingRequest pendingRequest = streamAndRequest.request;
+ if (!belongsToThisHandler(streamAndRequest)
+ || nowNanos - pendingRequest.getStartTimeNanos() <
maxRetryDurationNanos) {
+ ++keptRequests;
+ continue;
+ }
+ ++removedRequests;
+ iterator.remove();
+ try {
+ pendingRequest.completeWithStatus(CommitStatus.ABORTED);
+ } catch (RuntimeException e) {
+ LOG.warn("Exception while aborting commit due to retry timeout.",
e);
+ }
+ }
+ if (removedRequests > 0) {
+ LOG.info(
+ "Aborting {} commits which have exceeded retry deadline, kept
{}. Work will be retried as needed by service.",
+ removedRequests,
+ keptRequests);
+ }
+ }
if (status.isOk() && hasPendingRequests()) {
LOG.warn("Unexpected requests without responses on drained physical
stream, retrying.");
}
@@ -270,7 +308,7 @@ final class GrpcCommitWorkStream
if (requests.size() == 1) {
Map.Entry<Long, PendingRequest> elem =
requests.entrySet().iterator().next();
- if (elem.getValue().request().getSerializedSize()
+ if (elem.getValue().getRequest().getSerializedSize()
> AbstractWindmillStream.RPC_STREAM_CHUNK_SIZE) {
issueMultiChunkRequest(elem.getKey(), elem.getValue());
} else {
@@ -286,7 +324,7 @@ final class GrpcCommitWorkStream
StreamingCommitWorkRequest.Builder requestBuilder =
StreamingCommitWorkRequest.newBuilder();
requestBuilder
.addCommitChunkBuilder()
- .setComputationId(pendingRequest.computationId())
+ .setComputationId(pendingRequest.getComputationId())
.setRequestId(id)
.setShardingKey(pendingRequest.shardingKey())
.setSerializedWorkItemCommit(pendingRequest.serializedCommit());
@@ -311,9 +349,9 @@ final class GrpcCommitWorkStream
for (Map.Entry<Long, PendingRequest> entry : requests.entrySet()) {
PendingRequest request = entry.getValue();
StreamingCommitRequestChunk.Builder chunkBuilder =
requestBuilder.addCommitChunkBuilder();
- if (lastComputation == null ||
!lastComputation.equals(request.computationId())) {
- chunkBuilder.setComputationId(request.computationId());
- lastComputation = request.computationId();
+ if (lastComputation == null ||
!lastComputation.equals(request.getComputationId())) {
+ chunkBuilder.setComputationId(request.getComputationId());
+ lastComputation = request.getComputationId();
}
chunkBuilder
.setRequestId(entry.getKey())
@@ -338,7 +376,7 @@ final class GrpcCommitWorkStream
private void issueMultiChunkRequest(long id, PendingRequest pendingRequest)
throws WindmillStreamShutdownException {
- checkNotNull(pendingRequest.computationId(), "Cannot commit WorkItem w/o a
computationId.");
+ checkNotNull(pendingRequest.getComputationId(), "Cannot commit WorkItem
w/o a computationId.");
ByteString serializedCommit = pendingRequest.serializedCommit();
synchronized (this) {
if (isShutdown) {
@@ -359,7 +397,7 @@ final class GrpcCommitWorkStream
StreamingCommitRequestChunk.newBuilder()
.setRequestId(id)
.setSerializedWorkItemCommit(chunk)
- .setComputationId(pendingRequest.computationId())
+ .setComputationId(pendingRequest.getComputationId())
.setShardingKey(pendingRequest.shardingKey());
int remaining = serializedCommit.size() - end;
if (remaining > 0) {
@@ -376,34 +414,46 @@ final class GrpcCommitWorkStream
}
}
- @AutoValue
- abstract static class PendingRequest {
+ private static class PendingRequest {
+ private final String computationId;
+ private final WorkItemCommitRequest request;
+ private final Consumer<CommitStatus> onDone;
+ private final long startTimeNanos; // System.nanoTime() of when request
began.
- private static PendingRequest create(
+ private PendingRequest(
String computationId, WorkItemCommitRequest request,
Consumer<CommitStatus> onDone) {
- return new AutoValue_GrpcCommitWorkStream_PendingRequest(computationId,
request, onDone);
+ this.computationId = computationId;
+ this.request = request;
+ this.onDone = onDone;
+ this.startTimeNanos = System.nanoTime();
}
- abstract String computationId();
+ String getComputationId() {
+ return computationId;
+ }
- abstract WorkItemCommitRequest request();
+ WorkItemCommitRequest getRequest() {
+ return request;
+ }
- abstract Consumer<CommitStatus> onDone();
+ long getStartTimeNanos() {
+ return startTimeNanos;
+ }
private long getBytes() {
- return (long) request().getSerializedSize() + computationId().length();
+ return (long) request.getSerializedSize() + computationId.length();
}
private ByteString serializedCommit() {
- return request().toByteString();
+ return request.toByteString();
}
private void completeWithStatus(CommitStatus commitStatus) {
- onDone().accept(commitStatus);
+ onDone.accept(commitStatus);
}
private long shardingKey() {
- return request().getShardingKey();
+ return request.getShardingKey();
}
private void abort() {
@@ -462,7 +512,7 @@ final class GrpcCommitWorkStream
return false;
}
- PendingRequest request = PendingRequest.create(computation,
commitRequest, onDone);
+ PendingRequest request = new PendingRequest(computation, commitRequest,
onDone);
add(idGenerator.incrementAndGet(), request);
return true;
}
diff --git
a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/client/grpc/GrpcWindmillStreamFactory.java
b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/client/grpc/GrpcWindmillStreamFactory.java
index 0184b88d53c..97ca3c4e83d 100644
---
a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/client/grpc/GrpcWindmillStreamFactory.java
+++
b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/client/grpc/GrpcWindmillStreamFactory.java
@@ -100,6 +100,7 @@ public class GrpcWindmillStreamFactory implements
StatusDataProvider {
private final Consumer<List<ComputationHeartbeatResponse>>
processHeartbeatResponses;
private final java.time.Duration
directStreamingRpcPhysicalStreamHalfCloseAfter;
private final Supplier<ScheduledExecutorService> executorServiceSupplier;
+ private final java.time.Duration commitWorkStreamRetryTimeout;
private GrpcWindmillStreamFactory(
JobHeader jobHeader,
@@ -111,6 +112,7 @@ public class GrpcWindmillStreamFactory implements
StatusDataProvider {
Consumer<List<ComputationHeartbeatResponse>> processHeartbeatResponses,
Supplier<Duration> maxBackOffSupplier,
java.time.Duration directStreamingRpcPhysicalStreamHalfCloseAfter,
+ java.time.Duration commitWorkStreamRetryTimeout,
Supplier<ScheduledExecutorService> executorServiceSupplier) {
this.jobHeader = jobHeader;
this.logEveryNStreamFailures = logEveryNStreamFailures;
@@ -132,6 +134,7 @@ public class GrpcWindmillStreamFactory implements
StatusDataProvider {
this.directStreamingRpcPhysicalStreamHalfCloseAfter =
directStreamingRpcPhysicalStreamHalfCloseAfter;
this.executorServiceSupplier = executorServiceSupplier;
+ this.commitWorkStreamRetryTimeout = commitWorkStreamRetryTimeout;
}
/** @implNote Used for {@link AutoBuilder} {@link Builder} class, do not
call directly. */
@@ -146,6 +149,7 @@ public class GrpcWindmillStreamFactory implements
StatusDataProvider {
Supplier<Duration> maxBackOffSupplier,
int healthCheckIntervalMillis,
java.time.Duration directStreamingRpcPhysicalStreamHalfCloseAfter,
+ java.time.Duration commitWorkStreamRetryTimeout,
Supplier<ScheduledExecutorService> scheduledExecutorServiceSupplier) {
GrpcWindmillStreamFactory streamFactory =
new GrpcWindmillStreamFactory(
@@ -158,6 +162,7 @@ public class GrpcWindmillStreamFactory implements
StatusDataProvider {
processHeartbeatResponses,
maxBackOffSupplier,
directStreamingRpcPhysicalStreamHalfCloseAfter,
+ commitWorkStreamRetryTimeout,
scheduledExecutorServiceSupplier);
if (healthCheckIntervalMillis >= 0) {
@@ -200,6 +205,7 @@ public class GrpcWindmillStreamFactory implements
StatusDataProvider {
.setProcessHeartbeatResponses(ignored -> {})
.setDirectStreamingRpcPhysicalStreamHalfCloseAfter(
DEFAULT_DIRECT_STREAMING_RPC_PHYSICAL_STREAM_HALF_CLOSE_AFTER)
+ .setCommitWorkStreamRetryTimeout(java.time.Duration.ZERO)
.setScheduledExecutorServiceSupplier(() -> null);
}
@@ -347,6 +353,7 @@ public class GrpcWindmillStreamFactory implements
StatusDataProvider {
streamIdGenerator,
streamingRpcBatchLimit,
java.time.Duration.ZERO,
+ commitWorkStreamRetryTimeout,
executorForDispatchedStreams("CommitWork"));
}
@@ -363,6 +370,7 @@ public class GrpcWindmillStreamFactory implements
StatusDataProvider {
streamIdGenerator,
streamingRpcBatchLimit,
directStreamingRpcPhysicalStreamHalfCloseAfter,
+ java.time.Duration.ZERO,
executorForDirectStreams(connection.backendWorkerToken(),
"CommitWork"));
}
@@ -426,6 +434,8 @@ public class GrpcWindmillStreamFactory implements
StatusDataProvider {
Builder
setDirectStreamingRpcPhysicalStreamHalfCloseAfter(java.time.Duration timeout);
+ Builder setCommitWorkStreamRetryTimeout(java.time.Duration timeout);
+
Builder setScheduledExecutorServiceSupplier(
Supplier<ScheduledExecutorService> scheduledExecutorServiceSupplier);
diff --git
a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/windmill/client/grpc/GrpcCommitWorkStreamTest.java
b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/windmill/client/grpc/GrpcCommitWorkStreamTest.java
index e9fd55fa566..9c3d5c9c3ef 100644
---
a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/windmill/client/grpc/GrpcCommitWorkStreamTest.java
+++
b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/windmill/client/grpc/GrpcCommitWorkStreamTest.java
@@ -38,6 +38,7 @@ import java.util.concurrent.ExecutionException;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Supplier;
import
org.apache.beam.runners.dataflow.worker.windmill.CloudWindmillServiceV1Alpha1Grpc;
import org.apache.beam.runners.dataflow.worker.windmill.Windmill;
@@ -1133,6 +1134,87 @@ public class GrpcCommitWorkStreamTest {
assertTrue(commitWorkStream.awaitTermination(10, TimeUnit.SECONDS));
}
+ @Test
+ public void testCommitWorkItem_stopsRetriesAfterDuration() throws Exception {
+ int numCommits = 1;
+ CountDownLatch commitProcessed = new CountDownLatch(numCommits);
+ AtomicReference<Windmill.CommitStatus> commitStatus = new
AtomicReference<>();
+ GrpcCommitWorkStream commitWorkStream =
+ (GrpcCommitWorkStream)
+ GrpcWindmillStreamFactory.of(TEST_JOB_HEADER)
+
.setDirectStreamingRpcPhysicalStreamHalfCloseAfter(Duration.ofMinutes(1))
+ .setCommitWorkStreamRetryTimeout(Duration.ofNanos(1))
+ .build()
+
.createCommitWorkStream(CloudWindmillServiceV1Alpha1Grpc.newStub(inProcessChannel));
+ commitWorkStream.start();
+
+ FakeWindmillGrpcService.CommitStreamInfo streamInfo =
waitForConnectionAndConsumeHeader();
+ try (WindmillStream.CommitWorkStream.RequestBatcher batcher =
commitWorkStream.batcher()) {
+ assertTrue(
+ batcher.commitWorkItem(
+ COMPUTATION_ID,
+ workItemCommitRequest(0),
+ status -> {
+ commitStatus.set(status);
+ commitProcessed.countDown();
+ }));
+ }
+
+ // The next request should have some chunks.
+ assertThat(streamInfo.requests.take().getCommitChunkList()).isNotEmpty();
+
+ // We won't get responses so we will have some pending requests.
+ assertThat(commitProcessed.getCount()).isGreaterThan(0);
+ streamInfo.responseObserver.onError(new IOException("test error"));
+ commitProcessed.await();
+ assertThat(commitStatus.get()).isEqualTo(Windmill.CommitStatus.ABORTED);
+ }
+
+ @Test
+ public void testCommitWorkItem_retriesWithLongerCommitRetryTimeout() throws
Exception {
+ int numCommits = 1;
+ CountDownLatch commitProcessed = new CountDownLatch(numCommits);
+ AtomicReference<Windmill.CommitStatus> commitStatus = new
AtomicReference<>();
+ GrpcCommitWorkStream commitWorkStream =
+ (GrpcCommitWorkStream)
+ GrpcWindmillStreamFactory.of(TEST_JOB_HEADER)
+
.setDirectStreamingRpcPhysicalStreamHalfCloseAfter(Duration.ofMinutes(1))
+ // Verifies that if this is set but is not exceeded that a
retry occurs.
+ .setCommitWorkStreamRetryTimeout(Duration.ofMinutes(10))
+ .build()
+
.createCommitWorkStream(CloudWindmillServiceV1Alpha1Grpc.newStub(inProcessChannel));
+ commitWorkStream.start();
+
+ FakeWindmillGrpcService.CommitStreamInfo streamInfo =
waitForConnectionAndConsumeHeader();
+ try (WindmillStream.CommitWorkStream.RequestBatcher batcher =
commitWorkStream.batcher()) {
+ assertTrue(
+ batcher.commitWorkItem(
+ COMPUTATION_ID,
+ workItemCommitRequest(0),
+ status -> {
+ commitStatus.set(status);
+ commitProcessed.countDown();
+ }));
+ }
+
+ // The next request should have some chunks.
+ assertThat(streamInfo.requests.take().getCommitChunkList()).isNotEmpty();
+
+ // We won't get responses so we will have some pending requests.
+ assertThat(commitProcessed.getCount()).isGreaterThan(0);
+ streamInfo.responseObserver.onError(new IOException("test error"));
+
+ // The stream should reconnect and retry the requests.
+ FakeWindmillGrpcService.CommitStreamInfo reconnectStreamInfo =
+ waitForConnectionAndConsumeHeader();
+ Windmill.StreamingCommitWorkRequest reconnectRequest =
reconnectStreamInfo.requests.take();
+ assertEquals(1, reconnectRequest.getCommitChunkCount());
+ reconnectStreamInfo.responseObserver.onNext(
+ Windmill.StreamingCommitResponse.newBuilder().addRequestId(1).build());
+ commitProcessed.await();
+ assertThat(commitStatus.get()).isEqualTo(Windmill.CommitStatus.OK);
+ }
+
private FakeWindmillGrpcService.CommitStreamInfo
waitForConnectionAndConsumeHeader() {
try {
FakeWindmillGrpcService.CommitStreamInfo info =
fakeService.waitForConnectedCommitStream();