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 78fdc959cda [Dataflow Streaming] [Multi Key] Prepare
StreamingModeExecutionContext and StreamingWorkScheduler for multi-key
execution. (#38814)
78fdc959cda is described below
commit 78fdc959cda56a973d65a4cc9274d278200747e7
Author: Arun Pandian <[email protected]>
AuthorDate: Tue Jun 30 13:13:34 2026 -0700
[Dataflow Streaming] [Multi Key] Prepare StreamingModeExecutionContext and
StreamingWorkScheduler for multi-key execution. (#38814)
---
...it_Java_ValidatesRunner_Dataflow_Streaming.json | 2 +-
.../worker/StreamingModeExecutionContext.java | 370 +++++++++++++++++----
.../worker/WindmillReaderIteratorBase.java | 29 +-
.../dataflow/worker/WindowingWindmillReader.java | 119 +++----
.../worker/streaming/ComputationWorkExecutor.java | 43 +--
.../runners/dataflow/worker/streaming/Work.java | 29 +-
.../processing/ComputationWorkExecutorFactory.java | 33 +-
.../work/processing/StreamingWorkScheduler.java | 312 +++++++++--------
.../worker/StreamingDataflowWorkerTest.java | 36 +-
.../worker/StreamingModeExecutionContextTest.java | 116 ++++---
.../worker/WindmillReaderIteratorBaseTest.java | 92 +++++
.../worker/WindowingWindmillReaderTest.java | 275 +++++++++++++++
.../dataflow/worker/WorkerCustomSourcesTest.java | 51 ++-
.../dataflow/worker/streaming/WorkTest.java | 132 ++++++++
14 files changed, 1266 insertions(+), 373 deletions(-)
diff --git
a/.github/trigger_files/beam_PostCommit_Java_ValidatesRunner_Dataflow_Streaming.json
b/.github/trigger_files/beam_PostCommit_Java_ValidatesRunner_Dataflow_Streaming.json
index e623d3373a9..50d17c108f2 100644
---
a/.github/trigger_files/beam_PostCommit_Java_ValidatesRunner_Dataflow_Streaming.json
+++
b/.github/trigger_files/beam_PostCommit_Java_ValidatesRunner_Dataflow_Streaming.json
@@ -1,4 +1,4 @@
{
"comment": "Modify this file in a trivial way to cause this test suite to
run!",
- "modification": 1,
+ "modification": 2,
}
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 66df2233394..9401cc5f8ed 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
@@ -18,13 +18,15 @@
package org.apache.beam.runners.dataflow.worker;
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.checkNotNull;
import static
org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkState;
import com.google.api.services.dataflow.model.CounterUpdate;
import com.google.api.services.dataflow.model.SideInputInfo;
import java.io.Closeable;
import java.io.IOException;
-import java.util.Collection;
+import java.util.ArrayList;
+import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
@@ -33,6 +35,7 @@ import java.util.NavigableSet;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ThreadLocalRandom;
+import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import javax.annotation.concurrent.NotThreadSafe;
import org.apache.beam.repackaged.core.org.apache.commons.lang3.tuple.Pair;
@@ -45,10 +48,10 @@ import
org.apache.beam.runners.core.TimerInternals.TimerData;
import org.apache.beam.runners.core.metrics.ExecutionStateTracker;
import
org.apache.beam.runners.core.metrics.ExecutionStateTracker.ExecutionState;
import
org.apache.beam.runners.dataflow.worker.DataflowOperationContext.DataflowExecutionState;
-import
org.apache.beam.runners.dataflow.worker.StreamingModeExecutionContext.StepContext;
import org.apache.beam.runners.dataflow.worker.counters.CounterFactory;
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.Watermarks;
import org.apache.beam.runners.dataflow.worker.streaming.Work;
import
org.apache.beam.runners.dataflow.worker.streaming.config.StreamingGlobalConfig;
@@ -56,6 +59,10 @@ import
org.apache.beam.runners.dataflow.worker.streaming.config.StreamingGlobalC
import org.apache.beam.runners.dataflow.worker.streaming.sideinput.SideInput;
import
org.apache.beam.runners.dataflow.worker.streaming.sideinput.SideInputState;
import
org.apache.beam.runners.dataflow.worker.streaming.sideinput.SideInputStateFetcher;
+import
org.apache.beam.runners.dataflow.worker.streaming.sideinput.SideInputStateFetcherFactory;
+import org.apache.beam.runners.dataflow.worker.util.BoundedQueueExecutor;
+import
org.apache.beam.runners.dataflow.worker.util.common.worker.ElementCounter;
+import
org.apache.beam.runners.dataflow.worker.util.common.worker.OutputObjectAndByteCounter;
import org.apache.beam.runners.dataflow.worker.util.common.worker.WorkExecutor;
import org.apache.beam.runners.dataflow.worker.windmill.Windmill;
import org.apache.beam.runners.dataflow.worker.windmill.Windmill.GlobalDataId;
@@ -72,6 +79,7 @@ import
org.apache.beam.runners.dataflow.worker.windmill.state.WindmillTagEncodin
import
org.apache.beam.runners.dataflow.worker.windmill.state.WindmillTimerData;
import org.apache.beam.sdk.annotations.Internal;
import org.apache.beam.sdk.coders.Coder;
+import org.apache.beam.sdk.coders.CoderException;
import org.apache.beam.sdk.io.UnboundedSource;
import org.apache.beam.sdk.io.UnboundedSource.UnboundedReader;
import org.apache.beam.sdk.metrics.MetricsContainer;
@@ -109,7 +117,8 @@ import org.slf4j.LoggerFactory;
@SuppressWarnings({"deprecation"})
@NotThreadSafe
@Internal
-public class StreamingModeExecutionContext extends
DataflowExecutionContext<StepContext> {
+public class StreamingModeExecutionContext
+ extends
DataflowExecutionContext<StreamingModeExecutionContext.StepContext> {
private static final Logger LOG =
LoggerFactory.getLogger(StreamingModeExecutionContext.class);
@@ -141,6 +150,7 @@ public class StreamingModeExecutionContext extends
DataflowExecutionContext<Step
private @Nullable Work work;
private @Nullable WindmillComputationKey computationKey;
+ private final SideInputStateFetcherFactory sideInputStateFetcherFactory;
private @Nullable SideInputStateFetcher sideInputStateFetcher;
// OperationalLimits is updated in start() because a
StreamingModeExecutionContext can
// be used for processing many work items and these values can change during
the context's
@@ -150,15 +160,43 @@ public class StreamingModeExecutionContext extends
DataflowExecutionContext<Step
/**
* Current reader used for processing {@link Work}. Set by calling {@link
- * #setActiveReader(UnboundedReader)}, reset to null and cached when state
is persisted {@link
- * #flushState()}, or set to null and closed when {@link
StreamingModeExecutionContext} is
- * invalidated.
+ * #setActiveReader(UnboundedReader)}, reset to null and cached when state
is persisted or set to
+ * null and closed when {@link StreamingModeExecutionContext} is invalidated.
*/
private @Nullable UnboundedReader<?> activeReader;
private @Nullable WorkExecutor workExecutor;
private boolean finishKeyCalled = false;
+ @SuppressWarnings("UnusedVariable")
+ private @Nullable BoundedQueueExecutor workQueueExecutor;
+
+ @SuppressWarnings("UnusedVariable")
+ private @Nullable BoundedQueueExecutorWorkHandle budgetHandle;
+
+ private final HotKeyLogger hotKeyLogger;
+ private final boolean hotKeyLoggingEnabled;
+ private final String stepName;
+ private @Nullable Coder<?> keyCoder;
+
+ // Key switch listener to delegate MDC logging context and thread name
updates
+ public interface KeyTransitionListener {
+ void onKeyTransition(Work oldWork, Work newWork);
+ }
+
+ @SuppressWarnings("UnusedVariable")
+ private @Nullable KeyTransitionListener keyTransitionListener;
+
+ private List<Work> executedWorks = Collections.emptyList();
+ private List<Windmill.WorkItemCommitRequest.Builder> outputBuilders =
Collections.emptyList();
+
+ // Map<finalizerId, Pair<callbackExpiration, callback>>
+ private Map<Long, Pair<Instant, Runnable>> finalizationCallbacks =
Collections.emptyMap();
+ private AtomicBoolean workBatchFailed = new AtomicBoolean(false);
+ private @Nullable WindmillStateReader activeStateReader;
+ private long stateBytesRead = 0;
+ private final String sourceBytesProcessCounterName;
+
public StreamingModeExecutionContext(
CounterFactory counterFactory,
String computationId,
@@ -170,7 +208,12 @@ public class StreamingModeExecutionContext extends
DataflowExecutionContext<Step
StreamingModeExecutionStateRegistry executionStateRegistry,
StreamingGlobalConfigHandle globalConfigHandle,
long sinkByteLimit,
- boolean throwExceptionOnLargeOutput) {
+ boolean throwExceptionOnLargeOutput,
+ HotKeyLogger hotKeyLogger,
+ boolean hotKeyLoggingEnabled,
+ String stepName,
+ String sourceBytesProcessCounterName,
+ SideInputStateFetcherFactory sideInputStateFetcherFactory) {
super(
counterFactory,
metricsContainerRegistry,
@@ -185,6 +228,11 @@ public class StreamingModeExecutionContext extends
DataflowExecutionContext<Step
this.stateCache = stateCache;
this.backlogBytes = UnboundedReader.BACKLOG_UNKNOWN;
this.throwExceptionOnLargeOutput = throwExceptionOnLargeOutput;
+ this.hotKeyLogger = checkNotNull(hotKeyLogger);
+ this.hotKeyLoggingEnabled = hotKeyLoggingEnabled;
+ this.stepName = checkNotNull(stepName);
+ this.sourceBytesProcessCounterName =
checkNotNull(sourceBytesProcessCounterName);
+ this.sideInputStateFetcherFactory = sideInputStateFetcherFactory;
StreamingGlobalConfig config = globalConfigHandle.getConfig();
this.operationalLimits = config.operationalLimits();
this.windmillTagEncoding =
@@ -211,7 +259,7 @@ public class StreamingModeExecutionContext extends
DataflowExecutionContext<Step
}
public boolean workIsFailed() {
- return work != null && work.isFailed();
+ return workBatchFailed.get();
}
public boolean getDrainMode() {
@@ -243,50 +291,131 @@ public class StreamingModeExecutionContext extends
DataflowExecutionContext<Step
return checkStateNotNull(activeReader).getCurrentRecordOffset();
}
+ /** Reset context before using it on a new bundle */
+ public void reset() {
+ // these lists and maps are returned to callers after processing
+ // don't clear and reuse, instead reset the reference.
+ this.executedWorks = Collections.emptyList();
+ this.outputBuilders = Collections.emptyList();
+ this.finalizationCallbacks = Collections.emptyMap();
+ // Work from prior bundles might have a reference to the old
workBatchFailed.
+ // If the work gets retried it'll get the new workBatchFailed to notify
failure.
+ this.workBatchFailed = new AtomicBoolean(false);
+ this.sideInputCache.clear();
+ this.activeStateReader = null;
+ this.activeReader = null;
+ this.keyCoder = null;
+ this.workExecutor = null;
+ this.workQueueExecutor = null;
+ this.budgetHandle = null;
+ this.keyTransitionListener = null;
+ this.work = null;
+ this.key = null;
+ this.outputBuilder = null;
+ this.sideInputStateFetcher = null;
+ this.backlogBytes = UnboundedReader.BACKLOG_UNKNOWN;
+ clearSinkFullHint();
+ this.stateBytesRead = 0;
+ }
+
public void start(
- @Nullable Object key,
Work work,
WindmillStateReader stateReader,
- SideInputStateFetcher sideInputStateFetcher,
- Windmill.WorkItemCommitRequest.Builder outputBuilder,
- WorkExecutor workExecutor) {
- this.key = key;
- this.work = work;
+ WorkExecutor workExecutor,
+ BoundedQueueExecutor workQueueExecutor,
+ BoundedQueueExecutorWorkHandle budgetHandle,
+ @Nullable Coder<?> keyCoder,
+ KeyTransitionListener keyTransitionListener)
+ throws CoderException {
+ reset();
+ this.executedWorks = new ArrayList<>();
+ this.outputBuilders = new ArrayList<>();
+ this.finalizationCallbacks = new HashMap<>();
+ this.keyCoder = keyCoder;
this.workExecutor = workExecutor;
- this.finishKeyCalled = false;
- this.computationKey = WindmillComputationKey.create(computationId,
work.getShardedKey());
- this.sideInputStateFetcher = sideInputStateFetcher;
+ this.workQueueExecutor = workQueueExecutor;
+ this.budgetHandle = budgetHandle;
+ this.keyTransitionListener = keyTransitionListener;
+
StreamingGlobalConfig config = globalConfigHandle.getConfig();
// Snapshot the limits for entire bundle processing.
this.operationalLimits = config.operationalLimits();
- this.outputBuilder = outputBuilder;
- this.sideInputCache.clear();
- this.backlogBytes = UnboundedReader.BACKLOG_UNKNOWN;
- clearSinkFullHint();
- Instant processingTime =
computeProcessingTime(work.getWorkItem().getTimers().getTimersList());
+ startForNewKey(work, stateReader);
+ }
- Collection<? extends StepContext> stepContexts = getAllStepContexts();
- if (!stepContexts.isEmpty()) {
- // This must be only created once for the workItem as token validation
will fail if the same
- // work token is reused.
- WindmillStateCache.ForKey cacheForKey =
- stateCache.forKey(getComputationKey(),
getWorkItem().getCacheToken(), getWorkToken());
- for (StepContext stepContext : stepContexts) {
- stepContext.start(stateReader, processingTime, cacheForKey,
work.watermarks());
+ private @Nullable Object decodeKey(Work work) throws CoderException {
+ // If the read output KVs, then we can decode Windmill's byte key into
userland
+ // key object and provide it to the execution context for use with per-key
state.
+ // Otherwise, we pass null.
+ //
+ // The coder type that will be present is:
+ // WindowedValueCoder(TimerOrElementCoder(KvCoder))
+ if (keyCoder != null) {
+ try {
+ return keyCoder.decode(work.getWorkItem().getKey().newInput(),
Coder.Context.OUTER);
+ } catch (CoderException e) {
+ throw e;
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to decode key during processing",
e);
}
}
+ return null;
+ }
+
+ private Windmill.WorkItemCommitRequest.Builder createOutputBuilder(Work
work) {
+ return Windmill.WorkItemCommitRequest.newBuilder()
+ .setKey(work.getWorkItem().getKey())
+ .setShardingKey(work.getWorkItem().getShardingKey())
+ .setWorkToken(work.getWorkItem().getWorkToken())
+ .setCacheToken(work.getWorkItem().getCacheToken());
+ }
+
+ private void logHotKeyIfDetected(Work work, @Nullable Object decodedKey) {
+ if (work.getWorkItem().hasHotKeyInfo()) {
+ Windmill.HotKeyInfo hotKeyInfo = work.getWorkItem().getHotKeyInfo();
+ Duration hotKeyAge = Duration.millis(hotKeyInfo.getHotKeyAgeUsec() /
1000);
+ if (decodedKey != null && hotKeyLoggingEnabled) {
+ hotKeyLogger.logHotKeyDetection(stepName, hotKeyAge, decodedKey);
+ } else {
+ hotKeyLogger.logHotKeyDetection(stepName, hotKeyAge);
+ }
+ }
+ }
+
+ private void startStepContexts(
+ WindmillStateReader stateReader,
+ Instant processingTime,
+ WindmillStateCache.ForKey cacheForKey,
+ Watermarks watermarks) {
+ for (StepContext stepContext : getAllStepContexts()) {
+ stepContext.start(stateReader, processingTime, cacheForKey, watermarks);
+ }
}
public void finishKey() {
- checkState(!finishKeyCalled, "finishKey was already called");
- checkStateNotNull(workExecutor, "workExecutor must be set before calling
finishKey()");
+ WorkExecutor localExecutor =
+ checkStateNotNull(workExecutor, "workExecutor must be set before
calling finishKey()");
+ if (finishKeyCalled) {
+ return;
+ }
+ this.finishKeyCalled = true;
+ if (activeStateReader != null) {
+ this.stateBytesRead += activeStateReader.getBytesRead();
+ }
+ if (sideInputStateFetcher != null) {
+ this.stateBytesRead += sideInputStateFetcher.getBytesRead();
+ }
try {
- workExecutor.finishKey(key);
+ localExecutor.finishKey(key);
} catch (Exception e) {
throw new RuntimeException(e);
}
- this.finishKeyCalled = true;
+ }
+
+ public void flushState() {
+ checkState(finishKeyCalled, "finishKey must be called before flushState");
+ flushStateInternal();
}
/**
@@ -400,26 +529,6 @@ public class StreamingModeExecutionContext extends
DataflowExecutionContext<Step
return getWorkItem().getTimers().getTimersList();
}
- public @Nullable ByteString getSerializedKey() {
- return work == null ? null : work.getWorkItem().getKey();
- }
-
- public WindmillComputationKey getComputationKey() {
- return checkStateNotNull(computationKey);
- }
-
- public long getWorkToken() {
- return getWorkItem().getWorkToken();
- }
-
- public Windmill.WorkItem getWorkItem() {
- return checkStateNotNull(
- work,
- "work is null. A call to StreamingModeExecutionContext.start(...)
is required to set"
- + " work for execution.")
- .getWorkItem();
- }
-
public Windmill.WorkItemCommitRequest.Builder getOutputBuilder() {
return checkStateNotNull(outputBuilder);
}
@@ -440,20 +549,23 @@ public class StreamingModeExecutionContext extends
DataflowExecutionContext<Step
/** Invalidate the state and reader caches for this computation and key. */
public void invalidateCache() {
- ByteString key = getSerializedKey();
- if (key != null) {
- readerCache.invalidateReader(getComputationKey());
- if (activeReader != null) {
- try {
- activeReader.close();
- } catch (IOException e) {
- LOG.warn(
- "Failed to close reader for {}-{}", computationId,
getWorkItem().getShardingKey(), e);
- }
+ for (Work w : executedWorks) {
+ WindmillComputationKey compKey =
+ WindmillComputationKey.create(computationId, w.getShardedKey());
+ readerCache.invalidateReader(compKey);
+ stateCache.invalidate(w.getShardedKey());
+ }
+ if (activeReader != null) {
+ try {
+ activeReader.close();
+ } catch (IOException e) {
+ Windmill.WorkItem workItem = getWorkItem();
+ long shardingKey = workItem != null ? workItem.getShardingKey() : -1L;
+ LOG.warn("Failed to close reader for {}-{}", computationId,
shardingKey, e);
}
- activeReader = null;
- stateCache.invalidate(key, getWorkItem().getShardingKey());
}
+ activeReader = null;
+ activeStateReader = null;
}
public UnboundedSource.@Nullable CheckpointMark getReaderCheckpoint(
@@ -469,8 +581,7 @@ public class StreamingModeExecutionContext extends
DataflowExecutionContext<Step
}
}
- public Map<Long, Pair<Instant, Runnable>> flushState() {
- checkState(finishKeyCalled, "finishKey must be called before flushState");
+ private void flushStateInternal() {
Map<Long, Pair<Instant, Runnable>> callbacks = new HashMap<>();
for (StepContext stepContext : getAllStepContexts()) {
@@ -553,7 +664,128 @@ public class StreamingModeExecutionContext extends
DataflowExecutionContext<Step
// RestrictionTracker.getProgress() or GetSize() are not defined.
getOutputBuilder().setSourceBacklogBytes(backlogBytes);
}
- return callbacks;
+
+ this.finalizationCallbacks.putAll(callbacks);
+
+ getOutputBuilder()
+
.setSourceBytesProcessed(computeSourceBytesProcessed(sourceBytesProcessCounterName));
+ }
+
+ private final long computeSourceBytesProcessed(String
sourceBytesCounterName) {
+ if (!(workExecutor instanceof DataflowMapTaskExecutor)) {
+ return 0L;
+ }
+ HashMap<String, ElementCounter> counters =
+ ((DataflowMapTaskExecutor) workExecutor)
+ .getReadOperation()
+ .receivers[0]
+ .getOutputCounters();
+
+ return Optional.ofNullable(counters.get(sourceBytesCounterName))
+ .map(counter -> ((OutputObjectAndByteCounter)
counter).getByteCount().getAndReset())
+ .orElse(0L);
+ }
+
+ public boolean advance() {
+ // TODO: get more work from workQueueExecutor and merge into the bundle
here
+ return false;
+ }
+
+ private void startForNewKey(Work newWork, WindmillStateReader reader) throws
CoderException {
+ newWork.setState(Work.State.PROCESSING);
+ if (keyTransitionListener != null && this.work != null && this.work !=
newWork) {
+ keyTransitionListener.onKeyTransition(this.work, newWork);
+ }
+ this.key = decodeKey(newWork);
+ this.work = newWork;
+ this.finishKeyCalled = false;
+ this.computationKey = WindmillComputationKey.create(computationId,
newWork.getShardedKey());
+
+ this.outputBuilder = createOutputBuilder(newWork);
+ this.outputBuilders.add(this.outputBuilder);
+ newWork.setOnFailureListener(this.workBatchFailed);
+ this.executedWorks.add(newWork);
+
+ logHotKeyIfDetected(newWork, this.key);
+
+ this.sideInputStateFetcher =
+
sideInputStateFetcherFactory.createSideInputStateFetcher(newWork::fetchSideInput);
+ this.backlogBytes = UnboundedReader.BACKLOG_UNKNOWN;
+ this.activeReader = null;
+
+ // Note: We do NOT clear sideInputCache here, allowing Key B to reuse warm
side inputs!
+
+ // Re-initialize state cache and state/timer internals across all step
contexts
+ Instant processingTime =
+
computeProcessingTime(newWork.getWorkItem().getTimers().getTimersList());
+ if (getAllStepContexts().isEmpty()) {
+ checkState(this.activeStateReader == null);
+ } else {
+ // This must be only created once for a workItem as token validation
will fail if the same
+ // work token is reused.
+ WindmillStateCache.ForKey cacheForKey =
+ stateCache.forKey(
+ getComputationKey(), newWork.getWorkItem().getCacheToken(),
getWorkToken());
+ this.activeStateReader = reader;
+ startStepContexts(reader, processingTime, cacheForKey,
newWork.watermarks());
+ }
+ }
+
+ // Returns state bytes read during the bundle execution
+ public long getStateBytesRead() {
+ return stateBytesRead;
+ }
+
+ // Returns list of commit requests from the bundle
+ public List<Windmill.WorkItemCommitRequest> getWorkItemCommits() {
+ List<Windmill.WorkItemCommitRequest> commits = new
ArrayList<>(outputBuilders.size());
+ for (Windmill.WorkItemCommitRequest.Builder builder : outputBuilders) {
+ commits.add(builder.build());
+ }
+ return commits;
+ }
+
+ // Returns list of Work that was executed in the bundle
+ public List<Work> getExecutedWorks() {
+ return executedWorks;
+ }
+
+ // Returns finalization callbacks recorded during the bundle execution
+ public Map<Long, Pair<Instant, Runnable>> getFinalizationCallbacks() {
+ return finalizationCallbacks;
+ }
+
+ // Returns the current key being processed or null if an unkeyed stage.
+ public @Nullable Object getKey() {
+ return key;
+ }
+
+ // Returns the current Work being processed.
+ public Work getWork() {
+ return checkStateNotNull(
+ work,
+ "work is null. A call to StreamingModeExecutionContext.start(...) is
required to set"
+ + " work for execution.");
+ }
+
+ // Returns the serialized windmill key for the current Work
+ public @Nullable ByteString getSerializedKey() {
+ return work == null ? null : work.getWorkItem().getKey();
+ }
+
+ // Returns the serialized windmill key for the current Work
+ public WindmillComputationKey getComputationKey() {
+ return checkStateNotNull(computationKey);
+ }
+
+ // Returns the windmill work token for the current Work
+ public long getWorkToken() {
+ return getWorkItem().getWorkToken();
+ }
+
+ // Returns the windmill WorkItem proto for the current Work
+ public Windmill.WorkItem getWorkItem() {
+ return getWork().getWorkItem();
}
@Nullable
diff --git
a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/WindmillReaderIteratorBase.java
b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/WindmillReaderIteratorBase.java
index 075a1a8a425..134655a72a5 100644
---
a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/WindmillReaderIteratorBase.java
+++
b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/WindmillReaderIteratorBase.java
@@ -35,9 +35,9 @@ import org.slf4j.LoggerFactory;
public abstract class WindmillReaderIteratorBase<T>
extends NativeReader.NativeReaderIterator<WindowedValue<T>> {
private final StreamingModeExecutionContext context;
- private final Windmill.WorkItem work;
- private int bundleIndex = 0;
- private int messageIndex = -1;
+ private Windmill.WorkItem work;
+ private int bundleIndex;
+ private int messageIndex;
private @Nullable WindowedValue<T> current = null;
private final ValueProvider<Boolean> skipUndecodableElements;
private static final Logger LOG =
LoggerFactory.getLogger(WindmillReaderIteratorBase.class);
@@ -47,6 +47,8 @@ public abstract class WindmillReaderIteratorBase<T>
this.context = context;
this.skipUndecodableElements = skipUndecodableElements;
this.work = context.getWorkItem();
+ this.bundleIndex = 0;
+ this.messageIndex = -1;
}
@Override
@@ -57,15 +59,25 @@ public abstract class WindmillReaderIteratorBase<T>
@Override
public boolean advance() throws IOException {
if (context.workIsFailed()) {
- throw new
WorkItemCancelledException(context.getWorkItem().getShardingKey());
+ throw new
WorkItemCancelledException(checkNotNull(context.getWorkItem()).getShardingKey());
}
while (true) {
if (bundleIndex >= work.getMessageBundlesCount()) {
- current = null;
+ // If elements are exhausted, try advancing the execution context to
the next key in the
+ // group
context.finishKey();
+ if (context.advance()) {
+ // Transition succeeded! Update iterator references to the new work
item
+ resetWorkFromContext();
+ continue;
+ }
+
+ // All work items are exhausted.
+ current = null;
return false;
}
+
Windmill.InputMessageBundle bundle = work.getMessageBundles(bundleIndex);
++messageIndex;
if (messageIndex >= bundle.getMessagesCount()) {
@@ -73,6 +85,7 @@ public abstract class WindmillReaderIteratorBase<T>
++bundleIndex;
continue;
}
+
try {
current =
checkNotNull(decodeMessage(bundle.getMessages(messageIndex)));
return true;
@@ -91,6 +104,12 @@ public abstract class WindmillReaderIteratorBase<T>
}
}
+ private void resetWorkFromContext() {
+ this.work = context.getWorkItem();
+ this.bundleIndex = 0;
+ this.messageIndex = -1;
+ }
+
protected abstract WindowedValue<T> decodeMessage(Windmill.Message message)
throws IOException;
@Override
diff --git
a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/WindowingWindmillReader.java
b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/WindowingWindmillReader.java
index 488684769bd..c258e25146e 100644
---
a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/WindowingWindmillReader.java
+++
b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/WindowingWindmillReader.java
@@ -30,7 +30,6 @@ import
org.apache.beam.runners.dataflow.options.DataflowStreamingPipelineOptions
import org.apache.beam.runners.dataflow.util.CloudObject;
import org.apache.beam.runners.dataflow.worker.util.ValueInEmptyWindows;
import org.apache.beam.runners.dataflow.worker.util.common.worker.NativeReader;
-import org.apache.beam.runners.dataflow.worker.windmill.Windmill.WorkItem;
import org.apache.beam.sdk.annotations.Internal;
import org.apache.beam.sdk.coders.Coder;
import org.apache.beam.sdk.options.PipelineOptions;
@@ -49,7 +48,6 @@ import org.checkerframework.checker.nullness.qual.Nullable;
@Internal
class WindowingWindmillReader<K, T> extends
NativeReader<WindowedValue<KeyedWorkItem<K, T>>> {
- private final Coder<K> keyCoder;
private final Coder<T> valueCoder;
private final Coder<? extends BoundedWindow> windowCoder;
private final Coder<Collection<? extends BoundedWindow>> windowsCoder;
@@ -66,7 +64,6 @@ class WindowingWindmillReader<K, T> extends
NativeReader<WindowedValue<KeyedWork
this.windowCoder = inputCoder.getWindowCoder();
WindmillKeyedWorkItem.FakeKeyedWorkItemCoder<K, T> keyedWorkItemCoder =
(WindmillKeyedWorkItem.FakeKeyedWorkItemCoder<K, T>)
inputCoder.getValueCoder();
- this.keyCoder = keyedWorkItemCoder.getKeyCoder();
this.valueCoder = keyedWorkItemCoder.getElementCoder();
this.context = context;
this.skipUndecodableElements = skipUndecodableElements;
@@ -129,73 +126,77 @@ class WindowingWindmillReader<K, T> extends
NativeReader<WindowedValue<KeyedWork
return new WindowingWindmillReader<>(coder, context,
skipUndecodableElements);
}
+ private KeyedWorkItem<K, T> createKeyedWorkItem() {
+ @SuppressWarnings("unchecked")
+ @Nullable
+ K key = (K) context.getKey();
+ return new WindmillKeyedWorkItem<>(
+ key,
+ context.getWorkItem(),
+ windowCoder,
+ windowsCoder,
+ valueCoder,
+ context.getWindmillTagEncoding(),
+ context.getDrainMode(),
+ skipUndecodableElements.isAccessible()
+ && Boolean.TRUE.equals(skipUndecodableElements.get()));
+ }
+
+ private boolean isEmpty(KeyedWorkItem<K, T> keyedWorkItem) {
+ return Iterables.isEmpty(keyedWorkItem.timersIterable())
+ && Iterables.isEmpty(keyedWorkItem.elementsIterable());
+ }
+
@Override
public NativeReaderIterator<WindowedValue<KeyedWorkItem<K, T>>> iterator()
throws IOException {
- final K key =
- keyCoder.decode(
- checkStateNotNull(context.getSerializedKey()).newInput(),
Coder.Context.OUTER);
- final WorkItem workItem = context.getWorkItem();
- KeyedWorkItem<K, T> keyedWorkItem =
- new WindmillKeyedWorkItem<>(
- key,
- workItem,
- windowCoder,
- windowsCoder,
- valueCoder,
- context.getWindmillTagEncoding(),
- context.getDrainMode(),
- skipUndecodableElements.isAccessible()
- && Boolean.TRUE.equals(skipUndecodableElements.get()));
- final boolean isEmptyWorkItem =
- (Iterables.isEmpty(keyedWorkItem.timersIterable())
- && Iterables.isEmpty(keyedWorkItem.elementsIterable()));
- final WindowedValue<KeyedWorkItem<K, T>> value = new
ValueInEmptyWindows<>(keyedWorkItem);
-
- // Return a noop iterator when current workitem is an empty workitem.
- if (isEmptyWorkItem) {
- return new NativeReaderIterator<WindowedValue<KeyedWorkItem<K, T>>>() {
- @Override
- public boolean start() throws IOException {
- context.finishKey();
- return false;
- }
-
- @Override
- public boolean advance() throws IOException {
- return false;
+ return new NativeReaderIterator<WindowedValue<KeyedWorkItem<K, T>>>() {
+ private @Nullable WindowedValue<KeyedWorkItem<K, T>> current = null;
+
+ @Override
+ public boolean start() throws IOException {
+ if (context.workIsFailed()) {
+ throw new WorkItemCancelledException(
+ checkStateNotNull(context.getWorkItem()).getShardingKey());
}
-
- @Override
- public WindowedValue<KeyedWorkItem<K, T>> getCurrent() {
- throw new NoSuchElementException();
+ KeyedWorkItem<K, T> firstKeyedWorkItem = createKeyedWorkItem();
+ if (isEmpty(firstKeyedWorkItem)) {
+ return advance(); // Try to transition immediately if the first key
is empty!
}
- };
- } else {
- return new NativeReaderIterator<WindowedValue<KeyedWorkItem<K, T>>>() {
- private @Nullable WindowedValue<KeyedWorkItem<K, T>> current = null;
-
- @Override
- public boolean start() throws IOException {
- current = value;
- return true;
+ current = new ValueInEmptyWindows<>(firstKeyedWorkItem);
+ return true;
+ }
+
+ @Override
+ public boolean advance() throws IOException {
+ if (context.workIsFailed()) {
+ throw new WorkItemCancelledException(
+ checkStateNotNull(context.getWorkItem()).getShardingKey());
}
- @Override
- public boolean advance() throws IOException {
- current = null;
+ while (true) {
context.finishKey();
+ if (context.advance()) {
+ KeyedWorkItem<K, T> newKeyedWorkItem = createKeyedWorkItem();
+ if (isEmpty(newKeyedWorkItem)) {
+ continue;
+ }
+ current = new ValueInEmptyWindows<>(newKeyedWorkItem);
+ return true;
+ }
+
+ current = null;
return false;
}
+ }
- @Override
- public WindowedValue<KeyedWorkItem<K, T>> getCurrent() {
- if (current == null) {
- throw new NoSuchElementException();
- }
- return value;
+ @Override
+ public WindowedValue<KeyedWorkItem<K, T>> getCurrent() {
+ if (current == null) {
+ throw new NoSuchElementException();
}
- };
- }
+ return current;
+ }
+ };
}
@Override
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 b4f3a22a7f5..b8ee42c8ef8 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
@@ -18,21 +18,16 @@
package org.apache.beam.runners.dataflow.worker.streaming;
import com.google.auto.value.AutoValue;
-import java.util.HashMap;
import java.util.Optional;
import javax.annotation.concurrent.NotThreadSafe;
import org.apache.beam.runners.core.metrics.ExecutionStateTracker;
-import org.apache.beam.runners.dataflow.worker.DataflowMapTaskExecutor;
import org.apache.beam.runners.dataflow.worker.DataflowWorkExecutor;
import org.apache.beam.runners.dataflow.worker.StreamingModeExecutionContext;
-import
org.apache.beam.runners.dataflow.worker.streaming.sideinput.SideInputStateFetcher;
-import
org.apache.beam.runners.dataflow.worker.util.common.worker.ElementCounter;
-import
org.apache.beam.runners.dataflow.worker.util.common.worker.OutputObjectAndByteCounter;
-import org.apache.beam.runners.dataflow.worker.windmill.Windmill;
+import
org.apache.beam.runners.dataflow.worker.StreamingModeExecutionContext.KeyTransitionListener;
+import org.apache.beam.runners.dataflow.worker.util.BoundedQueueExecutor;
import
org.apache.beam.runners.dataflow.worker.windmill.state.WindmillStateReader;
import org.apache.beam.sdk.annotations.Internal;
import org.apache.beam.sdk.coders.Coder;
-import org.checkerframework.checker.nullness.qual.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -58,7 +53,7 @@ public abstract class ComputationWorkExecutor {
public abstract DataflowWorkExecutor workExecutor();
- public abstract StreamingModeExecutionContext context();
+ abstract StreamingModeExecutionContext context();
public abstract Optional<Coder<?>> keyCoder();
@@ -67,15 +62,24 @@ public abstract class ComputationWorkExecutor {
/**
* Executes DoFns for the Work. Blocks the calling thread until DoFn(s) have
completed execution.
*/
- public final void executeWork(
- @Nullable Object key,
+ public final StreamingModeExecutionContext executeWork(
Work work,
WindmillStateReader stateReader,
- SideInputStateFetcher sideInputStateFetcher,
- Windmill.WorkItemCommitRequest.Builder outputBuilder)
+ BoundedQueueExecutor workQueueExecutor,
+ BoundedQueueExecutorWorkHandle budgetHandle,
+ KeyTransitionListener keyTransitionListener)
throws Exception {
- context().start(key, work, stateReader, sideInputStateFetcher,
outputBuilder, workExecutor());
+ context()
+ .start(
+ work,
+ stateReader,
+ workExecutor(),
+ workQueueExecutor,
+ budgetHandle,
+ keyCoder().orElse(null),
+ keyTransitionListener);
workExecutor().execute();
+ return context();
}
/**
@@ -84,6 +88,7 @@ public abstract class ComputationWorkExecutor {
*/
public final void invalidate() {
context().invalidateCache();
+ context().reset();
try {
workExecutor().close();
} catch (Exception e) {
@@ -91,18 +96,6 @@ public abstract class ComputationWorkExecutor {
}
}
- public final long computeSourceBytesProcessed(String sourceBytesCounterName)
{
- HashMap<String, ElementCounter> counters =
- ((DataflowMapTaskExecutor) workExecutor())
- .getReadOperation()
- .receivers[0]
- .getOutputCounters();
-
- return Optional.ofNullable(counters.get(sourceBytesCounterName))
- .map(counter -> ((OutputObjectAndByteCounter)
counter).getByteCount().getAndReset())
- .orElse(0L);
- }
-
@AutoValue.Builder
public abstract static class Builder {
public abstract Builder setWorkExecutor(DataflowWorkExecutor workExecutor);
diff --git
a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/streaming/Work.java
b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/streaming/Work.java
index 53ed30fdedb..252a16a38bc 100644
---
a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/streaming/Work.java
+++
b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/streaming/Work.java
@@ -28,6 +28,8 @@ import java.util.Map.Entry;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import java.util.function.Supplier;
import javax.annotation.concurrent.NotThreadSafe;
@@ -82,6 +84,8 @@ public final class Work implements RefreshableWork {
private volatile TimedState currentState;
private volatile boolean isFailed;
private volatile String processingThreadName = "";
+ private final AtomicReference<@Nullable AtomicBoolean> onFailureListener =
+ new AtomicReference<>(null);
private final boolean drainMode;
private Work(
@@ -191,6 +195,10 @@ public final class Work implements RefreshableWork {
return serializedWorkItemSize;
}
+ public String getComputationId() {
+ return processingContext.computationId();
+ }
+
@Override
public ShardedKey getShardedKey() {
return shardedKey;
@@ -244,6 +252,19 @@ public final class Work implements RefreshableWork {
@Override
public void setFailed() {
this.isFailed = true;
+ AtomicBoolean listener = onFailureListener.get();
+ if (listener != null) {
+ listener.set(true);
+ }
+ }
+
+ // Sets the passed in boolean to true if the work fails
+ // Supports registering only one boolean at a time.
+ public void setOnFailureListener(@Nullable AtomicBoolean listener) {
+ onFailureListener.set(listener);
+ if (isFailed && listener != null) {
+ listener.set(true);
+ }
}
public boolean isCommitPending() {
@@ -268,6 +289,10 @@ public final class Work implements RefreshableWork {
processingContext.workCommitter().accept(Commit.create(commitRequest,
computationState, this));
}
+ public Consumer<Commit> workCommitter() {
+ return processingContext.workCommitter();
+ }
+
public WindmillStateReader createWindmillStateReader() {
return WindmillStateReader.forWork(this);
}
@@ -390,10 +415,6 @@ public final class Work implements RefreshableWork {
abstract Instant startTime();
}
- public String getComputationId() {
- return processingContext.computationId();
- }
-
public KeyGroup getKeyGroup() {
return keyGroup;
}
diff --git
a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/work/processing/ComputationWorkExecutorFactory.java
b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/work/processing/ComputationWorkExecutorFactory.java
index 26979990330..4a52d9fde77 100644
---
a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/work/processing/ComputationWorkExecutorFactory.java
+++
b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/work/processing/ComputationWorkExecutorFactory.java
@@ -29,6 +29,7 @@ import
org.apache.beam.runners.dataflow.worker.DataflowExecutionContext;
import org.apache.beam.runners.dataflow.worker.DataflowExecutionStateSampler;
import org.apache.beam.runners.dataflow.worker.DataflowMapTaskExecutor;
import org.apache.beam.runners.dataflow.worker.DataflowMapTaskExecutorFactory;
+import org.apache.beam.runners.dataflow.worker.HotKeyLogger;
import org.apache.beam.runners.dataflow.worker.IntrinsicMapTaskExecutorFactory;
import org.apache.beam.runners.dataflow.worker.ReaderCache;
import org.apache.beam.runners.dataflow.worker.ReaderRegistry;
@@ -48,6 +49,7 @@ 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.StageInfo;
import
org.apache.beam.runners.dataflow.worker.streaming.config.StreamingGlobalConfigHandle;
+import
org.apache.beam.runners.dataflow.worker.streaming.sideinput.SideInputStateFetcherFactory;
import
org.apache.beam.runners.dataflow.worker.util.common.worker.MapTaskExecutor;
import
org.apache.beam.runners.dataflow.worker.util.common.worker.OutputObjectAndByteCounter;
import
org.apache.beam.runners.dataflow.worker.util.common.worker.ReadOperation;
@@ -97,6 +99,8 @@ final class ComputationWorkExecutorFactory {
private final IdGenerator idGenerator;
private final StreamingGlobalConfigHandle globalConfigHandle;
private final boolean throwExceptionOnLargeOutput;
+ private final HotKeyLogger hotKeyLogger;
+ private final SideInputStateFetcherFactory sideInputStateFetcherFactory;
ComputationWorkExecutorFactory(
DataflowWorkerHarnessOptions options,
@@ -106,7 +110,9 @@ final class ComputationWorkExecutorFactory {
DataflowExecutionStateSampler sampler,
CounterSet pendingDeltaCounters,
IdGenerator idGenerator,
- StreamingGlobalConfigHandle globalConfigHandle) {
+ StreamingGlobalConfigHandle globalConfigHandle,
+ HotKeyLogger hotKeyLogger,
+ SideInputStateFetcherFactory sideInputStateFetcherFactory) {
this.options = options;
this.mapTaskExecutorFactory = mapTaskExecutorFactory;
this.readerCache = readerCache;
@@ -124,6 +130,8 @@ final class ComputationWorkExecutorFactory {
: StreamingDataflowWorker.MAX_SINK_BYTES;
this.throwExceptionOnLargeOutput =
hasExperiment(options, THROW_EXCEPTIONS_ON_LARGE_OUTPUT_EXPERIMENT);
+ this.hotKeyLogger = hotKeyLogger;
+ this.sideInputStateFetcherFactory = sideInputStateFetcherFactory;
}
private static Nodes.ParallelInstructionNode extractReadNode(
@@ -191,8 +199,12 @@ final class ComputationWorkExecutorFactory {
DataflowExecutionContext.DataflowExecutionStateTracker
executionStateTracker =
createExecutionStateTracker(stageInfo, mapTask, workLatencyTrackingId);
+ boolean hotKeyLoggingEnabled =
+ options.isHotKeyLoggingEnabled() || hasExperiment(options,
"enable_hot_key_logging");
+ String stepName = getShuffleTaskStepName(mapTask);
StreamingModeExecutionContext context =
- createExecutionContext(computationState, stageInfo,
executionStateTracker);
+ createExecutionContext(
+ computationState, stageInfo, executionStateTracker,
hotKeyLoggingEnabled, stepName);
DataflowMapTaskExecutor mapTaskExecutor =
createMapTaskExecutor(context, mapTask, mapTaskNetwork);
ReadOperation readOperation = getValidatedReadOperation(mapTaskExecutor);
@@ -255,7 +267,9 @@ final class ComputationWorkExecutorFactory {
private StreamingModeExecutionContext createExecutionContext(
ComputationState computationState,
StageInfo stageInfo,
- DataflowExecutionContext.DataflowExecutionStateTracker
executionStateTracker) {
+ DataflowExecutionContext.DataflowExecutionStateTracker
executionStateTracker,
+ boolean hotKeyLoggingEnabled,
+ String stepName) {
String computationId = computationState.getComputationId();
return new StreamingModeExecutionContext(
pendingDeltaCounters,
@@ -268,7 +282,12 @@ final class ComputationWorkExecutorFactory {
stageInfo.executionStateRegistry(),
globalConfigHandle,
maxSinkBytes,
- throwExceptionOnLargeOutput);
+ throwExceptionOnLargeOutput,
+ hotKeyLogger,
+ hotKeyLoggingEnabled,
+ stepName,
+ computationState.sourceBytesProcessCounterName(),
+ sideInputStateFetcherFactory);
}
private DataflowMapTaskExecutor createMapTaskExecutor(
@@ -286,6 +305,12 @@ final class ComputationWorkExecutorFactory {
idGenerator);
}
+ private static String getShuffleTaskStepName(MapTask mapTask) {
+ // The MapTask instruction is ordered by dependencies, such that the first
element is
+ // always going to be the shuffle task.
+ return mapTask.getInstructions().get(0).getName();
+ }
+
private DataflowExecutionContext.DataflowExecutionStateTracker
createExecutionStateTracker(
StageInfo stageInfo, MapTask mapTask, String workLatencyTrackingId) {
return new DataflowExecutionContext.DataflowExecutionStateTracker(
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 a3f23aebdf8..e9b85d720d2 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
@@ -17,22 +17,26 @@
*/
package org.apache.beam.runners.dataflow.worker.windmill.work.processing;
-import static org.apache.beam.sdk.options.ExperimentalOptions.hasExperiment;
+import static
org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkState;
import com.google.api.services.dataflow.model.MapTask;
import com.google.auto.value.AutoValue;
-import java.util.Optional;
+import java.util.List;
+import java.util.Map;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.function.Supplier;
import javax.annotation.concurrent.ThreadSafe;
+import org.apache.beam.repackaged.core.org.apache.commons.lang3.tuple.Pair;
import org.apache.beam.runners.dataflow.options.DataflowWorkerHarnessOptions;
import org.apache.beam.runners.dataflow.worker.DataflowExecutionStateSampler;
import org.apache.beam.runners.dataflow.worker.DataflowMapTaskExecutorFactory;
import org.apache.beam.runners.dataflow.worker.HotKeyLogger;
import org.apache.beam.runners.dataflow.worker.ReaderCache;
+import org.apache.beam.runners.dataflow.worker.StreamingModeExecutionContext;
+import
org.apache.beam.runners.dataflow.worker.StreamingModeExecutionContext.KeyTransitionListener;
import org.apache.beam.runners.dataflow.worker.WorkItemCancelledException;
import
org.apache.beam.runners.dataflow.worker.logging.DataflowWorkerLoggingMDC;
import
org.apache.beam.runners.dataflow.worker.streaming.BoundedQueueExecutorWorkHandle;
@@ -45,7 +49,6 @@ 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.StreamingGlobalConfigHandle;
import
org.apache.beam.runners.dataflow.worker.streaming.harness.StreamingCounters;
-import
org.apache.beam.runners.dataflow.worker.streaming.sideinput.SideInputStateFetcher;
import
org.apache.beam.runners.dataflow.worker.streaming.sideinput.SideInputStateFetcherFactory;
import org.apache.beam.runners.dataflow.worker.util.BoundedQueueExecutor;
import org.apache.beam.runners.dataflow.worker.util.ExceptionUtils;
@@ -57,12 +60,10 @@ import
org.apache.beam.runners.dataflow.worker.windmill.state.WindmillStateReade
import
org.apache.beam.runners.dataflow.worker.windmill.work.processing.failures.FailureTracker;
import
org.apache.beam.runners.dataflow.worker.windmill.work.processing.failures.WorkFailureProcessor;
import org.apache.beam.sdk.annotations.Internal;
-import org.apache.beam.sdk.coders.Coder;
import org.apache.beam.sdk.fn.IdGenerator;
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.checkerframework.checker.nullness.qual.Nullable;
-import org.joda.time.Duration;
import org.joda.time.Instant;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -78,41 +79,35 @@ public class StreamingWorkScheduler {
private static final Logger LOG =
LoggerFactory.getLogger(StreamingWorkScheduler.class);
- private final DataflowWorkerHarnessOptions options;
private final Supplier<Instant> clock;
private final ComputationWorkExecutorFactory computationWorkExecutorFactory;
- private final SideInputStateFetcherFactory sideInputStateFetcherFactory;
private final FailureTracker failureTracker;
private final WorkFailureProcessor workFailureProcessor;
private final StreamingCommitFinalizer commitFinalizer;
private final StreamingCounters streamingCounters;
- private final HotKeyLogger hotKeyLogger;
private final ConcurrentMap<String, StageInfo> stageInfoMap;
private final DataflowExecutionStateSampler sampler;
private final StreamingGlobalConfigHandle globalConfigHandle;
+ private final BoundedQueueExecutor workExecutor;
public StreamingWorkScheduler(
- DataflowWorkerHarnessOptions options,
Supplier<Instant> clock,
+ BoundedQueueExecutor workExecutor,
ComputationWorkExecutorFactory computationWorkExecutorFactory,
- SideInputStateFetcherFactory sideInputStateFetcherFactory,
FailureTracker failureTracker,
WorkFailureProcessor workFailureProcessor,
StreamingCommitFinalizer commitFinalizer,
StreamingCounters streamingCounters,
- HotKeyLogger hotKeyLogger,
ConcurrentMap<String, StageInfo> stageInfoMap,
DataflowExecutionStateSampler sampler,
StreamingGlobalConfigHandle globalConfigHandle) {
- this.options = options;
this.clock = clock;
+ this.workExecutor = workExecutor;
this.computationWorkExecutorFactory = computationWorkExecutorFactory;
- this.sideInputStateFetcherFactory = sideInputStateFetcherFactory;
this.failureTracker = failureTracker;
this.workFailureProcessor = workFailureProcessor;
this.commitFinalizer = commitFinalizer;
this.streamingCounters = streamingCounters;
- this.hotKeyLogger = hotKeyLogger;
this.stageInfoMap = stageInfoMap;
this.sampler = sampler;
this.globalConfigHandle = globalConfigHandle;
@@ -134,6 +129,9 @@ public class StreamingWorkScheduler {
IdGenerator idGenerator,
StreamingGlobalConfigHandle globalConfigHandle,
ConcurrentMap<String, StageInfo> stageInfoMap) {
+ SideInputStateFetcherFactory sideInputStateFetcherFactory =
+ SideInputStateFetcherFactory.fromOptions(options);
+
ComputationWorkExecutorFactory computationWorkExecutorFactory =
new ComputationWorkExecutorFactory(
options,
@@ -143,18 +141,18 @@ public class StreamingWorkScheduler {
sampler,
streamingCounters.pendingDeltaCounters(),
idGenerator,
- globalConfigHandle);
+ globalConfigHandle,
+ hotKeyLogger,
+ sideInputStateFetcherFactory);
return new StreamingWorkScheduler(
- options,
clock,
+ workExecutor,
computationWorkExecutorFactory,
- SideInputStateFetcherFactory.fromOptions(options),
failureTracker,
workFailureProcessor,
StreamingCommitFinalizer.create(workExecutor,
commitFinalizerCleanupExecutor),
streamingCounters,
- hotKeyLogger,
stageInfoMap,
sampler,
globalConfigHandle);
@@ -187,21 +185,16 @@ public class StreamingWorkScheduler {
/** Sets the stage name and workId of the Thread executing the {@link Work}
for logging. */
private static void setUpWorkLoggingContext(String workLatencyTrackingId,
String computationId) {
- DataflowWorkerLoggingMDC.setWorkId(workLatencyTrackingId);
- DataflowWorkerLoggingMDC.setStageName(computationId);
+ setLoggingContextWorkId(workLatencyTrackingId);
+ setLoggingContextComputation(computationId);
}
- private static String getShuffleTaskStepName(MapTask mapTask) {
- // The MapTask instruction is ordered by dependencies, such that the first
element is
- // always going to be the shuffle task.
- return mapTask.getInstructions().get(0).getName();
+ private static void setLoggingContextComputation(@Nullable String
computationId) {
+ DataflowWorkerLoggingMDC.setStageName(computationId);
}
- /** Resets logging context of the Thread executing the {@link Work} for
logging. */
- private void resetWorkLoggingContext(String workLatencyTrackingId) {
- sampler.resetForWorkId(workLatencyTrackingId);
- DataflowWorkerLoggingMDC.setWorkId(null);
- DataflowWorkerLoggingMDC.setStageName(null);
+ private static void setLoggingContextWorkId(@Nullable String
workLatencyTrackingId) {
+ DataflowWorkerLoggingMDC.setWorkId(workLatencyTrackingId);
}
/**
@@ -248,46 +241,39 @@ public class StreamingWorkScheduler {
}
private void processWork(
- ComputationState computationState, Work work,
BoundedQueueExecutorWorkHandle unusedHandle) {
+ ComputationState computationState, Work work,
BoundedQueueExecutorWorkHandle handle) {
Windmill.WorkItem workItem = work.getWorkItem();
String computationId = computationState.getComputationId();
- ByteString key = workItem.getKey();
work.setProcessingThreadName(Thread.currentThread().getName());
work.setState(Work.State.PROCESSING);
setUpWorkLoggingContext(work.getLatencyTrackingId(), computationId);
LOG.debug("Starting processing for {}:\n{}", computationId, work);
if (workItem.getSourceState().getOnlyFinalize()) {
- Windmill.WorkItemCommitRequest.Builder outputBuilder =
initializeOutputBuilder(key, workItem);
-
outputBuilder.setSourceStateUpdates(Windmill.SourceState.newBuilder().setOnlyFinalize(true));
- work.setState(Work.State.COMMIT_QUEUED);
- work.queueCommit(outputBuilder.build(), computationState);
+ handleOnlyFinalize(computationState, work, workItem);
return;
}
long processingStartTimeNanos = System.nanoTime();
- MapTask mapTask = computationState.getMapTask();
- StageInfo stageInfo =
- stageInfoMap.computeIfAbsent(
- mapTask.getStageName(), s -> StageInfo.create(s,
mapTask.getSystemName()));
+ StageInfo stageInfo = getStageInfo(computationState);
+ @Nullable List<Work> workBatch = null;
try {
if (work.isFailed()) {
throw new WorkItemCancelledException(workItem.getShardingKey());
}
- // Execute the user code for the Work.
- ExecuteWorkResult executeWorkResult = executeWork(work, stageInfo,
computationState);
- Windmill.WorkItemCommitRequest.Builder commitRequest =
executeWorkResult.commitWorkRequest();
+ // Execute the user code for the Work batch.
+ ExecuteWorkResult executeWorkResult = executeWork(work, stageInfo,
computationState, handle);
+ workBatch = executeWorkResult.workBatch();
+ List<Windmill.WorkItemCommitRequest> workItemCommits =
executeWorkResult.workItemCommits();
+
+
commitFinalizer.cacheCommitFinalizers(executeWorkResult.finalizationCallbacks());
- // Validate the commit request, possibly requesting truncation if the
commitSize is too large.
- Windmill.WorkItemCommitRequest validatedCommitRequest =
- validateCommitRequestSize(commitRequest.build(), computationId,
workItem);
+ commitWorkBatch(computationState, workBatch, workItemCommits);
- // Queue the commit.
- work.queueCommit(validatedCommitRequest, computationState);
- recordProcessingStats(commitRequest, workItem, executeWorkResult);
- LOG.debug("Processing done for work token: {}", workItem.getWorkToken());
+ recordProcessingStats(workBatch, workItemCommits,
executeWorkResult.stateBytesRead());
+ LOG.debug("Processing done for work batch size: {}", workBatch.size());
} catch (Throwable t) {
// OutOfMemoryError that are caught will be rethrown and trigger jvm
termination.
try {
@@ -305,23 +291,17 @@ public class StreamingWorkScheduler {
throw ExceptionUtils.safeWrapThrowableAsException(t2);
}
} finally {
+ List<Work> processedWorkBatch = workBatch != null ? workBatch :
ImmutableList.of(work);
// Update total processing time counters. Updating in finally clause
ensures that
// work items causing exceptions are also accounted in time spent.
- long processingTimeMsecs =
- TimeUnit.NANOSECONDS.toMillis(System.nanoTime() -
processingStartTimeNanos);
- stageInfo.totalProcessingMsecs().addValue(processingTimeMsecs);
+ recordProcessingTime(stageInfo, processedWorkBatch,
processingStartTimeNanos);
- // Attribute all the processing to timers if the work item contains any
timers.
- // Tests show that work items rarely contain both timers and message
bundles. It should
- // be a fairly close approximation.
- // Another option: Derive time split between messages and timers based
on recent totals.
- // either here or in DFE.
- if (work.getWorkItem().hasTimers()) {
- stageInfo.timerProcessingMsecs().addValue(processingTimeMsecs);
+ setLoggingContextWorkId(null);
+ setLoggingContextComputation(null);
+ sampler.resetForWorkId(work.getLatencyTrackingId());
+ for (Work w : processedWorkBatch) {
+ w.setProcessingThreadName("");
}
-
- resetWorkLoggingContext(work.getLatencyTrackingId());
- work.setProcessingThreadName("");
}
}
@@ -353,27 +333,37 @@ public class StreamingWorkScheduler {
}
private void recordProcessingStats(
- Windmill.WorkItemCommitRequest.Builder outputBuilder,
- Windmill.WorkItem workItem,
- ExecuteWorkResult executeWorkResult) {
- // Compute shuffle and state byte statistics these will be flushed
asynchronously.
- long stateBytesWritten =
- outputBuilder
- .clearOutputMessages()
- .clearPerWorkItemLatencyAttributions()
- .build()
- .getSerializedSize();
-
-
streamingCounters.windmillShuffleBytesRead().addValue(computeShuffleBytesRead(workItem));
-
streamingCounters.windmillStateBytesRead().addValue(executeWorkResult.stateBytesRead());
- streamingCounters.windmillStateBytesWritten().addValue(stateBytesWritten);
+ List<Work> workBatch,
+ List<Windmill.WorkItemCommitRequest> workItemCommits,
+ long totalStateBytesRead) {
+ long totalStateBytesWritten = 0;
+ long totalShuffleBytesRead = 0;
+ checkState(workBatch.size() == workItemCommits.size());
+ for (int i = 0; i < workBatch.size(); i++) {
+ Windmill.WorkItem workItem = workBatch.get(i).getWorkItem();
+ Windmill.WorkItemCommitRequest commit = workItemCommits.get(i);
+ // Compute shuffle and state byte statistics these will be flushed
asynchronously.
+ long stateBytesWritten =
+ commit
+ .toBuilder()
+ .clearOutputMessages()
+ .clearPerWorkItemLatencyAttributions()
+ .build()
+ .getSerializedSize();
+ totalStateBytesWritten += stateBytesWritten;
+ totalShuffleBytesRead += computeShuffleBytesRead(workItem);
+ }
+
streamingCounters.windmillShuffleBytesRead().addValue(totalShuffleBytesRead);
+ streamingCounters.windmillStateBytesRead().addValue(totalStateBytesRead);
+
streamingCounters.windmillStateBytesWritten().addValue(totalStateBytesWritten);
}
private ExecuteWorkResult executeWork(
- Work work, StageInfo stageInfo, ComputationState computationState)
throws Exception {
- Windmill.WorkItem workItem = work.getWorkItem();
- ByteString key = workItem.getKey();
- Windmill.WorkItemCommitRequest.Builder outputBuilder =
initializeOutputBuilder(key, workItem);
+ Work work,
+ StageInfo stageInfo,
+ ComputationState computationState,
+ BoundedQueueExecutorWorkHandle handle)
+ throws Exception {
ComputationWorkExecutor computationWorkExecutor =
computationState
.acquireComputationWorkExecutor()
@@ -384,89 +374,137 @@ public class StreamingWorkScheduler {
try {
WindmillStateReader stateReader = work.createWindmillStateReader();
- SideInputStateFetcher localSideInputStateFetcher =
-
sideInputStateFetcherFactory.createSideInputStateFetcher(work::fetchSideInput);
-
- // If the read output KVs, then we can decode Windmill's byte key into
userland
- // key object and provide it to the execution context for use with
per-key state.
- // Otherwise, we pass null.
- //
- // The coder type that will be present is:
- // WindowedValueCoder(TimerOrElementCoder(KvCoder))
- Optional<Coder<?>> keyCoder = computationWorkExecutor.keyCoder();
- @SuppressWarnings("deprecation")
- @Nullable
- final Object executionKey =
- !keyCoder.isPresent() ? null : keyCoder.get().decode(key.newInput(),
Coder.Context.OUTER);
-
- if (workItem.hasHotKeyInfo()) {
- Windmill.HotKeyInfo hotKeyInfo = workItem.getHotKeyInfo();
- Duration hotKeyAge = Duration.millis(hotKeyInfo.getHotKeyAgeUsec() /
1000);
-
- String stepName =
getShuffleTaskStepName(computationState.getMapTask());
- if (executionKey != null
- && (options.isHotKeyLoggingEnabled()
- || hasExperiment(options, "enable_hot_key_logging"))
- && keyCoder.isPresent()) {
- hotKeyLogger.logHotKeyDetection(stepName, hotKeyAge, executionKey);
- } else {
- hotKeyLogger.logHotKeyDetection(stepName, hotKeyAge);
+
+ KeyTransitionListener keyTransitionListener =
createKeyTransitionListener();
+
+ List<Work> workBatch;
+ List<Windmill.WorkItemCommitRequest> workItemCommits;
+ Map<Long, Pair<Instant, Runnable>> finalizationCallbacks;
+ long stateBytesRead;
+ {
+ // Blocks while executing work.
+ StreamingModeExecutionContext context =
+ computationWorkExecutor.executeWork(
+ work, stateReader, workExecutor, handle,
keyTransitionListener);
+ if (context.workIsFailed()) {
+ throw new
WorkItemCancelledException(work.getWorkItem().getShardingKey());
}
- }
+ context.flushState();
- // Blocks while executing work.
- computationWorkExecutor.executeWork(
- executionKey, work, stateReader, localSideInputStateFetcher,
outputBuilder);
+ // Retrieve executed works, work item commits, and accumulated
callbacks from execution
+ // context
+ workBatch = context.getExecutedWorks();
+ workItemCommits = context.getWorkItemCommits();
+ finalizationCallbacks = context.getFinalizationCallbacks();
+ stateBytesRead = context.getStateBytesRead();
- if (work.isFailed()) {
- throw new WorkItemCancelledException(workItem.getShardingKey());
- }
-
- // Reports source bytes processed to WorkItemCommitRequest if available.
- try {
- long sourceBytesProcessed =
- computationWorkExecutor.computeSourceBytesProcessed(
- computationState.sourceBytesProcessCounterName());
- outputBuilder.setSourceBytesProcessed(sourceBytesProcessed);
- } catch (Exception e) {
- LOG.error("{}", e.toString());
+ context.reset(); // Don't use context after this.
}
-
-
commitFinalizer.cacheCommitFinalizers(computationWorkExecutor.context().flushState());
-
// Release the execution state for another thread to use.
computationState.releaseComputationWorkExecutor(computationWorkExecutor);
computationWorkExecutor = null;
- work.setState(Work.State.COMMIT_QUEUED);
-
outputBuilder.addAllPerWorkItemLatencyAttributions(work.getLatencyAttributions(sampler));
-
return ExecuteWorkResult.create(
- outputBuilder, stateReader.getBytesRead() +
localSideInputStateFetcher.getBytesRead());
+ workBatch, workItemCommits, finalizationCallbacks, stateBytesRead);
} catch (Throwable t) {
if (computationWorkExecutor != null) {
// If processing failed due to a thrown exception, close the
executionState. Do not
// return/release the executionState back to computationState as that
will lead to this
// executionState instance being reused.
- LOG.debug("Invalidating executor after work item {} failed",
workItem.getWorkToken(), t);
+ LOG.debug(
+ "Invalidating executor after work item {} failed",
+ work.getWorkItem().getWorkToken(),
+ t);
computationWorkExecutor.invalidate();
}
-
// Re-throw the exception, it will be caught and handled by
workFailureProcessor downstream.
throw t;
}
}
+ private void handleOnlyFinalize(
+ ComputationState computationState, Work work, Windmill.WorkItem
workItem) {
+ Windmill.WorkItemCommitRequest.Builder outputBuilder =
+ initializeOutputBuilder(workItem.getKey(), workItem);
+
outputBuilder.setSourceStateUpdates(Windmill.SourceState.newBuilder().setOnlyFinalize(true));
+ work.setState(Work.State.COMMIT_QUEUED);
+ work.queueCommit(outputBuilder.build(), computationState);
+ }
+
+ private StageInfo getStageInfo(ComputationState computationState) {
+ MapTask mapTask = computationState.getMapTask();
+ return stageInfoMap.computeIfAbsent(
+ mapTask.getStageName(), s -> StageInfo.create(s,
mapTask.getSystemName()));
+ }
+
+ private void commitWorkBatch(
+ ComputationState computationState,
+ List<Work> workBatch,
+ List<Windmill.WorkItemCommitRequest> workItemCommits) {
+ checkState(workBatch.size() == 1, "Expected single-key work batch, got: "
+ workBatch.size());
+ checkState(workBatch.size() == workItemCommits.size());
+ commitSingleKeyWork(computationState, workBatch.get(0),
workItemCommits.get(0));
+ }
+
+ private void commitSingleKeyWork(
+ ComputationState computationState, Work work,
Windmill.WorkItemCommitRequest commitRequest) {
+ // Validate the commit request, possibly requesting truncation if the
commitSize is too large.
+ Windmill.WorkItemCommitRequest validatedCommitRequest =
+ validateCommitRequestSize(
+ commitRequest, computationState.getComputationId(),
work.getWorkItem());
+ work.setState(Work.State.COMMIT_QUEUED);
+ validatedCommitRequest =
+ validatedCommitRequest
+ .toBuilder()
+
.addAllPerWorkItemLatencyAttributions(work.getLatencyAttributions(sampler))
+ .build();
+ work.queueCommit(validatedCommitRequest, computationState);
+ }
+
+ private void recordProcessingTime(
+ StageInfo stageInfo, List<Work> worksToCleanup, long
processingStartTimeNanos) {
+ long processingTimeMsecs =
+ TimeUnit.NANOSECONDS.toMillis(System.nanoTime() -
processingStartTimeNanos);
+ stageInfo.totalProcessingMsecs().addValue(processingTimeMsecs);
+ if (anyWorkHasTimers(worksToCleanup)) {
+ // Attribute all the processing to timers if the work item contains any
timers.
+ // Tests show that work items rarely contain both timers and message
bundles. It should
+ // be a fairly close approximation.
+ // Another option: Derive time split between messages and timers based
on recent totals.
+ // either here or in DFE.
+ stageInfo.timerProcessingMsecs().addValue(processingTimeMsecs);
+ }
+ }
+
+ private static boolean anyWorkHasTimers(List<Work> works) {
+ return works.stream().anyMatch(w -> w.getWorkItem().hasTimers());
+ }
+
+ private KeyTransitionListener createKeyTransitionListener() {
+ return (oldWork, newWork) -> {
+ setLoggingContextWorkId(newWork.getLatencyTrackingId());
+ newWork.setProcessingThreadName(oldWork.getProcessingThreadName());
+ oldWork.setProcessingThreadName("");
+ };
+ }
+
@AutoValue
abstract static class ExecuteWorkResult {
-
- private static ExecuteWorkResult create(
- Windmill.WorkItemCommitRequest.Builder commitWorkRequest, long
stateBytesRead) {
+ static ExecuteWorkResult create(
+ List<Work> workBatch,
+ List<Windmill.WorkItemCommitRequest> workItemCommits,
+ Map<Long, Pair<Instant, Runnable>> finalizationCallbacks,
+ long stateBytesRead) {
return new AutoValue_StreamingWorkScheduler_ExecuteWorkResult(
- commitWorkRequest, stateBytesRead);
+ workBatch, workItemCommits, finalizationCallbacks, stateBytesRead);
}
- abstract Windmill.WorkItemCommitRequest.Builder commitWorkRequest();
+ abstract List<Work> workBatch();
+
+ abstract List<Windmill.WorkItemCommitRequest> workItemCommits();
+
+ // Map<finalizerId, Pair<callbackExpiration, callback>>
+ abstract Map<Long, Pair<Instant, Runnable>> finalizationCallbacks();
abstract long stateBytesRead();
}
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 5bcdffcc256..23730bc5770 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
@@ -420,6 +420,7 @@ public class StreamingDataflowWorkerTest {
CloudObjects.asCloudObject(IntervalWindowCoder.of(), /*
sdkComponents= */ null)));
return new ParallelInstruction()
+ .setName(DEFAULT_SOURCE_SYSTEM_NAME)
.setSystemName(DEFAULT_SOURCE_SYSTEM_NAME)
.setOriginalName(DEFAULT_SOURCE_ORIGINAL_NAME)
.setRead(
@@ -439,6 +440,7 @@ public class StreamingDataflowWorkerTest {
private ParallelInstruction makeSourceInstruction(Coder<?> coder) {
return new ParallelInstruction()
+ .setName(DEFAULT_SOURCE_SYSTEM_NAME)
.setSystemName(DEFAULT_SOURCE_SYSTEM_NAME)
.setOriginalName(DEFAULT_SOURCE_ORIGINAL_NAME)
.setRead(
@@ -527,6 +529,7 @@ public class StreamingDataflowWorkerTest {
CloudObject spec = CloudObject.forClass(WindmillSink.class);
addString(spec, "stream_id", streamId);
return new ParallelInstruction()
+ .setName(streamId)
.setSystemName(DEFAULT_SINK_SYSTEM_NAME)
.setOriginalName(DEFAULT_SINK_ORIGINAL_NAME)
.setWrite(
@@ -571,11 +574,16 @@ public class StreamingDataflowWorkerTest {
Windmill.GetWorkResponse.Builder builder =
Windmill.GetWorkResponse.newBuilder();
TextFormat.merge(input, builder);
if (metadata != null) {
- Windmill.InputMessageBundle.Builder messageBundleBuilder =
-
builder.getWorkBuilder(0).getWorkBuilder(0).getMessageBundlesBuilder(0);
- for (Windmill.Message.Builder messageBuilder :
- messageBundleBuilder.getMessagesBuilderList()) {
- messageBuilder.setMetadata(addPaneTag(PaneInfo.NO_FIRING, metadata));
+ for (Windmill.ComputationWorkItems.Builder compBuilder :
builder.getWorkBuilderList()) {
+ for (Windmill.WorkItem.Builder workBuilder :
compBuilder.getWorkBuilderList()) {
+ for (Windmill.InputMessageBundle.Builder messageBundleBuilder :
+ workBuilder.getMessageBundlesBuilderList()) {
+ for (Windmill.Message.Builder messageBuilder :
+ messageBundleBuilder.getMessagesBuilderList()) {
+ messageBuilder.setMetadata(addPaneTag(PaneInfo.NO_FIRING,
metadata));
+ }
+ }
+ }
}
}
@@ -1327,7 +1335,7 @@ public class StreamingDataflowWorkerTest {
makeExpectedTruncationRequestOutput(
1, "large_key", DEFAULT_SHARDING_KEY,
largeCommit.getEstimatedWorkItemCommitBytes())
.build(),
- largeCommit);
+ removeDynamicFields(largeCommit));
// Check this explicitly since the estimated commit bytes weren't actually
// checked against an expected value in the previous step
@@ -2495,6 +2503,7 @@ public class StreamingDataflowWorkerTest {
return Arrays.asList(
new ParallelInstruction()
+ .setName("Read")
.setSystemName("Read")
.setOriginalName("OriginalReadName")
.setRead(
@@ -3954,11 +3963,16 @@ public class StreamingDataflowWorkerTest {
LatencyAttribution.newBuilder().setState(State.ACTIVE).setTotalDurationMillis(100);
for (LatencyAttribution la :
commit.getPerWorkItemLatencyAttributionsList()) {
if (la.getState() == State.ACTIVE) {
- assertThat(la.getActiveLatencyBreakdownCount(), equalTo(1));
- assertThat(
- la.getActiveLatencyBreakdown(0).getUserStepName(),
equalTo(DEFAULT_PARDO_USER_NAME));
-
Assert.assertTrue(la.getActiveLatencyBreakdown(0).hasProcessingTimesDistribution());
-
Assert.assertFalse(la.getActiveLatencyBreakdown(0).hasActiveMessageMetadata());
+ LatencyAttribution.ActiveLatencyBreakdown pardoBreakdown = null;
+ for (LatencyAttribution.ActiveLatencyBreakdown lb :
la.getActiveLatencyBreakdownList()) {
+ if (DEFAULT_PARDO_USER_NAME.equals(lb.getUserStepName())) {
+ pardoBreakdown = lb;
+ break;
+ }
+ }
+ Assert.assertNotNull("Expected breakdown for " +
DEFAULT_PARDO_USER_NAME, pardoBreakdown);
+ Assert.assertTrue(pardoBreakdown.hasProcessingTimesDistribution());
+ Assert.assertFalse(pardoBreakdown.hasActiveMessageMetadata());
}
}
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 056185c587f..561596f68d0 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
@@ -63,7 +63,7 @@ import org.apache.beam.runners.dataflow.worker.streaming.Work;
import
org.apache.beam.runners.dataflow.worker.streaming.config.FakeGlobalConfigHandle;
import
org.apache.beam.runners.dataflow.worker.streaming.config.StreamingGlobalConfig;
import
org.apache.beam.runners.dataflow.worker.streaming.config.StreamingGlobalConfigHandle;
-import
org.apache.beam.runners.dataflow.worker.streaming.sideinput.SideInputStateFetcher;
+import
org.apache.beam.runners.dataflow.worker.streaming.sideinput.SideInputStateFetcherFactory;
import org.apache.beam.runners.dataflow.worker.util.common.worker.WorkExecutor;
import org.apache.beam.runners.dataflow.worker.windmill.Windmill;
import
org.apache.beam.runners.dataflow.worker.windmill.client.getdata.FakeGetDataClient;
@@ -73,6 +73,8 @@ import
org.apache.beam.runners.dataflow.worker.windmill.state.WindmillTagEncodin
import
org.apache.beam.runners.dataflow.worker.windmill.state.WindmillTagEncodingV2;
import
org.apache.beam.runners.dataflow.worker.windmill.work.refresh.HeartbeatSender;
import org.apache.beam.sdk.Pipeline;
+import org.apache.beam.sdk.coders.Coder;
+import org.apache.beam.sdk.coders.CoderException;
import org.apache.beam.sdk.metrics.MetricsContainer;
import org.apache.beam.sdk.options.PipelineOptionsFactory;
import org.apache.beam.sdk.state.TimeDomain;
@@ -100,7 +102,6 @@ import org.mockito.MockitoAnnotations;
public class StreamingModeExecutionContextTest {
@Rule public transient Timeout globalTimeout = Timeout.seconds(600);
- @Mock private SideInputStateFetcher sideInputStateFetcher;
@Mock private WindmillStateReader stateReader;
@Mock private WorkExecutor workExecutor;
@@ -137,7 +138,12 @@ public class StreamingModeExecutionContextTest {
executionStateRegistry,
configHandle,
Long.MAX_VALUE,
- /*throwExceptionOnLargeOutput=*/ false);
+ /*throwExceptionOnLargeOutput=*/ false,
+ new HotKeyLogger(),
+ /*hotKeyLoggingEnabled=*/ false,
+ /*stepName=*/ "stepName",
+ "sourceBytesProcessCounterName",
+ SideInputStateFetcherFactory.fromOptions(options));
}
@Before
@@ -159,25 +165,45 @@ public class StreamingModeExecutionContextTest {
Instant::now);
}
+ private void start(Work work) {
+ start(executionContext, work, null);
+ }
+
+ private void start(Work work, Coder<?> keyCoder) {
+ start(executionContext, work, keyCoder);
+ }
+
+ private void start(StreamingModeExecutionContext context, Work work) {
+ start(context, work, null);
+ }
+
+ private void start(StreamingModeExecutionContext context, Work work,
Coder<?> keyCoder) {
+ try {
+ context.start(
+ work,
+ stateReader,
+ workExecutor,
+ /* workQueueExecutor= */ null,
+ /* budgetHandle= */ null,
+ keyCoder,
+ /* keyTransitionListener= */ (k, c) -> {});
+ } catch (CoderException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
@Test
public void testTimerInternalsSetTimer() throws Exception {
- Windmill.WorkItemCommitRequest.Builder outputBuilder =
- Windmill.WorkItemCommitRequest.newBuilder();
NameContext nameContext = NameContextsForTests.nameContextForTest();
DataflowOperationContext operationContext =
executionContext.createOperationContext(nameContext);
StreamingModeExecutionContext.StepContext stepContext =
executionContext.getStepContext(operationContext);
- executionContext.start(
- "key",
+ start(
createMockWork(
Windmill.WorkItem.newBuilder().setKey(ByteString.EMPTY).setWorkToken(17L).build(),
- Watermarks.builder().setInputDataWatermark(new
Instant(1000)).build()),
- stateReader,
- sideInputStateFetcher,
- outputBuilder,
- workExecutor);
+ Watermarks.builder().setInputDataWatermark(new
Instant(1000)).build()));
TimerInternals timerInternals = stepContext.timerInternals();
@@ -191,6 +217,7 @@ public class StreamingModeExecutionContextTest {
executionContext.finishKey();
executionContext.flushState();
+ Windmill.WorkItemCommitRequest.Builder outputBuilder =
executionContext.getOutputBuilder();
Windmill.Timer timer = outputBuilder.buildPartial().getOutputTimers(0);
assertThat(timer.getTag().toStringUtf8(), equalTo("/skey+0:5000"));
assertThat(timer.getTimestamp(),
equalTo(TimeUnit.MILLISECONDS.toMicros(5000)));
@@ -199,9 +226,6 @@ public class StreamingModeExecutionContextTest {
@Test
public void testTimerInternalsProcessingTimeSkew() {
- Windmill.WorkItemCommitRequest.Builder outputBuilder =
- Windmill.WorkItemCommitRequest.newBuilder();
-
NameContext nameContext = NameContextsForTests.nameContextForTest();
DataflowOperationContext operationContext =
executionContext.createOperationContext(nameContext);
@@ -221,15 +245,10 @@ public class StreamingModeExecutionContextTest {
.setTimestamp(timerTimestamp.getMillis() * 1000)
.setType(Windmill.Timer.Type.REALTIME);
- executionContext.start(
- "key",
+ start(
createMockWork(
workItemBuilder.build(),
- Watermarks.builder().setInputDataWatermark(new
Instant(1000)).build()),
- stateReader,
- sideInputStateFetcher,
- outputBuilder,
- workExecutor);
+ Watermarks.builder().setInputDataWatermark(new
Instant(1000)).build()));
TimerInternals timerInternals = stepContext.timerInternals();
assertTrue(timerTimestamp.isBefore(timerInternals.currentProcessingTime()));
}
@@ -437,50 +456,65 @@ public class StreamingModeExecutionContextTest {
@Test
public void testSetBacklogBytes() {
- Windmill.WorkItemCommitRequest.Builder outputBuilder =
- Windmill.WorkItemCommitRequest.newBuilder();
NameContext nameContext = NameContextsForTests.nameContextForTest();
DataflowOperationContext operationContext =
executionContext.createOperationContext(nameContext);
StreamingModeExecutionContext.StepContext stepContext =
executionContext.getStepContext(operationContext);
- executionContext.start(
- "key",
+ start(
createMockWork(
Windmill.WorkItem.newBuilder().setKey(ByteString.EMPTY).setWorkToken(17L).build(),
- Watermarks.builder().setInputDataWatermark(new
Instant(1000)).build()),
- stateReader,
- sideInputStateFetcher,
- outputBuilder,
- workExecutor);
+ Watermarks.builder().setInputDataWatermark(new
Instant(1000)).build()));
stepContext.setBacklogBytes(1234.0);
executionContext.finishKey();
executionContext.flushState();
- assertEquals(1234, outputBuilder.getSourceBacklogBytes());
+ assertEquals(1234,
executionContext.getOutputBuilder().getSourceBacklogBytes());
+ }
+
+ @Test
+ public void testFinishKeyReentrantSafety() {
+ start(
+ createMockWork(
+
Windmill.WorkItem.newBuilder().setKey(ByteString.EMPTY).setWorkToken(17L).build(),
+ Watermarks.builder().setInputDataWatermark(new
Instant(1000)).build()));
+
+ // First call
+ executionContext.finishKey();
+ // Second call - should not throw any Exception
+ executionContext.finishKey();
+ }
+
+ @Test
+ public void testStart_internalKeyDecoding() throws Exception {
+ Windmill.WorkItem workItem =
+ Windmill.WorkItem.newBuilder()
+ .setKey(ByteString.copyFromUtf8("decodedKey"))
+ .setWorkToken(17L)
+ .build();
+ Work work =
+ createMockWork(
+ workItem, Watermarks.builder().setInputDataWatermark(new
Instant(1000)).build());
+
+ start(work, org.apache.beam.sdk.coders.StringUtf8Coder.of());
+
+ assertEquals("decodedKey", executionContext.getKey());
}
@Test
public void testInternalsPoisonedAfterFlushState() throws Exception {
- Windmill.WorkItemCommitRequest.Builder outputBuilder =
- Windmill.WorkItemCommitRequest.newBuilder();
NameContext nameContext = NameContextsForTests.nameContextForTest();
DataflowOperationContext operationContext =
executionContext.createOperationContext(nameContext);
StreamingModeExecutionContext.StepContext stepContext =
executionContext.getStepContext(operationContext);
- executionContext.start(
- "key",
+ start(
createMockWork(
Windmill.WorkItem.newBuilder().setKey(ByteString.EMPTY).setWorkToken(17L).build(),
- Watermarks.builder().setInputDataWatermark(new
Instant(1000)).build()),
- stateReader,
- sideInputStateFetcher,
- outputBuilder,
- workExecutor);
+ Watermarks.builder().setInputDataWatermark(new
Instant(1000)).build()));
TimerInternals timerInternals = stepContext.timerInternals();
StateInternals stateInternals = stepContext.stateInternals();
diff --git
a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/WindmillReaderIteratorBaseTest.java
b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/WindmillReaderIteratorBaseTest.java
index 539c38eeb1d..b45e0de6447 100644
---
a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/WindmillReaderIteratorBaseTest.java
+++
b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/WindmillReaderIteratorBaseTest.java
@@ -30,6 +30,7 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
+import org.apache.beam.runners.dataflow.worker.streaming.Work;
import org.apache.beam.runners.dataflow.worker.windmill.Windmill;
import org.apache.beam.sdk.coders.CoderException;
import org.apache.beam.sdk.options.ValueProvider;
@@ -122,6 +123,7 @@ public class WindmillReaderIteratorBaseTest {
.build())
.build();
when(mockContext.getWorkItem()).thenReturn(workItem);
+ when(mockContext.advance()).thenReturn(false);
try (TestWindmillReaderIterator iter =
new TestWindmillReaderIterator(mockContext,
ValueProvider.StaticValueProvider.of(false))) {
@@ -131,6 +133,76 @@ public class WindmillReaderIteratorBaseTest {
}
}
+ @Test
+ public void testAdvanceKeyChaining() throws Exception {
+ StreamingModeExecutionContext mockContext =
mock(StreamingModeExecutionContext.class);
+ when(mockContext.workIsFailed()).thenReturn(false);
+
+ // Work item A (1 message)
+ Windmill.WorkItem workItemA =
+ Windmill.WorkItem.newBuilder()
+ .setKey(ByteString.copyFromUtf8("keyA"))
+ .setWorkToken(100L)
+ .addMessageBundles(
+ Windmill.InputMessageBundle.newBuilder()
+ .setSourceComputationId("foo")
+ .addMessages(
+ Windmill.Message.newBuilder()
+ .setTimestamp(1000)
+ .setData(ByteString.EMPTY)
+ .build())
+ .build())
+ .build();
+ when(mockContext.getWorkItem()).thenReturn(workItemA);
+
+ // Work item B (1 message)
+ Windmill.WorkItem workItemB =
+ Windmill.WorkItem.newBuilder()
+ .setKey(ByteString.copyFromUtf8("keyB"))
+ .setWorkToken(200L)
+ .addMessageBundles(
+ Windmill.InputMessageBundle.newBuilder()
+ .setSourceComputationId("foo")
+ .addMessages(
+ Windmill.Message.newBuilder()
+ .setTimestamp(2000)
+ .setData(ByteString.EMPTY)
+ .build())
+ .build())
+ .build();
+
+ // Set up context.advance() to mock transition
+ when(mockContext.advance())
+ .thenAnswer(
+ new org.mockito.stubbing.Answer<Boolean>() {
+ private int count = 0;
+
+ @Override
+ public Boolean answer(org.mockito.invocation.InvocationOnMock
invocation) {
+ if (count == 0) {
+ count++;
+ when(mockContext.getWorkItem()).thenReturn(workItemB);
+ return true;
+ }
+ return false;
+ }
+ });
+
+ try (TestWindmillReaderIterator iter =
+ new TestWindmillReaderIterator(mockContext,
ValueProvider.StaticValueProvider.of(false))) {
+ assertTrue(iter.start());
+ assertEquals(1000L, iter.getCurrent().getValue().longValue());
+
+ // Advance should trigger context.advance(), transition to workItemB,
and decode message from
+ // workItemB (timestamp 2000)
+ assertTrue(iter.advance());
+ assertEquals(2000L, iter.getCurrent().getValue().longValue());
+
+ // Next advance should exhaust it and return false
+ assertFalse(iter.advance());
+ }
+ }
+
private void testForMessageBundleCounts(int... messageBundleCounts) throws
IOException {
testForMessageBundleCounts(false, messageBundleCounts);
}
@@ -179,4 +251,24 @@ public class WindmillReaderIteratorBaseTest {
assertEquals(Arrays.toString(messageBundleCounts) + skipErrors,
expected, actual);
}
}
+
+ private static Work createMockWork(Windmill.WorkItem workItem) {
+ return Work.create(
+ workItem,
+ workItem.getSerializedSize(),
+ org.apache.beam.runners.dataflow.worker.streaming.Watermarks.builder()
+ .setInputDataWatermark(new org.joda.time.Instant(1000))
+ .build(),
+ Work.createProcessingContext(
+ "computationId",
+ mock(
+
org.apache.beam.runners.dataflow.worker.windmill.client.getdata.GetDataClient
+ .class),
+ ignored -> {},
+ mock(
+
org.apache.beam.runners.dataflow.worker.windmill.work.refresh.HeartbeatSender
+ .class)),
+ false,
+ org.joda.time.Instant::now);
+ }
}
diff --git
a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/WindowingWindmillReaderTest.java
b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/WindowingWindmillReaderTest.java
new file mode 100644
index 00000000000..2e7c80330cf
--- /dev/null
+++
b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/WindowingWindmillReaderTest.java
@@ -0,0 +1,275 @@
+/*
+ * 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;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.io.IOException;
+import java.util.List;
+import org.apache.beam.runners.core.KeyedWorkItem;
+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.common.worker.NativeReader;
+import org.apache.beam.runners.dataflow.worker.windmill.Windmill;
+import
org.apache.beam.runners.dataflow.worker.windmill.client.getdata.FakeGetDataClient;
+import
org.apache.beam.runners.dataflow.worker.windmill.state.WindmillTagEncodingV1;
+import
org.apache.beam.runners.dataflow.worker.windmill.work.refresh.HeartbeatSender;
+import org.apache.beam.sdk.coders.Coder;
+import org.apache.beam.sdk.coders.KvCoder;
+import org.apache.beam.sdk.coders.ListCoder;
+import org.apache.beam.sdk.coders.StringUtf8Coder;
+import org.apache.beam.sdk.coders.VarLongCoder;
+import org.apache.beam.sdk.options.ValueProvider;
+import org.apache.beam.sdk.transforms.windowing.IntervalWindow;
+import
org.apache.beam.sdk.transforms.windowing.IntervalWindow.IntervalWindowCoder;
+import org.apache.beam.sdk.transforms.windowing.PaneInfo;
+import org.apache.beam.sdk.transforms.windowing.PaneInfo.PaneInfoCoder;
+import org.apache.beam.sdk.util.ByteStringOutputStream;
+import org.apache.beam.sdk.values.WindowedValue;
+import org.apache.beam.sdk.values.WindowedValues.FullWindowedValueCoder;
+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.collect.Iterables;
+import org.joda.time.Instant;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class WindowingWindmillReaderTest {
+ private StreamingModeExecutionContext mockContext;
+ private WindowingWindmillReader<String, Long> reader;
+
+ @SuppressWarnings("unchecked")
+ @Before
+ public void setUp() {
+ mockContext = mock(StreamingModeExecutionContext.class);
+ when(mockContext.workIsFailed()).thenReturn(false);
+
when(mockContext.getWindmillTagEncoding()).thenReturn(WindmillTagEncodingV1.instance());
+ when(mockContext.getDrainMode()).thenReturn(false);
+
+ Coder<String> keyCoder = StringUtf8Coder.of();
+ Coder<Long> valueCoder = VarLongCoder.of();
+ KvCoder<String, Long> kvCoder = KvCoder.of(keyCoder, valueCoder);
+ WindmillKeyedWorkItem.FakeKeyedWorkItemCoder<String, Long>
keyedWorkItemCoder =
+ (WindmillKeyedWorkItem.FakeKeyedWorkItemCoder<String, Long>)
+ WindmillKeyedWorkItem.FakeKeyedWorkItemCoder.of(kvCoder);
+ FullWindowedValueCoder<KeyedWorkItem<String, Long>> coder =
+ FullWindowedValueCoder.of(keyedWorkItemCoder,
IntervalWindowCoder.of());
+
+ reader =
+ WindowingWindmillReader.create(
+ coder, mockContext, ValueProvider.StaticValueProvider.of(false));
+ }
+
+ private static Work createMockWork(Windmill.WorkItem workItem) {
+ return Work.create(
+ workItem,
+ workItem.getSerializedSize(),
+ Watermarks.builder().setInputDataWatermark(new Instant(1000)).build(),
+ Work.createProcessingContext(
+ "computationId", new FakeGetDataClient(), ignored -> {},
mock(HeartbeatSender.class)),
+ false,
+ Instant::now);
+ }
+
+ private static ByteString encodeMetadata(List<IntervalWindow> windows)
throws IOException {
+ ByteStringOutputStream stream = new ByteStringOutputStream();
+ PaneInfoCoder.INSTANCE.encode(PaneInfo.NO_FIRING, stream);
+ ListCoder.of(IntervalWindowCoder.of()).encode(windows, stream);
+ return stream.toByteString();
+ }
+
+ private static ByteString encodeValue(long value) throws IOException {
+ ByteStringOutputStream stream = new ByteStringOutputStream();
+ VarLongCoder.of().encode(value, stream);
+ return stream.toByteString();
+ }
+
+ @Test
+ public void testSingleNonEmptyKey() throws IOException {
+ IntervalWindow window = new IntervalWindow(new Instant(0), new
Instant(1000));
+ Windmill.WorkItem workItem =
+ Windmill.WorkItem.newBuilder()
+ .setKey(ByteString.copyFromUtf8("key1"))
+ .setWorkToken(100L)
+ .addMessageBundles(
+ Windmill.InputMessageBundle.newBuilder()
+ .setSourceComputationId("foo")
+ .addMessages(
+ Windmill.Message.newBuilder()
+ .setTimestamp(1000)
+ .setData(encodeValue(42L))
+
.setMetadata(encodeMetadata(ImmutableList.of(window)))
+ .build())
+ .build())
+ .build();
+ Work work = createMockWork(workItem);
+
+ when(mockContext.getKey()).thenReturn("key1");
+ when(mockContext.getWorkItem()).thenReturn(workItem);
+ when(mockContext.getWork()).thenReturn(work);
+ when(mockContext.advance()).thenReturn(false);
+
+ try (NativeReader.NativeReaderIterator<WindowedValue<KeyedWorkItem<String,
Long>>> iter =
+ reader.iterator()) {
+ assertTrue(iter.start());
+ WindowedValue<KeyedWorkItem<String, Long>> current = iter.getCurrent();
+ assertEquals("key1", current.getValue().key());
+ assertFalse(Iterables.isEmpty(current.getValue().elementsIterable()));
+ WindowedValue<Long> elem =
Iterables.getOnlyElement(current.getValue().elementsIterable());
+ assertEquals(42L, elem.getValue().longValue());
+
+ assertFalse(iter.advance());
+ verify(mockContext).finishKey();
+ }
+ }
+
+ @Test
+ public void testSingleEmptyKey() throws IOException {
+ Windmill.WorkItem workItem =
+ Windmill.WorkItem.newBuilder()
+ .setKey(ByteString.copyFromUtf8("key1"))
+ .setWorkToken(100L)
+ .build(); // No message bundles or timers
+ Work work = createMockWork(workItem);
+
+ when(mockContext.getKey()).thenReturn("key1");
+ when(mockContext.getWorkItem()).thenReturn(workItem);
+ when(mockContext.getWork()).thenReturn(work);
+ when(mockContext.advance()).thenReturn(false);
+
+ try (NativeReader.NativeReaderIterator<WindowedValue<KeyedWorkItem<String,
Long>>> iter =
+ reader.iterator()) {
+ assertFalse(
+ iter.start()); // Should skip the empty key and return false because
advance returns false
+ verify(mockContext).finishKey();
+ }
+ }
+
+ @Test
+ public void testMultipleKeys_withEmptyAndNonEmpty() throws IOException {
+ IntervalWindow window = new IntervalWindow(new Instant(0), new
Instant(1000));
+ // Key 1: Empty
+ Windmill.WorkItem workItem1 =
+ Windmill.WorkItem.newBuilder()
+ .setKey(ByteString.copyFromUtf8("key1"))
+ .setWorkToken(100L)
+ .build();
+ Work work1 = createMockWork(workItem1);
+
+ // Key 2: Non-empty
+ Windmill.WorkItem workItem2 =
+ Windmill.WorkItem.newBuilder()
+ .setKey(ByteString.copyFromUtf8("key2"))
+ .setWorkToken(200L)
+ .addMessageBundles(
+ Windmill.InputMessageBundle.newBuilder()
+ .setSourceComputationId("foo")
+ .addMessages(
+ Windmill.Message.newBuilder()
+ .setTimestamp(2000)
+ .setData(encodeValue(84L))
+
.setMetadata(encodeMetadata(ImmutableList.of(window)))
+ .build())
+ .build())
+ .build();
+ Work work2 = createMockWork(workItem2);
+
+ // Key 3: Empty
+ Windmill.WorkItem workItem3 =
+ Windmill.WorkItem.newBuilder()
+ .setKey(ByteString.copyFromUtf8("key3"))
+ .setWorkToken(300L)
+ .build();
+ Work work3 = createMockWork(workItem3);
+
+ // Initial state
+ when(mockContext.getKey()).thenReturn("key1");
+ when(mockContext.getWorkItem()).thenReturn(workItem1);
+ when(mockContext.getWork()).thenReturn(work1);
+
+ // Mock transition behaviour of context.advance()
+ when(mockContext.advance())
+ .thenAnswer(
+ new org.mockito.stubbing.Answer<Boolean>() {
+ private int count = 0;
+
+ @Override
+ public Boolean answer(org.mockito.invocation.InvocationOnMock
invocation) {
+ if (count == 0) {
+ count++;
+ when(mockContext.getKey()).thenReturn("key2");
+ when(mockContext.getWorkItem()).thenReturn(workItem2);
+ when(mockContext.getWork()).thenReturn(work2);
+ return true;
+ } else if (count == 1) {
+ count++;
+ when(mockContext.getKey()).thenReturn("key3");
+ when(mockContext.getWorkItem()).thenReturn(workItem3);
+ when(mockContext.getWork()).thenReturn(work3);
+ return true;
+ }
+ return false;
+ }
+ });
+
+ try (NativeReader.NativeReaderIterator<WindowedValue<KeyedWorkItem<String,
Long>>> iter =
+ reader.iterator()) {
+ // Key 1 is empty, so start() calls advance() which calls finishKey(1)
and advance() to Key 2.
+ // Key 2 is non-empty, so start() returns true yielding Key 2.
+ assertTrue(iter.start());
+ assertEquals("key2", iter.getCurrent().getValue().key());
+ WindowedValue<Long> elem =
+
Iterables.getOnlyElement(iter.getCurrent().getValue().elementsIterable());
+ assertEquals(84L, elem.getValue().longValue());
+
+ // Next advance() calls finishKey(2), calls advance() to Key 3.
+ // Key 3 is empty, so it loops, calls finishKey(3), calls advance()
which returns false.
+ // So iter.advance() should return false.
+ assertFalse(iter.advance());
+
+ verify(mockContext, times(3))
+ .finishKey(); // finishKey should have been called on key1, key2,
key3
+ }
+ }
+
+ @Test
+ public void testWorkItemCancelled() throws IOException {
+ when(mockContext.workIsFailed()).thenReturn(true);
+ Windmill.WorkItem workItem =
+
Windmill.WorkItem.newBuilder().setKey(ByteString.EMPTY).setWorkToken(0L).build();
+ when(mockContext.getWorkItem()).thenReturn(workItem);
+
+ try (NativeReader.NativeReaderIterator<WindowedValue<KeyedWorkItem<String,
Long>>> iter =
+ reader.iterator()) {
+ iter.start();
+ fail("Expected WorkItemCancelledException");
+ } catch (WorkItemCancelledException e) {
+ // Expected
+ }
+ }
+}
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 d5cf2948d92..31ea1bab07a 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
@@ -83,6 +83,7 @@ import
org.apache.beam.runners.dataflow.options.DataflowPipelineOptions;
import org.apache.beam.runners.dataflow.util.CloudObject;
import org.apache.beam.runners.dataflow.util.PropertyNames;
import
org.apache.beam.runners.dataflow.worker.DataflowExecutionContext.DataflowExecutionStateTracker;
+import
org.apache.beam.runners.dataflow.worker.StreamingModeExecutionContext.KeyTransitionListener;
import
org.apache.beam.runners.dataflow.worker.StreamingModeExecutionContext.StreamingModeExecutionStateRegistry;
import
org.apache.beam.runners.dataflow.worker.WorkerCustomSources.SplittableOnlyBoundedSource;
import org.apache.beam.runners.dataflow.worker.counters.CounterSet;
@@ -93,7 +94,7 @@ import org.apache.beam.runners.dataflow.worker.streaming.Work;
import
org.apache.beam.runners.dataflow.worker.streaming.config.FixedGlobalConfigHandle;
import
org.apache.beam.runners.dataflow.worker.streaming.config.StreamingGlobalConfig;
import
org.apache.beam.runners.dataflow.worker.streaming.config.StreamingGlobalConfigHandle;
-import
org.apache.beam.runners.dataflow.worker.streaming.sideinput.SideInputStateFetcher;
+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.common.worker.NativeReader;
import
org.apache.beam.runners.dataflow.worker.util.common.worker.NativeReader.NativeReaderIterator;
@@ -106,6 +107,7 @@ import
org.apache.beam.runners.dataflow.worker.windmill.work.refresh.HeartbeatSe
import org.apache.beam.sdk.Pipeline;
import org.apache.beam.sdk.coders.BigEndianIntegerCoder;
import org.apache.beam.sdk.coders.Coder;
+import org.apache.beam.sdk.coders.CoderException;
import org.apache.beam.sdk.coders.KvCoder;
import org.apache.beam.sdk.coders.VarIntCoder;
import org.apache.beam.sdk.extensions.gcp.auth.TestCredential;
@@ -209,6 +211,21 @@ public class WorkerCustomSourcesTest {
Instant::now);
}
+ private void startContext(StreamingModeExecutionContext context, Work work) {
+ try {
+ context.start(
+ work,
+ mock(WindmillStateReader.class),
+ mock(WorkExecutor.class),
+ /* workQueueExecutor= */ null,
+ /* budgetHandle= */ null,
+ /* keyCoder= */ null,
+ /* keyTransitionListener= */ mock(KeyTransitionListener.class));
+ } catch (CoderException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
private static class SourceProducingSubSourcesInSplit extends MockSource {
int numDesiredBundle;
int sourceObjectSize;
@@ -620,7 +637,12 @@ public class WorkerCustomSourcesTest {
executionStateRegistry,
globalConfigHandle,
Long.MAX_VALUE,
- /*throwExceptionOnLargeOutput=*/ false);
+ /*throwExceptionOnLargeOutput=*/ false,
+ new HotKeyLogger(),
+ /*hotKeyLoggingEnabled=*/ false,
+ /*stepName=*/ "stepName",
+ "sourceBytesProcessCounterName",
+ SideInputStateFetcherFactory.fromOptions(options));
options.setNumWorkers(5);
int maxElements = 10;
@@ -631,8 +653,8 @@ public class WorkerCustomSourcesTest {
for (int i = 0; i < 10 * maxElements;
/* Incremented in inner loop */ ) {
// Initialize streaming context with state from previous iteration.
- context.start(
- "key",
+ startContext(
+ context,
createMockWork(
Windmill.WorkItem.newBuilder()
.setKey(ByteString.copyFromUtf8("0000000000000001")) // key
is zero-padded index.
@@ -641,11 +663,7 @@ public class WorkerCustomSourcesTest {
.setSourceState(
Windmill.SourceState.newBuilder().setState(state).build()) // Source state.
.build(),
- Watermarks.builder().setInputDataWatermark(new
Instant(0)).build()),
- mock(WindmillStateReader.class),
- mock(SideInputStateFetcher.class),
- Windmill.WorkItemCommitRequest.newBuilder(),
- mock(WorkExecutor.class));
+ Watermarks.builder().setInputDataWatermark(new
Instant(0)).build()));
@SuppressWarnings({"unchecked", "rawtypes"})
NativeReader<WindowedValue<ValueWithRecordId<KV<Integer, Integer>>>>
reader =
@@ -992,7 +1010,12 @@ public class WorkerCustomSourcesTest {
executionStateRegistry,
globalConfigHandle,
Long.MAX_VALUE,
- /*throwExceptionOnLargeOutput=*/ false);
+ /*throwExceptionOnLargeOutput=*/ false,
+ new HotKeyLogger(),
+ /*hotKeyLoggingEnabled=*/ false,
+ /*stepName=*/ "stepName",
+ "sourceBytesProcessCounterName",
+ SideInputStateFetcherFactory.fromOptions(options));
options.setNumWorkers(5);
int maxElements = 100;
@@ -1020,13 +1043,7 @@ public class WorkerCustomSourcesTest {
mock(HeartbeatSender.class)),
false,
Instant::now);
- context.start(
- "key",
- dummyWork,
- mock(WindmillStateReader.class),
- mock(SideInputStateFetcher.class),
- Windmill.WorkItemCommitRequest.newBuilder(),
- mock(WorkExecutor.class));
+ startContext(context, dummyWork);
@SuppressWarnings({"unchecked", "rawtypes"})
NativeReader<WindowedValue<ValueWithRecordId<KV<Integer, Integer>>>>
reader =
diff --git
a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/streaming/WorkTest.java
b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/streaming/WorkTest.java
new file mode 100644
index 00000000000..80ca91da462
--- /dev/null
+++
b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/streaming/WorkTest.java
@@ -0,0 +1,132 @@
+/*
+ * 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;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import org.apache.beam.runners.dataflow.worker.windmill.Windmill;
+import
org.apache.beam.runners.dataflow.worker.windmill.work.refresh.HeartbeatSender;
+import org.apache.beam.vendor.grpc.v1p69p0.com.google.protobuf.ByteString;
+import org.joda.time.Instant;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class WorkTest {
+
+ private static Work createTestWork() {
+ Windmill.WorkItem workItem =
+ Windmill.WorkItem.newBuilder()
+ .setKey(ByteString.copyFromUtf8("key"))
+ .setWorkToken(1L)
+ .setShardingKey(2L)
+ .build();
+ return Work.create(
+ workItem,
+ workItem.getSerializedSize(),
+ Watermarks.builder().setInputDataWatermark(Instant.now()).build(),
+ Work.createProcessingContext(
+ "comp",
+ mock(
+
org.apache.beam.runners.dataflow.worker.windmill.client.getdata.GetDataClient
+ .class),
+ commit -> {},
+ mock(HeartbeatSender.class)),
+ false,
+ Instant::now);
+ }
+
+ @Test
+ public void testSetFailedBeforeListener() {
+ Work work = createTestWork();
+ assertFalse(work.isFailed());
+
+ work.setFailed();
+ assertTrue(work.isFailed());
+
+ AtomicBoolean listener = new AtomicBoolean(false);
+ work.setOnFailureListener(listener);
+ assertTrue(listener.get());
+ }
+
+ @Test
+ public void testSetFailedAfterListener() {
+ Work work = createTestWork();
+ AtomicBoolean listener = new AtomicBoolean(false);
+ work.setOnFailureListener(listener);
+ assertFalse(listener.get());
+ assertFalse(work.isFailed());
+
+ work.setFailed();
+ assertTrue(work.isFailed());
+ assertTrue(listener.get());
+ }
+
+ @Test
+ public void testConcurrentSetFailedAndSetOnFailureListener() throws
Exception {
+ int numTrials = 5000;
+ ExecutorService executor = Executors.newFixedThreadPool(2);
+ try {
+ for (int i = 0; i < numTrials; i++) {
+ Work work = createTestWork();
+ AtomicBoolean listener = new AtomicBoolean(false);
+ CountDownLatch latch = new CountDownLatch(1);
+
+ Future<?> f1 =
+ executor.submit(
+ () -> {
+ try {
+ latch.await();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ work.setFailed();
+ });
+
+ Future<?> f2 =
+ executor.submit(
+ () -> {
+ try {
+ latch.await();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ work.setOnFailureListener(listener);
+ });
+
+ latch.countDown();
+ f1.get(5, TimeUnit.SECONDS);
+ f2.get(5, TimeUnit.SECONDS);
+
+ assertTrue("Trial " + i + " failed: work should be failed",
work.isFailed());
+ assertTrue("Trial " + i + " failed: listener should be set to true",
listener.get());
+ }
+ } finally {
+ executor.shutdownNow();
+ }
+ }
+}