1717 */
1818package 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+
2023import java .util .concurrent .ConcurrentLinkedQueue ;
2124import java .util .concurrent .LinkedBlockingQueue ;
2225import java .util .concurrent .ThreadFactory ;
2326import java .util .concurrent .ThreadPoolExecutor ;
2427import java .util .concurrent .TimeUnit ;
2528import java .util .concurrent .atomic .AtomicBoolean ;
2629import 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 ;
2734import org .apache .beam .vendor .guava .v32_1_2_jre .com .google .common .util .concurrent .Monitor ;
2835import 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 ;
0 commit comments