Skip to content

Commit f33feaf

Browse files
authored
[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.
1 parent 0165344 commit f33feaf

5 files changed

Lines changed: 183 additions & 22 deletions

File tree

runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/options/DataflowStreamingPipelineOptions.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,13 @@ public interface DataflowStreamingPipelineOptions extends PipelineOptions {
196196

197197
void setStuckCommitDurationMillis(int value);
198198

199+
@Description(
200+
"Retry commits on stream errors until this much time has elapsed since the commit was scheduled. If zero, retry forever.")
201+
@Default.InstanceFactory(CommitWorkStreamRetryTimeoutMillisFactory.class)
202+
long getCommitWorkStreamRetryTimeoutMillis();
203+
204+
void setCommitWorkStreamRetryTimeoutMillis(long value);
205+
199206
@Description(
200207
"Period for sending 'global get config' requests to the service. The duration is "
201208
+ "specified as seconds in 'PTx.yS' format, e.g. 'PT5.125S'."
@@ -343,4 +350,14 @@ public Boolean create(PipelineOptions options) {
343350
return ExperimentalOptions.hasExperiment(options, "enable_windmill_service_direct_path");
344351
}
345352
}
353+
354+
class CommitWorkStreamRetryTimeoutMillisFactory implements DefaultValueFactory<Long> {
355+
@Override
356+
public Long create(PipelineOptions options) {
357+
if (ExperimentalOptions.hasExperiment(options, "disable_commit_retry_timeout")) {
358+
return 0L;
359+
}
360+
return Duration.standardMinutes(30).getMillis();
361+
}
362+
}
346363
}

runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/StreamingDataflowWorker.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -756,6 +756,8 @@ public static StreamingDataflowWorker fromOptions(DataflowWorkerHarnessOptions o
756756
new WorkHeartbeatResponseProcessor(computationStateCache::get))
757757
.setHealthCheckIntervalMillis(
758758
options.getWindmillServiceStreamingRpcHealthCheckPeriodMs())
759+
.setCommitWorkStreamRetryTimeout(
760+
java.time.Duration.ofMillis(options.getCommitWorkStreamRetryTimeoutMillis()))
759761
.build();
760762
return ConfigFetcherComputationStateCacheAndWindmillClient.builder()
761763
.setWindmillDispatcherClient(dispatcherClient)

runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/client/grpc/GrpcCommitWorkStream.java

Lines changed: 72 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkNotNull;
2121
import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkState;
2222

23-
import com.google.auto.value.AutoValue;
2423
import java.io.PrintWriter;
2524
import java.time.Duration;
2625
import java.util.HashMap;
@@ -77,6 +76,7 @@ private static class StreamAndRequest {
7776
private final JobHeader jobHeader;
7877
private final int streamingRpcBatchLimit;
7978
private volatile boolean logMissingResponse = true;
79+
private final Duration maxRetryDuration;
8080

8181
private GrpcCommitWorkStream(
8282
String backendWorkerToken,
@@ -90,6 +90,7 @@ private GrpcCommitWorkStream(
9090
AtomicLong idGenerator,
9191
int streamingRpcBatchLimit,
9292
Duration halfClosePhysicalStreamAfter,
93+
Duration maxRetryDuration,
9394
ScheduledExecutorService executor) {
9495
super(
9596
LOG,
@@ -104,6 +105,7 @@ private GrpcCommitWorkStream(
104105
this.idGenerator = idGenerator;
105106
this.jobHeader = jobHeader;
106107
this.streamingRpcBatchLimit = streamingRpcBatchLimit;
108+
this.maxRetryDuration = maxRetryDuration;
107109
}
108110

109111
static GrpcCommitWorkStream create(
@@ -118,6 +120,7 @@ static GrpcCommitWorkStream create(
118120
AtomicLong idGenerator,
119121
int streamingRpcBatchLimit,
120122
Duration halfClosePhysicalStreamAfter,
123+
Duration maxRetryDuration,
121124
ScheduledExecutorService executor) {
122125
return new GrpcCommitWorkStream(
123126
backendWorkerToken,
@@ -130,6 +133,7 @@ static GrpcCommitWorkStream create(
130133
idGenerator,
131134
streamingRpcBatchLimit,
132135
halfClosePhysicalStreamAfter,
136+
maxRetryDuration,
133137
executor);
134138
}
135139

@@ -224,14 +228,48 @@ public void onResponse(StreamingCommitResponse response) {
224228
failureHandler.throwIfNonEmpty();
225229
}
226230

227-
@Override
228231
@SuppressWarnings("ReferenceEquality")
232+
private boolean belongsToThisHandler(StreamAndRequest streamAndRequest) {
233+
return streamAndRequest.handler == this;
234+
}
235+
236+
@Override
229237
public boolean hasPendingRequests() {
230-
return pending.entrySet().stream().anyMatch(e -> e.getValue().handler == this);
238+
return pending.entrySet().stream().anyMatch(e -> belongsToThisHandler(e.getValue()));
231239
}
232240

233241
@Override
242+
@SuppressWarnings("ReferenceEquality")
234243
public void onDone(Status status) {
244+
if (maxRetryDuration.compareTo(Duration.ZERO) > 0) {
245+
// Remove the requests that have exceeded the retry time so they are not retried.
246+
long nowNanos = System.nanoTime();
247+
long maxRetryDurationNanos = maxRetryDuration.toNanos();
248+
Iterator<Map.Entry<Long, StreamAndRequest>> iterator = pending.entrySet().iterator();
249+
int keptRequests = 0, removedRequests = 0;
250+
while (iterator.hasNext()) {
251+
StreamAndRequest streamAndRequest = checkNotNull(iterator.next().getValue());
252+
PendingRequest pendingRequest = streamAndRequest.request;
253+
if (!belongsToThisHandler(streamAndRequest)
254+
|| nowNanos - pendingRequest.getStartTimeNanos() < maxRetryDurationNanos) {
255+
++keptRequests;
256+
continue;
257+
}
258+
++removedRequests;
259+
iterator.remove();
260+
try {
261+
pendingRequest.completeWithStatus(CommitStatus.ABORTED);
262+
} catch (RuntimeException e) {
263+
LOG.warn("Exception while aborting commit due to retry timeout.", e);
264+
}
265+
}
266+
if (removedRequests > 0) {
267+
LOG.info(
268+
"Aborting {} commits which have exceeded retry deadline, kept {}. Work will be retried as needed by service.",
269+
removedRequests,
270+
keptRequests);
271+
}
272+
}
235273
if (status.isOk() && hasPendingRequests()) {
236274
LOG.warn("Unexpected requests without responses on drained physical stream, retrying.");
237275
}
@@ -270,7 +308,7 @@ private void flushInternal(Map<Long, PendingRequest> requests)
270308

271309
if (requests.size() == 1) {
272310
Map.Entry<Long, PendingRequest> elem = requests.entrySet().iterator().next();
273-
if (elem.getValue().request().getSerializedSize()
311+
if (elem.getValue().getRequest().getSerializedSize()
274312
> AbstractWindmillStream.RPC_STREAM_CHUNK_SIZE) {
275313
issueMultiChunkRequest(elem.getKey(), elem.getValue());
276314
} else {
@@ -286,7 +324,7 @@ private void issueSingleRequest(long id, PendingRequest pendingRequest)
286324
StreamingCommitWorkRequest.Builder requestBuilder = StreamingCommitWorkRequest.newBuilder();
287325
requestBuilder
288326
.addCommitChunkBuilder()
289-
.setComputationId(pendingRequest.computationId())
327+
.setComputationId(pendingRequest.getComputationId())
290328
.setRequestId(id)
291329
.setShardingKey(pendingRequest.shardingKey())
292330
.setSerializedWorkItemCommit(pendingRequest.serializedCommit());
@@ -311,9 +349,9 @@ private void issueBatchedRequest(Map<Long, PendingRequest> requests)
311349
for (Map.Entry<Long, PendingRequest> entry : requests.entrySet()) {
312350
PendingRequest request = entry.getValue();
313351
StreamingCommitRequestChunk.Builder chunkBuilder = requestBuilder.addCommitChunkBuilder();
314-
if (lastComputation == null || !lastComputation.equals(request.computationId())) {
315-
chunkBuilder.setComputationId(request.computationId());
316-
lastComputation = request.computationId();
352+
if (lastComputation == null || !lastComputation.equals(request.getComputationId())) {
353+
chunkBuilder.setComputationId(request.getComputationId());
354+
lastComputation = request.getComputationId();
317355
}
318356
chunkBuilder
319357
.setRequestId(entry.getKey())
@@ -338,7 +376,7 @@ private void issueBatchedRequest(Map<Long, PendingRequest> requests)
338376

339377
private void issueMultiChunkRequest(long id, PendingRequest pendingRequest)
340378
throws WindmillStreamShutdownException {
341-
checkNotNull(pendingRequest.computationId(), "Cannot commit WorkItem w/o a computationId.");
379+
checkNotNull(pendingRequest.getComputationId(), "Cannot commit WorkItem w/o a computationId.");
342380
ByteString serializedCommit = pendingRequest.serializedCommit();
343381
synchronized (this) {
344382
if (isShutdown) {
@@ -359,7 +397,7 @@ private void issueMultiChunkRequest(long id, PendingRequest pendingRequest)
359397
StreamingCommitRequestChunk.newBuilder()
360398
.setRequestId(id)
361399
.setSerializedWorkItemCommit(chunk)
362-
.setComputationId(pendingRequest.computationId())
400+
.setComputationId(pendingRequest.getComputationId())
363401
.setShardingKey(pendingRequest.shardingKey());
364402
int remaining = serializedCommit.size() - end;
365403
if (remaining > 0) {
@@ -376,34 +414,46 @@ private void issueMultiChunkRequest(long id, PendingRequest pendingRequest)
376414
}
377415
}
378416

379-
@AutoValue
380-
abstract static class PendingRequest {
417+
private static class PendingRequest {
418+
private final String computationId;
419+
private final WorkItemCommitRequest request;
420+
private final Consumer<CommitStatus> onDone;
421+
private final long startTimeNanos; // System.nanoTime() of when request began.
381422

382-
private static PendingRequest create(
423+
private PendingRequest(
383424
String computationId, WorkItemCommitRequest request, Consumer<CommitStatus> onDone) {
384-
return new AutoValue_GrpcCommitWorkStream_PendingRequest(computationId, request, onDone);
425+
this.computationId = computationId;
426+
this.request = request;
427+
this.onDone = onDone;
428+
this.startTimeNanos = System.nanoTime();
385429
}
386430

387-
abstract String computationId();
431+
String getComputationId() {
432+
return computationId;
433+
}
388434

389-
abstract WorkItemCommitRequest request();
435+
WorkItemCommitRequest getRequest() {
436+
return request;
437+
}
390438

391-
abstract Consumer<CommitStatus> onDone();
439+
long getStartTimeNanos() {
440+
return startTimeNanos;
441+
}
392442

393443
private long getBytes() {
394-
return (long) request().getSerializedSize() + computationId().length();
444+
return (long) request.getSerializedSize() + computationId.length();
395445
}
396446

397447
private ByteString serializedCommit() {
398-
return request().toByteString();
448+
return request.toByteString();
399449
}
400450

401451
private void completeWithStatus(CommitStatus commitStatus) {
402-
onDone().accept(commitStatus);
452+
onDone.accept(commitStatus);
403453
}
404454

405455
private long shardingKey() {
406-
return request().getShardingKey();
456+
return request.getShardingKey();
407457
}
408458

409459
private void abort() {
@@ -462,7 +512,7 @@ public boolean commitWorkItem(
462512
return false;
463513
}
464514

465-
PendingRequest request = PendingRequest.create(computation, commitRequest, onDone);
515+
PendingRequest request = new PendingRequest(computation, commitRequest, onDone);
466516
add(idGenerator.incrementAndGet(), request);
467517
return true;
468518
}

runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/client/grpc/GrpcWindmillStreamFactory.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ public class GrpcWindmillStreamFactory implements StatusDataProvider {
100100
private final Consumer<List<ComputationHeartbeatResponse>> processHeartbeatResponses;
101101
private final java.time.Duration directStreamingRpcPhysicalStreamHalfCloseAfter;
102102
private final Supplier<ScheduledExecutorService> executorServiceSupplier;
103+
private final java.time.Duration commitWorkStreamRetryTimeout;
103104

104105
private GrpcWindmillStreamFactory(
105106
JobHeader jobHeader,
@@ -111,6 +112,7 @@ private GrpcWindmillStreamFactory(
111112
Consumer<List<ComputationHeartbeatResponse>> processHeartbeatResponses,
112113
Supplier<Duration> maxBackOffSupplier,
113114
java.time.Duration directStreamingRpcPhysicalStreamHalfCloseAfter,
115+
java.time.Duration commitWorkStreamRetryTimeout,
114116
Supplier<ScheduledExecutorService> executorServiceSupplier) {
115117
this.jobHeader = jobHeader;
116118
this.logEveryNStreamFailures = logEveryNStreamFailures;
@@ -132,6 +134,7 @@ private GrpcWindmillStreamFactory(
132134
this.directStreamingRpcPhysicalStreamHalfCloseAfter =
133135
directStreamingRpcPhysicalStreamHalfCloseAfter;
134136
this.executorServiceSupplier = executorServiceSupplier;
137+
this.commitWorkStreamRetryTimeout = commitWorkStreamRetryTimeout;
135138
}
136139

137140
/** @implNote Used for {@link AutoBuilder} {@link Builder} class, do not call directly. */
@@ -146,6 +149,7 @@ static GrpcWindmillStreamFactory create(
146149
Supplier<Duration> maxBackOffSupplier,
147150
int healthCheckIntervalMillis,
148151
java.time.Duration directStreamingRpcPhysicalStreamHalfCloseAfter,
152+
java.time.Duration commitWorkStreamRetryTimeout,
149153
Supplier<ScheduledExecutorService> scheduledExecutorServiceSupplier) {
150154
GrpcWindmillStreamFactory streamFactory =
151155
new GrpcWindmillStreamFactory(
@@ -158,6 +162,7 @@ static GrpcWindmillStreamFactory create(
158162
processHeartbeatResponses,
159163
maxBackOffSupplier,
160164
directStreamingRpcPhysicalStreamHalfCloseAfter,
165+
commitWorkStreamRetryTimeout,
161166
scheduledExecutorServiceSupplier);
162167

163168
if (healthCheckIntervalMillis >= 0) {
@@ -200,6 +205,7 @@ public static GrpcWindmillStreamFactory.Builder of(JobHeader jobHeader) {
200205
.setProcessHeartbeatResponses(ignored -> {})
201206
.setDirectStreamingRpcPhysicalStreamHalfCloseAfter(
202207
DEFAULT_DIRECT_STREAMING_RPC_PHYSICAL_STREAM_HALF_CLOSE_AFTER)
208+
.setCommitWorkStreamRetryTimeout(java.time.Duration.ZERO)
203209
.setScheduledExecutorServiceSupplier(() -> null);
204210
}
205211

@@ -347,6 +353,7 @@ public CommitWorkStream createCommitWorkStream(CloudWindmillServiceV1Alpha1Stub
347353
streamIdGenerator,
348354
streamingRpcBatchLimit,
349355
java.time.Duration.ZERO,
356+
commitWorkStreamRetryTimeout,
350357
executorForDispatchedStreams("CommitWork"));
351358
}
352359

@@ -363,6 +370,7 @@ public CommitWorkStream createDirectCommitWorkStream(WindmillConnection connecti
363370
streamIdGenerator,
364371
streamingRpcBatchLimit,
365372
directStreamingRpcPhysicalStreamHalfCloseAfter,
373+
java.time.Duration.ZERO,
366374
executorForDirectStreams(connection.backendWorkerToken(), "CommitWork"));
367375
}
368376

@@ -426,6 +434,8 @@ Builder setProcessHeartbeatResponses(
426434

427435
Builder setDirectStreamingRpcPhysicalStreamHalfCloseAfter(java.time.Duration timeout);
428436

437+
Builder setCommitWorkStreamRetryTimeout(java.time.Duration timeout);
438+
429439
Builder setScheduledExecutorServiceSupplier(
430440
Supplier<ScheduledExecutorService> scheduledExecutorServiceSupplier);
431441

0 commit comments

Comments
 (0)