Skip to content

Commit 98ca01f

Browse files
authored
[Dataflow Streaming] Prepare BoundedQueueExecutor for MultiKey bundles (#38592)
1 parent 2ed225f commit 98ca01f

12 files changed

Lines changed: 363 additions & 66 deletions

File tree

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
package org.apache.beam.runners.dataflow.worker.streaming;
19+
20+
/**
21+
* A handle to use when requesting pulling more work from @BoundedQueueExecutor
22+
* via @BoundedQueueExecutor.pollWork
23+
*/
24+
public interface BoundedQueueExecutorWorkHandle {}

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

Lines changed: 40 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,25 +17,49 @@
1717
*/
1818
package org.apache.beam.runners.dataflow.worker.streaming;
1919

20-
import com.google.auto.value.AutoValue;
21-
import java.util.function.Consumer;
20+
import java.util.Objects;
21+
import java.util.function.BiConsumer;
22+
import org.apache.beam.runners.dataflow.worker.util.ExceptionUtils;
2223
import org.apache.beam.runners.dataflow.worker.windmill.Windmill;
2324

2425
/** {@link Work} instance and a processing function used to process the work. */
25-
@AutoValue
26-
public abstract class ExecutableWork implements Runnable {
26+
public final class ExecutableWork {
2727

28-
public static ExecutableWork create(Work work, Consumer<Work> executeWorkFn) {
29-
return new AutoValue_ExecutableWork(work, executeWorkFn);
28+
private final Work work;
29+
private final BiConsumer<Work, BoundedQueueExecutorWorkHandle> executeWorkFn;
30+
31+
private ExecutableWork(
32+
Work work, BiConsumer<Work, BoundedQueueExecutorWorkHandle> executeWorkFn) {
33+
this.work = Objects.requireNonNull(work);
34+
this.executeWorkFn = Objects.requireNonNull(executeWorkFn);
3035
}
3136

32-
public abstract Work work();
37+
/**
38+
* Creates an {@link ExecutableWork} instance.
39+
*
40+
* @param executeWorkFn The function executing the work. It'll be called along with a
41+
* BoundedQueueExecutorWorkHandle. The handle needs to be passed to BoundedQueueExecutor when
42+
* requesting more work to process inline.
43+
*/
44+
public static ExecutableWork create(
45+
Work work, BiConsumer<Work, BoundedQueueExecutorWorkHandle> executeWorkFn) {
46+
return new ExecutableWork(work, executeWorkFn);
47+
}
3348

34-
public abstract Consumer<Work> executeWorkFn();
49+
public Work work() {
50+
return work;
51+
}
3552

36-
@Override
37-
public void run() {
38-
executeWorkFn().accept(work());
53+
public BiConsumer<Work, BoundedQueueExecutorWorkHandle> executeWorkFn() {
54+
return executeWorkFn;
55+
}
56+
57+
public void run(BoundedQueueExecutorWorkHandle handle) {
58+
try {
59+
executeWorkFn().accept(work(), handle);
60+
} catch (Throwable t) {
61+
throw ExceptionUtils.safeWrapThrowableAsException(t);
62+
}
3963
}
4064

4165
public final WorkId id() {
@@ -45,4 +69,9 @@ public final WorkId id() {
4569
public final Windmill.WorkItem getWorkItem() {
4670
return work().getWorkItem();
4771
}
72+
73+
@Override
74+
public String toString() {
75+
return "ExecutableWork{" + id() + "}";
76+
}
4877
}

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

Lines changed: 151 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,20 @@
1717
*/
1818
package org.apache.beam.runners.dataflow.worker.util;
1919

20+
import static org.apache.beam.sdk.util.Preconditions.checkArgumentNotNull;
21+
import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkArgument;
22+
2023
import java.util.concurrent.ConcurrentLinkedQueue;
2124
import java.util.concurrent.LinkedBlockingQueue;
2225
import java.util.concurrent.ThreadFactory;
2326
import java.util.concurrent.ThreadPoolExecutor;
2427
import java.util.concurrent.TimeUnit;
2528
import java.util.concurrent.atomic.AtomicBoolean;
2629
import javax.annotation.concurrent.GuardedBy;
30+
import org.apache.beam.runners.dataflow.worker.streaming.BoundedQueueExecutorWorkHandle;
31+
import org.apache.beam.runners.dataflow.worker.streaming.ExecutableWork;
32+
import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.annotations.VisibleForTesting;
33+
import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions;
2734
import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.util.concurrent.Monitor;
2835
import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.util.concurrent.Monitor.Guard;
2936

@@ -38,7 +45,19 @@ public class BoundedQueueExecutor {
3845

3946
// Used to guard elementsOutstanding and bytesOutstanding.
4047
private final Monitor monitor;
41-
private final ConcurrentLinkedQueue<Long> decrementQueue = new ConcurrentLinkedQueue<>();
48+
49+
private static class Budget {
50+
51+
final int elements;
52+
final long bytes;
53+
54+
Budget(int elements, long bytes) {
55+
this.elements = elements;
56+
this.bytes = bytes;
57+
}
58+
}
59+
60+
private final ConcurrentLinkedQueue<Budget> decrementQueue = new ConcurrentLinkedQueue<>();
4261
private final Object decrementQueueDrainLock = new Object();
4362
private final AtomicBoolean isDecrementBatchPending = new AtomicBoolean(false);
4463
private int elementsOutstanding = 0;
@@ -106,7 +125,7 @@ protected void afterExecute(Runnable r, Throwable t) {
106125

107126
// Before adding a Work to the queue, check that there are enough bytes of space or no other
108127
// outstanding elements of work.
109-
public void execute(Runnable work, long workBytes) {
128+
public void execute(ExecutableWork work, long workBytes) {
110129
monitor.enterWhenUninterruptibly(
111130
new Guard(monitor) {
112131
@Override
@@ -119,12 +138,18 @@ public boolean isSatisfied() {
119138
executeMonitorHeld(work, workBytes);
120139
}
121140

122-
// Forcibly add something to the queue, ignoring the length limit.
123-
public void forceExecute(Runnable work, long workBytes) {
141+
// Forcibly add ExecutableWork to the queue, ignoring the limits.
142+
public void forceExecute(ExecutableWork work, long workBytes) {
124143
monitor.enter();
125144
executeMonitorHeld(work, workBytes);
126145
}
127146

147+
/** Forcibly execute a Runnable callback with 0 bytes of size. */
148+
public void forceExecute(Runnable runnable) {
149+
monitor.enter();
150+
executeMonitorHeld(runnable);
151+
}
152+
128153
// Set the maximum/core pool size of the executor.
129154
public synchronized void setMaximumPoolSize(int maximumPoolSize, int maximumElementsOutstanding) {
130155
// For ThreadPoolExecutor, the maximum pool size should always greater than or equal to core
@@ -221,8 +246,115 @@ public String summaryHtml() {
221246
}
222247
}
223248

224-
private void executeMonitorHeld(Runnable work, long workBytes) {
249+
/**
250+
* A handle to use when requesting pulling more work from @BoundedQueueExecutor
251+
* via @BoundedQueueExecutor.pollWork. A single handle aggregates all budgets from work pulled for
252+
* inline execution and releases the budget after the multi work bundle is complete.
253+
*/
254+
final class BoundedQueueExecutorWorkHandleImpl
255+
implements BoundedQueueExecutorWorkHandle, AutoCloseable {
256+
257+
@GuardedBy("this")
258+
private int elements;
259+
260+
@GuardedBy("this")
261+
private long bytes;
262+
263+
@GuardedBy("this")
264+
private boolean closed = false;
265+
266+
private BoundedQueueExecutorWorkHandleImpl(int elements, long bytes) {
267+
checkArgument(elements >= 0 && bytes >= 0);
268+
this.elements = elements;
269+
this.bytes = bytes;
270+
}
271+
272+
/**
273+
* Merges the budget from another handle into this handle.
274+
*
275+
* <p>This transfers the budget (elements and bytes) from the {@code other} handle to this
276+
* handle, and marks the {@code other} handle as closed to prevent it from releasing the budget
277+
* again if it is closed.
278+
*/
279+
public void merge(BoundedQueueExecutorWorkHandleImpl other) {
280+
checkArgumentNotNull(other);
281+
synchronized (this) {
282+
Preconditions.checkState(!closed, "Cannot merge into a closed handle");
283+
synchronized (other) {
284+
Preconditions.checkState(!other.closed, "Cannot merge a closed handle");
285+
this.elements += other.elements;
286+
this.bytes += other.bytes;
287+
other.closed = true;
288+
other.elements = 0;
289+
other.bytes = 0;
290+
}
291+
}
292+
}
293+
294+
public synchronized boolean isClosed() {
295+
return closed;
296+
}
297+
298+
@VisibleForTesting
299+
synchronized int elements() {
300+
return elements;
301+
}
302+
303+
@VisibleForTesting
304+
synchronized long bytes() {
305+
return bytes;
306+
}
307+
308+
@Override
309+
public synchronized void close() {
310+
if (closed) return;
311+
closed = true;
312+
decrementCounters(this.elements, this.bytes);
313+
}
314+
}
315+
316+
private static final class QueuedWork implements Runnable {
317+
318+
private final ExecutableWork work;
319+
private final BoundedQueueExecutorWorkHandleImpl handle;
320+
321+
public QueuedWork(ExecutableWork work, BoundedQueueExecutorWorkHandleImpl handle) {
322+
this.work = work;
323+
this.handle = handle;
324+
}
325+
326+
public ExecutableWork getWork() {
327+
return work;
328+
}
329+
330+
public BoundedQueueExecutorWorkHandleImpl getHandle() {
331+
return handle;
332+
}
333+
334+
@Override
335+
public void run() {
336+
checkArgument(!handle.isClosed());
337+
try (handle) {
338+
work.run(handle);
339+
}
340+
}
341+
}
342+
343+
private void executeMonitorHeld(ExecutableWork work, long workBytes) {
344+
++elementsOutstanding;
225345
bytesOutstanding += workBytes;
346+
monitor.leave();
347+
BoundedQueueExecutorWorkHandleImpl handle =
348+
new BoundedQueueExecutorWorkHandleImpl(1, workBytes);
349+
try {
350+
executor.execute(new QueuedWork(work, handle));
351+
} catch (Throwable t) {
352+
handle.close();
353+
throw ExceptionUtils.safeWrapThrowableAsException(t);
354+
}
355+
}
356+
357+
private void executeMonitorHeld(Runnable work) {
226358
++elementsOutstanding;
227359
monitor.leave();
228360

@@ -232,21 +364,25 @@ private void executeMonitorHeld(Runnable work, long workBytes) {
232364
try {
233365
work.run();
234366
} finally {
235-
decrementCounters(workBytes);
367+
decrementCounters(1, 0L);
236368
}
237369
});
238-
} catch (RuntimeException e) {
239-
// If the execute() call threw an exception, decrement counters here.
240-
decrementCounters(workBytes);
241-
throw e;
370+
} catch (Throwable t) {
371+
decrementCounters(1, 0L);
372+
throw ExceptionUtils.safeWrapThrowableAsException(t);
242373
}
243374
}
244375

245-
private void decrementCounters(long workBytes) {
376+
@VisibleForTesting
377+
BoundedQueueExecutorWorkHandleImpl createBudgetHandle(int elements, long bytes) {
378+
return new BoundedQueueExecutorWorkHandleImpl(elements, bytes);
379+
}
380+
381+
private void decrementCounters(int elements, long bytes) {
246382
// All threads queue decrements and one thread grabs the monitor and updates
247383
// counters. We do this to reduce contention on monitor which is locked by
248384
// GetWork thread
249-
decrementQueue.add(workBytes);
385+
decrementQueue.add(new Budget(elements, bytes));
250386
boolean submittedToExistingBatch = isDecrementBatchPending.getAndSet(true);
251387
if (submittedToExistingBatch) {
252388
// There is already a thread about to drain the decrement queue
@@ -265,12 +401,12 @@ private void decrementCounters(long workBytes) {
265401
long bytesToDecrement = 0;
266402
int elementsToDecrement = 0;
267403
while (true) {
268-
Long pollResult = decrementQueue.poll();
404+
Budget pollResult = decrementQueue.poll();
269405
if (pollResult == null) {
270406
break;
271407
}
272-
bytesToDecrement += pollResult;
273-
++elementsToDecrement;
408+
bytesToDecrement += pollResult.bytes;
409+
elementsToDecrement += pollResult.elements;
274410
}
275411
if (elementsToDecrement == 0) {
276412
return;
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
package org.apache.beam.runners.dataflow.worker.util;
19+
20+
import javax.annotation.CheckReturnValue;
21+
import org.apache.beam.sdk.annotations.Internal;
22+
23+
/** Utility methods for simplifying work with exceptions and throwables. */
24+
@Internal
25+
public final class ExceptionUtils {
26+
27+
private ExceptionUtils() {}
28+
29+
/**
30+
* Returns the {@code throwable} as-is if it is an instance of {@link RuntimeException} throws if
31+
* it is an {@link Error}, or returns the {@code throwable} wrapped in a {@code RuntimeException}.
32+
*/
33+
@CheckReturnValue
34+
public static RuntimeException safeWrapThrowableAsException(Throwable throwable) {
35+
if (throwable instanceof RuntimeException) {
36+
return (RuntimeException) throwable;
37+
} else if (throwable instanceof Error) {
38+
throw (Error) throwable;
39+
} else {
40+
return new RuntimeException(throwable);
41+
}
42+
}
43+
}

runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/work/processing/StreamingCommitFinalizer.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ public void finalizeCommits(Iterable<Long> finalizeIds) {
156156
}
157157
for (Runnable callback : callbacksToExecute) {
158158
try {
159-
finalizationExecutor.forceExecute(callback, 0);
159+
finalizationExecutor.forceExecute(callback);
160160
} catch (OutOfMemoryError oom) {
161161
throw oom;
162162
} catch (Throwable t) {

0 commit comments

Comments
 (0)