This is an automated email from the ASF dual-hosted git repository.

derrickaw 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 18db4896cf1 [Multikey] [Dataflow Streaming] Replace KeyGroupWorkQueue 
with a sharded KeyGroupIndex (#40593)
18db4896cf1 is described below

commit 18db4896cf142b11f4f6e91d57ad4183d76c6115
Author: Arun Pandian <[email protected]>
AuthorDate: Mon Oct 19 13:48:38 2026 -0700

    [Multikey] [Dataflow Streaming] Replace KeyGroupWorkQueue with a sharded 
KeyGroupIndex (#40593)
    
    * [Dataflow Streaming] Replace KeyGroupWorkQueue with a sharded 
KeyGroupWorkIndex
    
    `KeyGroupWorkQueue` guarded a doubly-linked global queue and per-key-group 
queues with a single `ReentrantLock`, creating lock contention across worker 
and `GetWork` threads when multi-key bundles are enabled.
    
    Use `LinkedBlockingQueue` as the `BoundedQueueExecutor` work queue 
unconditionally, and index stealable work in `KeyGroupWorkIndex` across 64 
fixed `HashMap<QueueKey, ArrayDeque<QueuedWork>>` shards synchronized per 
shard. An executor thread and `pollWork` coordinate on a `QueuedWork` via a 
single atomic state transition, clearing work and budget references immediately 
upon claim. Claimed entries at the head of a key-group queue are drained in 
amortized O(1) time during index mutation [...]
    
    * Address comments + refactorings + change purge logic
    
    * Address comments
    
    * Clarify comments regarding maximumElementsOutstanding
---
 .../dataflow/worker/StreamingDataflowWorker.java   |   7 +-
 .../runners/dataflow/worker/streaming/Work.java    |   3 +-
 .../dataflow/worker/util/BoundedQueueExecutor.java | 224 +++++++--
 .../dataflow/worker/util/KeyGroupWorkIndex.java    | 190 ++++++++
 .../dataflow/worker/util/KeyGroupWorkQueue.java    | 472 -------------------
 .../worker/StreamingDataflowWorkerTest.java        |  12 +-
 .../worker/util/BoundedQueueExecutorTest.java      | 428 +++++++++++------
 .../worker/util/KeyGroupWorkIndexTest.java         | 319 +++++++++++++
 .../worker/util/KeyGroupWorkQueueTest.java         | 523 ---------------------
 .../processing/StreamingCommitFinalizerTest.java   |   2 +-
 .../failures/WorkFailureProcessorTest.java         |   2 +-
 .../work/refresh/ActiveWorkRefresherTest.java      |   2 +-
 12 files changed, 991 insertions(+), 1193 deletions(-)

diff --git 
a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/StreamingDataflowWorker.java
 
b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/StreamingDataflowWorker.java
index fd00cea775f..dd96142c41a 100644
--- 
a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/StreamingDataflowWorker.java
+++ 
b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/StreamingDataflowWorker.java
@@ -1055,7 +1055,6 @@ public final class StreamingDataflowWorker {
       DataflowWorkerHarnessOptions options, MultiKeyBundleOptions 
multiKeyBundleOptions) {
     boolean useFairMonitor =
         DataflowRunner.hasExperiment(options, 
BOUNDED_QUEUE_EXECUTOR_USE_FAIR_MONITOR_EXPERIMENT);
-    boolean useKeyGroupWorkQueue = 
multiKeyBundleOptions.multiKeyBundleEnabled();
     return new BoundedQueueExecutor(
         chooseMaxThreads(options),
         THREAD_EXPIRATION_TIME_SEC,
@@ -1064,7 +1063,7 @@ public final class StreamingDataflowWorker {
         chooseMaxBytesOutstanding(options),
         new 
ThreadFactoryBuilder().setNameFormat("DataflowWorkUnits-%d").setDaemon(true).build(),
         useFairMonitor,
-        useKeyGroupWorkQueue);
+        multiKeyBundleOptions.multiKeyBundleEnabled());
   }
 
   public static void main(String[] args) throws Exception {
@@ -1134,8 +1133,8 @@ public final class StreamingDataflowWorker {
   }
 
   @VisibleForTesting
-  public boolean workExecutorIsEmpty() {
-    return workUnitExecutor.executorQueueIsEmpty();
+  public boolean workExecutorIsEmptyForTesting() {
+    return workUnitExecutor.executorQueueIsEmptyForTesting();
   }
 
   @VisibleForTesting
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 2acee9410fa..724668b689f 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
@@ -25,7 +25,6 @@ import java.util.EnumMap;
 import java.util.IntSummaryStatistics;
 import java.util.Map;
 import java.util.Map.Entry;
-import java.util.Objects;
 import java.util.Optional;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicBoolean;
@@ -554,7 +553,7 @@ public final class Work implements RefreshableWork {
 
     @Override
     public int hashCode() {
-      return Objects.hash(high, low);
+      return 31 * Long.hashCode(high) + Long.hashCode(low);
     }
 
     @Override
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 046d8cae9f9..1d10d074b11 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
@@ -30,6 +30,8 @@ import java.util.concurrent.ThreadFactory;
 import java.util.concurrent.ThreadPoolExecutor;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
 import javax.annotation.concurrent.GuardedBy;
 import 
org.apache.beam.runners.dataflow.worker.streaming.BoundedQueueExecutorWorkHandle;
 import org.apache.beam.runners.dataflow.worker.streaming.ExecutableWork;
@@ -41,13 +43,20 @@ import 
org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.util.concurren
 import 
org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.util.concurrent.Monitor.Guard;
 import org.checkerframework.checker.nullness.qual.Nullable;
 
-/** An executor for executing work on windmill items. */
+/**
+ * An executor for executing work on windmill items.
+ *
+ * <p>When multi-key bundles are enabled, batchable work is also indexed in 
{@link
+ * KeyGroupWorkIndex} so running bundles can steal it via {@link #pollWork}. 
Stolen work leaves a
+ * no-op stale shell in the executor queue, which is discarded when dequeued 
or purged in bulk if
+ * the queue grows too large.
+ */
 public class BoundedQueueExecutor {
 
   private final ThreadPoolExecutor executor;
   private final long maximumBytesOutstanding;
 
-  // Used to guard elementsOutstanding and bytesOutstanding.
+  // Used to guard writes to elementsOutstanding and bytesOutstanding.
   private final Monitor monitor;
 
   private static class Budget {
@@ -64,11 +73,10 @@ public class BoundedQueueExecutor {
   private final ConcurrentLinkedQueue<Budget> decrementQueue = new 
ConcurrentLinkedQueue<>();
   private final Object decrementQueueDrainLock = new Object();
   private final AtomicBoolean isDecrementBatchPending = new 
AtomicBoolean(false);
-  private int elementsOutstanding = 0;
+  private final AtomicInteger elementsOutstanding = new AtomicInteger();
   private long bytesOutstanding = 0;
 
-  @GuardedBy("this")
-  private int maximumElementsOutstanding;
+  private volatile int maximumElementsOutstanding;
 
   @GuardedBy("this")
   private int activeCount;
@@ -82,8 +90,10 @@ public class BoundedQueueExecutor {
   @GuardedBy("this")
   private long totalTimeMaxActiveThreadsUsed;
 
-  // If set the keyGroupWorkQueue is used by the underlying executor.
-  private final @Nullable KeyGroupWorkQueue keyGroupWorkQueue;
+  private final LinkedBlockingQueue<Runnable> workQueue = new 
LinkedBlockingQueue<>();
+  private final @Nullable KeyGroupWorkIndex keyGroupWorkIndex;
+
+  private final AtomicBoolean purgingStaleShells = new AtomicBoolean(false);
 
   public BoundedQueueExecutor(
       int initialMaximumPoolSize,
@@ -93,8 +103,8 @@ public class BoundedQueueExecutor {
       long maximumBytesOutstanding,
       ThreadFactory threadFactory,
       boolean useFairMonitor,
-      boolean useKeyGroupWorkQueue) {
-    this.keyGroupWorkQueue = useKeyGroupWorkQueue ? new 
KeyGroupWorkQueue(useFairMonitor) : null;
+      boolean multiKeyBundleEnabled) {
+    this.keyGroupWorkIndex = multiKeyBundleEnabled ? new KeyGroupWorkIndex() : 
null;
     this.maximumPoolSize = initialMaximumPoolSize;
     monitor = new Monitor(useFairMonitor);
     executor =
@@ -103,7 +113,7 @@ public class BoundedQueueExecutor {
             initialMaximumPoolSize,
             keepAliveTime,
             unit,
-            keyGroupWorkQueue != null ? keyGroupWorkQueue : new 
LinkedBlockingQueue<>(),
+            workQueue,
             threadFactory) {
           @Override
           protected void beforeExecute(Thread t, Runnable r) {
@@ -139,9 +149,9 @@ public class BoundedQueueExecutor {
         new Guard(monitor) {
           @Override
           public boolean isSatisfied() {
-            return elementsOutstanding == 0
-                || (bytesAvailable() >= workBytes
-                    && elementsOutstanding < maximumElementsOutstanding());
+            int outstanding = elementsOutstanding.get();
+            return outstanding == 0
+                || (bytesAvailable() >= workBytes && outstanding < 
maximumElementsOutstanding());
           }
         });
     executeMonitorHeld(work, workBytes);
@@ -181,8 +191,21 @@ public class BoundedQueueExecutor {
     }
   }
 
-  public boolean executorQueueIsEmpty() {
-    return executor.getQueue().isEmpty();
+  @VisibleForTesting
+  public boolean executorQueueIsEmptyForTesting() {
+    return workQueue.stream().allMatch(BoundedQueueExecutor::isStaleShell);
+  }
+
+  /** Returns the raw number of entries in the executor queue, including stale 
shells. */
+  @VisibleForTesting
+  int rawQueueSizeForTesting() {
+    return workQueue.size();
+  }
+
+  @VisibleForTesting
+  boolean keyGroupWorkIndexIsEmpty() {
+    @Nullable KeyGroupWorkIndex index = keyGroupWorkIndex;
+    return index == null || index.isEmpty();
   }
 
   public synchronized long allThreadsActiveTime() {
@@ -203,19 +226,14 @@ public class BoundedQueueExecutor {
   }
 
   public int elementsOutstanding() {
-    monitor.enter();
-    try {
-      return elementsOutstanding;
-    } finally {
-      monitor.leave();
-    }
+    return elementsOutstanding.get();
   }
 
   public long maximumBytesOutstanding() {
     return maximumBytesOutstanding;
   }
 
-  public synchronized int maximumElementsOutstanding() {
+  public int maximumElementsOutstanding() {
     return maximumElementsOutstanding;
   }
 
@@ -238,7 +256,7 @@ public class BoundedQueueExecutor {
       builder.append("<br>/n");
 
       builder.append("Work Queue Size: ");
-      builder.append(elementsOutstanding);
+      builder.append(elementsOutstanding.get());
       builder.append("/");
       builder.append(maximumElementsOutstanding());
       builder.append("<br>/n");
@@ -328,49 +346,132 @@ public class BoundedQueueExecutor {
     }
   }
 
+  /**
+   * A {@link Runnable} for an {@link ExecutableWork} that can be claimed 
either by an executor
+   * thread in {@link #run} or by a multi-key bundle in {@link #pollWork}.
+   *
+   * <p>Thread safety: {@link #state} is {@code volatile} and transitions at 
most once from {@link
+   * #QUEUED} to {@link #STARTED} or {@link #STOLEN} via {@link #STATE}{@code 
.compareAndSet}.
+   * Non-final fields {@link #work} and {@link #handle} are written before 
{@code state} is
+   * initialized in the constructor, and are subsequently read and cleared 
only by the single thread
+   * that wins that {@code compareAndSet}. All other accesses read only {@code 
final} fields or
+   * {@code volatile state}.
+   */
   static final class QueuedWork implements Runnable {
 
-    private final ExecutableWork work;
-    private final BoundedQueueExecutorWorkHandleImpl handle;
-
-    public QueuedWork(ExecutableWork work, BoundedQueueExecutorWorkHandleImpl 
handle) {
+    private static final int QUEUED = 0;
+    private static final int STARTED = 1; // Claimed by an executor thread in 
run().
+    private static final int STOLEN = 2; // Claimed by a multi-key bundle in 
pollWork().
+
+    private static final AtomicIntegerFieldUpdater<QueuedWork> STATE =
+        AtomicIntegerFieldUpdater.newUpdater(QueuedWork.class, "state");
+
+    // Non-null iff the work is eligible to be stolen by a multi-key bundle.
+    private final @Nullable KeyGroupWorkIndex keyGroupWorkIndex;
+    private final KeyGroupWorkIndex.@Nullable QueueKey queueKey;
+    // Cleared upon claim so stale shells and claimed index entries do not 
retain work references.
+    private @Nullable ExecutableWork work;
+    private @Nullable BoundedQueueExecutorWorkHandleImpl handle;
+    private volatile int state = QUEUED;
+
+    QueuedWork(
+        ExecutableWork work,
+        BoundedQueueExecutorWorkHandleImpl handle,
+        @Nullable KeyGroupWorkIndex keyGroupWorkIndex) {
       this.work = work;
       this.handle = handle;
+      Work w = work.work();
+      if (keyGroupWorkIndex != null
+          && !w.getKeyGroup().equals(Work.KeyGroup.DEFAULT)
+          && !w.isMultiKeyBatchingDisabled()) {
+        this.keyGroupWorkIndex = keyGroupWorkIndex;
+        this.queueKey = new KeyGroupWorkIndex.QueueKey(w.getComputationId(), 
w.getKeyGroup());
+      } else {
+        this.keyGroupWorkIndex = null;
+        this.queueKey = null;
+      }
+    }
+
+    @VisibleForTesting
+    ExecutableWork getWork() {
+      return checkStateNotNull(work, "QueuedWork was already claimed.");
+    }
+
+    private BoundedQueueExecutorWorkHandleImpl getHandle() {
+      return checkStateNotNull(handle, "QueuedWork was already claimed.");
+    }
+
+    @VisibleForTesting
+    boolean isStealable() {
+      return keyGroupWorkIndex != null;
+    }
+
+    KeyGroupWorkIndex.QueueKey getQueueKey() {
+      return checkStateNotNull(queueKey);
     }
 
-    public ExecutableWork getWork() {
-      return work;
+    boolean isClaimed() {
+      return state != QUEUED;
     }
 
-    public BoundedQueueExecutorWorkHandleImpl getHandle() {
-      return handle;
+    private boolean isStolen() {
+      return state == STOLEN;
+    }
+
+    @VisibleForTesting
+    boolean tryClaimForExecution() {
+      return STATE.compareAndSet(this, QUEUED, STARTED);
+    }
+
+    boolean tryClaimForSteal() {
+      return STATE.compareAndSet(this, QUEUED, STOLEN);
+    }
+
+    private void clearClaimedWork() {
+      work = null;
+      handle = null;
     }
 
     @Override
     public void run() {
-      checkArgument(!handle.isClosed());
-      try (handle) {
-        work.run(handle);
+      if (!tryClaimForExecution()) {
+        return; // Already stolen by a multi-key bundle.
+      }
+      ExecutableWork workToRun = getWork();
+      BoundedQueueExecutorWorkHandleImpl workHandle = getHandle();
+      clearClaimedWork();
+      @Nullable KeyGroupWorkIndex index = keyGroupWorkIndex;
+      if (index != null) {
+        index.drainClaimed(this);
+      }
+      checkArgument(!workHandle.isClosed());
+      try (workHandle) {
+        workToRun.run(workHandle);
       }
     }
   }
 
   private void executeMonitorHeld(ExecutableWork work, long workBytes) {
-    ++elementsOutstanding;
+    elementsOutstanding.incrementAndGet();
     bytesOutstanding += workBytes;
     monitor.leave();
     BoundedQueueExecutorWorkHandleImpl handle =
         new BoundedQueueExecutorWorkHandleImpl(work.work(), workBytes);
+    QueuedWork queuedWork = new QueuedWork(work, handle, keyGroupWorkIndex);
     try {
-      executor.execute(new QueuedWork(work, handle));
+      executor.execute(queuedWork);
     } catch (Throwable t) {
       handle.close();
       throw ExceptionUtils.safeWrapThrowableAsException(t);
     }
+    if (queuedWork.isStealable()) {
+      // Index after queuing so work rejected after shutdown cannot be stolen.
+      checkStateNotNull(keyGroupWorkIndex).add(queuedWork);
+    }
   }
 
   private void executeMonitorHeld(Runnable work) {
-    ++elementsOutstanding;
+    elementsOutstanding.incrementAndGet();
     monitor.leave();
 
     try {
@@ -403,23 +504,56 @@ public class BoundedQueueExecutor {
     checkArgument(handle instanceof BoundedQueueExecutorWorkHandleImpl);
     checkStateNotNull(onFailedWorkHandler);
     BoundedQueueExecutorWorkHandleImpl internalHandle = 
(BoundedQueueExecutorWorkHandleImpl) handle;
-    if (keyGroupWorkQueue == null) {
+    @Nullable KeyGroupWorkIndex index = keyGroupWorkIndex;
+    if (index == null) {
       return null;
     }
     while (true) {
-      @Nullable QueuedWork queuedWork = 
keyGroupWorkQueue.pollWork(computationId, keyGroup);
-      if (queuedWork == null) {
+      @Nullable QueuedWork stolen = index.pollAndClaim(computationId, 
keyGroup);
+      if (stolen == null) {
         return null;
       }
-      Work work = queuedWork.getWork().work();
+      ExecutableWork stolenWork = stolen.getWork();
+      BoundedQueueExecutorWorkHandleImpl stolenHandle = stolen.getHandle();
+      stolen.clearClaimedWork();
+      onWorkStolen();
+      Work work = stolenWork.work();
       if (work.isFailed()) {
-        queuedWork.getHandle().close();
+        stolenHandle.close();
         onFailedWorkHandler.onFailedWork(work);
         continue;
       }
-      internalHandle.merge(queuedWork.getHandle());
-      return queuedWork.getWork();
+      internalHandle.merge(stolenHandle);
+      return stolenWork;
+    }
+  }
+
+  /**
+   * Purges stale shells in bulk if they accumulate while all executor threads 
are busy running
+   * multi-key bundles.
+   *
+   * <p>{@code forceExecute} can queue more than {@code 
maximumElementsOutstanding} unstarted items
+   * (e.g. retries or finalizations). {@code elementsOutstanding} counts 
non-shell work items, so
+   * comparing {@code workQueue.size()} against {@code elementsOutstanding +
+   * maximumElementsOutstanding} means there is at least {@code 
maximumElementsOutstanding} closed
+   * stale shells before running an O(n) {@code removeIf}.
+   */
+  private void onWorkStolen() {
+    if (workQueue.size() <= (long) elementsOutstanding.get() + 
maximumElementsOutstanding
+        || !purgingStaleShells.compareAndSet(false, true)) {
+      return;
     }
+    try {
+      if (!executor.isShutdown()) {
+        workQueue.removeIf(BoundedQueueExecutor::isStaleShell);
+      }
+    } finally {
+      purgingStaleShells.set(false);
+    }
+  }
+
+  private static boolean isStaleShell(Runnable runnable) {
+    return runnable instanceof QueuedWork && ((QueuedWork) 
runnable).isStolen();
   }
 
   private void decrementCounters(int elements, long bytes) {
@@ -457,7 +591,7 @@ public class BoundedQueueExecutor {
       }
 
       monitor.enter();
-      elementsOutstanding -= elementsToDecrement;
+      elementsOutstanding.addAndGet(-elementsToDecrement);
       bytesOutstanding -= bytesToDecrement;
       monitor.leave();
     }
diff --git 
a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/util/KeyGroupWorkIndex.java
 
b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/util/KeyGroupWorkIndex.java
new file mode 100644
index 00000000000..10cdab083b5
--- /dev/null
+++ 
b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/util/KeyGroupWorkIndex.java
@@ -0,0 +1,190 @@
+/*
+ * 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 static 
org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkArgument;
+
+import java.util.ArrayDeque;
+import java.util.HashMap;
+import java.util.Map;
+import javax.annotation.concurrent.GuardedBy;
+import javax.annotation.concurrent.ThreadSafe;
+import org.apache.beam.runners.dataflow.worker.streaming.Work;
+import 
org.apache.beam.runners.dataflow.worker.util.BoundedQueueExecutor.QueuedWork;
+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.collect.ImmutableList;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/**
+ * Sharded FIFO index of {@link QueuedWork} by computation and {@link 
Work.KeyGroup} for work
+ * stealing by multi-key bundles.
+ */
+@ThreadSafe
+final class KeyGroupWorkIndex {
+
+  private static final int NUM_SHARDS = 64;
+  private static final int INITIAL_QUEUE_CAPACITY = 4;
+
+  private final ImmutableList<Shard> shards;
+
+  KeyGroupWorkIndex() {
+    ImmutableList.Builder<Shard> builder = 
ImmutableList.builderWithExpectedSize(NUM_SHARDS);
+    for (int i = 0; i < NUM_SHARDS; i++) {
+      builder.add(new Shard());
+    }
+    this.shards = builder.build();
+  }
+
+  private Shard shardFor(QueueKey queueKey) {
+    return shards.get(Math.floorMod(queueKey.hashCode(), NUM_SHARDS));
+  }
+
+  /** Adds {@code work} to its key-group queue if it has not been claimed yet. 
*/
+  void add(QueuedWork work) {
+    if (work.isClaimed()) {
+      return;
+    }
+    QueueKey queueKey = work.getQueueKey();
+    shardFor(queueKey).add(queueKey, work);
+  }
+
+  /**
+   * Drains claimed work from the head of {@code work}'s key-group queue. If 
an earlier item is
+   * still unclaimed, {@code work} remains in the queue with its references 
cleared and is drained
+   * in amortized O(1) time once the earlier item is claimed.
+   */
+  void drainClaimed(QueuedWork completedWork) {
+    checkArgument(completedWork.isClaimed());
+    QueueKey queueKey = completedWork.getQueueKey();
+    shardFor(queueKey).drainClaimed(queueKey);
+  }
+
+  /** Claims and returns the oldest unclaimed work for the computation and key 
group, or null. */
+  @Nullable QueuedWork pollAndClaim(String computationId, Work.KeyGroup 
keyGroup) {
+    QueueKey queueKey = new QueueKey(computationId, keyGroup);
+    return shardFor(queueKey).pollAndClaim(queueKey);
+  }
+
+  @VisibleForTesting
+  boolean isEmpty() {
+    for (Shard shard : shards) {
+      if (!shard.isEmpty()) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  /** A shard of key-group queues; the map and its queues are guarded by the 
shard's monitor. */
+  private static final class Shard {
+
+    @GuardedBy("this")
+    private final Map<QueueKey, ArrayDeque<QueuedWork>> queuesByKeyGroup = new 
HashMap<>();
+
+    private synchronized void add(QueueKey queueKey, QueuedWork work) {
+      if (work.isClaimed()) {
+        return;
+      }
+      ArrayDeque<QueuedWork> queue =
+          queuesByKeyGroup.computeIfAbsent(
+              queueKey, unused -> new ArrayDeque<>(INITIAL_QUEUE_CAPACITY));
+      drainClaimedFromHead(queue);
+      queue.addLast(work);
+    }
+
+    private synchronized void drainClaimed(QueueKey queueKey) {
+      @Nullable ArrayDeque<QueuedWork> queue = queuesByKeyGroup.get(queueKey);
+      if (queue == null) {
+        return;
+      }
+      drainClaimedFromHead(queue);
+      if (queue.isEmpty()) {
+        queuesByKeyGroup.remove(queueKey);
+      }
+    }
+
+    private synchronized @Nullable QueuedWork pollAndClaim(QueueKey queueKey) {
+      @Nullable ArrayDeque<QueuedWork> queue = queuesByKeyGroup.get(queueKey);
+      if (queue == null) {
+        return null;
+      }
+      @Nullable QueuedWork claimed = null;
+      while (claimed == null && !queue.isEmpty()) {
+        QueuedWork candidate = queue.removeFirst();
+        if (candidate.tryClaimForSteal()) {
+          claimed = candidate;
+        }
+      }
+      drainClaimedFromHead(queue);
+      if (queue.isEmpty()) {
+        queuesByKeyGroup.remove(queueKey);
+      }
+      return claimed;
+    }
+
+    private synchronized boolean isEmpty() {
+      return queuesByKeyGroup.isEmpty();
+    }
+
+    private static void drainClaimedFromHead(ArrayDeque<QueuedWork> queue) {
+      while (!queue.isEmpty() && queue.peekFirst().isClaimed()) {
+        queue.removeFirst();
+      }
+    }
+  }
+
+  static final class QueueKey {
+    private final String computationId;
+    private final Work.KeyGroup keyGroup;
+    private final int hashCode;
+
+    QueueKey(String computationId, Work.KeyGroup keyGroup) {
+      this.computationId = computationId;
+      this.keyGroup = keyGroup;
+      this.hashCode = 31 * computationId.hashCode() + keyGroup.hashCode();
+    }
+
+    @Override
+    public boolean equals(@Nullable Object o) {
+      if (this == o) {
+        return true;
+      }
+      if (!(o instanceof QueueKey)) {
+        return false;
+      }
+      QueueKey other = (QueueKey) o;
+      return computationId.equals(other.computationId) && 
keyGroup.equals(other.keyGroup);
+    }
+
+    @Override
+    public int hashCode() {
+      return hashCode;
+    }
+
+    @Override
+    public String toString() {
+      return "QueueKey{"
+          + "computationId='"
+          + computationId
+          + '\''
+          + ", keyGroup="
+          + keyGroup
+          + '}';
+    }
+  }
+}
diff --git 
a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/util/KeyGroupWorkQueue.java
 
b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/util/KeyGroupWorkQueue.java
deleted file mode 100644
index dd409616ab9..00000000000
--- 
a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/util/KeyGroupWorkQueue.java
+++ /dev/null
@@ -1,472 +0,0 @@
-/*
- * 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 static org.apache.beam.sdk.util.Preconditions.checkArgumentNotNull;
-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.checkArgument;
-import static 
org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkState;
-
-import java.util.AbstractQueue;
-import java.util.Collection;
-import java.util.HashMap;
-import java.util.Iterator;
-import java.util.Map;
-import java.util.Objects;
-import java.util.concurrent.BlockingQueue;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.locks.Condition;
-import java.util.concurrent.locks.ReentrantLock;
-import javax.annotation.concurrent.GuardedBy;
-import org.apache.beam.runners.dataflow.worker.streaming.Work;
-import org.apache.beam.runners.dataflow.worker.streaming.Work.KeyGroup;
-import 
org.apache.beam.runners.dataflow.worker.util.BoundedQueueExecutor.QueuedWork;
-import 
org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableList;
-import org.checkerframework.checker.nullness.qual.NonNull;
-import org.checkerframework.checker.nullness.qual.Nullable;
-
-/**
- * A custom, thread-safe doubly-linked BlockingQueue. In addition to global 
FIFO ordering, the queue
- * supports polling work by computation + key group in FIFO order
- */
-class KeyGroupWorkQueue extends AbstractQueue<Runnable> implements 
BlockingQueue<Runnable> {
-
-  public static final Runnable SENTINEL_RUNNABLE =
-      () -> {
-        throw new IllegalStateException("sentinel runnable called");
-      };
-
-  static class Node {
-    // If keyGroup is non-null, task is an instance of QueuedWork
-    final Runnable task;
-    final @Nullable String computationId;
-    final Work.@Nullable KeyGroup keyGroup;
-    // cached keyGroupList if the Node is part of one.
-    @Nullable KeyGroupWorkList keyGroupList;
-
-    // prevNode, nextNode are used for the global order across all queued 
Runnables
-    @Nullable Node prevNode;
-    @Nullable Node nextNode;
-
-    // prevKeyGroupNode and nextKeyGroupNode are used for the keyGroup level 
lists linking
-    // QueuedWork with same keyGroup
-    @Nullable Node prevKeyGroupNode;
-    @Nullable Node nextKeyGroupNode;
-
-    private static boolean isMultiKeyBatchingDisabled(Runnable task) {
-      return !(task instanceof QueuedWork)
-          || ((QueuedWork) task).getWork().isMultiKeyBatchingDisabled();
-    }
-
-    Node(Runnable task) {
-      this.task = task;
-      if (!isMultiKeyBatchingDisabled(task)) {
-        this.computationId = ((QueuedWork) task).getWork().getComputationId();
-        this.keyGroup = ((QueuedWork) task).getWork().getKeyGroup();
-      } else {
-        this.computationId = null;
-        this.keyGroup = null;
-      }
-    }
-  }
-
-  /** Double linked list implementing key group level queue */
-  private static class KeyGroupWorkList {
-    final Node head = new Node(SENTINEL_RUNNABLE);
-    final Node tail = new Node(SENTINEL_RUNNABLE);
-
-    KeyGroupWorkList() {
-      head.nextKeyGroupNode = tail;
-      tail.prevKeyGroupNode = head;
-    }
-
-    boolean isEmpty() {
-      return head.nextKeyGroupNode == tail;
-    }
-
-    void append(Node node) {
-      Node last = checkStateNotNull(tail.prevKeyGroupNode);
-      node.prevKeyGroupNode = last;
-      node.nextKeyGroupNode = tail;
-      last.nextKeyGroupNode = node;
-      tail.prevKeyGroupNode = node;
-    }
-
-    void remove(Node node) {
-      @Nullable Node prev = node.prevKeyGroupNode;
-      @Nullable Node next = node.nextKeyGroupNode;
-      if (prev != null && next != null) {
-        prev.nextKeyGroupNode = next;
-        next.prevKeyGroupNode = prev;
-        node.prevKeyGroupNode = null;
-        node.nextKeyGroupNode = null;
-      }
-    }
-  }
-
-  private final ReentrantLock lock;
-  private final Condition notEmpty;
-
-  // Sentinels for the global list
-  @GuardedBy("lock")
-  private final Node globalHead = new Node(SENTINEL_RUNNABLE);
-
-  @GuardedBy("lock")
-  private final Node globalTail = new Node(SENTINEL_RUNNABLE);
-
-  @GuardedBy("lock")
-  private final Map<QueueKey, KeyGroupWorkList> keyGroupQueueMap = new 
HashMap<>();
-
-  @GuardedBy("lock")
-  private int size = 0;
-
-  public KeyGroupWorkQueue(boolean fair) {
-    this.lock = new ReentrantLock(fair);
-    this.notEmpty = lock.newCondition();
-    globalHead.nextNode = globalTail;
-    globalTail.prevNode = globalHead;
-  }
-
-  @GuardedBy("lock")
-  private void unlinkNode(Node node) {
-    // An existing node should always have previous and next since we have 
sentinels
-    // 1. Unlink from global list
-    Node prevG = checkArgumentNotNull(node.prevNode);
-    Node nextG = checkArgumentNotNull(node.nextNode);
-    prevG.nextNode = nextG;
-    nextG.prevNode = prevG;
-    node.prevNode = null;
-    node.nextNode = null;
-
-    // 2. Unlink from key group list
-    KeyGroupWorkList list = node.keyGroupList;
-    if (list != null) {
-      list.remove(node);
-      if (list.isEmpty()) {
-        String compId = checkStateNotNull(node.computationId);
-        Work.KeyGroup keyGroup = checkStateNotNull(node.keyGroup);
-        QueueKey key = new QueueKey(compId, keyGroup);
-        keyGroupQueueMap.remove(key);
-      }
-      node.keyGroupList = null;
-    }
-    --size;
-  }
-
-  @GuardedBy("lock")
-  private @Nullable Node removeFirstGlobal() {
-    Node first = checkStateNotNull(globalHead.nextNode);
-    if (first == globalTail) {
-      return null;
-    }
-    unlinkNode(first);
-    return first;
-  }
-
-  /**
-   * Remove and Return QueuedWork for the computationId, keyGroup in the FIFO 
order. Returns null,
-   * if there are no matches.
-   *
-   * @param keyGroup should not be equal to KeyGroup.DEFAULT
-   */
-  public @Nullable QueuedWork pollWork(String computationId, Work.KeyGroup 
keyGroup) {
-    checkArgument(computationId != null && keyGroup != null && 
!keyGroup.equals(KeyGroup.DEFAULT));
-    QueueKey key = new QueueKey(computationId, keyGroup);
-    lock.lock();
-    try {
-      KeyGroupWorkList keyGroupWorkList = keyGroupQueueMap.get(key);
-      if (keyGroupWorkList == null || keyGroupWorkList.isEmpty()) {
-        return null;
-      }
-
-      // Retrieve the first pending task for this computation and keyGroup in 
O(1)
-      Node firstNode = 
checkStateNotNull(keyGroupWorkList.head.nextKeyGroupNode);
-      if (firstNode == keyGroupWorkList.tail) {
-        return null;
-      }
-
-      // MultiKeyBatchingDisabled items should not be in keyGroupWorkList
-      checkState(!Node.isMultiKeyBatchingDisabled(firstNode.task));
-
-      unlinkNode(firstNode);
-
-      return (QueuedWork) firstNode.task;
-    } finally {
-      lock.unlock();
-    }
-  }
-
-  @Override
-  public boolean offer(@NonNull Runnable runnable) {
-    Node node = new Node(checkStateNotNull(runnable));
-    lock.lock();
-    try {
-      // Append to global list tail
-      Node lastG = checkStateNotNull(globalTail.prevNode);
-      node.prevNode = lastG;
-      node.nextNode = globalTail;
-      lastG.nextNode = node;
-      globalTail.prevNode = node;
-
-      // Append to key group list if applicable
-      String compId = node.computationId;
-      Work.KeyGroup keyGroup = node.keyGroup;
-      if (compId != null && keyGroup != null && 
!keyGroup.equals(KeyGroup.DEFAULT)) {
-        QueueKey key = new QueueKey(compId, keyGroup);
-        KeyGroupWorkList keyGroupWorkList =
-            keyGroupQueueMap.computeIfAbsent(key, k -> new KeyGroupWorkList());
-        keyGroupWorkList.append(node);
-        node.keyGroupList = keyGroupWorkList;
-      }
-
-      ++size;
-      notEmpty.signal();
-      return true;
-    } finally {
-      lock.unlock();
-    }
-  }
-
-  @Override
-  public void put(Runnable e) throws InterruptedException {
-    offer(e); // Unbounded queue
-  }
-
-  @Override
-  public boolean offer(Runnable e, long timeout, TimeUnit unit) throws 
InterruptedException {
-    return offer(e); // Unbounded queue
-  }
-
-  @Override
-  public @Nullable Runnable poll() {
-    lock.lock();
-    try {
-      @Nullable Node node = removeFirstGlobal();
-      return (node != null) ? node.task : null;
-    } finally {
-      lock.unlock();
-    }
-  }
-
-  @Override
-  public Runnable take() throws InterruptedException {
-    lock.lockInterruptibly();
-    try {
-      while (size == 0) {
-        notEmpty.await();
-      }
-      @Nullable Node node = removeFirstGlobal();
-      checkStateNotNull(node, "Queue is empty but size was " + size);
-      return node.task;
-    } finally {
-      lock.unlock();
-    }
-  }
-
-  @Override
-  public @Nullable Runnable poll(long timeout, TimeUnit unit) throws 
InterruptedException {
-    long nanos = unit.toNanos(timeout);
-    lock.lockInterruptibly();
-    try {
-      while (size == 0) {
-        if (nanos <= 0) {
-          return null;
-        }
-        nanos = notEmpty.awaitNanos(nanos);
-      }
-      @Nullable Node node = removeFirstGlobal();
-      return (node != null) ? node.task : null;
-    } finally {
-      lock.unlock();
-    }
-  }
-
-  @Override
-  public @Nullable Runnable peek() {
-    lock.lock();
-    try {
-      Node first = checkStateNotNull(globalHead.nextNode);
-      if (first == globalTail) {
-        return null;
-      }
-      return first.task;
-    } finally {
-      lock.unlock();
-    }
-  }
-
-  @Override
-  public int size() {
-    lock.lock();
-    try {
-      return size;
-    } finally {
-      lock.unlock();
-    }
-  }
-
-  @Override
-  public boolean isEmpty() {
-    lock.lock();
-    try {
-      return size == 0;
-    } finally {
-      lock.unlock();
-    }
-  }
-
-  @Override
-  public boolean remove(Object o) {
-    if (o == null) return false;
-    lock.lock();
-    try {
-      // Walk the global queue in O(N) to find and unlink the node
-      Node curr = checkStateNotNull(globalHead.nextNode);
-      while (curr != globalTail) {
-        if (o.equals(curr.task)) {
-          unlinkNode(curr);
-          return true;
-        }
-        curr = checkStateNotNull(curr.nextNode);
-      }
-      return false;
-    } finally {
-      lock.unlock();
-    }
-  }
-
-  @Override
-  public boolean contains(Object o) {
-    if (o == null) return false;
-    lock.lock();
-    try {
-      Node curr = checkStateNotNull(globalHead.nextNode);
-      while (curr != globalTail) {
-        if (o.equals(curr.task)) {
-          return true;
-        }
-        curr = checkStateNotNull(curr.nextNode);
-      }
-      return false;
-    } finally {
-      lock.unlock();
-    }
-  }
-
-  @Override
-  public int remainingCapacity() {
-    return Integer.MAX_VALUE;
-  }
-
-  @Override
-  public int drainTo(Collection<? super Runnable> c) {
-    return drainTo(c, Integer.MAX_VALUE);
-  }
-
-  @Override
-  public int drainTo(Collection<? super Runnable> c, int maxElements) {
-    if (c == null) throw new NullPointerException();
-    if (c == this) throw new IllegalArgumentException();
-    if (maxElements <= 0) return 0;
-    lock.lock();
-    try {
-      int added = 0;
-      Node curr = checkStateNotNull(globalHead.nextNode);
-      while (curr != globalTail && added < maxElements) {
-        Node next = checkStateNotNull(curr.nextNode);
-        unlinkNode(curr);
-        Runnable task = curr.task;
-        c.add(task);
-        ++added;
-        curr = next;
-      }
-      return added;
-    } finally {
-      lock.unlock();
-    }
-  }
-
-  @Override
-  public void clear() {
-    lock.lock();
-    try {
-      Node curr = checkStateNotNull(globalHead.nextNode);
-      while (curr != globalTail) {
-        Node next = checkStateNotNull(curr.nextNode);
-        unlinkNode(curr);
-        curr = next;
-      }
-    } finally {
-      lock.unlock();
-    }
-  }
-
-  @Override
-  public Iterator<Runnable> iterator() {
-    lock.lock();
-    try {
-      ImmutableList.Builder<Runnable> builder = 
ImmutableList.builderWithExpectedSize(size);
-      Node curr = checkStateNotNull(globalHead.nextNode);
-      while (curr != globalTail) {
-        builder.add(curr.task);
-        curr = checkStateNotNull(curr.nextNode);
-      }
-      return builder.build().iterator();
-    } finally {
-      lock.unlock();
-    }
-  }
-
-  static final class QueueKey {
-    private final String computationId;
-    private final Work.KeyGroup keyGroup;
-
-    QueueKey(String computationId, Work.KeyGroup keyGroup) {
-      this.computationId = computationId;
-      this.keyGroup = keyGroup;
-    }
-
-    @Override
-    public boolean equals(@Nullable Object o) {
-      if (this == o) {
-        return true;
-      }
-      if (!(o instanceof QueueKey)) {
-        return false;
-      }
-      QueueKey other = (QueueKey) o;
-      return computationId.equals(other.computationId) && 
keyGroup.equals(other.keyGroup);
-    }
-
-    @Override
-    public int hashCode() {
-      return Objects.hash(computationId, keyGroup);
-    }
-
-    @Override
-    public String toString() {
-      return "QueueKey{"
-          + "computationId='"
-          + computationId
-          + '\''
-          + ", keyGroup="
-          + keyGroup
-          + '}';
-    }
-  }
-}
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 b679ca40a95..f3371b54673 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
@@ -2405,10 +2405,10 @@ public class StreamingDataflowWorkerTest {
 
     // Wait until the worker has given up.
     int maxTries = 100;
-    while (maxTries-- > 0 && !worker.workExecutorIsEmpty()) {
+    while (maxTries-- > 0 && !worker.workExecutorIsEmptyForTesting()) {
       Uninterruptibles.sleepUninterruptibly(100, TimeUnit.MILLISECONDS);
     }
-    assertTrue(worker.workExecutorIsEmpty());
+    assertTrue(worker.workExecutorIsEmptyForTesting());
 
     // Spam worker updates a few times.
     maxTries = 10;
@@ -4005,7 +4005,7 @@ public class StreamingDataflowWorkerTest {
                 .setDaemon(true)
                 .build(),
             /* useFairMonitor= */ false,
-            /* useKeyGroupWorkQueue= */ false);
+            /* multiKeyBundleEnabled= */ false);
 
     ComputationState computationState =
         new ComputationState(
@@ -4067,7 +4067,7 @@ public class StreamingDataflowWorkerTest {
                 .setDaemon(true)
                 .build(),
             /* useFairMonitor= */ false,
-            /* useKeyGroupWorkQueue= */ false);
+            /* multiKeyBundleEnabled= */ false);
 
     ComputationState computationState =
         new ComputationState(
@@ -4138,7 +4138,7 @@ public class StreamingDataflowWorkerTest {
                 .setDaemon(true)
                 .build(),
             /* useFairMonitor= */ false,
-            /* useKeyGroupWorkQueue= */ false);
+            /* multiKeyBundleEnabled= */ false);
 
     ComputationState computationState =
         new ComputationState(
@@ -4213,7 +4213,7 @@ public class StreamingDataflowWorkerTest {
                 .setDaemon(true)
                 .build(),
             /* useFairMonitor= */ false,
-            /* useKeyGroupWorkQueue= */ false);
+            /* multiKeyBundleEnabled= */ false);
 
     ComputationState computationState =
         new ComputationState(
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 c1408db5b4a..4446d1ea2cb 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
@@ -23,6 +23,7 @@ import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
@@ -31,6 +32,9 @@ import java.util.Arrays;
 import java.util.Collection;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicIntegerArray;
 import java.util.concurrent.atomic.AtomicReference;
 import java.util.function.BiConsumer;
 import java.util.function.Consumer;
@@ -99,28 +103,10 @@ public class BoundedQueueExecutorTest {
 
   private static ExecutableWork createWorkWithCompIdAndKeyGroupAndWorkToken(
       String computationId, Work.KeyGroup keyGroup, long workToken, 
Consumer<Work> executeWorkFn) {
-    WorkItem workItem =
-        WorkItem.newBuilder()
-            .setKey(ByteString.EMPTY)
-            .setShardingKey(1)
-            .setWorkToken(workToken)
-            .setCacheToken(1)
-            .setKeyGroup(
-                Windmill.Uint128Proto.newBuilder()
-                    .setHigh(keyGroup.high())
-                    .setLow(keyGroup.low())
-                    .build())
-            .build();
-    return ExecutableWork.create(
-        Work.create(
-            workItem,
-            workItem.getSerializedSize(),
-            Watermarks.builder().setInputDataWatermark(Instant.now()).build(),
-            Work.createProcessingContext(
-                computationId, new FakeGetDataClient(), ignored -> {}, 
mock(HeartbeatSender.class)),
-            false,
-            Instant::now,
-            ImmutableList.of()),
+    return createWork(
+        createProcessingContext(computationId),
+        keyGroup,
+        workToken,
         (work, handle) -> executeWorkFn.accept(work));
   }
 
@@ -128,11 +114,25 @@ public class BoundedQueueExecutorTest {
       String computationId,
       Work.KeyGroup keyGroup,
       BiConsumer<Work, BoundedQueueExecutorWorkHandle> executeWorkFn) {
+    return createWork(
+        createProcessingContext(computationId), keyGroup, /* workToken= */ 33, 
executeWorkFn);
+  }
+
+  private static Work.ProcessingContext createProcessingContext(String 
computationId) {
+    return Work.createProcessingContext(
+        computationId, new FakeGetDataClient(), ignored -> {}, 
mock(HeartbeatSender.class));
+  }
+
+  private static ExecutableWork createWork(
+      Work.ProcessingContext processingContext,
+      Work.KeyGroup keyGroup,
+      long workToken,
+      BiConsumer<Work, BoundedQueueExecutorWorkHandle> executeWorkFn) {
     WorkItem workItem =
         WorkItem.newBuilder()
             .setKey(ByteString.EMPTY)
             .setShardingKey(1)
-            .setWorkToken(33)
+            .setWorkToken(workToken)
             .setCacheToken(1)
             .setKeyGroup(
                 Windmill.Uint128Proto.newBuilder()
@@ -145,8 +145,7 @@ public class BoundedQueueExecutorTest {
             workItem,
             workItem.getSerializedSize(),
             Watermarks.builder().setInputDataWatermark(Instant.now()).build(),
-            Work.createProcessingContext(
-                computationId, new FakeGetDataClient(), ignored -> {}, 
mock(HeartbeatSender.class)),
+            processingContext,
             false,
             Instant::now,
             ImmutableList.of()),
@@ -179,7 +178,7 @@ public class BoundedQueueExecutorTest {
                 .setDaemon(true)
                 .build(),
             useFairMonitor,
-            /* useKeyGroupWorkQueue= */ false);
+            /* multiKeyBundleEnabled= */ false);
   }
 
   @Test
@@ -201,13 +200,13 @@ public class BoundedQueueExecutorTest {
     // m1 and m2 have started and all threads are occupied so m3 will be 
queued and not executed.
     executor.execute(m3, 1);
     assertFalse(processStart3.await(1000, TimeUnit.MILLISECONDS));
-    assertFalse(executor.executorQueueIsEmpty());
+    assertFalse(executor.executorQueueIsEmptyForTesting());
 
     // Stop m1 so there is an available thread for m3 to run.
     processStop1.countDown();
     processStart3.await();
     // m3 started.
-    assertTrue(executor.executorQueueIsEmpty());
+    assertTrue(executor.executorQueueIsEmptyForTesting());
     processStop2.countDown();
     processStop3.countDown();
     executor.shutdown();
@@ -236,7 +235,7 @@ public class BoundedQueueExecutorTest {
     assertFalse(processStart2.await(1000, TimeUnit.MILLISECONDS));
     // m2 will wait for monitor instead of being queued.
     assertEquals(Thread.State.WAITING, m2Runner.getState());
-    assertTrue(executor.executorQueueIsEmpty());
+    assertTrue(executor.executorQueueIsEmptyForTesting());
 
     // Stop m1 so there are available bytes for m2 to run.
     processStop1.countDown();
@@ -487,55 +486,23 @@ public class BoundedQueueExecutorTest {
   @Test
   public void testPollWork() throws Exception {
     // Create separate BoundedQueueExecutor with 1 thread so we can block it 
easily
-    BoundedQueueExecutor testExecutor =
-        new BoundedQueueExecutor(
-            1,
-            60,
-            TimeUnit.SECONDS,
-            100,
-            10000000,
-            new 
ThreadFactoryBuilder().setNameFormat("testStealing-%d").setDaemon(true).build(),
-            useFairMonitor,
-            /* useKeyGroupWorkQueue= */ true);
+    BoundedQueueExecutor testExecutor = createMultiKeyBundleExecutor(1, 100);
 
     // 1. Create blocker task to occupy the worker thread
-    CountDownLatch blockerStart = new CountDownLatch(1);
     CountDownLatch blockerStop = new CountDownLatch(1);
-    AtomicReference<BoundedQueueExecutorWorkHandle> blockerHandleRef = new 
AtomicReference<>();
-    ExecutableWork blockerWork =
-        createWorkWithHandle(
-            "blockerComp",
-            DEFAULT_KEY_GROUP,
-            (work, handle) -> {
-              blockerHandleRef.set(handle);
-              blockerStart.countDown();
-              try {
-                blockerStop.await();
-              } catch (InterruptedException e) {
-                throw new RuntimeException(e);
-              }
-            });
-
-    testExecutor.execute(blockerWork, 0);
-    blockerStart.await();
-    BoundedQueueExecutorWorkHandleImpl stealHandle =
-        (BoundedQueueExecutorWorkHandleImpl) blockerHandleRef.get();
-    assertNotNull(stealHandle);
+    BoundedQueueExecutorWorkHandleImpl stealHandle = 
occupyThread(testExecutor, blockerStop);
 
     // 2. Create two distinct key groups
     Work.KeyGroup keyGroup1 = Work.KeyGroup.create(1, 1);
     Work.KeyGroup keyGroup2 = Work.KeyGroup.create(1, 2);
 
     // Create executable tasks
-    CountDownLatch targetStart = new CountDownLatch(1);
-    ExecutableWork work1 = createWorkWithCompIdAndKeyGroup("compA", keyGroup1, 
ignored -> {});
+    AtomicInteger work1Runs = new AtomicInteger();
+    AtomicInteger work2Runs = new AtomicInteger();
+    ExecutableWork work1 =
+        createWorkWithCompIdAndKeyGroup("compA", keyGroup1, ignored -> 
work1Runs.incrementAndGet());
     ExecutableWork work2 =
-        createWorkWithCompIdAndKeyGroup(
-            "compA",
-            keyGroup2,
-            ignored -> {
-              targetStart.countDown();
-            });
+        createWorkWithCompIdAndKeyGroup("compA", keyGroup2, ignored -> 
work2Runs.incrementAndGet());
 
     // Enqueue tasks (they will wait in the queue because the thread is 
blocked)
     testExecutor.execute(work1, 100);
@@ -543,70 +510,66 @@ public class BoundedQueueExecutorTest {
 
     // Total outstanding elements must be 3 (blocker + work1 + work2)
     assertEquals(3, testExecutor.elementsOutstanding());
+    assertFalse(testExecutor.executorQueueIsEmptyForTesting());
 
     // Steal work2 using pollWork with compA and keyGroup2
-    ExecutableWork stolen = testExecutor.pollWork("compA", keyGroup2, 
stealHandle, ignored -> {});
-    assertNotNull(stolen);
+    ExecutableWork stolen =
+        testExecutor.pollWork("compA", keyGroup2, stealHandle, 
FAILING_FAILED_WORK_HANDLER);
     assertEquals(work2, stolen);
 
     // Run the stolen task
     stolen.run(stealHandle);
-    targetStart.await();
+    assertEquals(1, work2Runs.get());
 
     // Steal work1 using pollWork with compA and keyGroup1
     ExecutableWork stolen1 =
         testExecutor.pollWork("compA", keyGroup1, stealHandle, 
FAILING_FAILED_WORK_HANDLER);
-    assertNotNull(stolen1);
     assertEquals(work1, stolen1);
+    stolen1.run(stealHandle);
+    assertEquals(1, work1Runs.get());
 
-    // Unblock the blocker and shut down
+    // All queued work was stolen; only stale shells remain in the executor 
queue
+    assertTrue(testExecutor.keyGroupWorkIndexIsEmpty());
+    assertTrue(testExecutor.executorQueueIsEmptyForTesting());
+    assertEquals(2, testExecutor.rawQueueSizeForTesting());
+    assertEquals(3, testExecutor.elementsOutstanding());
+    assertEquals(250, testExecutor.bytesOutstanding());
+
+    // Closing the merged handle releases the budget for the blocker and both 
stolen tasks
+    stealHandle.close();
+    assertEquals(0, testExecutor.elementsOutstanding());
+    assertEquals(0, testExecutor.bytesOutstanding());
+
+    // Unblock the blocker and shut down; dequeued stale shells are no-ops
     blockerStop.countDown();
     testExecutor.shutdown();
+    assertEquals(1, work1Runs.get());
+    assertEquals(1, work2Runs.get());
+    assertEquals(0, testExecutor.rawQueueSizeForTesting());
+    assertEquals(0, testExecutor.elementsOutstanding());
+    assertEquals(0, testExecutor.bytesOutstanding());
   }
 
   @Test
-  public void testPollWorkWithLinkedBlockingQueue() throws Exception {
+  public void testPollWork_multiKeyBundleDisabled() throws Exception {
     BoundedQueueExecutor testExecutor =
         new BoundedQueueExecutor(
             1,
-            60,
+            DEFAULT_THREAD_EXPIRATION_SEC,
             TimeUnit.SECONDS,
             100,
-            10000000,
-            new 
ThreadFactoryBuilder().setNameFormat("testLinkedQueue-%d").setDaemon(true).build(),
+            MAXIMUM_BYTES_OUTSTANDING,
+            new 
ThreadFactoryBuilder().setNameFormat("testDisabled-%d").setDaemon(true).build(),
             useFairMonitor,
-            /* useKeyGroupWorkQueue= */ false);
-
-    CountDownLatch blockerStart = new CountDownLatch(1);
+            /* multiKeyBundleEnabled= */ false);
     CountDownLatch blockerStop = new CountDownLatch(1);
-    AtomicReference<BoundedQueueExecutorWorkHandle> blockerHandleRef = new 
AtomicReference<>();
-    ExecutableWork blockerWork =
-        createWorkWithHandle(
-            "blockerComp",
-            DEFAULT_KEY_GROUP,
-            (work, handle) -> {
-              blockerHandleRef.set(handle);
-              blockerStart.countDown();
-              try {
-                blockerStop.await();
-              } catch (InterruptedException e) {
-                throw new RuntimeException(e);
-              }
-            });
-
-    testExecutor.execute(blockerWork, 0);
-    blockerStart.await();
-    BoundedQueueExecutorWorkHandleImpl stealHandle =
-        (BoundedQueueExecutorWorkHandleImpl) blockerHandleRef.get();
-    assertNotNull(stealHandle);
+    BoundedQueueExecutorWorkHandleImpl stealHandle = 
occupyThread(testExecutor, blockerStop);
 
     Work.KeyGroup keyGroup = Work.KeyGroup.create(1, 1);
     ExecutableWork work = createWorkWithCompIdAndKeyGroup("compA", keyGroup, 
ignored -> {});
     testExecutor.execute(work, 100);
 
-    ExecutableWork stolen =
-        testExecutor.pollWork("compA", keyGroup, stealHandle, 
FAILING_FAILED_WORK_HANDLER);
-    assertNull(stolen);
+    assertNull(testExecutor.pollWork("compA", keyGroup, stealHandle, 
FAILING_FAILED_WORK_HANDLER));
 
     blockerStop.countDown();
     testExecutor.shutdown();
@@ -614,39 +577,10 @@ public class BoundedQueueExecutorTest {
 
   @Test
   public void testPollWork_skipsFailedWorkAndCallsOnFailedWorkHandler() throws 
Exception {
-    BoundedQueueExecutor testExecutor =
-        new BoundedQueueExecutor(
-            1,
-            60,
-            TimeUnit.SECONDS,
-            100,
-            10000000,
-            new 
ThreadFactoryBuilder().setNameFormat("testPollWork-%d").setDaemon(true).build(),
-            useFairMonitor,
-            /* useKeyGroupWorkQueue= */ true);
-
-    CountDownLatch blockerStart = new CountDownLatch(1);
+    BoundedQueueExecutor testExecutor = createMultiKeyBundleExecutor(1, 100);
     CountDownLatch blockerStop = new CountDownLatch(1);
-    AtomicReference<BoundedQueueExecutorWorkHandle> blockerHandleRef = new 
AtomicReference<>();
-    ExecutableWork blockerWork =
-        createWorkWithHandle(
-            "compA",
-            DEFAULT_KEY_GROUP,
-            (work, handle) -> {
-              blockerHandleRef.set(handle);
-              blockerStart.countDown();
-              try {
-                blockerStop.await();
-              } catch (InterruptedException e) {
-                throw new RuntimeException(e);
-              }
-            });
-
-    testExecutor.execute(blockerWork, 10);
-    blockerStart.await();
     BoundedQueueExecutorWorkHandleImpl stealHandle =
-        (BoundedQueueExecutorWorkHandleImpl) blockerHandleRef.get();
-    assertNotNull(stealHandle);
+        occupyThread(testExecutor, blockerStop, /* bytes= */ 10);
 
     Work.KeyGroup keyGroup = Work.KeyGroup.create(1, 1);
     FailedWorkHandler onFailedWorkHandler = mock(FailedWorkHandler.class);
@@ -658,7 +592,7 @@ public class BoundedQueueExecutorTest {
     ExecutableWork work3 =
         createWorkWithCompIdAndKeyGroupAndWorkToken("compA", keyGroup, 103, 
ignored -> {});
 
-    // Enqueue both tasks (they will wait in the queue because the thread is 
blocked).
+    // Enqueue tasks (they will wait in the queue because the thread is 
blocked).
     testExecutor.execute(work1, 100);
     testExecutor.execute(work2, 150);
     testExecutor.execute(work3, 200);
@@ -674,9 +608,7 @@ public class BoundedQueueExecutorTest {
     // onFailedWorkHandler callback, and return work2.
     ExecutableWork stolen =
         testExecutor.pollWork("compA", keyGroup, stealHandle, 
onFailedWorkHandler);
-    assertNotNull(stolen);
     assertEquals(work2, stolen);
-
     verify(onFailedWorkHandler).onFailedWork(work1.work());
 
     // Verify stealHandle merged (blockerWork: 10 bytes, work2: 150 bytes).
@@ -693,9 +625,229 @@ public class BoundedQueueExecutorTest {
     assertNull(testExecutor.pollWork("compA", keyGroup, stealHandle, 
onFailedWorkHandler));
 
     blockerStop.countDown();
-    stealHandle.close();
+    testExecutor.shutdown();
+    assertEquals(0, testExecutor.elementsOutstanding());
+    assertEquals(0, testExecutor.bytesOutstanding());
+  }
+
+  @Test
+  public void testPollWork_doesNotStealWorkStartedByExecutor() throws 
Exception {
+    BoundedQueueExecutor testExecutor = createMultiKeyBundleExecutor(1, 1000);
+    Work.KeyGroup keyGroup = Work.KeyGroup.create(1, 1);
+    CountDownLatch release = new CountDownLatch(1);
+    BoundedQueueExecutorWorkHandleImpl handle =
+        occupyThread(testExecutor, "compA", keyGroup, release, /* bytes= */ 
100);
+
+    assertTrue(testExecutor.keyGroupWorkIndexIsEmpty());
+    assertNull(testExecutor.pollWork("compA", keyGroup, handle, 
FAILING_FAILED_WORK_HANDLER));
+
+    release.countDown();
+    testExecutor.shutdown();
+    assertEquals(0, testExecutor.elementsOutstanding());
+    assertEquals(0, testExecutor.bytesOutstanding());
+  }
+
+  @Test
+  public void testPollWork_doesNotStealWorkWithMultiKeyBatchingDisabled() 
throws Exception {
+    BoundedQueueExecutor testExecutor = createMultiKeyBundleExecutor(1, 1000);
+    CountDownLatch releaseBlocker = new CountDownLatch(1);
+    BoundedQueueExecutorWorkHandleImpl stealHandle = 
occupyThread(testExecutor, releaseBlocker);
+
+    Work.KeyGroup keyGroup = Work.KeyGroup.create(1, 1);
+    AtomicBoolean disabledWorkRan = new AtomicBoolean();
+    ExecutableWork disabledWork =
+        createWorkWithCompIdAndKeyGroup("compA", keyGroup, ignored -> 
disabledWorkRan.set(true));
+    disabledWork.work().setMultiKeyBatchingDisabled(true);
+    ExecutableWork enabledWork = createWorkWithCompIdAndKeyGroup("compA", 
keyGroup, ignored -> {});
+    testExecutor.execute(disabledWork, 100);
+    testExecutor.execute(enabledWork, 100);
+
+    assertEquals(
+        enabledWork,
+        testExecutor.pollWork("compA", keyGroup, stealHandle, 
FAILING_FAILED_WORK_HANDLER));
+    assertNull(testExecutor.pollWork("compA", keyGroup, stealHandle, 
FAILING_FAILED_WORK_HANDLER));
+
+    releaseBlocker.countDown();
+    testExecutor.shutdown();
+    assertTrue(disabledWorkRan.get());
+    assertEquals(0, testExecutor.elementsOutstanding());
+  }
+
+  @Test
+  public void testPollWork_purgesStaleShellsToBoundExecutorQueue() throws 
Exception {
+    int maximumElementsOutstanding = 4;
+    BoundedQueueExecutor testExecutor = createMultiKeyBundleExecutor(2, 
maximumElementsOutstanding);
+    CountDownLatch releaseBundles = new CountDownLatch(1);
+    BoundedQueueExecutorWorkHandleImpl bundle1 = occupyThread(testExecutor, 
releaseBundles);
+    BoundedQueueExecutorWorkHandleImpl bundle2 = occupyThread(testExecutor, 
releaseBundles);
+
+    Work.KeyGroup keyGroup = Work.KeyGroup.create(1, 1);
+    int numWorks = 20;
+    for (int i = 0; i < numWorks; i++) {
+      testExecutor.forceExecute(
+          createWorkWithCompIdAndKeyGroup("compA", keyGroup, ignored -> {}), 
10);
+    }
+
+    // Each steal leaves a stale shell in the executor queue. While bundle1 is 
open, its stolen
+    // work is still counted in elementsOutstanding, so there is nothing to 
purge.
+    int numStolenByBundle1 = maximumElementsOutstanding + 2;
+    for (int i = 0; i < numStolenByBundle1; i++) {
+      assertNotNull(testExecutor.pollWork("compA", keyGroup, bundle1, 
FAILING_FAILED_WORK_HANDLER));
+    }
+    // 2 = bundle1 + bundle2.
+    assertEquals(2 + numWorks, testExecutor.elementsOutstanding());
+    assertEquals(numWorks, testExecutor.rawQueueSizeForTesting());
+
+    // Closing bundle1 leaves its 6 stale shells uncounted. The executor queue 
now exceeds
+    // elementsOutstanding (bundle2 + unstarted work) by more than 
maximumElementsOutstanding, so
+    // the next steal purges every stale shell.
+    bundle1.close();
+    int numStolenByBundle2 = 1;
+    assertNotNull(testExecutor.pollWork("compA", keyGroup, bundle2, 
FAILING_FAILED_WORK_HANDLER));
+    // 1 = bundle2.
+    assertEquals(1 + numWorks - numStolenByBundle1, 
testExecutor.elementsOutstanding());
+    assertEquals(
+        numWorks - numStolenByBundle1 - numStolenByBundle2, 
testExecutor.rawQueueSizeForTesting());
+
+    releaseBundles.countDown();
+    testExecutor.shutdown();
+    assertEquals(0, testExecutor.rawQueueSizeForTesting());
+    assertEquals(0, testExecutor.elementsOutstanding());
+    assertEquals(0, testExecutor.bytesOutstanding());
+  }
+
+  @Test
+  public void testConcurrentExecuteAndPollWork_processesEachWorkExactlyOnce() 
throws Exception {
+    int numThreads = 4;
+    int numWorks = 4000;
+    int maxStolenPerBundle = 10;
+    BoundedQueueExecutor testExecutor =
+        createMultiKeyBundleExecutor(numThreads, /* 
maximumElementsOutstanding= */ 8);
+    Work.ProcessingContext processingContext = 
createProcessingContext("compA");
+    ImmutableList<Work.KeyGroup> keyGroups =
+        ImmutableList.of(
+            Work.KeyGroup.create(1, 1),
+            Work.KeyGroup.create(1, 2),
+            Work.KeyGroup.create(2, 1),
+            Work.KeyGroup.create(2, 2));
+
+    AtomicIntegerArray processedCounts = new AtomicIntegerArray(numWorks);
+    AtomicInteger numStolen = new AtomicInteger();
+    CountDownLatch allProcessed = new CountDownLatch(numWorks);
+    BiConsumer<Work, BoundedQueueExecutorWorkHandle> bundleFn =
+        (work, handle) -> {
+          processedCounts.incrementAndGet((int) 
work.getWorkItem().getWorkToken());
+          allProcessed.countDown();
+          for (int i = 0; i < maxStolenPerBundle; i++) {
+            ExecutableWork stolen =
+                testExecutor.pollWork(
+                    work.getComputationId(),
+                    work.getKeyGroup(),
+                    handle,
+                    FAILING_FAILED_WORK_HANDLER);
+            if (stolen == null) {
+              break;
+            }
+            numStolen.incrementAndGet();
+            processedCounts.incrementAndGet((int) 
stolen.work().getWorkItem().getWorkToken());
+            allProcessed.countDown();
+          }
+        };
+
+    // Hold all threads until half of the work is queued so bundles steal from 
a backlog.
+    CountDownLatch releaseBlockers = new CountDownLatch(1);
+    for (int i = 0; i < numThreads; i++) {
+      occupyThread(testExecutor, releaseBlockers);
+    }
+    for (int workToken = 0; workToken < numWorks; workToken++) {
+      if (workToken == numWorks / 2) {
+        releaseBlockers.countDown();
+      }
+      testExecutor.forceExecute(
+          createWork(
+              processingContext, keyGroups.get(workToken % keyGroups.size()), 
workToken, bundleFn),
+          1);
+    }
+    allProcessed.await();
+    testExecutor.shutdown();
+
+    for (int workToken = 0; workToken < numWorks; workToken++) {
+      assertEquals("work token " + workToken, 1, 
processedCounts.get(workToken));
+    }
+    assertThat(numStolen.get(), greaterThan(0));
+    assertEquals(0, testExecutor.rawQueueSizeForTesting());
+    assertTrue(testExecutor.keyGroupWorkIndexIsEmpty());
     assertEquals(0, testExecutor.elementsOutstanding());
     assertEquals(0, testExecutor.bytesOutstanding());
+  }
+
+  @Test
+  public void testExecuteAfterShutdown_rejectedWorkIsNotStealable() throws 
Exception {
+    BoundedQueueExecutor testExecutor = createMultiKeyBundleExecutor(1, 1000);
     testExecutor.shutdown();
+
+    ExecutableWork work =
+        createWorkWithCompIdAndKeyGroup(
+            "compA", Work.KeyGroup.create(1, 1), ignored -> Assert.fail());
+    assertThrows(RuntimeException.class, () -> testExecutor.execute(work, 
100));
+
+    assertTrue(testExecutor.keyGroupWorkIndexIsEmpty());
+    assertEquals(0, testExecutor.elementsOutstanding());
+    assertEquals(0, testExecutor.bytesOutstanding());
+  }
+
+  private BoundedQueueExecutor createMultiKeyBundleExecutor(
+      int maxThreads, int maximumElementsOutstanding) {
+    return new BoundedQueueExecutor(
+        maxThreads,
+        DEFAULT_THREAD_EXPIRATION_SEC,
+        TimeUnit.SECONDS,
+        maximumElementsOutstanding,
+        MAXIMUM_BYTES_OUTSTANDING,
+        new 
ThreadFactoryBuilder().setNameFormat("testMultiKeyBundle-%d").setDaemon(true).build(),
+        useFairMonitor,
+        /* multiKeyBundleEnabled= */ true);
+  }
+
+  private static BoundedQueueExecutorWorkHandleImpl occupyThread(
+      BoundedQueueExecutor testExecutor, CountDownLatch release) throws 
InterruptedException {
+    return occupyThread(testExecutor, release, /* bytes= */ 0);
+  }
+
+  private static BoundedQueueExecutorWorkHandleImpl occupyThread(
+      BoundedQueueExecutor testExecutor, CountDownLatch release, long bytes)
+      throws InterruptedException {
+    return occupyThread(testExecutor, "blockerComp", Work.KeyGroup.DEFAULT, 
release, bytes);
+  }
+
+  private static BoundedQueueExecutorWorkHandleImpl occupyThread(
+      BoundedQueueExecutor testExecutor,
+      String computationId,
+      Work.KeyGroup keyGroup,
+      CountDownLatch release,
+      long bytes)
+      throws InterruptedException {
+    CountDownLatch started = new CountDownLatch(1);
+    AtomicReference<BoundedQueueExecutorWorkHandle> handle = new 
AtomicReference<>();
+    testExecutor.execute(
+        createWorkWithHandle(
+            computationId,
+            keyGroup,
+            (work, workHandle) -> {
+              handle.set(workHandle);
+              started.countDown();
+              awaitUninterruptibly(release);
+            }),
+        bytes);
+    started.await();
+    return (BoundedQueueExecutorWorkHandleImpl) handle.get();
+  }
+
+  private static void awaitUninterruptibly(CountDownLatch latch) {
+    try {
+      latch.await();
+    } catch (InterruptedException e) {
+      throw new RuntimeException(e);
+    }
   }
 }
diff --git 
a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/util/KeyGroupWorkIndexTest.java
 
b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/util/KeyGroupWorkIndexTest.java
new file mode 100644
index 00000000000..f41b0da4eec
--- /dev/null
+++ 
b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/util/KeyGroupWorkIndexTest.java
@@ -0,0 +1,319 @@
+/*
+ * 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 static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.ThreadLocalRandom;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicIntegerArray;
+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.QueuedWork;
+import org.apache.beam.runners.dataflow.worker.windmill.Windmill;
+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;
+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.util.concurrent.ThreadFactoryBuilder;
+import org.joda.time.Instant;
+import org.junit.After;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.Timeout;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class KeyGroupWorkIndexTest {
+
+  private static final String COMP_A = "compA";
+  private static final String COMP_B = "compB";
+  private static final Work.KeyGroup KEY_GROUP_1 = Work.KeyGroup.create(1, 1);
+  private static final Work.KeyGroup KEY_GROUP_2 = Work.KeyGroup.create(1, 2);
+
+  @Rule public transient Timeout globalTimeout = Timeout.seconds(300);
+
+  // Creates the budget handles of the QueuedWork created by the tests. Its 
executor is never used.
+  private final BoundedQueueExecutor owner =
+      new BoundedQueueExecutor(
+          1,
+          60,
+          TimeUnit.SECONDS,
+          100,
+          10000000,
+          new ThreadFactoryBuilder()
+              .setNameFormat("KeyGroupWorkIndexTest-%d")
+              .setDaemon(true)
+              .build(),
+          /* useFairMonitor= */ false,
+          /* multiKeyBundleEnabled= */ true);
+  private final KeyGroupWorkIndex index = new KeyGroupWorkIndex();
+  private long nextWorkToken = 0;
+
+  @After
+  public void tearDown() throws InterruptedException {
+    owner.shutdown();
+  }
+
+  private QueuedWork createQueuedWork(String computationId, Work.KeyGroup 
keyGroup) {
+    long workToken = nextWorkToken++;
+    WorkItem workItem =
+        WorkItem.newBuilder()
+            .setKey(ByteString.EMPTY)
+            .setShardingKey(workToken)
+            .setWorkToken(workToken)
+            .setCacheToken(1)
+            .setKeyGroup(
+                Windmill.Uint128Proto.newBuilder()
+                    .setHigh(keyGroup.high())
+                    .setLow(keyGroup.low())
+                    .build())
+            .build();
+    Work.ProcessingContext processingContext =
+        Work.createProcessingContext(
+            computationId, new FakeGetDataClient(), ignored -> {}, 
mock(HeartbeatSender.class));
+    ExecutableWork work =
+        ExecutableWork.create(
+            Work.create(
+                workItem,
+                workItem.getSerializedSize(),
+                
Watermarks.builder().setInputDataWatermark(Instant.now()).build(),
+                processingContext,
+                false,
+                Instant::now,
+                ImmutableList.of()),
+            (w, handle) -> {});
+    QueuedWork queuedWork =
+        new QueuedWork(work, owner.createBudgetHandle(work.work(), /* bytes= 
*/ 0), index);
+    assertTrue(queuedWork.isStealable());
+    return queuedWork;
+  }
+
+  private static int workToken(QueuedWork queuedWork) {
+    return (int) queuedWork.getWork().work().getWorkItem().getWorkToken();
+  }
+
+  @Test
+  public void testPollAndClaim_returnsWorkInFifoOrder() {
+    QueuedWork work1 = createQueuedWork(COMP_A, KEY_GROUP_1);
+    QueuedWork work2 = createQueuedWork(COMP_A, KEY_GROUP_1);
+    QueuedWork work3 = createQueuedWork(COMP_A, KEY_GROUP_1);
+    index.add(work1);
+    index.add(work2);
+    index.add(work3);
+
+    assertSame(work1, index.pollAndClaim(COMP_A, KEY_GROUP_1));
+    assertSame(work2, index.pollAndClaim(COMP_A, KEY_GROUP_1));
+    assertSame(work3, index.pollAndClaim(COMP_A, KEY_GROUP_1));
+    assertNull(index.pollAndClaim(COMP_A, KEY_GROUP_1));
+    assertTrue(work1.isClaimed());
+    assertTrue(work2.isClaimed());
+    assertTrue(work3.isClaimed());
+    assertTrue(index.isEmpty());
+  }
+
+  @Test
+  public void testPollAndClaim_isolatesComputationsAndKeyGroups() {
+    QueuedWork workA1 = createQueuedWork(COMP_A, KEY_GROUP_1);
+    QueuedWork workA2 = createQueuedWork(COMP_A, KEY_GROUP_2);
+    QueuedWork workB1 = createQueuedWork(COMP_B, KEY_GROUP_1);
+    index.add(workA1);
+    index.add(workA2);
+    index.add(workB1);
+
+    assertNull(index.pollAndClaim(COMP_A, Work.KeyGroup.create(3, 4)));
+    assertNull(index.pollAndClaim(COMP_B, KEY_GROUP_2));
+    assertSame(workA2, index.pollAndClaim(COMP_A, KEY_GROUP_2));
+    assertNull(index.pollAndClaim(COMP_A, KEY_GROUP_2));
+    assertSame(workB1, index.pollAndClaim(COMP_B, KEY_GROUP_1));
+    assertNull(index.pollAndClaim(COMP_B, KEY_GROUP_1));
+    assertSame(workA1, index.pollAndClaim(COMP_A, KEY_GROUP_1));
+    assertNull(index.pollAndClaim(COMP_A, KEY_GROUP_1));
+    assertTrue(index.isEmpty());
+  }
+
+  @Test
+  public void testPollAndClaim_skipsWorkClaimedByExecutor() {
+    QueuedWork work1 = createQueuedWork(COMP_A, KEY_GROUP_1);
+    QueuedWork work2 = createQueuedWork(COMP_A, KEY_GROUP_1);
+    index.add(work1);
+    index.add(work2);
+
+    assertTrue(work1.tryClaimForExecution());
+
+    assertSame(work2, index.pollAndClaim(COMP_A, KEY_GROUP_1));
+    assertNull(index.pollAndClaim(COMP_A, KEY_GROUP_1));
+    assertTrue(index.isEmpty());
+    index.drainClaimed(work1);
+    assertTrue(index.isEmpty());
+
+    QueuedWork work3 = createQueuedWork(COMP_A, KEY_GROUP_1);
+    QueuedWork work4 = createQueuedWork(COMP_A, KEY_GROUP_1);
+    index.add(work3);
+    index.add(work4);
+
+    assertTrue(work4.tryClaimForExecution());
+    index.drainClaimed(work4);
+    assertFalse(index.isEmpty());
+
+    assertSame(work3, index.pollAndClaim(COMP_A, KEY_GROUP_1));
+    assertTrue(index.isEmpty());
+  }
+
+  @Test
+  public void testAdd_skipsClaimedWork() {
+    QueuedWork work = createQueuedWork(COMP_A, KEY_GROUP_1);
+    assertTrue(work.tryClaimForExecution());
+
+    index.add(work);
+
+    assertTrue(index.isEmpty());
+    assertNull(index.pollAndClaim(COMP_A, KEY_GROUP_1));
+  }
+
+  @Test
+  public void testDrainClaimed_drainsFromHead() {
+    QueuedWork work1 = createQueuedWork(COMP_A, KEY_GROUP_1);
+    QueuedWork work2 = createQueuedWork(COMP_A, KEY_GROUP_1);
+    index.add(work1);
+    index.add(work2);
+
+    assertTrue(work2.tryClaimForExecution());
+    index.drainClaimed(work2);
+    assertFalse(index.isEmpty());
+    assertTrue(work1.tryClaimForExecution());
+    index.drainClaimed(work1);
+    assertTrue(index.isEmpty());
+
+    QueuedWork work3 = createQueuedWork(COMP_A, KEY_GROUP_1);
+    index.add(work3);
+    assertSame(work3, index.pollAndClaim(COMP_A, KEY_GROUP_1));
+    assertTrue(index.isEmpty());
+  }
+
+  @Test
+  public void 
testConcurrentAddDrainAndPollAndClaim_claimsEachWorkExactlyOnce() throws 
Exception {
+    int numProducers = 2;
+    int numExecutors = 2;
+    int numStealers = 2;
+    int numWorksPerProducer = 10000;
+    ImmutableList<String> computations = ImmutableList.of(COMP_A, COMP_B);
+    ImmutableList<Work.KeyGroup> keyGroups =
+        ImmutableList.of(KEY_GROUP_1, KEY_GROUP_2, Work.KeyGroup.create(2, 1));
+
+    List<List<QueuedWork>> worksByProducer = new ArrayList<>();
+    for (int producer = 0; producer < numProducers; producer++) {
+      List<QueuedWork> works = new ArrayList<>();
+      for (int i = 0; i < numWorksPerProducer; i++) {
+        works.add(
+            createQueuedWork(
+                computations.get(i % computations.size()), keyGroups.get(i % 
keyGroups.size())));
+      }
+      worksByProducer.add(works);
+    }
+    int numWorks = numProducers * numWorksPerProducer;
+    // Simulates the executor queue: work is queued before it is indexed.
+    ConcurrentLinkedQueue<QueuedWork> executorQueue = new 
ConcurrentLinkedQueue<>();
+    AtomicIntegerArray executedCounts = new AtomicIntegerArray(numWorks);
+    AtomicIntegerArray stolenCounts = new AtomicIntegerArray(numWorks);
+    AtomicBoolean producersDone = new AtomicBoolean(false);
+    AtomicBoolean executorsDone = new AtomicBoolean(false);
+
+    ExecutorService threads =
+        Executors.newFixedThreadPool(numProducers + numExecutors + 
numStealers);
+    List<Future<?>> producerFutures = new ArrayList<>();
+    for (List<QueuedWork> works : worksByProducer) {
+      producerFutures.add(
+          threads.submit(
+              () -> {
+                for (QueuedWork work : works) {
+                  executorQueue.add(work);
+                  index.add(work);
+                }
+              }));
+    }
+    List<Future<?>> executorFutures = new ArrayList<>();
+    for (int i = 0; i < numExecutors; i++) {
+      executorFutures.add(
+          threads.submit(
+              () -> {
+                while (!producersDone.get() || !executorQueue.isEmpty()) {
+                  QueuedWork work = executorQueue.poll();
+                  if (work != null && work.tryClaimForExecution()) {
+                    index.drainClaimed(work);
+                    executedCounts.incrementAndGet(workToken(work));
+                  }
+                }
+              }));
+    }
+    List<Future<?>> stealerFutures = new ArrayList<>();
+    for (int i = 0; i < numStealers; i++) {
+      stealerFutures.add(
+          threads.submit(
+              () -> {
+                while (!executorsDone.get()) {
+                  ThreadLocalRandom random = ThreadLocalRandom.current();
+                  QueuedWork work =
+                      index.pollAndClaim(
+                          
computations.get(random.nextInt(computations.size())),
+                          keyGroups.get(random.nextInt(keyGroups.size())));
+                  if (work != null) {
+                    stolenCounts.incrementAndGet(workToken(work));
+                  }
+                }
+              }));
+    }
+
+    for (Future<?> future : producerFutures) {
+      future.get();
+    }
+    producersDone.set(true);
+    for (Future<?> future : executorFutures) {
+      future.get();
+    }
+    executorsDone.set(true);
+    for (Future<?> future : stealerFutures) {
+      future.get();
+    }
+    threads.shutdown();
+    assertTrue(threads.awaitTermination(30, TimeUnit.SECONDS));
+
+    for (int workToken = 0; workToken < numWorks; workToken++) {
+      assertEquals(
+          "work token " + workToken,
+          1,
+          executedCounts.get(workToken) + stolenCounts.get(workToken));
+    }
+    assertTrue(index.isEmpty());
+  }
+}
diff --git 
a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/util/KeyGroupWorkQueueTest.java
 
b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/util/KeyGroupWorkQueueTest.java
deleted file mode 100644
index a58851cf370..00000000000
--- 
a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/util/KeyGroupWorkQueueTest.java
+++ /dev/null
@@ -1,523 +0,0 @@
-/*
- * 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 static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
-import static org.mockito.Mockito.mock;
-
-import java.lang.Thread.State;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Iterator;
-import java.util.List;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.ExecutionException;
-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.AtomicReference;
-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.QueuedWork;
-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;
-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.util.concurrent.ThreadFactoryBuilder;
-import org.checkerframework.checker.nullness.qual.Nullable;
-import org.joda.time.Instant;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
-import org.junit.runners.Parameterized.Parameters;
-
-@RunWith(Parameterized.class)
-public class KeyGroupWorkQueueTest {
-
-  @Parameters(name = "fairQueue={0}")
-  public static Iterable<Object[]> data() {
-    return Arrays.asList(new Object[][] {{true}, {false}});
-  }
-
-  @Parameterized.Parameter public boolean fairQueue;
-  private BoundedQueueExecutor executor;
-
-  @Before
-  public void setUp() {
-    executor =
-        new BoundedQueueExecutor(
-            2,
-            60,
-            TimeUnit.SECONDS,
-            100,
-            10000000,
-            new 
ThreadFactoryBuilder().setNameFormat("Test-%d").setDaemon(true).build(),
-            fairQueue,
-            /* useKeyGroupWorkQueue= */ true);
-  }
-
-  private static final Work.KeyGroup TEST_KEY_GROUP = Work.KeyGroup.create(1, 
2);
-
-  private QueuedWork createQueuedWork(String computationId, long workBytes) {
-    return createQueuedWork(computationId, TEST_KEY_GROUP, workBytes);
-  }
-
-  private QueuedWork createQueuedWork(
-      String computationId, Work.@Nullable KeyGroup keyGroup, long workBytes) {
-    WorkItem.Builder workItemBuilder =
-        WorkItem.newBuilder()
-            .setKey(ByteString.EMPTY)
-            .setShardingKey(1)
-            .setWorkToken(33)
-            .setCacheToken(1);
-    if (keyGroup != null) {
-      workItemBuilder.setKeyGroup(
-          
org.apache.beam.runners.dataflow.worker.windmill.Windmill.Uint128Proto.newBuilder()
-              .setHigh(keyGroup.high())
-              .setLow(keyGroup.low())
-              .build());
-    }
-    WorkItem workItem = workItemBuilder.build();
-    ExecutableWork work =
-        ExecutableWork.create(
-            Work.create(
-                workItem,
-                workItem.getSerializedSize(),
-                
Watermarks.builder().setInputDataWatermark(Instant.now()).build(),
-                Work.createProcessingContext(
-                    computationId,
-                    new FakeGetDataClient(),
-                    ignored -> {},
-                    mock(HeartbeatSender.class)),
-                false,
-                Instant::now,
-                ImmutableList.of()),
-            (w, h) -> {});
-    return new QueuedWork(work, executor.createBudgetHandle(work.work(), 
workBytes));
-  }
-
-  private static class NoOpRunnable implements Runnable {
-    final String id;
-
-    NoOpRunnable(String id) {
-      this.id = id;
-    }
-
-    @Override
-    public void run() {}
-
-    @Override
-    public String toString() {
-      return "NoOpRunnable(" + id + ")";
-    }
-  }
-
-  @Test
-  public void testBasicOfferAndPoll() {
-    KeyGroupWorkQueue queue = new KeyGroupWorkQueue(fairQueue);
-    assertTrue(queue.isEmpty());
-    assertEquals(0, queue.size());
-
-    NoOpRunnable task1 = new NoOpRunnable("1");
-    NoOpRunnable task2 = new NoOpRunnable("2");
-
-    assertTrue(queue.offer(task1));
-    assertTrue(queue.offer(task2));
-    assertEquals(2, queue.size());
-
-    assertEquals(task1, queue.poll());
-    assertEquals(task2, queue.poll());
-    assertNull(queue.poll());
-    assertTrue(queue.isEmpty());
-  }
-
-  @Test
-  public void testRemove() {
-    KeyGroupWorkQueue queue = new KeyGroupWorkQueue(fairQueue);
-    NoOpRunnable task1 = new NoOpRunnable("1");
-    NoOpRunnable task2 = new NoOpRunnable("2");
-
-    queue.offer(task1);
-    queue.offer(task2);
-
-    assertTrue(queue.remove(task1));
-    assertEquals(1, queue.size());
-    assertEquals(task2, queue.poll());
-    assertFalse(queue.remove(task1)); // Already gone
-  }
-
-  @Test
-  public void testDrainTo() {
-    KeyGroupWorkQueue queue = new KeyGroupWorkQueue(fairQueue);
-    NoOpRunnable task1 = new NoOpRunnable("1");
-    NoOpRunnable task2 = new NoOpRunnable("2");
-    queue.offer(task1);
-    queue.offer(task2);
-
-    List<Runnable> drained = new ArrayList<>();
-    assertEquals(2, queue.drainTo(drained));
-    assertEquals(2, drained.size());
-    assertEquals(task1, drained.get(0));
-    assertEquals(task2, drained.get(1));
-    assertTrue(queue.isEmpty());
-  }
-
-  @Test
-  public void testIteratorSafeTraversalAndImmutable() {
-    KeyGroupWorkQueue queue = new KeyGroupWorkQueue(fairQueue);
-    NoOpRunnable task1 = new NoOpRunnable("1");
-    NoOpRunnable task2 = new NoOpRunnable("2");
-    queue.offer(task1);
-    queue.offer(task2);
-
-    Iterator<Runnable> it = queue.iterator();
-    assertTrue(it.hasNext());
-    assertEquals(task1, it.next());
-    assertTrue(it.hasNext());
-    assertEquals(task2, it.next());
-    assertFalse(it.hasNext());
-
-    // Assert that mutating the iterator throws UnsupportedOperationException
-    it = queue.iterator();
-    assertTrue(it.hasNext());
-    it.next();
-    try {
-      it.remove();
-      fail("Iterator must be immutable");
-    } catch (UnsupportedOperationException e) {
-      // Expected
-    }
-  }
-
-  @Test
-  public void testPollWorkTargeted() {
-    KeyGroupWorkQueue queue = new KeyGroupWorkQueue(fairQueue);
-
-    QueuedWork workA1 = createQueuedWork("compA", 100);
-    QueuedWork workB1 = createQueuedWork("compB", 200);
-    QueuedWork workA2 = createQueuedWork("compA", 150);
-
-    queue.offer(workA1);
-    queue.offer(workB1);
-    queue.offer(workA2);
-
-    assertEquals(3, queue.size());
-    assertFalse(queue.isEmpty());
-
-    // Targeted poll A
-    QueuedWork polledA1 = queue.pollWork("compA", TEST_KEY_GROUP);
-    assertNotNull(polledA1);
-    assertEquals("compA", polledA1.getWork().getComputationId());
-    assertEquals(100, polledA1.getHandle().bytes());
-    assertEquals(2, queue.size());
-    assertFalse(queue.isEmpty());
-
-    // Poll next should be B1 (since A1 was stolen, B1 is now first global)
-    assertEquals(workB1, queue.poll());
-    assertEquals(1, queue.size());
-    assertFalse(queue.isEmpty());
-
-    // Last should be A2
-    assertEquals(workA2, queue.poll());
-    assertEquals(0, queue.size());
-    assertTrue(queue.isEmpty());
-
-    assertEquals(null, queue.poll());
-  }
-
-  @Test
-  public void testConcurrentStress() throws InterruptedException, 
ExecutionException {
-    KeyGroupWorkQueue queue = new KeyGroupWorkQueue(fairQueue);
-    int producerThreads = 4;
-    int consumerThreads = 4;
-    int tasksPerProducer = 1000;
-    int totalTasks = producerThreads * tasksPerProducer;
-    Runnable poisonPill =
-        new Runnable() {
-          @Override
-          public void run() {}
-
-          @Override
-          public String toString() {
-            return "POISON_PILL";
-          }
-        };
-
-    ExecutorService executorService =
-        Executors.newFixedThreadPool(producerThreads + consumerThreads);
-    CountDownLatch startLatch = new CountDownLatch(1);
-    CountDownLatch producersDoneLatch = new CountDownLatch(producerThreads);
-    CountDownLatch consumersDoneLatch = new CountDownLatch(consumerThreads);
-    CountDownLatch consumedLatch = new CountDownLatch(totalTasks);
-    List<Future<?>> futures = new ArrayList<>();
-
-    // Start consumers
-    for (int i = 0; i < consumerThreads; i++) {
-      int consumerId = i;
-      futures.add(
-          executorService.submit(
-              () -> {
-                try {
-                  startLatch.await();
-                  int iteration = consumerId % 4;
-                  while (true) {
-                    int strategy = iteration;
-                    iteration = (iteration + 1) % 4;
-                    Runnable task = null;
-                    if (strategy == 0) {
-                      String compId = "comp-" + (consumedLatch.getCount() % 5);
-                      task = queue.pollWork(compId, TEST_KEY_GROUP);
-                    } else if (strategy == 1) {
-                      task = queue.poll();
-                    } else if (strategy == 2) {
-                      task = queue.poll(10, TimeUnit.MICROSECONDS);
-                    } else if (strategy == 3) {
-                      task = queue.take();
-                    }
-
-                    if (task == poisonPill) {
-                      break;
-                    }
-                    if (task != null) {
-                      consumedLatch.countDown();
-                    }
-                  }
-                } catch (Exception e) {
-                  throw new RuntimeException(e);
-                } finally {
-                  consumersDoneLatch.countDown();
-                }
-              }));
-    }
-    // Start producers
-    for (int i = 0; i < producerThreads; i++) {
-      futures.add(
-          executorService.submit(
-              () -> {
-                try {
-                  startLatch.await();
-                  for (int j = 0; j < tasksPerProducer; j++) {
-                    String compId = "comp-" + (j % 5);
-                    queue.offer(createQueuedWork(compId, 10));
-                  }
-                } catch (Exception e) {
-                  throw new RuntimeException(e);
-                } finally {
-                  producersDoneLatch.countDown();
-                }
-              }));
-    }
-
-    // Release the start latch to start the test
-    startLatch.countDown();
-
-    // Wait for all tasks to be consumed
-    assertTrue(consumedLatch.await(30, TimeUnit.SECONDS));
-
-    // Send poison pills to stop all consumers
-    for (int i = 0; i < consumerThreads; i++) {
-      queue.offer(poisonPill);
-    }
-
-    // Wait for consumers to finish
-    assertTrue(consumersDoneLatch.await(30, TimeUnit.SECONDS));
-    // Wait for producers to finish
-    assertTrue(producersDoneLatch.await(30, TimeUnit.SECONDS));
-
-    // Check for exceptions in threads
-    for (Future<?> future : futures) {
-      future.get();
-    }
-
-    executorService.shutdown();
-    assertTrue(executorService.awaitTermination(30, TimeUnit.SECONDS));
-
-    assertEquals(0, queue.size());
-    assertTrue(queue.isEmpty());
-  }
-
-  @Test
-  public void testTakeBlocksAndWakesUp() throws InterruptedException {
-    final KeyGroupWorkQueue queue = new KeyGroupWorkQueue(fairQueue);
-    final NoOpRunnable task = new NoOpRunnable("take-task");
-    final AtomicReference<Runnable> result = new AtomicReference<>();
-    final CountDownLatch started = new CountDownLatch(1);
-    final CountDownLatch finished = new CountDownLatch(1);
-
-    Thread t =
-        new Thread(
-            () -> {
-              started.countDown();
-              try {
-                result.set(queue.take());
-              } catch (InterruptedException e) {
-                // Ignore
-              } finally {
-                finished.countDown();
-              }
-            });
-    t.setDaemon(true);
-    t.start();
-
-    assertTrue(started.await(30, TimeUnit.SECONDS));
-    waitForThreadState(t, State.WAITING);
-
-    queue.offer(task);
-
-    assertTrue(finished.await(30, TimeUnit.SECONDS));
-    assertEquals(task, result.get());
-  }
-
-  @Test
-  public void testPollWithTimeout() throws InterruptedException {
-    final KeyGroupWorkQueue queue = new KeyGroupWorkQueue(fairQueue);
-    final NoOpRunnable task = new NoOpRunnable("poll-task");
-    final AtomicReference<Runnable> result = new AtomicReference<>();
-    final CountDownLatch started = new CountDownLatch(1);
-    final CountDownLatch finished = new CountDownLatch(1);
-
-    // 1. Verify timeout returns null
-    Thread t1 =
-        new Thread(
-            () -> {
-              started.countDown();
-              try {
-                result.set(queue.poll(500, TimeUnit.MILLISECONDS));
-              } catch (InterruptedException e) {
-                // Ignore
-              } finally {
-                finished.countDown();
-              }
-            });
-    t1.setDaemon(true);
-    t1.start();
-
-    assertTrue(started.await(30, TimeUnit.SECONDS));
-    waitForThreadState(t1, State.TIMED_WAITING);
-
-    assertTrue(finished.await(30, TimeUnit.SECONDS));
-    assertNull(result.get());
-
-    // 2. Verify timed poll receives task offered concurrently
-    final CountDownLatch started2 = new CountDownLatch(1);
-    final CountDownLatch finished2 = new CountDownLatch(1);
-    final AtomicReference<Runnable> result2 = new AtomicReference<>();
-
-    Thread t2 =
-        new Thread(
-            () -> {
-              started2.countDown();
-              try {
-                result2.set(queue.poll(2, TimeUnit.SECONDS));
-              } catch (InterruptedException e) {
-                // Ignore
-              } finally {
-                finished2.countDown();
-              }
-            });
-    t2.setDaemon(true);
-    t2.start();
-
-    assertTrue(started2.await(30, TimeUnit.SECONDS));
-    waitForThreadState(t2, State.TIMED_WAITING);
-
-    queue.offer(task);
-
-    assertTrue(finished2.await(30, TimeUnit.SECONDS));
-    assertEquals(task, result2.get());
-  }
-
-  @Test
-  public void testPollWorkWithKeyGroup() {
-    KeyGroupWorkQueue queue = new KeyGroupWorkQueue(fairQueue);
-
-    Work.KeyGroup keyGroup1 = Work.KeyGroup.create(1, 1);
-    Work.KeyGroup keyGroup2 = Work.KeyGroup.create(1, 2);
-    Work.KeyGroup keyGroupNotExist = Work.KeyGroup.create(3, 4);
-
-    QueuedWork workA1 = createQueuedWork("compA", keyGroup1, 100);
-    QueuedWork workA2 = createQueuedWork("compA", keyGroup2, 150);
-
-    queue.offer(workA1);
-    queue.offer(workA2);
-
-    assertEquals(2, queue.size());
-
-    QueuedWork polledNotExist = queue.pollWork("compA", keyGroupNotExist);
-    assertNull(polledNotExist);
-    assertEquals(2, queue.size());
-    // Poll with keyGroup2 first - should return workA2
-    QueuedWork polledA2 = queue.pollWork("compA", keyGroup2);
-    assertNotNull(polledA2);
-    assertEquals(workA2, polledA2);
-    assertEquals(1, queue.size());
-
-    // Poll with keyGroup2 again - should return null
-    assertNull(queue.pollWork("compA", keyGroup2));
-
-    // Poll with keyGroup1 - should return workA1
-    QueuedWork polledA1 = queue.pollWork("compA", keyGroup1);
-    assertNotNull(polledA1);
-    assertEquals(workA1, polledA1);
-    assertTrue(queue.isEmpty());
-    polledNotExist = queue.pollWork("compA", keyGroupNotExist);
-    assertNull(polledNotExist);
-    assertTrue(queue.isEmpty());
-  }
-
-  @Test
-  public void testOffer_multiKeyBatchingDisabled_notInsertedInKeyGroupQueue() {
-    KeyGroupWorkQueue queue = new KeyGroupWorkQueue(fairQueue);
-    QueuedWork workDisabled = createQueuedWork("compA", 100);
-    workDisabled.getWork().work().setMultiKeyBatchingDisabled(true);
-    QueuedWork workEnabled = createQueuedWork("compA", 200);
-
-    queue.offer(workDisabled);
-    queue.offer(workEnabled);
-    assertEquals(2, queue.size());
-
-    QueuedWork polledWork = queue.pollWork("compA", TEST_KEY_GROUP);
-    assertNotNull(polledWork);
-    assertEquals(workEnabled, polledWork);
-    assertEquals(1, queue.size());
-
-    assertNull(queue.pollWork("compA", TEST_KEY_GROUP));
-    assertEquals(workDisabled, queue.poll());
-    assertTrue(queue.isEmpty());
-  }
-
-  private void waitForThreadState(Thread t, State state) throws 
InterruptedException {
-    long timeoutMs = 30000;
-    long start = System.currentTimeMillis();
-    while (t.getState() != state) {
-      if (System.currentTimeMillis() - start > timeoutMs) {
-        fail("Thread did not reach " + state + " state within " + timeoutMs + 
"ms");
-      }
-      Thread.sleep(1);
-    }
-  }
-}
diff --git 
a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/windmill/work/processing/StreamingCommitFinalizerTest.java
 
b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/windmill/work/processing/StreamingCommitFinalizerTest.java
index a11fb1f3ad3..688bad933e0 100644
--- 
a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/windmill/work/processing/StreamingCommitFinalizerTest.java
+++ 
b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/windmill/work/processing/StreamingCommitFinalizerTest.java
@@ -63,7 +63,7 @@ public class StreamingCommitFinalizerTest {
                 .setDaemon(true)
                 .build(),
             /* useFairMonitor= */ false,
-            /* useKeyGroupWorkQueue= */ false);
+            /* multiKeyBundleEnabled= */ false);
 
     cleanupExecutor =
         Executors.newScheduledThreadPool(
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 6f440c9cf30..f11add07f6f 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
@@ -68,7 +68,7 @@ public class WorkFailureProcessorTest {
                 .setDaemon(true)
                 .build(),
             /* useFairMonitor= */ false,
-            /* useKeyGroupWorkQueue= */ false);
+            /* multiKeyBundleEnabled= */ false);
 
     return WorkFailureProcessor.forTesting(workExecutor, failureTracker, 
Optional::empty, clock, 0);
   }
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 017a22ca684..8ad5896dfab 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
@@ -75,7 +75,7 @@ public class ActiveWorkRefresherTest {
         10000000,
         new 
ThreadFactoryBuilder().setNameFormat("DataflowWorkUnits-%d").setDaemon(true).build(),
         /* useFairMonitor= */ false,
-        /* useKeyGroupWorkQueue= */ false);
+        /* multiKeyBundleEnabled= */ false);
   }
 
   private static ComputationState createComputationState(int 
computationIdSuffix) {

Reply via email to