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 98ca01f415c [Dataflow Streaming] Prepare BoundedQueueExecutor for 
MultiKey bundles (#38592)
98ca01f415c is described below

commit 98ca01f415cd090274718f7f2dbe2d73552d1116
Author: Arun Pandian <[email protected]>
AuthorDate: Tue Jun 2 00:43:26 2026 -0700

    [Dataflow Streaming] Prepare BoundedQueueExecutor for MultiKey bundles 
(#38592)
---
 ...rk.java => BoundedQueueExecutorWorkHandle.java} |  34 +----
 .../dataflow/worker/streaming/ExecutableWork.java  |  51 +++++--
 .../dataflow/worker/util/BoundedQueueExecutor.java | 166 +++++++++++++++++++--
 .../dataflow/worker/util/ExceptionUtils.java       |  43 ++++++
 .../work/processing/StreamingCommitFinalizer.java  |   2 +-
 .../work/processing/StreamingWorkScheduler.java    |  18 ++-
 .../worker/StreamingDataflowWorkerTest.java        |   4 +-
 .../worker/streaming/ActiveWorkStateTest.java      |   4 +-
 .../streaming/ComputationStateCacheTest.java       |   2 +-
 .../worker/util/BoundedQueueExecutorTest.java      | 107 +++++++++----
 .../failures/WorkFailureProcessorTest.java         |   4 +-
 .../work/refresh/ActiveWorkRefresherTest.java      |   4 +-
 12 files changed, 344 insertions(+), 95 deletions(-)

diff --git 
a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/streaming/ExecutableWork.java
 
b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/streaming/BoundedQueueExecutorWorkHandle.java
similarity index 53%
copy from 
runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/streaming/ExecutableWork.java
copy to 
runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/streaming/BoundedQueueExecutorWorkHandle.java
index db279f06663..1ca53496694 100644
--- 
a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/streaming/ExecutableWork.java
+++ 
b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/streaming/BoundedQueueExecutorWorkHandle.java
@@ -17,32 +17,8 @@
  */
 package org.apache.beam.runners.dataflow.worker.streaming;
 
-import com.google.auto.value.AutoValue;
-import java.util.function.Consumer;
-import org.apache.beam.runners.dataflow.worker.windmill.Windmill;
-
-/** {@link Work} instance and a processing function used to process the work. 
*/
-@AutoValue
-public abstract class ExecutableWork implements Runnable {
-
-  public static ExecutableWork create(Work work, Consumer<Work> executeWorkFn) 
{
-    return new AutoValue_ExecutableWork(work, executeWorkFn);
-  }
-
-  public abstract Work work();
-
-  public abstract Consumer<Work> executeWorkFn();
-
-  @Override
-  public void run() {
-    executeWorkFn().accept(work());
-  }
-
-  public final WorkId id() {
-    return work().id();
-  }
-
-  public final Windmill.WorkItem getWorkItem() {
-    return work().getWorkItem();
-  }
-}
+/**
+ * A handle to use when requesting pulling more work from @BoundedQueueExecutor
+ * via @BoundedQueueExecutor.pollWork
+ */
+public interface BoundedQueueExecutorWorkHandle {}
diff --git 
a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/streaming/ExecutableWork.java
 
b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/streaming/ExecutableWork.java
index db279f06663..ecaa673f557 100644
--- 
a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/streaming/ExecutableWork.java
+++ 
b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/streaming/ExecutableWork.java
@@ -17,25 +17,49 @@
  */
 package org.apache.beam.runners.dataflow.worker.streaming;
 
-import com.google.auto.value.AutoValue;
-import java.util.function.Consumer;
+import java.util.Objects;
+import java.util.function.BiConsumer;
+import org.apache.beam.runners.dataflow.worker.util.ExceptionUtils;
 import org.apache.beam.runners.dataflow.worker.windmill.Windmill;
 
 /** {@link Work} instance and a processing function used to process the work. 
*/
-@AutoValue
-public abstract class ExecutableWork implements Runnable {
+public final class ExecutableWork {
 
-  public static ExecutableWork create(Work work, Consumer<Work> executeWorkFn) 
{
-    return new AutoValue_ExecutableWork(work, executeWorkFn);
+  private final Work work;
+  private final BiConsumer<Work, BoundedQueueExecutorWorkHandle> executeWorkFn;
+
+  private ExecutableWork(
+      Work work, BiConsumer<Work, BoundedQueueExecutorWorkHandle> 
executeWorkFn) {
+    this.work = Objects.requireNonNull(work);
+    this.executeWorkFn = Objects.requireNonNull(executeWorkFn);
   }
 
-  public abstract Work work();
+  /**
+   * Creates an {@link ExecutableWork} instance.
+   *
+   * @param executeWorkFn The function executing the work. It'll be called 
along with a
+   *     BoundedQueueExecutorWorkHandle. The handle needs to be passed to 
BoundedQueueExecutor when
+   *     requesting more work to process inline.
+   */
+  public static ExecutableWork create(
+      Work work, BiConsumer<Work, BoundedQueueExecutorWorkHandle> 
executeWorkFn) {
+    return new ExecutableWork(work, executeWorkFn);
+  }
 
-  public abstract Consumer<Work> executeWorkFn();
+  public Work work() {
+    return work;
+  }
 
-  @Override
-  public void run() {
-    executeWorkFn().accept(work());
+  public BiConsumer<Work, BoundedQueueExecutorWorkHandle> executeWorkFn() {
+    return executeWorkFn;
+  }
+
+  public void run(BoundedQueueExecutorWorkHandle handle) {
+    try {
+      executeWorkFn().accept(work(), handle);
+    } catch (Throwable t) {
+      throw ExceptionUtils.safeWrapThrowableAsException(t);
+    }
   }
 
   public final WorkId id() {
@@ -45,4 +69,9 @@ public abstract class ExecutableWork implements Runnable {
   public final Windmill.WorkItem getWorkItem() {
     return work().getWorkItem();
   }
+
+  @Override
+  public String toString() {
+    return "ExecutableWork{" + id() + "}";
+  }
 }
diff --git 
a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/util/BoundedQueueExecutor.java
 
b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/util/BoundedQueueExecutor.java
index 9079c3cc69b..c6fd96e0a4c 100644
--- 
a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/util/BoundedQueueExecutor.java
+++ 
b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/util/BoundedQueueExecutor.java
@@ -17,6 +17,9 @@
  */
 package org.apache.beam.runners.dataflow.worker.util;
 
+import static org.apache.beam.sdk.util.Preconditions.checkArgumentNotNull;
+import static 
org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkArgument;
+
 import java.util.concurrent.ConcurrentLinkedQueue;
 import java.util.concurrent.LinkedBlockingQueue;
 import java.util.concurrent.ThreadFactory;
@@ -24,6 +27,10 @@ import java.util.concurrent.ThreadPoolExecutor;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicBoolean;
 import javax.annotation.concurrent.GuardedBy;
+import 
org.apache.beam.runners.dataflow.worker.streaming.BoundedQueueExecutorWorkHandle;
+import org.apache.beam.runners.dataflow.worker.streaming.ExecutableWork;
+import 
org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.annotations.VisibleForTesting;
+import 
org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions;
 import 
org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.util.concurrent.Monitor;
 import 
org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.util.concurrent.Monitor.Guard;
 
@@ -38,7 +45,19 @@ public class BoundedQueueExecutor {
 
   // Used to guard elementsOutstanding and bytesOutstanding.
   private final Monitor monitor;
-  private final ConcurrentLinkedQueue<Long> decrementQueue = new 
ConcurrentLinkedQueue<>();
+
+  private static class Budget {
+
+    final int elements;
+    final long bytes;
+
+    Budget(int elements, long bytes) {
+      this.elements = elements;
+      this.bytes = bytes;
+    }
+  }
+
+  private final ConcurrentLinkedQueue<Budget> decrementQueue = new 
ConcurrentLinkedQueue<>();
   private final Object decrementQueueDrainLock = new Object();
   private final AtomicBoolean isDecrementBatchPending = new 
AtomicBoolean(false);
   private int elementsOutstanding = 0;
@@ -106,7 +125,7 @@ public class BoundedQueueExecutor {
 
   // Before adding a Work to the queue, check that there are enough bytes of 
space or no other
   // outstanding elements of work.
-  public void execute(Runnable work, long workBytes) {
+  public void execute(ExecutableWork work, long workBytes) {
     monitor.enterWhenUninterruptibly(
         new Guard(monitor) {
           @Override
@@ -119,12 +138,18 @@ public class BoundedQueueExecutor {
     executeMonitorHeld(work, workBytes);
   }
 
-  // Forcibly add something to the queue, ignoring the length limit.
-  public void forceExecute(Runnable work, long workBytes) {
+  // Forcibly add ExecutableWork to the queue, ignoring the limits.
+  public void forceExecute(ExecutableWork work, long workBytes) {
     monitor.enter();
     executeMonitorHeld(work, workBytes);
   }
 
+  /** Forcibly execute a Runnable callback with 0 bytes of size. */
+  public void forceExecute(Runnable runnable) {
+    monitor.enter();
+    executeMonitorHeld(runnable);
+  }
+
   // Set the maximum/core pool size of the executor.
   public synchronized void setMaximumPoolSize(int maximumPoolSize, int 
maximumElementsOutstanding) {
     // For ThreadPoolExecutor, the maximum pool size should always greater 
than or equal to core
@@ -221,8 +246,115 @@ public class BoundedQueueExecutor {
     }
   }
 
-  private void executeMonitorHeld(Runnable work, long workBytes) {
+  /**
+   * A handle to use when requesting pulling more work from 
@BoundedQueueExecutor
+   * via @BoundedQueueExecutor.pollWork. A single handle aggregates all 
budgets from work pulled for
+   * inline execution and releases the budget after the multi work bundle is 
complete.
+   */
+  final class BoundedQueueExecutorWorkHandleImpl
+      implements BoundedQueueExecutorWorkHandle, AutoCloseable {
+
+    @GuardedBy("this")
+    private int elements;
+
+    @GuardedBy("this")
+    private long bytes;
+
+    @GuardedBy("this")
+    private boolean closed = false;
+
+    private BoundedQueueExecutorWorkHandleImpl(int elements, long bytes) {
+      checkArgument(elements >= 0 && bytes >= 0);
+      this.elements = elements;
+      this.bytes = bytes;
+    }
+
+    /**
+     * Merges the budget from another handle into this handle.
+     *
+     * <p>This transfers the budget (elements and bytes) from the {@code 
other} handle to this
+     * handle, and marks the {@code other} handle as closed to prevent it from 
releasing the budget
+     * again if it is closed.
+     */
+    public void merge(BoundedQueueExecutorWorkHandleImpl other) {
+      checkArgumentNotNull(other);
+      synchronized (this) {
+        Preconditions.checkState(!closed, "Cannot merge into a closed handle");
+        synchronized (other) {
+          Preconditions.checkState(!other.closed, "Cannot merge a closed 
handle");
+          this.elements += other.elements;
+          this.bytes += other.bytes;
+          other.closed = true;
+          other.elements = 0;
+          other.bytes = 0;
+        }
+      }
+    }
+
+    public synchronized boolean isClosed() {
+      return closed;
+    }
+
+    @VisibleForTesting
+    synchronized int elements() {
+      return elements;
+    }
+
+    @VisibleForTesting
+    synchronized long bytes() {
+      return bytes;
+    }
+
+    @Override
+    public synchronized void close() {
+      if (closed) return;
+      closed = true;
+      decrementCounters(this.elements, this.bytes);
+    }
+  }
+
+  private static final class QueuedWork implements Runnable {
+
+    private final ExecutableWork work;
+    private final BoundedQueueExecutorWorkHandleImpl handle;
+
+    public QueuedWork(ExecutableWork work, BoundedQueueExecutorWorkHandleImpl 
handle) {
+      this.work = work;
+      this.handle = handle;
+    }
+
+    public ExecutableWork getWork() {
+      return work;
+    }
+
+    public BoundedQueueExecutorWorkHandleImpl getHandle() {
+      return handle;
+    }
+
+    @Override
+    public void run() {
+      checkArgument(!handle.isClosed());
+      try (handle) {
+        work.run(handle);
+      }
+    }
+  }
+
+  private void executeMonitorHeld(ExecutableWork work, long workBytes) {
+    ++elementsOutstanding;
     bytesOutstanding += workBytes;
+    monitor.leave();
+    BoundedQueueExecutorWorkHandleImpl handle =
+        new BoundedQueueExecutorWorkHandleImpl(1, workBytes);
+    try {
+      executor.execute(new QueuedWork(work, handle));
+    } catch (Throwable t) {
+      handle.close();
+      throw ExceptionUtils.safeWrapThrowableAsException(t);
+    }
+  }
+
+  private void executeMonitorHeld(Runnable work) {
     ++elementsOutstanding;
     monitor.leave();
 
@@ -232,21 +364,25 @@ public class BoundedQueueExecutor {
             try {
               work.run();
             } finally {
-              decrementCounters(workBytes);
+              decrementCounters(1, 0L);
             }
           });
-    } catch (RuntimeException e) {
-      // If the execute() call threw an exception, decrement counters here.
-      decrementCounters(workBytes);
-      throw e;
+    } catch (Throwable t) {
+      decrementCounters(1, 0L);
+      throw ExceptionUtils.safeWrapThrowableAsException(t);
     }
   }
 
-  private void decrementCounters(long workBytes) {
+  @VisibleForTesting
+  BoundedQueueExecutorWorkHandleImpl createBudgetHandle(int elements, long 
bytes) {
+    return new BoundedQueueExecutorWorkHandleImpl(elements, bytes);
+  }
+
+  private void decrementCounters(int elements, long bytes) {
     // All threads queue decrements and one thread grabs the monitor and 
updates
     // counters. We do this to reduce contention on monitor which is locked by
     // GetWork thread
-    decrementQueue.add(workBytes);
+    decrementQueue.add(new Budget(elements, bytes));
     boolean submittedToExistingBatch = isDecrementBatchPending.getAndSet(true);
     if (submittedToExistingBatch) {
       // There is already a thread about to drain the decrement queue
@@ -265,12 +401,12 @@ public class BoundedQueueExecutor {
       long bytesToDecrement = 0;
       int elementsToDecrement = 0;
       while (true) {
-        Long pollResult = decrementQueue.poll();
+        Budget pollResult = decrementQueue.poll();
         if (pollResult == null) {
           break;
         }
-        bytesToDecrement += pollResult;
-        ++elementsToDecrement;
+        bytesToDecrement += pollResult.bytes;
+        elementsToDecrement += pollResult.elements;
       }
       if (elementsToDecrement == 0) {
         return;
diff --git 
a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/util/ExceptionUtils.java
 
b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/util/ExceptionUtils.java
new file mode 100644
index 00000000000..d04a385d6e6
--- /dev/null
+++ 
b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/util/ExceptionUtils.java
@@ -0,0 +1,43 @@
+/*
+ * 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.util;
+
+import javax.annotation.CheckReturnValue;
+import org.apache.beam.sdk.annotations.Internal;
+
+/** Utility methods for simplifying work with exceptions and throwables. */
+@Internal
+public final class ExceptionUtils {
+
+  private ExceptionUtils() {}
+
+  /**
+   * Returns the {@code throwable} as-is if it is an instance of {@link 
RuntimeException} throws if
+   * it is an {@link Error}, or returns the {@code throwable} wrapped in a 
{@code RuntimeException}.
+   */
+  @CheckReturnValue
+  public static RuntimeException safeWrapThrowableAsException(Throwable 
throwable) {
+    if (throwable instanceof RuntimeException) {
+      return (RuntimeException) throwable;
+    } else if (throwable instanceof Error) {
+      throw (Error) throwable;
+    } else {
+      return new RuntimeException(throwable);
+    }
+  }
+}
diff --git 
a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/work/processing/StreamingCommitFinalizer.java
 
b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/work/processing/StreamingCommitFinalizer.java
index 5a66545ab33..22573bf1ced 100644
--- 
a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/work/processing/StreamingCommitFinalizer.java
+++ 
b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/work/processing/StreamingCommitFinalizer.java
@@ -156,7 +156,7 @@ final class StreamingCommitFinalizer {
     }
     for (Runnable callback : callbacksToExecute) {
       try {
-        finalizationExecutor.forceExecute(callback, 0);
+        finalizationExecutor.forceExecute(callback);
       } catch (OutOfMemoryError oom) {
         throw oom;
       } catch (Throwable t) {
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 1428037d9ca..364608be82c 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
@@ -35,6 +35,7 @@ import org.apache.beam.runners.dataflow.worker.HotKeyLogger;
 import org.apache.beam.runners.dataflow.worker.ReaderCache;
 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;
 import org.apache.beam.runners.dataflow.worker.streaming.ComputationState;
 import 
org.apache.beam.runners.dataflow.worker.streaming.ComputationWorkExecutor;
 import org.apache.beam.runners.dataflow.worker.streaming.ExecutableWork;
@@ -47,6 +48,7 @@ import 
org.apache.beam.runners.dataflow.worker.streaming.harness.StreamingCounte
 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;
 import org.apache.beam.runners.dataflow.worker.windmill.Windmill;
 import 
org.apache.beam.runners.dataflow.worker.windmill.Windmill.LatencyAttribution;
 import org.apache.beam.runners.dataflow.worker.windmill.client.commits.Commit;
@@ -218,7 +220,7 @@ public class StreamingWorkScheduler {
         ExecutableWork.create(
             Work.create(
                 workItem, serializedWorkItemSize, watermarks, 
processingContext, drainMode, clock),
-            work -> processWork(computationState, work, 
getWorkStreamLatencies)));
+            (work, handle) -> processWork(computationState, work, 
getWorkStreamLatencies, handle)));
   }
 
   /** Adds any applied finalize ids to the commit finalizer to have their 
callbacks executed. */
@@ -232,16 +234,19 @@ public class StreamingWorkScheduler {
    * internally if processing fails due to uncaught {@link Exception}(s).
    *
    * @implNote This will block the calling thread during execution of user 
DoFns.
+   * @param handle handled to pass to BoundedQueueExecutor.pollWork, currently 
unused
    */
   private void processWork(
       ComputationState computationState,
       Work work,
-      ImmutableList<LatencyAttribution> getWorkStreamLatencies) {
+      ImmutableList<LatencyAttribution> getWorkStreamLatencies,
+      BoundedQueueExecutorWorkHandle handle) {
     work.recordGetWorkStreamLatencies(getWorkStreamLatencies);
-    processWork(computationState, work);
+    processWork(computationState, work, handle);
   }
 
-  private void processWork(ComputationState computationState, Work work) {
+  private void processWork(
+      ComputationState computationState, Work work, 
BoundedQueueExecutorWorkHandle unusedHandle) {
     Windmill.WorkItem workItem = work.getWorkItem();
     String computationId = computationState.getComputationId();
     ByteString key = workItem.getKey();
@@ -289,7 +294,7 @@ public class StreamingWorkScheduler {
       try {
         workFailureProcessor.logAndProcessFailure(
             computationId,
-            ExecutableWork.create(work, retry -> processWork(computationState, 
retry)),
+            ExecutableWork.create(work, (retry, h) -> 
processWork(computationState, retry, h)),
             t,
             invalidWork ->
                 computationState.completeWorkAndScheduleNextWorkForKey(
@@ -297,7 +302,8 @@ public class StreamingWorkScheduler {
       } catch (OutOfMemoryError oom) {
         throw oom;
       } catch (Throwable t2) {
-        throw new RuntimeException(t2);
+        LOG.warn("Failed to process work failure safely for work {}", 
work.id(), t2);
+        throw ExceptionUtils.safeWrapThrowableAsException(t2);
       }
     } finally {
       // Update total processing time counters. Updating in finally clause 
ensures that
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 ff82a1ab5c4..d58f2007699 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
@@ -373,7 +373,9 @@ public class StreamingDataflowWorkerTest {
                 computationId, new FakeGetDataClient(), ignored -> {}, 
mock(HeartbeatSender.class)),
             false,
             Instant::now),
-        processWorkFn);
+        (work, handle) -> {
+          processWorkFn.accept(work);
+        });
   }
 
   private byte[] intervalWindowBytes(IntervalWindow window) throws Exception {
diff --git 
a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/streaming/ActiveWorkStateTest.java
 
b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/streaming/ActiveWorkStateTest.java
index 865ae261280..0f14efdd0c0 100644
--- 
a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/streaming/ActiveWorkStateTest.java
+++ 
b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/streaming/ActiveWorkStateTest.java
@@ -73,7 +73,7 @@ public class ActiveWorkStateTest {
             createWorkProcessingContext(),
             false,
             Instant::now),
-        ignored -> {});
+        (work, handle) -> {});
   }
 
   private static ExecutableWork expiredWork(Windmill.WorkItem workItem) {
@@ -85,7 +85,7 @@ public class ActiveWorkStateTest {
             createWorkProcessingContext(),
             false,
             () -> Instant.EPOCH),
-        ignored -> {});
+        (work, handle) -> {});
   }
 
   private static Work.ProcessingContext createWorkProcessingContext() {
diff --git 
a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/streaming/ComputationStateCacheTest.java
 
b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/streaming/ComputationStateCacheTest.java
index 1c8b8fca131..30ad97140e1 100644
--- 
a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/streaming/ComputationStateCacheTest.java
+++ 
b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/streaming/ComputationStateCacheTest.java
@@ -77,7 +77,7 @@ public class ComputationStateCacheTest {
                 mock(HeartbeatSender.class)),
             false,
             Instant::now),
-        ignored -> {});
+        (work, handle) -> {});
   }
 
   @Before
diff --git 
a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/util/BoundedQueueExecutorTest.java
 
b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/util/BoundedQueueExecutorTest.java
index d7ea039bb80..55fe82c7163 100644
--- 
a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/util/BoundedQueueExecutorTest.java
+++ 
b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/util/BoundedQueueExecutorTest.java
@@ -32,6 +32,7 @@ import java.util.function.Consumer;
 import org.apache.beam.runners.dataflow.worker.streaming.ExecutableWork;
 import org.apache.beam.runners.dataflow.worker.streaming.Watermarks;
 import org.apache.beam.runners.dataflow.worker.streaming.Work;
+import 
org.apache.beam.runners.dataflow.worker.util.BoundedQueueExecutor.BoundedQueueExecutorWorkHandleImpl;
 import org.apache.beam.runners.dataflow.worker.windmill.Windmill.WorkItem;
 import 
org.apache.beam.runners.dataflow.worker.windmill.client.getdata.FakeGetDataClient;
 import 
org.apache.beam.runners.dataflow.worker.windmill.work.refresh.HeartbeatSender;
@@ -85,18 +86,21 @@ public class BoundedQueueExecutorTest {
                 mock(HeartbeatSender.class)),
             false,
             Instant::now),
-        executeWorkFn);
+        (work, handle) -> {
+          executeWorkFn.accept(work);
+        });
   }
 
-  private Runnable createSleepProcessWorkFn(CountDownLatch start, 
CountDownLatch stop) {
-    return () -> {
-      start.countDown();
-      try {
-        stop.await();
-      } catch (Exception e) {
-        throw new RuntimeException(e);
-      }
-    };
+  private ExecutableWork createSleepProcessWork(CountDownLatch start, 
CountDownLatch stop) {
+    return createWork(
+        ignored -> {
+          start.countDown();
+          try {
+            stop.await();
+          } catch (Exception e) {
+            throw new RuntimeException(e);
+          }
+        });
   }
 
   @Before
@@ -123,9 +127,9 @@ public class BoundedQueueExecutorTest {
     CountDownLatch processStop2 = new CountDownLatch(1);
     CountDownLatch processStart3 = new CountDownLatch(1);
     CountDownLatch processStop3 = new CountDownLatch(1);
-    Runnable m1 = createSleepProcessWorkFn(processStart1, processStop1);
-    Runnable m2 = createSleepProcessWorkFn(processStart2, processStop2);
-    Runnable m3 = createSleepProcessWorkFn(processStart3, processStop3);
+    ExecutableWork m1 = createSleepProcessWork(processStart1, processStop1);
+    ExecutableWork m2 = createSleepProcessWork(processStart2, processStop2);
+    ExecutableWork m3 = createSleepProcessWork(processStart3, processStop3);
 
     executor.execute(m1, 1);
     processStart1.await();
@@ -152,8 +156,8 @@ public class BoundedQueueExecutorTest {
     CountDownLatch processStop1 = new CountDownLatch(1);
     CountDownLatch processStart2 = new CountDownLatch(1);
     CountDownLatch processStop2 = new CountDownLatch(1);
-    Runnable m1 = createSleepProcessWorkFn(processStart1, processStop1);
-    Runnable m2 = createSleepProcessWorkFn(processStart2, processStop2);
+    ExecutableWork m1 = createSleepProcessWork(processStart1, processStop1);
+    ExecutableWork m2 = createSleepProcessWork(processStart2, processStop2);
 
     executor.execute(m1, 10000000);
     processStart1.await();
@@ -187,9 +191,9 @@ public class BoundedQueueExecutorTest {
     CountDownLatch processStart2 = new CountDownLatch(1);
     CountDownLatch processStart3 = new CountDownLatch(1);
     CountDownLatch stop = new CountDownLatch(1);
-    Runnable m1 = createSleepProcessWorkFn(processStart1, stop);
-    Runnable m2 = createSleepProcessWorkFn(processStart2, stop);
-    Runnable m3 = createSleepProcessWorkFn(processStart3, stop);
+    ExecutableWork m1 = createSleepProcessWork(processStart1, stop);
+    ExecutableWork m2 = createSleepProcessWork(processStart2, stop);
+    ExecutableWork m3 = createSleepProcessWork(processStart3, stop);
 
     // Initial state.
     assertEquals(0, executor.activeCount());
@@ -225,9 +229,9 @@ public class BoundedQueueExecutorTest {
     CountDownLatch processStart2 = new CountDownLatch(1);
     CountDownLatch processStart3 = new CountDownLatch(1);
     CountDownLatch stop = new CountDownLatch(1);
-    Runnable m1 = createSleepProcessWorkFn(processStart1, stop);
-    Runnable m2 = createSleepProcessWorkFn(processStart2, stop);
-    Runnable m3 = createSleepProcessWorkFn(processStart3, stop);
+    ExecutableWork m1 = createSleepProcessWork(processStart1, stop);
+    ExecutableWork m2 = createSleepProcessWork(processStart2, stop);
+    ExecutableWork m3 = createSleepProcessWork(processStart3, stop);
 
     // Initial state.
     assertEquals(0, executor.activeCount());
@@ -264,9 +268,9 @@ public class BoundedQueueExecutorTest {
     CountDownLatch processStart2 = new CountDownLatch(1);
     CountDownLatch processStart3 = new CountDownLatch(1);
     CountDownLatch stop = new CountDownLatch(1);
-    Runnable m1 = createSleepProcessWorkFn(processStart1, stop);
-    Runnable m2 = createSleepProcessWorkFn(processStart2, stop);
-    Runnable m3 = createSleepProcessWorkFn(processStart3, stop);
+    ExecutableWork m1 = createSleepProcessWork(processStart1, stop);
+    ExecutableWork m2 = createSleepProcessWork(processStart2, stop);
+    ExecutableWork m3 = createSleepProcessWork(processStart3, stop);
 
     // Initial state.
     assertEquals(0, executor.activeCount());
@@ -308,9 +312,9 @@ public class BoundedQueueExecutorTest {
     CountDownLatch processStop2 = new CountDownLatch(1);
     CountDownLatch processStart3 = new CountDownLatch(1);
     CountDownLatch processStop3 = new CountDownLatch(1);
-    Runnable m1 = createSleepProcessWorkFn(processStart1, processStop1);
-    Runnable m2 = createSleepProcessWorkFn(processStart2, processStop2);
-    Runnable m3 = createSleepProcessWorkFn(processStart3, processStop3);
+    ExecutableWork m1 = createSleepProcessWork(processStart1, processStop1);
+    ExecutableWork m2 = createSleepProcessWork(processStart2, processStop2);
+    ExecutableWork m3 = createSleepProcessWork(processStart3, processStop3);
 
     // Initial state.
     assertEquals(0, executor.activeCount());
@@ -351,6 +355,55 @@ public class BoundedQueueExecutorTest {
     executor.shutdown();
   }
 
+  @Test
+  public void testRunnableExceptionPropagationDecrementsCounters() throws 
Exception {
+    CountDownLatch processStart = new CountDownLatch(1);
+    CountDownLatch processStop = new CountDownLatch(1);
+
+    Runnable work =
+        () -> {
+          processStart.countDown();
+          try {
+            processStop.await();
+          } catch (InterruptedException e) {
+            throw new RuntimeException(e);
+          }
+          throw new RuntimeException("Simulated finalizer processing 
exception");
+        };
+
+    executor.forceExecute(work);
+    processStart.await();
+
+    assertEquals(1, executor.elementsOutstanding());
+
+    processStop.countDown();
+
+    // Wait until outstanding elements are released
+    while (executor.elementsOutstanding() != 0) {
+      Thread.sleep(10);
+    }
+
+    assertEquals(0, executor.elementsOutstanding());
+  }
+
+  @Test
+  public void testHandleMerge() throws Exception {
+    BoundedQueueExecutorWorkHandleImpl handle1 = 
executor.createBudgetHandle(1, 100L);
+    BoundedQueueExecutorWorkHandleImpl handle2 = 
executor.createBudgetHandle(2, 200L);
+
+    handle1.merge(handle2);
+
+    // Verify that handle2 has 0 budget and is closed.
+    assertEquals(0, handle2.elements());
+    assertEquals(0, handle2.bytes());
+    assertTrue(handle2.isClosed());
+
+    // Verify that handle1 has the combined budget and is not closed.
+    assertEquals(3, handle1.elements());
+    assertEquals(300L, handle1.bytes());
+    assertFalse(handle1.isClosed());
+  }
+
   @Test
   public void testRenderSummaryHtml() {
     String expectedSummaryHtml =
diff --git 
a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/windmill/work/processing/failures/WorkFailureProcessorTest.java
 
b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/windmill/work/processing/failures/WorkFailureProcessorTest.java
index 68a11895fa1..51bd4816b03 100644
--- 
a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/windmill/work/processing/failures/WorkFailureProcessorTest.java
+++ 
b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/windmill/work/processing/failures/WorkFailureProcessorTest.java
@@ -98,7 +98,9 @@ public class WorkFailureProcessorTest {
                 mock(HeartbeatSender.class)),
             false,
             clock),
-        processWorkFn);
+        (work, handle) -> {
+          processWorkFn.accept(work);
+        });
   }
 
   private static ExecutableWork createWork(Consumer<Work> processWorkFn) {
diff --git 
a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/windmill/work/refresh/ActiveWorkRefresherTest.java
 
b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/windmill/work/refresh/ActiveWorkRefresherTest.java
index 054db878c86..88a82c6f76b 100644
--- 
a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/windmill/work/refresh/ActiveWorkRefresherTest.java
+++ 
b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/windmill/work/refresh/ActiveWorkRefresherTest.java
@@ -137,7 +137,9 @@ public class ActiveWorkRefresherTest {
                 "computationId", new FakeGetDataClient(), ignored -> {}, 
heartbeatSender),
             false,
             ActiveWorkRefresherTest::aLongTimeAgo),
-        processWork);
+        (work, handle) -> {
+          processWork.accept(work);
+        });
   }
 
   @Test


Reply via email to