Skip to content

Commit d9117f4

Browse files
authored
[Dataflow Streaming] [Multi Key] Introduce KeyGroupWorkQueue and integrate with BoundedQueueExecutor (#38767)
* [Dataflow Streaming] Introduce KeyGroupWorkQueue and integrate with BoundedQueueExecutor - Add Uint128Proto and key_group to WorkItem in windmill.proto. - Update Work model to support key groups. - Implement KeyGroupWorkQueue for grouping tasks by key group. KepGroupWorkQueue is a FIFO queue that allows polling elements based on global order and also by order within a key group. - Integrate BoundedQueueExecutor with KepGroupWorkQueue and support targeted polling.
1 parent 66e5439 commit d9117f4

12 files changed

Lines changed: 1247 additions & 21 deletions

File tree

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,9 @@ public final class StreamingDataflowWorker {
179179
// Experiment make the monitor within BoundedQueueExecutor fair
180180
public static final String BOUNDED_QUEUE_EXECUTOR_USE_FAIR_MONITOR_EXPERIMENT =
181181
"windmill_bounded_queue_executor_use_fair_monitor";
182+
// Don't use. Experiment guarding multi key bundles. The feature is work in progress and
183+
// incomplete.
184+
private static final String UNSTABLE_ENABLE_MULTI_KEY_BUNDLE = "unstable_enable_multi_key_bundle";
182185

183186
private final WindmillStateCache stateCache;
184187
private AtomicReference<StreamingWorkerStatusPages> statusPages = new AtomicReference<>();
@@ -1018,14 +1021,17 @@ private static JobHeader createJobHeader(DataflowWorkerHarnessOptions options, l
10181021
private static BoundedQueueExecutor createWorkUnitExecutor(DataflowWorkerHarnessOptions options) {
10191022
boolean useFairMonitor =
10201023
DataflowRunner.hasExperiment(options, BOUNDED_QUEUE_EXECUTOR_USE_FAIR_MONITOR_EXPERIMENT);
1024+
boolean useKeyGroupWorkQueue =
1025+
DataflowRunner.hasExperiment(options, UNSTABLE_ENABLE_MULTI_KEY_BUNDLE);
10211026
return new BoundedQueueExecutor(
10221027
chooseMaxThreads(options),
10231028
THREAD_EXPIRATION_TIME_SEC,
10241029
TimeUnit.SECONDS,
10251030
chooseMaxBundlesOutstanding(options),
10261031
chooseMaxBytesOutstanding(options),
10271032
new ThreadFactoryBuilder().setNameFormat("DataflowWorkUnits-%d").setDaemon(true).build(),
1028-
useFairMonitor);
1033+
useFairMonitor,
1034+
useKeyGroupWorkQueue);
10291035
}
10301036

10311037
public static void main(String[] args) throws Exception {

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

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,16 +62,24 @@ public void run(BoundedQueueExecutorWorkHandle handle) {
6262
}
6363
}
6464

65-
public final WorkId id() {
65+
public WorkId id() {
6666
return work().id();
6767
}
6868

69-
public final Windmill.WorkItem getWorkItem() {
69+
public Windmill.WorkItem getWorkItem() {
7070
return work().getWorkItem();
7171
}
7272

7373
@Override
7474
public String toString() {
7575
return "ExecutableWork{" + id() + "}";
7676
}
77+
78+
public String getComputationId() {
79+
return work().getComputationId();
80+
}
81+
82+
public Work.KeyGroup getKeyGroup() {
83+
return work().getKeyGroup();
84+
}
7785
}

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

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import java.util.IntSummaryStatistics;
2626
import java.util.Map;
2727
import java.util.Map.Entry;
28+
import java.util.Objects;
2829
import java.util.Optional;
2930
import java.util.concurrent.TimeUnit;
3031
import java.util.function.Consumer;
@@ -52,6 +53,7 @@
5253
import org.apache.beam.sdk.annotations.Internal;
5354
import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions;
5455
import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableList;
56+
import org.checkerframework.checker.nullness.qual.Nullable;
5557
import org.joda.time.Duration;
5658
import org.joda.time.Instant;
5759

@@ -74,6 +76,7 @@ public final class Work implements RefreshableWork {
7476
private final Instant startTime;
7577
private final Map<LatencyAttribution.State, Duration> totalDurationPerState;
7678
private final WorkId id;
79+
private final KeyGroup keyGroup;
7780
private final String latencyTrackingId;
7881
private final long serializedWorkItemSize;
7982
private volatile TimedState currentState;
@@ -101,6 +104,10 @@ private Work(
101104
// keyUniverse inside EnumMap every time.
102105
this.totalDurationPerState = new EnumMap<>(EMPTY_ENUM_MAP);
103106
this.id = WorkId.of(workItem);
107+
this.keyGroup =
108+
workItem.hasKeyGroup()
109+
? KeyGroup.create(workItem.getKeyGroup().getHigh(), workItem.getKeyGroup().getLow())
110+
: KeyGroup.DEFAULT;
104111
this.latencyTrackingId =
105112
Long.toHexString(workItem.getShardingKey())
106113
+ '-'
@@ -383,6 +390,14 @@ private boolean isCommitPending() {
383390
abstract Instant startTime();
384391
}
385392

393+
public String getComputationId() {
394+
return processingContext.computationId();
395+
}
396+
397+
public KeyGroup getKeyGroup() {
398+
return keyGroup;
399+
}
400+
386401
@AutoValue
387402
public abstract static class ProcessingContext {
388403

@@ -416,4 +431,60 @@ private Optional<KeyedGetDataResponse> fetchKeyedState(KeyedGetDataRequest reque
416431
return Optional.ofNullable(getDataClient().getStateData(computationId(), request));
417432
}
418433
}
434+
435+
/**
436+
* WorkItems with same key group and computation are eligible to be executed together in a
437+
* multi-key bundle.
438+
*/
439+
public static final class KeyGroup {
440+
441+
// Work items equaling to the default keyGroup will always be executed
442+
// separately and not in a multi-key bundle
443+
public static final KeyGroup DEFAULT = new KeyGroup(0, 0);
444+
445+
private final long high;
446+
private final long low;
447+
448+
private KeyGroup(long high, long low) {
449+
this.high = high;
450+
this.low = low;
451+
}
452+
453+
public static KeyGroup create(long high, long low) {
454+
if (high == 0 && low == 0) {
455+
return DEFAULT;
456+
}
457+
return new KeyGroup(high, low);
458+
}
459+
460+
public long high() {
461+
return high;
462+
}
463+
464+
public long low() {
465+
return low;
466+
}
467+
468+
@Override
469+
public boolean equals(@Nullable Object o) {
470+
if (this == o) {
471+
return true;
472+
}
473+
if (!(o instanceof KeyGroup)) {
474+
return false;
475+
}
476+
KeyGroup other = (KeyGroup) o;
477+
return high == other.high && low == other.low;
478+
}
479+
480+
@Override
481+
public int hashCode() {
482+
return Objects.hash(high, low);
483+
}
484+
485+
@Override
486+
public String toString() {
487+
return String.format("%016x%016x", high, low);
488+
}
489+
}
419490
}

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

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,15 +29,15 @@
2929
import javax.annotation.concurrent.GuardedBy;
3030
import org.apache.beam.runners.dataflow.worker.streaming.BoundedQueueExecutorWorkHandle;
3131
import org.apache.beam.runners.dataflow.worker.streaming.ExecutableWork;
32+
import org.apache.beam.runners.dataflow.worker.streaming.Work;
33+
import org.apache.beam.runners.dataflow.worker.streaming.Work.KeyGroup;
3234
import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.annotations.VisibleForTesting;
3335
import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions;
3436
import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.util.concurrent.Monitor;
3537
import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.util.concurrent.Monitor.Guard;
38+
import org.checkerframework.checker.nullness.qual.Nullable;
3639

3740
/** An executor for executing work on windmill items. */
38-
@SuppressWarnings({
39-
"nullness" // TODO(https://github.com/apache/beam/issues/20497)
40-
})
4141
public class BoundedQueueExecutor {
4242

4343
private final ThreadPoolExecutor executor;
@@ -78,14 +78,19 @@ private static class Budget {
7878
@GuardedBy("this")
7979
private long totalTimeMaxActiveThreadsUsed;
8080

81+
// If set the keyGroupWorkQueue is used by the underlying executor.
82+
private final @Nullable KeyGroupWorkQueue keyGroupWorkQueue;
83+
8184
public BoundedQueueExecutor(
8285
int initialMaximumPoolSize,
8386
long keepAliveTime,
8487
TimeUnit unit,
8588
int maximumElementsOutstanding,
8689
long maximumBytesOutstanding,
8790
ThreadFactory threadFactory,
88-
boolean useFairMonitor) {
91+
boolean useFairMonitor,
92+
boolean useKeyGroupWorkQueue) {
93+
this.keyGroupWorkQueue = useKeyGroupWorkQueue ? new KeyGroupWorkQueue(useFairMonitor) : null;
8994
this.maximumPoolSize = initialMaximumPoolSize;
9095
monitor = new Monitor(useFairMonitor);
9196
executor =
@@ -94,7 +99,7 @@ public BoundedQueueExecutor(
9499
initialMaximumPoolSize,
95100
keepAliveTime,
96101
unit,
97-
new LinkedBlockingQueue<>(),
102+
keyGroupWorkQueue != null ? keyGroupWorkQueue : new LinkedBlockingQueue<>(),
98103
threadFactory) {
99104
@Override
100105
protected void beforeExecute(Thread t, Runnable r) {
@@ -313,7 +318,7 @@ public synchronized void close() {
313318
}
314319
}
315320

316-
private static final class QueuedWork implements Runnable {
321+
static final class QueuedWork implements Runnable {
317322

318323
private final ExecutableWork work;
319324
private final BoundedQueueExecutorWorkHandleImpl handle;
@@ -378,6 +383,22 @@ BoundedQueueExecutorWorkHandleImpl createBudgetHandle(int elements, long bytes)
378383
return new BoundedQueueExecutorWorkHandleImpl(elements, bytes);
379384
}
380385

386+
public @Nullable ExecutableWork pollWork(
387+
String computationId, Work.KeyGroup keyGroup, BoundedQueueExecutorWorkHandle handle) {
388+
checkArgument(handle instanceof BoundedQueueExecutorWorkHandleImpl);
389+
checkArgument(computationId != null && keyGroup != null && !keyGroup.equals(KeyGroup.DEFAULT));
390+
BoundedQueueExecutorWorkHandleImpl internalHandle = (BoundedQueueExecutorWorkHandleImpl) handle;
391+
if (keyGroupWorkQueue == null) {
392+
return null;
393+
}
394+
@Nullable QueuedWork queuedWork = keyGroupWorkQueue.pollWork(computationId, keyGroup);
395+
if (queuedWork == null) {
396+
return null;
397+
}
398+
internalHandle.merge(queuedWork.getHandle());
399+
return queuedWork.getWork();
400+
}
401+
381402
private void decrementCounters(int elements, long bytes) {
382403
// All threads queue decrements and one thread grabs the monitor and updates
383404
// counters. We do this to reduce contention on monitor which is locked by

0 commit comments

Comments
 (0)