This is an automated email from the ASF dual-hosted git repository.
Abacn 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 dd896e2b239 [Dataflow Streaming] [Multi Key] Drop failed work in
BoundedQueueExecutor::pollWork (#38920)
dd896e2b239 is described below
commit dd896e2b2395937201b7721251d9f66936f9340c
Author: Arun Pandian <[email protected]>
AuthorDate: Fri Aug 14 17:37:18 2026 -0700
[Dataflow Streaming] [Multi Key] Drop failed work in
BoundedQueueExecutor::pollWork (#38920)
* Drop failed work in BoundedQueueExecutor::pollWork
* address comment
* Revert "address comment"
This reverts commit 3670a036aa83a3c18e18f3b9435214f3cbb0ad13.
* Revert "Drop failed work in BoundedQueueExecutor::pollWork"
This reverts commit 2c1278636a603fb5e06a5f5bf6f179b0fbf41d7c.
* [Dataflow Streaming] Remove finalizeCommits from processWork
* Plumb ComputationState to ProcessingContext
* Drop failed workitems during pollwork
* Improve tests
* address comments
* address comments
* address comments
---
.../worker/StreamingModeExecutionContext.java | 17 ++-
.../worker/streaming/ComputationWorkExecutor.java | 6 +-
.../worker/streaming/FailedWorkHandler.java | 24 ++++
.../dataflow/worker/util/BoundedQueueExecutor.java | 26 +++-
.../work/processing/StreamingWorkScheduler.java | 22 +--
.../processing/failures/WorkFailureProcessor.java | 6 +-
.../worker/StreamingDataflowWorkerTest.java | 154 ++++++++++++++++++++-
.../worker/StreamingModeExecutionContextTest.java | 95 +++++++++++--
.../dataflow/worker/WorkerCustomSourcesTest.java | 9 +-
.../worker/util/BoundedQueueExecutorTest.java | 129 ++++++++++++++++-
10 files changed, 442 insertions(+), 46 deletions(-)
diff --git
a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/StreamingModeExecutionContext.java
b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/StreamingModeExecutionContext.java
index 68dbd61f15f..365ebbdc1f9 100644
---
a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/StreamingModeExecutionContext.java
+++
b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/StreamingModeExecutionContext.java
@@ -53,6 +53,7 @@ import
org.apache.beam.runners.dataflow.worker.counters.NameContext;
import
org.apache.beam.runners.dataflow.worker.profiler.ScopedProfiler.ProfileScope;
import
org.apache.beam.runners.dataflow.worker.streaming.BoundedQueueExecutorWorkHandle;
import org.apache.beam.runners.dataflow.worker.streaming.ExecutableWork;
+import org.apache.beam.runners.dataflow.worker.streaming.FailedWorkHandler;
import
org.apache.beam.runners.dataflow.worker.streaming.KeyCommitTooLargeException;
import org.apache.beam.runners.dataflow.worker.streaming.Watermarks;
import org.apache.beam.runners.dataflow.worker.streaming.Work;
@@ -172,10 +173,7 @@ public class StreamingModeExecutionContext
private @Nullable WorkExecutor workExecutor;
private boolean finishKeyCalled = false;
- @SuppressWarnings("UnusedVariable")
private @Nullable BoundedQueueExecutor workQueueExecutor;
-
- @SuppressWarnings("UnusedVariable")
private @Nullable BoundedQueueExecutorWorkHandle budgetHandle;
private final HotKeyLogger hotKeyLogger;
@@ -192,8 +190,8 @@ public class StreamingModeExecutionContext
void onKeyTransition(@Nullable Work oldWork, Work newWork);
}
- @SuppressWarnings("UnusedVariable")
private @Nullable KeyTransitionListener keyTransitionListener;
+ private @Nullable FailedWorkHandler onFailedWorkHandler;
private List<Work> executedWorks = Collections.emptyList();
private List<Windmill.WorkItemCommitRequest.Builder> outputBuilders =
Collections.emptyList();
@@ -335,6 +333,7 @@ public class StreamingModeExecutionContext
this.workQueueExecutor = null;
this.budgetHandle = null;
this.keyTransitionListener = null;
+ this.onFailedWorkHandler = null;
this.work = null;
this.key = null;
this.outputBuilder = null;
@@ -350,7 +349,8 @@ public class StreamingModeExecutionContext
BoundedQueueExecutor workQueueExecutor,
BoundedQueueExecutorWorkHandle budgetHandle,
@Nullable Coder<?> keyCoder,
- KeyTransitionListener keyTransitionListener)
+ KeyTransitionListener keyTransitionListener,
+ FailedWorkHandler onFailedWorkHandler)
throws CoderException {
reset();
this.executedWorks = new ArrayList<>();
@@ -361,6 +361,7 @@ public class StreamingModeExecutionContext
this.workQueueExecutor = workQueueExecutor;
this.budgetHandle = budgetHandle;
this.keyTransitionListener = keyTransitionListener;
+ this.onFailedWorkHandler = checkStateNotNull(onFailedWorkHandler);
this.workItemsPolled = 1;
this.bundleStartTimeNanos = System.nanoTime();
@@ -779,7 +780,11 @@ public class StreamingModeExecutionContext
@Nullable
ExecutableWork additionalWork =
- executor.pollWork(computationId, activeWork.getKeyGroup(), handle);
+ executor.pollWork(
+ computationId,
+ activeWork.getKeyGroup(),
+ handle,
+ checkStateNotNull(onFailedWorkHandler));
if (additionalWork != null) {
flushStateInternal();
Work newWork = additionalWork.work();
diff --git
a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/streaming/ComputationWorkExecutor.java
b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/streaming/ComputationWorkExecutor.java
index 9391b842f03..dabf72ba4ea 100644
---
a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/streaming/ComputationWorkExecutor.java
+++
b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/streaming/ComputationWorkExecutor.java
@@ -65,7 +65,8 @@ public abstract class ComputationWorkExecutor {
Work work,
BoundedQueueExecutor workQueueExecutor,
BoundedQueueExecutorWorkHandle budgetHandle,
- KeyTransitionListener keyTransitionListener)
+ KeyTransitionListener keyTransitionListener,
+ FailedWorkHandler onFailedWorkHandler)
throws Exception {
context()
.start(
@@ -74,7 +75,8 @@ public abstract class ComputationWorkExecutor {
workQueueExecutor,
budgetHandle,
keyCoder().orElse(null),
- keyTransitionListener);
+ keyTransitionListener,
+ onFailedWorkHandler);
workExecutor().execute();
return context();
}
diff --git
a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/streaming/FailedWorkHandler.java
b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/streaming/FailedWorkHandler.java
new file mode 100644
index 00000000000..683ecf5d660
--- /dev/null
+++
b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/streaming/FailedWorkHandler.java
@@ -0,0 +1,24 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.beam.runners.dataflow.worker.streaming;
+
+/** Handler for failed {@link Work}. */
+@FunctionalInterface
+public interface FailedWorkHandler {
+ void onFailedWork(Work work);
+}
diff --git
a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/util/BoundedQueueExecutor.java
b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/util/BoundedQueueExecutor.java
index 9eb9a37b1b7..2dd0f971168 100644
---
a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/util/BoundedQueueExecutor.java
+++
b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/util/BoundedQueueExecutor.java
@@ -18,6 +18,7 @@
package org.apache.beam.runners.dataflow.worker.util;
import static org.apache.beam.sdk.util.Preconditions.checkArgumentNotNull;
+import static org.apache.beam.sdk.util.Preconditions.checkStateNotNull;
import static
org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkArgument;
import java.util.ArrayList;
@@ -31,6 +32,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
import javax.annotation.concurrent.GuardedBy;
import
org.apache.beam.runners.dataflow.worker.streaming.BoundedQueueExecutorWorkHandle;
import org.apache.beam.runners.dataflow.worker.streaming.ExecutableWork;
+import org.apache.beam.runners.dataflow.worker.streaming.FailedWorkHandler;
import org.apache.beam.runners.dataflow.worker.streaming.Work;
import
org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.annotations.VisibleForTesting;
import
org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions;
@@ -387,20 +389,32 @@ public class BoundedQueueExecutor {
}
public @Nullable ExecutableWork pollWork(
- String computationId, Work.KeyGroup keyGroup,
BoundedQueueExecutorWorkHandle handle) {
+ String computationId,
+ Work.KeyGroup keyGroup,
+ BoundedQueueExecutorWorkHandle handle,
+ FailedWorkHandler onFailedWorkHandler) {
checkArgument(
computationId != null && keyGroup != null &&
!keyGroup.equals(Work.KeyGroup.DEFAULT));
checkArgument(handle instanceof BoundedQueueExecutorWorkHandleImpl);
+ checkStateNotNull(onFailedWorkHandler);
BoundedQueueExecutorWorkHandleImpl internalHandle =
(BoundedQueueExecutorWorkHandleImpl) handle;
if (keyGroupWorkQueue == null) {
return null;
}
- @Nullable QueuedWork queuedWork =
keyGroupWorkQueue.pollWork(computationId, keyGroup);
- if (queuedWork == null) {
- return null;
+ while (true) {
+ @Nullable QueuedWork queuedWork =
keyGroupWorkQueue.pollWork(computationId, keyGroup);
+ if (queuedWork == null) {
+ return null;
+ }
+ Work work = queuedWork.getWork().work();
+ if (work.isFailed()) {
+ queuedWork.getHandle().close();
+ onFailedWorkHandler.onFailedWork(work);
+ continue;
+ }
+ internalHandle.merge(queuedWork.getHandle());
+ return queuedWork.getWork();
}
- internalHandle.merge(queuedWork.getHandle());
- return queuedWork.getWork();
}
private void decrementCounters(int elements, long bytes) {
diff --git
a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/work/processing/StreamingWorkScheduler.java
b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/work/processing/StreamingWorkScheduler.java
index 27952569e0e..63cfad5a9a6 100644
---
a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/work/processing/StreamingWorkScheduler.java
+++
b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/work/processing/StreamingWorkScheduler.java
@@ -45,6 +45,7 @@ import
org.apache.beam.runners.dataflow.worker.streaming.BoundedQueueExecutorWor
import org.apache.beam.runners.dataflow.worker.streaming.ComputationState;
import
org.apache.beam.runners.dataflow.worker.streaming.ComputationWorkExecutor;
import org.apache.beam.runners.dataflow.worker.streaming.ExecutableWork;
+import org.apache.beam.runners.dataflow.worker.streaming.FailedWorkHandler;
import org.apache.beam.runners.dataflow.worker.streaming.StageInfo;
import org.apache.beam.runners.dataflow.worker.streaming.Watermarks;
import org.apache.beam.runners.dataflow.worker.streaming.Work;
@@ -321,8 +322,11 @@ public class StreamingWorkScheduler {
try {
StreamingModeExecutionContext context =
computationWorkExecutor.context();
+ FailedWorkHandler onFailedWorkHandler =
getFailedWorkHandler(computationState);
+
// Blocks while executing work.
- computationWorkExecutor.executeWork(work, workExecutor, handle,
keyTransitionListener);
+ computationWorkExecutor.executeWork(
+ work, workExecutor, handle, keyTransitionListener,
onFailedWorkHandler);
List<Work> workBatch;
List<Windmill.WorkItemCommitRequest> workItemCommits;
@@ -465,14 +469,10 @@ public class StreamingWorkScheduler {
ExecutableWork.create(w, (retry, h) ->
processWork(computationState, retry, h)));
}
+ FailedWorkHandler onFailedWorkHandler =
getFailedWorkHandler(computationState);
+
workFailureProcessor.logAndProcessFailureBatch(
- computationId,
- systemName,
- executableWorks,
- t,
- invalidWork ->
- computationState.completeWorkAndScheduleNextWorkForKey(
- invalidWork.getShardedKey(), invalidWork.id()));
+ computationId, systemName, executableWorks, t, onFailedWorkHandler);
} catch (OutOfMemoryError oom) {
throw oom;
} catch (Throwable t2) {
@@ -481,6 +481,12 @@ public class StreamingWorkScheduler {
}
}
+ private static FailedWorkHandler getFailedWorkHandler(ComputationState
computationState) {
+ return failedWork ->
+ computationState.completeWorkAndScheduleNextWorkForKey(
+ failedWork.getShardedKey(), failedWork.id());
+ }
+
private void recordProcessingTime(
StageInfo stageInfo, List<Work> workBatch, long
processingStartTimeNanos) {
long processingTimeMsecs =
diff --git
a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/work/processing/failures/WorkFailureProcessor.java
b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/work/processing/failures/WorkFailureProcessor.java
index de33d3d3961..d23c870178e 100644
---
a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/work/processing/failures/WorkFailureProcessor.java
+++
b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/work/processing/failures/WorkFailureProcessor.java
@@ -19,12 +19,12 @@ package
org.apache.beam.runners.dataflow.worker.windmill.work.processing.failure
import java.util.List;
import java.util.concurrent.TimeUnit;
-import java.util.function.Consumer;
import java.util.function.Supplier;
import javax.annotation.Nullable;
import javax.annotation.concurrent.ThreadSafe;
import
org.apache.beam.runners.dataflow.worker.status.LastExceptionDataProvider;
import org.apache.beam.runners.dataflow.worker.streaming.ExecutableWork;
+import org.apache.beam.runners.dataflow.worker.streaming.FailedWorkHandler;
import org.apache.beam.runners.dataflow.worker.streaming.Work;
import org.apache.beam.runners.dataflow.worker.util.BoundedQueueExecutor;
import org.apache.beam.sdk.annotations.Internal;
@@ -103,7 +103,7 @@ public final class WorkFailureProcessor {
String systemName,
List<ExecutableWork> executableWorks,
Throwable t,
- Consumer<Work> onInvalidWork)
+ FailedWorkHandler onFailedWorkHandler)
throws Throwable {
List<ExecutableWork> worksToRetryLocally = new java.util.ArrayList<>();
@@ -112,7 +112,7 @@ public final class WorkFailureProcessor {
case DO_NOT_RETRY:
// Consider the item invalid. It will eventually be retried by
Windmill if it still needs
// to be processed.
- onInvalidWork.accept(executableWork.work());
+ onFailedWorkHandler.onFailedWork(executableWork.work());
break;
case RETRY_LOCALLY:
// Try again after some delay and at the end of the queue to avoid a
tight loop.
diff --git
a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/StreamingDataflowWorkerTest.java
b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/StreamingDataflowWorkerTest.java
index 2d35da51a79..c48b30ecf64 100644
---
a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/StreamingDataflowWorkerTest.java
+++
b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/StreamingDataflowWorkerTest.java
@@ -1522,13 +1522,146 @@ public class StreamingDataflowWorkerTest {
worker.stop();
}
+ @Test
+ public void
testMultiKeyCommit_queuedWorkItemFailsAndSubsequentWorkItemPickedUp()
+ throws Exception {
+ if (!streamingEngine) {
+ return;
+ }
+ BlockingKvDoFn.reset();
+ StreamingDataflowWorker worker = makeMultiKeyEnabledWorker(new
BlockingKvDoFn());
+ worker.start();
+
+ String batchInputText1 =
+ "work {"
+ + " computation_id: \""
+ + DEFAULT_COMPUTATION_ID
+ + "\""
+ + " input_data_watermark: 0"
+ + " work {"
+ + " key: \"key1\""
+ + " sharding_key: 1"
+ + " work_token: 1"
+ + " cache_token: 2"
+ + " key_group { high: 0 low: 1 }"
+ + " message_bundles {"
+ + " source_computation_id: \""
+ + DEFAULT_SOURCE_COMPUTATION_ID
+ + "\""
+ + " messages {"
+ + " timestamp: 0"
+ + " data: \"data1\""
+ + " }"
+ + " }"
+ + " }"
+ + " work {"
+ + " key: \"key2\""
+ + " sharding_key: 2"
+ + " work_token: 2"
+ + " cache_token: 3"
+ + " key_group { high: 0 low: 1 }"
+ + " message_bundles {"
+ + " source_computation_id: \""
+ + DEFAULT_SOURCE_COMPUTATION_ID
+ + "\""
+ + " messages {"
+ + " timestamp: 0"
+ + " data: \"data2\""
+ + " }"
+ + " }"
+ + " }"
+ + "}";
+
+ String batchInputText2 =
+ "work {"
+ + " computation_id: \""
+ + DEFAULT_COMPUTATION_ID
+ + "\""
+ + " input_data_watermark: 0"
+ + " work {"
+ + " key: \"key2\""
+ + " sharding_key: 2"
+ + " work_token: 3"
+ + " cache_token: 4"
+ + " key_group { high: 0 low: 1 }"
+ + " message_bundles {"
+ + " source_computation_id: \""
+ + DEFAULT_SOURCE_COMPUTATION_ID
+ + "\""
+ + " messages {"
+ + " timestamp: 0"
+ + " data: \"data3\""
+ + " }"
+ + " }"
+ + " }"
+ + "}";
+ Windmill.GetWorkResponse batchInput1 =
+ buildInput(
+ batchInputText1,
+ CoderUtils.encodeToByteArray(
+ CollectionCoder.of(IntervalWindow.getCoder()),
+ Collections.singletonList(DEFAULT_WINDOW)));
+ Windmill.GetWorkResponse batchInput2 =
+ buildInput(
+ batchInputText2,
+ CoderUtils.encodeToByteArray(
+ CollectionCoder.of(IntervalWindow.getCoder()),
+ Collections.singletonList(DEFAULT_WINDOW)));
+
+
server.whenGetDataCalled().answerByDefault(StreamingDataflowWorkerTest::emptyDataResponder);
+
+ server.whenGetWorkCalled().thenReturn(batchInput1).thenReturn(batchInput2);
+ server.waitForEmptyWorkQueue();
+
+ // Wait for key1 to start processing and block on BlockingKvDoFn.
+ BlockingKvDoFn.counter.get().acquire(1);
+
+ // Fail key2 (work token 2) via failed heartbeat while key1 is still
processing.
+ ComputationHeartbeatResponse.Builder failedHeartbeat =
+ ComputationHeartbeatResponse.newBuilder();
+ failedHeartbeat
+ .setComputationId(DEFAULT_COMPUTATION_ID)
+ .addHeartbeatResponsesBuilder()
+ .setCacheToken(3)
+ .setWorkToken(2)
+ .setShardingKey(2)
+ .setFailed(true);
+
+ // Fake server processes heartbeat responses are processed synchronously
in sendFailedHeartbeats
+
server.sendFailedHeartbeats(Collections.singletonList(failedHeartbeat.build()));
+
+ // Unblock key1 to allow bundle to poll key2 (token 2 -> failed, skipped)
and key2 (token 3).
+ BlockingKvDoFn.blocker.get().countDown();
+
+ Map<Long, Windmill.WorkItemCommitRequest> result =
server.waitForAndGetCommits(2);
+
+ assertTrue(result.containsKey(1L));
+ assertTrue(result.containsKey(3L));
+ assertFalse(result.containsKey(2L));
+
+ List<Windmill.MultiKeyWorkItemCommitRequest> multiKeyCommits =
+ server.getMultiKeyCommitsReceived();
+ assertEquals(1, multiKeyCommits.size());
+ Windmill.MultiKeyWorkItemCommitRequest multiKeyCommit =
multiKeyCommits.get(0);
+ assertEquals(2, multiKeyCommit.getRequestsCount());
+ assertEquals(1, multiKeyCommit.getRequests(0).getWorkToken());
+ assertEquals(3, multiKeyCommit.getRequests(1).getWorkToken());
+
+ worker.stop();
+ }
+
private StreamingDataflowWorker makeMultiKeyEnabledWorker() {
+ return makeMultiKeyEnabledWorker(new WorkDoFn());
+ }
+
+ private StreamingDataflowWorker makeMultiKeyEnabledWorker(
+ DoFn<KV<String, String>, KV<String, String>> doFn) {
KvCoder<String, String> kvCoder = KvCoder.of(StringUtf8Coder.of(),
StringUtf8Coder.of());
List<ParallelInstruction> instructions =
Arrays.asList(
makeSourceInstruction(kvCoder),
- makeDoFnInstruction(new WorkDoFn(), 0, kvCoder),
+ makeDoFnInstruction(doFn, 0, kvCoder),
makeSinkInstruction(kvCoder, 1));
StreamingDataflowWorker worker =
@@ -4825,6 +4958,25 @@ public class StreamingDataflowWorkerTest {
}
}
+ static class BlockingKvDoFn extends DoFn<KV<String, String>, KV<String,
String>> {
+ public static final AtomicReference<CountDownLatch> blocker =
+ new AtomicReference<>(new CountDownLatch(1));
+ public static final AtomicReference<Semaphore> counter =
+ new AtomicReference<>(new Semaphore(0));
+
+ @ProcessElement
+ public void processElement(ProcessContext c) throws InterruptedException {
+ counter.get().release();
+ blocker.get().await();
+ c.output(c.element());
+ }
+
+ public static void reset() {
+ blocker.set(new CountDownLatch(1));
+ counter.set(new Semaphore(0));
+ }
+ }
+
static class LargeCommitFn extends DoFn<KV<String, String>, KV<String,
String>> {
@ProcessElement
diff --git
a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/StreamingModeExecutionContextTest.java
b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/StreamingModeExecutionContextTest.java
index eb6bb51e420..5aceb0ca956 100644
---
a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/StreamingModeExecutionContextTest.java
+++
b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/StreamingModeExecutionContextTest.java
@@ -54,6 +54,7 @@ import
org.apache.beam.runners.core.metrics.ExecutionStateTracker.ExecutionState
import org.apache.beam.runners.dataflow.options.DataflowWorkerHarnessOptions;
import
org.apache.beam.runners.dataflow.worker.DataflowExecutionContext.DataflowExecutionStateTracker;
import
org.apache.beam.runners.dataflow.worker.MetricsToCounterUpdateConverter.Kind;
+import
org.apache.beam.runners.dataflow.worker.StreamingModeExecutionContext.KeyTransitionListener;
import
org.apache.beam.runners.dataflow.worker.StreamingModeExecutionContext.StreamingModeExecutionState;
import
org.apache.beam.runners.dataflow.worker.StreamingModeExecutionContext.StreamingModeExecutionStateRegistry;
import org.apache.beam.runners.dataflow.worker.counters.CounterSet;
@@ -62,6 +63,7 @@ import
org.apache.beam.runners.dataflow.worker.profiler.ScopedProfiler.NoopProfi
import
org.apache.beam.runners.dataflow.worker.profiler.ScopedProfiler.ProfileScope;
import
org.apache.beam.runners.dataflow.worker.streaming.BoundedQueueExecutorWorkHandle;
import org.apache.beam.runners.dataflow.worker.streaming.ExecutableWork;
+import org.apache.beam.runners.dataflow.worker.streaming.FailedWorkHandler;
import org.apache.beam.runners.dataflow.worker.streaming.Watermarks;
import org.apache.beam.runners.dataflow.worker.streaming.Work;
import
org.apache.beam.runners.dataflow.worker.streaming.config.FakeGlobalConfigHandle;
@@ -96,6 +98,7 @@ import
org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Lists;
import org.hamcrest.Matchers;
import org.joda.time.Duration;
import org.joda.time.Instant;
+import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
@@ -109,6 +112,15 @@ import org.mockito.MockitoAnnotations;
@RunWith(JUnit4.class)
public class StreamingModeExecutionContextTest {
+ private static final FailedWorkHandler FAILING_FAILED_WORK_HANDLER =
+ ignored -> {
+ Assert.fail();
+ };
+
+ private static final KeyTransitionListener FAILING_KEY_TRANSISITON =
+ (oldWork, newWork) -> {
+ Assert.fail();
+ };
@Rule public transient Timeout globalTimeout = Timeout.seconds(600);
@Mock private WorkExecutor workExecutor;
@@ -198,10 +210,11 @@ public class StreamingModeExecutionContextTest {
context.start(
work,
workExecutor,
- /* workQueueExecutor= */ null,
- /* budgetHandle= */ null,
+ /* workQueueExecutor= */ mock(BoundedQueueExecutor.class),
+ /* budgetHandle= */ mock(BoundedQueueExecutorWorkHandle.class),
keyCoder,
- /* keyTransitionListener= */ (k, c) -> {});
+ FAILING_KEY_TRANSISITON,
+ /* onFailedWorkHandler= */ FAILING_FAILED_WORK_HANDLER);
} catch (CoderException e) {
throw new RuntimeException(e);
}
@@ -546,13 +559,20 @@ public class StreamingModeExecutionContextTest {
workItem2,
Watermarks.builder().setInputDataWatermark(Instant.EPOCH).build());
ExecutableWork executableWork2 = ExecutableWork.create(work2, (w, h) ->
{});
- when(mockExecutor.pollWork(eq(COMPUTATION_ID), eq(work1.getKeyGroup()),
eq(mockHandle)))
+ when(mockExecutor.pollWork(eq(COMPUTATION_ID), eq(work1.getKeyGroup()),
eq(mockHandle), any()))
.thenReturn(executableWork2)
.thenReturn(null);
StreamingModeExecutionContext.KeyTransitionListener mockListener =
mock(StreamingModeExecutionContext.KeyTransitionListener.class);
- executionContext.start(work1, workExecutor, mockExecutor, mockHandle,
null, mockListener);
+ executionContext.start(
+ work1,
+ workExecutor,
+ mockExecutor,
+ mockHandle,
+ null,
+ mockListener,
+ FAILING_FAILED_WORK_HANDLER);
assertTrue(executionContext.advance());
assertEquals("key2", executionContext.getSerializedKey().toStringUtf8());
@@ -577,11 +597,17 @@ public class StreamingModeExecutionContextTest {
createMockWork(
workItem1,
Watermarks.builder().setInputDataWatermark(Instant.EPOCH).build());
- when(mockExecutor.pollWork(eq(COMPUTATION_ID), eq(work1.getKeyGroup()),
eq(mockHandle)))
+ when(mockExecutor.pollWork(eq(COMPUTATION_ID), eq(work1.getKeyGroup()),
eq(mockHandle), any()))
.thenReturn(null);
executionContext.start(
- work1, workExecutor, mockExecutor, mockHandle, null, (oldWork,
newWork) -> {});
+ work1,
+ workExecutor,
+ mockExecutor,
+ mockHandle,
+ null,
+ FAILING_KEY_TRANSISITON,
+ FAILING_FAILED_WORK_HANDLER);
assertFalse(executionContext.advance());
}
@@ -611,7 +637,14 @@ public class StreamingModeExecutionContextTest {
createMockWork(
workItem1,
Watermarks.builder().setInputDataWatermark(Instant.EPOCH).build());
- context.start(work1, workExecutor, mockExecutor, mockHandle, null,
(oldWork, newWork) -> {});
+ context.start(
+ work1,
+ workExecutor,
+ mockExecutor,
+ mockHandle,
+ null,
+ FAILING_KEY_TRANSISITON,
+ FAILING_FAILED_WORK_HANDLER);
assertFalse(context.advance());
verifyNoInteractions(mockExecutor);
@@ -642,7 +675,14 @@ public class StreamingModeExecutionContextTest {
createMockWork(
workItem1,
Watermarks.builder().setInputDataWatermark(Instant.EPOCH).build());
- context.start(work1, workExecutor, mockExecutor, mockHandle, null,
(oldWork, newWork) -> {});
+ context.start(
+ work1,
+ workExecutor,
+ mockExecutor,
+ mockHandle,
+ null,
+ FAILING_KEY_TRANSISITON,
+ FAILING_FAILED_WORK_HANDLER);
assertFalse(context.advance());
verifyNoInteractions(mockExecutor);
@@ -666,7 +706,13 @@ public class StreamingModeExecutionContextTest {
workItem1,
Watermarks.builder().setInputDataWatermark(Instant.EPOCH).build());
executionContext.start(
- work1, workExecutor, mockExecutor, mockHandle, null, (oldWork,
newWork) -> {});
+ work1,
+ workExecutor,
+ mockExecutor,
+ mockHandle,
+ null,
+ FAILING_KEY_TRANSISITON,
+ FAILING_FAILED_WORK_HANDLER);
work1.setFailed();
@@ -689,7 +735,13 @@ public class StreamingModeExecutionContextTest {
workItem1,
Watermarks.builder().setInputDataWatermark(Instant.EPOCH).build());
executionContext.start(
- work1, workExecutor, mockExecutor, mockHandle, null, (oldWork,
newWork) -> {});
+ work1,
+ workExecutor,
+ mockExecutor,
+ mockHandle,
+ null,
+ FAILING_KEY_TRANSISITON,
+ FAILING_FAILED_WORK_HANDLER);
assertFalse(executionContext.advance());
verifyNoInteractions(mockExecutor);
@@ -717,7 +769,14 @@ public class StreamingModeExecutionContextTest {
createMockWork(
workItem1,
Watermarks.builder().setInputDataWatermark(Instant.EPOCH).build());
- context.start(work1, workExecutor, mockExecutor, mockHandle, null,
(oldWork, newWork) -> {});
+ context.start(
+ work1,
+ workExecutor,
+ mockExecutor,
+ mockHandle,
+ null,
+ FAILING_KEY_TRANSISITON,
+ FAILING_FAILED_WORK_HANDLER);
assertFalse(context.advance());
verifyNoInteractions(mockExecutor);
@@ -750,11 +809,19 @@ public class StreamingModeExecutionContextTest {
createMockWork(
workItem1,
Watermarks.builder().setInputDataWatermark(Instant.EPOCH).build());
- context.start(work1, workExecutor, mockExecutor, mockHandle, null,
(oldWork, newWork) -> {});
+ context.start(
+ work1,
+ workExecutor,
+ mockExecutor,
+ mockHandle,
+ null,
+ FAILING_KEY_TRANSISITON,
+ FAILING_FAILED_WORK_HANDLER);
context.reportBytesSinked(50);
assertFalse(context.advance());
- verify(mockExecutor).pollWork(COMPUTATION_ID, work1.getKeyGroup(),
mockHandle);
+ verify(mockExecutor)
+ .pollWork(eq(COMPUTATION_ID), eq(work1.getKeyGroup()), eq(mockHandle),
any());
reset(mockExecutor);
diff --git
a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/WorkerCustomSourcesTest.java
b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/WorkerCustomSourcesTest.java
index 679227a11dc..2a741da7fd8 100644
---
a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/WorkerCustomSourcesTest.java
+++
b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/WorkerCustomSourcesTest.java
@@ -89,6 +89,7 @@ import
org.apache.beam.runners.dataflow.worker.WorkerCustomSources.SplittableOnl
import org.apache.beam.runners.dataflow.worker.counters.CounterSet;
import org.apache.beam.runners.dataflow.worker.counters.NameContext;
import
org.apache.beam.runners.dataflow.worker.profiler.ScopedProfiler.NoopProfileScope;
+import
org.apache.beam.runners.dataflow.worker.streaming.BoundedQueueExecutorWorkHandle;
import org.apache.beam.runners.dataflow.worker.streaming.Watermarks;
import org.apache.beam.runners.dataflow.worker.streaming.Work;
import
org.apache.beam.runners.dataflow.worker.streaming.config.FixedGlobalConfigHandle;
@@ -97,6 +98,7 @@ import
org.apache.beam.runners.dataflow.worker.streaming.config.StreamingGlobalC
import
org.apache.beam.runners.dataflow.worker.streaming.harness.StreamingCounters;
import
org.apache.beam.runners.dataflow.worker.streaming.sideinput.SideInputStateFetcherFactory;
import org.apache.beam.runners.dataflow.worker.testing.TestCountingSource;
+import org.apache.beam.runners.dataflow.worker.util.BoundedQueueExecutor;
import org.apache.beam.runners.dataflow.worker.util.common.worker.NativeReader;
import
org.apache.beam.runners.dataflow.worker.util.common.worker.NativeReader.NativeReaderIterator;
import org.apache.beam.runners.dataflow.worker.util.common.worker.WorkExecutor;
@@ -218,10 +220,11 @@ public class WorkerCustomSourcesTest {
context.start(
work,
mock(WorkExecutor.class),
- /* workQueueExecutor= */ null,
- /* budgetHandle= */ null,
+ /* workQueueExecutor= */ mock(BoundedQueueExecutor.class),
+ /* budgetHandle= */ mock(BoundedQueueExecutorWorkHandle.class),
/* keyCoder= */ null,
- /* keyTransitionListener= */ mock(KeyTransitionListener.class));
+ /* keyTransitionListener= */ mock(KeyTransitionListener.class),
+ /* onFailedWorkHandler= */ ignored -> {});
} catch (CoderException e) {
throw new RuntimeException(e);
}
diff --git
a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/util/BoundedQueueExecutorTest.java
b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/util/BoundedQueueExecutorTest.java
index 0e75fa01f4f..75230594fc1 100644
---
a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/util/BoundedQueueExecutorTest.java
+++
b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/util/BoundedQueueExecutorTest.java
@@ -25,6 +25,7 @@ import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
import java.util.Arrays;
import java.util.Collection;
@@ -35,6 +36,7 @@ import java.util.function.BiConsumer;
import java.util.function.Consumer;
import
org.apache.beam.runners.dataflow.worker.streaming.BoundedQueueExecutorWorkHandle;
import org.apache.beam.runners.dataflow.worker.streaming.ExecutableWork;
+import org.apache.beam.runners.dataflow.worker.streaming.FailedWorkHandler;
import org.apache.beam.runners.dataflow.worker.streaming.Watermarks;
import org.apache.beam.runners.dataflow.worker.streaming.Work;
import
org.apache.beam.runners.dataflow.worker.util.BoundedQueueExecutor.BoundedQueueExecutorWorkHandleImpl;
@@ -46,6 +48,7 @@ import
org.apache.beam.vendor.grpc.v1p69p0.com.google.protobuf.ByteString;
import
org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableList;
import
org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.util.concurrent.ThreadFactoryBuilder;
import org.joda.time.Instant;
+import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
@@ -67,6 +70,10 @@ public class BoundedQueueExecutorTest {
return Arrays.asList(new Object[][] {{false}, {true}});
}
+ private static final FailedWorkHandler FAILING_FAILED_WORK_HANDLER =
+ ignored -> {
+ Assert.fail();
+ };
private static final long MAXIMUM_BYTES_OUTSTANDING = 10000000;
private static final int DEFAULT_MAX_THREADS = 2;
private static final int DEFAULT_THREAD_EXPIRATION_SEC = 60;
@@ -90,6 +97,33 @@ public class BoundedQueueExecutorTest {
computationId, keyGroup, (work, handle) -> executeWorkFn.accept(work));
}
+ private static ExecutableWork createWorkWithCompIdAndKeyGroupAndWorkToken(
+ String computationId, Work.KeyGroup keyGroup, long workToken,
Consumer<Work> executeWorkFn) {
+ WorkItem workItem =
+ WorkItem.newBuilder()
+ .setKey(ByteString.EMPTY)
+ .setShardingKey(1)
+ .setWorkToken(workToken)
+ .setCacheToken(1)
+ .setKeyGroup(
+ Windmill.Uint128Proto.newBuilder()
+ .setHigh(keyGroup.high())
+ .setLow(keyGroup.low())
+ .build())
+ .build();
+ return ExecutableWork.create(
+ Work.create(
+ workItem,
+ workItem.getSerializedSize(),
+ Watermarks.builder().setInputDataWatermark(Instant.now()).build(),
+ Work.createProcessingContext(
+ computationId, new FakeGetDataClient(), ignored -> {},
mock(HeartbeatSender.class)),
+ false,
+ Instant::now,
+ ImmutableList.of()),
+ (work, handle) -> executeWorkFn.accept(work));
+ }
+
private static ExecutableWork createWorkWithHandle(
String computationId,
Work.KeyGroup keyGroup,
@@ -511,7 +545,7 @@ public class BoundedQueueExecutorTest {
assertEquals(3, testExecutor.elementsOutstanding());
// Steal work2 using pollWork with compA and keyGroup2
- ExecutableWork stolen = testExecutor.pollWork("compA", keyGroup2,
stealHandle);
+ ExecutableWork stolen = testExecutor.pollWork("compA", keyGroup2,
stealHandle, ignored -> {});
assertNotNull(stolen);
assertEquals(work2, stolen);
@@ -520,7 +554,8 @@ public class BoundedQueueExecutorTest {
targetStart.await();
// Steal work1 using pollWork with compA and keyGroup1
- ExecutableWork stolen1 = testExecutor.pollWork("compA", keyGroup1,
stealHandle);
+ ExecutableWork stolen1 =
+ testExecutor.pollWork("compA", keyGroup1, stealHandle,
FAILING_FAILED_WORK_HANDLER);
assertNotNull(stolen1);
assertEquals(work1, stolen1);
@@ -569,10 +604,98 @@ public class BoundedQueueExecutorTest {
ExecutableWork work = createWorkWithCompIdAndKeyGroup("compA", keyGroup,
ignored -> {});
testExecutor.execute(work, 100);
- ExecutableWork stolen = testExecutor.pollWork("compA", keyGroup,
stealHandle);
+ ExecutableWork stolen =
+ testExecutor.pollWork("compA", keyGroup, stealHandle,
FAILING_FAILED_WORK_HANDLER);
assertNull(stolen);
blockerStop.countDown();
testExecutor.shutdown();
}
+
+ @Test
+ public void testPollWork_skipsFailedWorkAndCallsOnFailedWorkHandler() throws
Exception {
+ BoundedQueueExecutor testExecutor =
+ new BoundedQueueExecutor(
+ 1,
+ 60,
+ TimeUnit.SECONDS,
+ 100,
+ 10000000,
+ new
ThreadFactoryBuilder().setNameFormat("testPollWork-%d").setDaemon(true).build(),
+ useFairMonitor,
+ /* useKeyGroupWorkQueue= */ true);
+
+ CountDownLatch blockerStart = new CountDownLatch(1);
+ CountDownLatch blockerStop = new CountDownLatch(1);
+ AtomicReference<BoundedQueueExecutorWorkHandle> blockerHandleRef = new
AtomicReference<>();
+ ExecutableWork blockerWork =
+ createWorkWithHandle(
+ "compA",
+ DEFAULT_KEY_GROUP,
+ (work, handle) -> {
+ blockerHandleRef.set(handle);
+ blockerStart.countDown();
+ try {
+ blockerStop.await();
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ });
+
+ testExecutor.execute(blockerWork, 10);
+ blockerStart.await();
+ BoundedQueueExecutorWorkHandleImpl stealHandle =
+ (BoundedQueueExecutorWorkHandleImpl) blockerHandleRef.get();
+ assertNotNull(stealHandle);
+
+ Work.KeyGroup keyGroup = Work.KeyGroup.create(1, 1);
+ FailedWorkHandler onFailedWorkHandler = mock(FailedWorkHandler.class);
+
+ ExecutableWork work1 =
+ createWorkWithCompIdAndKeyGroupAndWorkToken("compA", keyGroup, 101,
ignored -> {});
+ ExecutableWork work2 =
+ createWorkWithCompIdAndKeyGroupAndWorkToken("compA", keyGroup, 102,
ignored -> {});
+ ExecutableWork work3 =
+ createWorkWithCompIdAndKeyGroupAndWorkToken("compA", keyGroup, 103,
ignored -> {});
+
+ // Enqueue both tasks (they will wait in the queue because the thread is
blocked).
+ testExecutor.execute(work1, 100);
+ testExecutor.execute(work2, 150);
+ testExecutor.execute(work3, 200);
+
+ assertEquals(4, testExecutor.elementsOutstanding());
+ assertEquals(460, testExecutor.bytesOutstanding());
+
+ // Mark work1, work3 as failed while waiting in the queue.
+ work1.work().setFailed();
+ work3.work().setFailed();
+
+ // pollWork should skip work1, close work1's handle, invoke
+ // onFailedWorkHandler callback, and return work2.
+ ExecutableWork stolen =
+ testExecutor.pollWork("compA", keyGroup, stealHandle,
onFailedWorkHandler);
+ assertNotNull(stolen);
+ assertEquals(work2, stolen);
+
+ verify(onFailedWorkHandler).onFailedWork(work1.work());
+
+ // Verify stealHandle merged (blockerWork: 10 bytes, work2: 150 bytes).
+ assertEquals(160, stealHandle.bytes());
+
+ stolen = testExecutor.pollWork("compA", keyGroup, stealHandle,
onFailedWorkHandler);
+ assertNull(stolen);
+ verify(onFailedWorkHandler).onFailedWork(work3.work());
+
+ // Still 160, nothing should be merged in.
+ assertEquals(160, stealHandle.bytes());
+
+ // Polling again should return null since no more tasks exist for keyGroup.
+ assertNull(testExecutor.pollWork("compA", keyGroup, stealHandle,
onFailedWorkHandler));
+
+ blockerStop.countDown();
+ stealHandle.close();
+ assertEquals(0, testExecutor.elementsOutstanding());
+ assertEquals(0, testExecutor.bytesOutstanding());
+ testExecutor.shutdown();
+ }
}