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

merlimat pushed a commit to branch branch-4.18
in repository https://gitbox.apache.org/repos/asf/bookkeeper.git


The following commit(s) were added to refs/heads/branch-4.18 by this push:
     new 06e9409b0d Use batch-draining queues in SingleThreadExecutor (#4836)
06e9409b0d is described below

commit 06e9409b0d3b7eca33c6a42c1dfd4a536c24e1e3
Author: Matteo Merli <[email protected]>
AuthorDate: Thu Jul 16 09:25:18 2026 -0700

    Use batch-draining queues in SingleThreadExecutor (#4836)
    
    * Use batch-draining queues in SingleThreadExecutor
    
    * Remove GrowableMpScArrayConsumerBlockingQueue
    
    * Address review comments in GrowableBatchedArrayBlockingQueue
    
    (cherry picked from commit 68cc8dcbd1e8a7e95c68e70d183e178ad6d84ede)
---
 .../GrowableBatchedArrayBlockingQueue.java         | 401 +++++++++++++++++++++
 .../GrowableMpScArrayConsumerBlockingQueue.java    | 331 -----------------
 .../common/util/SingleThreadExecutor.java          |  41 ++-
 .../collections/BatchedArrayBlockingQueueTest.java |   2 +-
 .../GrowableArrayBlockingQueueTest.java            | 273 --------------
 .../GrowableBatchedArrayBlockingQueueTest.java     | 380 +++++++++++++++++++
 .../common/SingleThreadExecutorQueueBenchmark.java | 139 +++++++
 7 files changed, 942 insertions(+), 625 deletions(-)

diff --git 
a/bookkeeper-common/src/main/java/org/apache/bookkeeper/common/collections/GrowableBatchedArrayBlockingQueue.java
 
b/bookkeeper-common/src/main/java/org/apache/bookkeeper/common/collections/GrowableBatchedArrayBlockingQueue.java
new file mode 100644
index 0000000000..2135096002
--- /dev/null
+++ 
b/bookkeeper-common/src/main/java/org/apache/bookkeeper/common/collections/GrowableBatchedArrayBlockingQueue.java
@@ -0,0 +1,401 @@
+/*
+ *
+ * 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.bookkeeper.common.collections;
+
+import java.util.AbstractQueue;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Iterator;
+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 org.apache.bookkeeper.common.util.MathUtils;
+
+/**
+ * This implements a {@link BlockingQueue} backed by an array with no fixed 
capacity.
+ *
+ * <p>It is the unbounded companion of {@link BatchedArrayBlockingQueue}: when 
the backing array
+ * is full it is doubled in size (and never shrunk), so producers never block.
+ *
+ * <p>This queue only allows 1 consumer thread to dequeue items and multiple 
producer threads.
+ */
+public class GrowableBatchedArrayBlockingQueue<T>
+        extends AbstractQueue<T>
+        implements BlockingQueue<T>, BatchedBlockingQueue<T> {
+
+    private final ReentrantLock lock = new ReentrantLock();
+
+    private final Condition notEmpty = lock.newCondition();
+
+    private T[] data;
+
+    private int size;
+
+    private int consumerIdx;
+    private int producerIdx;
+
+    public GrowableBatchedArrayBlockingQueue() {
+        this(64);
+    }
+
+    @SuppressWarnings("unchecked")
+    public GrowableBatchedArrayBlockingQueue(int initialCapacity) {
+        int capacity = MathUtils.findNextPositivePowerOfTwo(initialCapacity);
+        data = (T[]) new Object[capacity];
+    }
+
+    private T dequeueOne() {
+        T item = data[consumerIdx];
+        data[consumerIdx] = null;
+        if (++consumerIdx == data.length) {
+            consumerIdx = 0;
+        }
+
+        --size;
+        return item;
+    }
+
+    private void enqueueOne(T item) {
+        if (size == data.length) {
+            grow(size + 1);
+        }
+
+        data[producerIdx] = item;
+        if (++producerIdx == data.length) {
+            producerIdx = 0;
+        }
+
+        if (size++ == 0) {
+            // There is a single consumer thread, so no need to use signalAll()
+            notEmpty.signal();
+        }
+    }
+
+    // must be called while holding the lock
+    @SuppressWarnings("unchecked")
+    private void grow(int minCapacity) {
+        if (minCapacity < 0) {
+            // The requested capacity overflowed the int range
+            throw new IllegalStateException("Queue capacity would exceed the 
maximum array size");
+        }
+
+        int newCapacity = data.length;
+        while (newCapacity < minCapacity) {
+            newCapacity *= 2;
+            if (newCapacity <= 0) {
+                // Doubling overflowed: fall back to the exact requested 
capacity
+                newCapacity = minCapacity;
+            }
+        }
+
+        T[] newData = (T[]) new Object[newCapacity];
+
+        int firstSpan = Math.min(size, data.length - consumerIdx);
+        System.arraycopy(data, consumerIdx, newData, 0, firstSpan);
+
+        int secondSpan = size - firstSpan;
+        if (secondSpan > 0) {
+            System.arraycopy(data, 0, newData, firstSpan, secondSpan);
+        }
+
+        data = newData;
+        consumerIdx = 0;
+        producerIdx = size;
+    }
+
+    @Override
+    public T poll() {
+        lock.lock();
+
+        try {
+            if (size == 0) {
+                return null;
+            }
+
+            return dequeueOne();
+        } finally {
+            lock.unlock();
+        }
+    }
+
+    @Override
+    public T peek() {
+        lock.lock();
+
+        try {
+            if (size == 0) {
+                return null;
+            }
+
+            return data[consumerIdx];
+        } finally {
+            lock.unlock();
+        }
+    }
+
+    @Override
+    public boolean offer(T e) {
+        Objects.requireNonNull(e);
+
+        lock.lock();
+
+        try {
+            enqueueOne(e);
+
+            return true;
+        } finally {
+            lock.unlock();
+        }
+    }
+
+    @Override
+    public void put(T e) {
+        offer(e);
+    }
+
+    @Override
+    public boolean offer(T e, long timeout, TimeUnit unit) {
+        // Queue is unbounded and it will never reject new items
+        return offer(e);
+    }
+
+    @Override
+    public void putAll(T[] a, int offset, int len) {
+        Objects.requireNonNull(a);
+        Objects.checkFromIndexSize(offset, len, a.length);
+
+        lock.lock();
+
+        try {
+            if (len > data.length - size) {
+                grow(size + len);
+            }
+
+            int capacity = data.length;
+            int producerIdx = this.producerIdx;
+
+            // First span
+            int firstSpan = Math.min(len, capacity - producerIdx);
+            System.arraycopy(a, offset, data, producerIdx, firstSpan);
+            producerIdx += firstSpan;
+
+            int secondSpan = len - firstSpan;
+            if (secondSpan > 0) {
+                System.arraycopy(a, offset + firstSpan, data, 0, secondSpan);
+                producerIdx = secondSpan;
+            }
+
+            if (producerIdx == capacity) {
+                producerIdx = 0;
+            }
+
+            this.producerIdx = producerIdx;
+
+            if (size == 0) {
+                // There is a single consumer thread, so no need to use 
signalAll()
+                notEmpty.signal();
+            }
+
+            size += len;
+        } finally {
+            lock.unlock();
+        }
+    }
+
+    @Override
+    public T take() throws InterruptedException {
+        lock.lockInterruptibly();
+
+        try {
+            while (size == 0) {
+                notEmpty.await();
+            }
+
+            return dequeueOne();
+        } finally {
+            lock.unlock();
+        }
+    }
+
+    @Override
+    public T poll(long timeout, TimeUnit unit) throws InterruptedException {
+        long remainingTimeNanos = unit.toNanos(timeout);
+
+        lock.lockInterruptibly();
+        try {
+            while (size == 0) {
+                if (remainingTimeNanos <= 0L) {
+                    return null;
+                }
+
+                remainingTimeNanos = notEmpty.awaitNanos(remainingTimeNanos);
+            }
+
+            return dequeueOne();
+        } finally {
+            lock.unlock();
+        }
+    }
+
+    @Override
+    public int remainingCapacity() {
+        return Integer.MAX_VALUE;
+    }
+
+    @Override
+    public int drainTo(Collection<? super T> c) {
+        return drainTo(c, Integer.MAX_VALUE);
+    }
+
+    @Override
+    public int drainTo(Collection<? super T> c, int maxElements) {
+        Objects.requireNonNull(c);
+        if (c == this) {
+            throw new IllegalArgumentException("Cannot drain a queue into 
itself");
+        }
+        if (maxElements <= 0) {
+            return 0;
+        }
+
+        lock.lock();
+        try {
+            int toDrain = Math.min(size, maxElements);
+
+            int capacity = data.length;
+            int consumerIdx = this.consumerIdx;
+            int drained = 0;
+
+            try {
+                while (drained < toDrain) {
+                    T item = data[consumerIdx];
+                    c.add(item);
+
+                    // Only clear the slot once the item was accepted by the 
target collection
+                    data[consumerIdx] = null;
+                    if (++consumerIdx == capacity) {
+                        consumerIdx = 0;
+                    }
+                    ++drained;
+                }
+            } finally {
+                // Even if c.add() threw, commit the items that were actually 
transferred
+                this.consumerIdx = consumerIdx;
+                size -= drained;
+            }
+
+            return drained;
+        } finally {
+            lock.unlock();
+        }
+    }
+
+    @Override
+    public int takeAll(T[] array) throws InterruptedException {
+        return internalTakeAll(array, true, 0, TimeUnit.SECONDS);
+    }
+
+    @Override
+    public int pollAll(T[] array, long timeout, TimeUnit unit) throws 
InterruptedException {
+        return internalTakeAll(array, false, timeout, unit);
+    }
+
+    private int internalTakeAll(T[] array, boolean waitForever, long timeout, 
TimeUnit unit)
+            throws InterruptedException {
+        if (array.length == 0) {
+            return 0;
+        }
+
+        long remainingTimeNanos = unit.toNanos(timeout);
+
+        lock.lockInterruptibly();
+        try {
+            while (size == 0) {
+                if (waitForever) {
+                    notEmpty.await();
+                } else {
+                    if (remainingTimeNanos <= 0L) {
+                        return 0;
+                    }
+
+                    remainingTimeNanos = 
notEmpty.awaitNanos(remainingTimeNanos);
+                }
+            }
+
+            int toDrain = Math.min(size, array.length);
+
+            int capacity = data.length;
+            int consumerIdx = this.consumerIdx;
+
+            // First span
+            int firstSpan = Math.min(toDrain, capacity - consumerIdx);
+            System.arraycopy(data, consumerIdx, array, 0, firstSpan);
+            Arrays.fill(data, consumerIdx, consumerIdx + firstSpan, null);
+            consumerIdx += firstSpan;
+
+            int secondSpan = toDrain - firstSpan;
+            if (secondSpan > 0) {
+                System.arraycopy(data, 0, array, firstSpan, secondSpan);
+                Arrays.fill(data, 0, secondSpan, null);
+                consumerIdx = secondSpan;
+            }
+
+            if (consumerIdx == capacity) {
+                consumerIdx = 0;
+            }
+            this.consumerIdx = consumerIdx;
+
+            size -= toDrain;
+            return toDrain;
+        } finally {
+            lock.unlock();
+        }
+    }
+
+    @Override
+    public void clear() {
+        lock.lock();
+        try {
+            while (size > 0) {
+                dequeueOne();
+            }
+        } finally {
+            lock.unlock();
+        }
+    }
+
+    @Override
+    public int size() {
+        lock.lock();
+
+        try {
+            return size;
+        } finally {
+            lock.unlock();
+        }
+    }
+
+    @Override
+    public Iterator<T> iterator() {
+        throw new UnsupportedOperationException();
+    }
+}
diff --git 
a/bookkeeper-common/src/main/java/org/apache/bookkeeper/common/collections/GrowableMpScArrayConsumerBlockingQueue.java
 
b/bookkeeper-common/src/main/java/org/apache/bookkeeper/common/collections/GrowableMpScArrayConsumerBlockingQueue.java
deleted file mode 100644
index 1e614f1820..0000000000
--- 
a/bookkeeper-common/src/main/java/org/apache/bookkeeper/common/collections/GrowableMpScArrayConsumerBlockingQueue.java
+++ /dev/null
@@ -1,331 +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.bookkeeper.common.collections;
-
-import java.util.AbstractQueue;
-import java.util.Collection;
-import java.util.Iterator;
-import java.util.NoSuchElementException;
-import java.util.concurrent.BlockingQueue;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicInteger;
-import java.util.concurrent.locks.LockSupport;
-import java.util.concurrent.locks.StampedLock;
-import org.apache.bookkeeper.common.util.MathUtils;
-
-
-/**
- * This implements a {@link BlockingQueue} backed by an array with no fixed 
capacity.
- *
- * <p>When the capacity is reached, data will be moved to a bigger array.
- *
- * <p>This queue only allows 1 consumer thread to dequeue items and multiple 
producer threads.
- */
-public class GrowableMpScArrayConsumerBlockingQueue<T> extends 
AbstractQueue<T> implements BlockingQueue<T> {
-
-    private final StampedLock headLock = new StampedLock();
-    private final PaddedInt headIndex = new PaddedInt();
-    private final PaddedInt tailIndex = new PaddedInt();
-    private final StampedLock tailLock = new StampedLock();
-
-    private T[] data;
-    private final AtomicInteger size = new AtomicInteger(0);
-
-    private volatile Thread waitingConsumer;
-
-    public GrowableMpScArrayConsumerBlockingQueue() {
-        this(64);
-    }
-
-    @SuppressWarnings("unchecked")
-    public GrowableMpScArrayConsumerBlockingQueue(int initialCapacity) {
-        int capacity = MathUtils.findNextPositivePowerOfTwo(initialCapacity);
-        data = (T[]) new Object[capacity];
-    }
-
-    @Override
-    public T remove() {
-        T item = poll();
-        if (item == null) {
-            throw new NoSuchElementException();
-        }
-
-        return item;
-    }
-
-    @Override
-    public T poll() {
-        if (size.get() > 0) {
-            // Since this is a single-consumer queue, we don't expect multiple 
threads calling poll(), though we need
-            // to protect against array expansions
-            long stamp = headLock.readLock();
-
-            try {
-                T item = data[headIndex.value];
-                data[headIndex.value] = null;
-                headIndex.value = (headIndex.value + 1) & (data.length - 1);
-                size.decrementAndGet();
-                return item;
-            } finally {
-                headLock.unlockRead(stamp);
-            }
-        } else {
-            return null;
-        }
-    }
-
-    @Override
-    public T element() {
-        T item = peek();
-        if (item == null) {
-            throw new NoSuchElementException();
-        }
-
-        return item;
-    }
-
-    @Override
-    public T peek() {
-        if (size.get() > 0) {
-            long stamp = headLock.readLock();
-
-            try {
-                return data[headIndex.value];
-            } finally {
-                headLock.unlockRead(stamp);
-            }
-        } else {
-            return null;
-        }
-    }
-
-    @Override
-    public boolean offer(T e) {
-        // Queue is unbounded and it will never reject new items
-        put(e);
-        return true;
-    }
-
-    @Override
-    public void put(T e) {
-        long stamp = tailLock.writeLock();
-
-        try {
-            int oldSize = size.get();
-            if (oldSize == data.length) {
-                expandArray();
-            }
-
-            data[tailIndex.value] = e;
-            tailIndex.value = (tailIndex.value + 1) & (data.length - 1);
-
-            if (size.getAndIncrement() == 0 && waitingConsumer != null) {
-                Thread waitingConsumer = this.waitingConsumer;
-                this.waitingConsumer = null;
-                LockSupport.unpark(waitingConsumer);
-            }
-        } finally {
-            tailLock.unlockWrite(stamp);
-        }
-    }
-
-    @Override
-    public boolean add(T e) {
-        put(e);
-        return true;
-    }
-
-    @Override
-    public boolean offer(T e, long timeout, TimeUnit unit) {
-        // Queue is unbounded and it will never reject new items
-        put(e);
-        return true;
-    }
-
-    @Override
-    public T take() throws InterruptedException {
-        while (size() == 0) {
-            waitingConsumer = Thread.currentThread();
-
-            // Double check that size has not changed after we have registered 
ourselves for notification
-            if (size() == 0) {
-                LockSupport.park();
-                if (Thread.interrupted()) {
-                    throw new InterruptedException();
-                }
-            }
-        }
-
-        return poll();
-    }
-
-    @Override
-    public T poll(long timeout, TimeUnit unit) throws InterruptedException {
-        long deadline = System.currentTimeMillis() + unit.toMillis(timeout);
-
-        while (size.get() == 0) {
-            waitingConsumer = Thread.currentThread();
-
-            // Double check that size has not changed after we have registered 
ourselves for notification
-            if (size.get() == 0) {
-                LockSupport.parkUntil(deadline);
-                if (Thread.interrupted()) {
-                    throw new InterruptedException();
-                }
-
-                if (System.currentTimeMillis() >= deadline) {
-                    return null;
-                }
-            }
-        }
-
-        return poll();
-    }
-
-    @Override
-    public int remainingCapacity() {
-        return Integer.MAX_VALUE;
-    }
-
-    @Override
-    public int drainTo(Collection<? super T> c) {
-        return drainTo(c, Integer.MAX_VALUE);
-    }
-
-    @Override
-    public int drainTo(Collection<? super T> c, int maxElements) {
-        long stamp = headLock.readLock();
-
-        try {
-            int toDrain = Math.min(size.get(), maxElements);
-
-            for (int i = 0; i < toDrain; i++) {
-                T item = data[headIndex.value];
-                data[headIndex.value] = null;
-                c.add(item);
-
-                headIndex.value = (headIndex.value + 1) & (data.length - 1);
-            }
-
-            this.size.addAndGet(-toDrain);
-            return toDrain;
-        } finally {
-            headLock.unlockRead(stamp);
-        }
-    }
-
-    @Override
-    public void clear() {
-        long stamp = headLock.readLock();
-
-        try {
-            int size = this.size.get();
-
-            for (int i = 0; i < size; i++) {
-                data[headIndex.value] = null;
-                headIndex.value = (headIndex.value + 1) & (data.length - 1);
-            }
-
-            this.size.addAndGet(-size);
-        } finally {
-            headLock.unlockRead(stamp);
-        }
-    }
-
-    @Override
-    public int size() {
-        return size.get();
-    }
-
-    @Override
-    public Iterator<T> iterator() {
-        throw new UnsupportedOperationException();
-    }
-
-    @Override
-    public String toString() {
-        StringBuilder sb = new StringBuilder();
-
-        long tailStamp = tailLock.writeLock();
-        long headStamp = headLock.writeLock();
-
-        try {
-            int headIndex = this.headIndex.value;
-            int size = this.size.get();
-
-            sb.append('[');
-
-            for (int i = 0; i < size; i++) {
-                T item = data[headIndex];
-                if (i > 0) {
-                    sb.append(", ");
-                }
-
-                sb.append(item);
-
-                headIndex = (headIndex + 1) & (data.length - 1);
-            }
-
-            sb.append(']');
-        } finally {
-            headLock.unlockWrite(headStamp);
-            tailLock.unlockWrite(tailStamp);
-        }
-        return sb.toString();
-    }
-
-    @SuppressWarnings("unchecked")
-    private void expandArray() {
-        // We already hold the tailLock
-        long headLockStamp = headLock.writeLock();
-
-        try {
-            int size = this.size.get();
-            int newCapacity = data.length * 2;
-            T[] newData = (T[]) new Object[newCapacity];
-
-
-            int oldHeadIndex = headIndex.value;
-            int lenHeadToEnd = Math.min(size, data.length - oldHeadIndex);
-
-            System.arraycopy(data, oldHeadIndex, newData, 0, lenHeadToEnd);
-            System.arraycopy(data, 0, newData, lenHeadToEnd, size - 
lenHeadToEnd);
-
-            data = newData;
-            headIndex.value = 0;
-            tailIndex.value = size;
-        } finally {
-            headLock.unlockWrite(headLockStamp);
-        }
-    }
-
-    private static final class PaddedInt {
-        int value = 0;
-
-        // Padding to avoid false sharing
-        public volatile int pi1 = 1;
-        public volatile long p1 = 1L, p2 = 2L, p3 = 3L, p4 = 4L, p5 = 5L, p6 = 
6L;
-
-        public long exposeToAvoidOptimization() {
-            return pi1 + p1 + p2 + p3 + p4 + p5 + p6;
-        }
-    }
-}
diff --git 
a/bookkeeper-common/src/main/java/org/apache/bookkeeper/common/util/SingleThreadExecutor.java
 
b/bookkeeper-common/src/main/java/org/apache/bookkeeper/common/util/SingleThreadExecutor.java
index 31cb8aef32..840a4c6dec 100644
--- 
a/bookkeeper-common/src/main/java/org/apache/bookkeeper/common/util/SingleThreadExecutor.java
+++ 
b/bookkeeper-common/src/main/java/org/apache/bookkeeper/common/util/SingleThreadExecutor.java
@@ -23,8 +23,6 @@ import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.concurrent.AbstractExecutorService;
-import java.util.concurrent.ArrayBlockingQueue;
-import java.util.concurrent.BlockingQueue;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.RejectedExecutionException;
@@ -34,7 +32,9 @@ import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
 import java.util.concurrent.atomic.LongAdder;
 import lombok.CustomLog;
 import lombok.SneakyThrows;
-import 
org.apache.bookkeeper.common.collections.GrowableMpScArrayConsumerBlockingQueue;
+import org.apache.bookkeeper.common.collections.BatchedArrayBlockingQueue;
+import org.apache.bookkeeper.common.collections.BatchedBlockingQueue;
+import 
org.apache.bookkeeper.common.collections.GrowableBatchedArrayBlockingQueue;
 import org.apache.bookkeeper.stats.Gauge;
 import org.apache.bookkeeper.stats.StatsLogger;
 
@@ -46,7 +46,10 @@ import org.apache.bookkeeper.stats.StatsLogger;
  */
 @CustomLog
 public class SingleThreadExecutor extends AbstractExecutorService implements 
ExecutorService, Runnable {
-    private final BlockingQueue<Runnable> queue;
+
+    private static final int MAX_DRAIN_BATCH_SIZE = 1024;
+
+    private final BatchedBlockingQueue<Runnable> queue;
     private final Thread runner;
 
     private final boolean rejectExecution;
@@ -83,9 +86,9 @@ public class SingleThreadExecutor extends 
AbstractExecutorService implements Exe
         }
 
         if (maxQueueCapacity > 0) {
-            this.queue = new ArrayBlockingQueue<>(maxQueueCapacity);
+            this.queue = new BatchedArrayBlockingQueue<>(maxQueueCapacity);
         } else {
-            this.queue = new GrowableMpScArrayConsumerBlockingQueue<>();
+            this.queue = new GrowableBatchedArrayBlockingQueue<>();
         }
         this.maxQueueCapacity = maxQueueCapacity;
 
@@ -102,7 +105,10 @@ public class SingleThreadExecutor extends 
AbstractExecutorService implements Exe
     public void run() {
         try {
             boolean isInitialized = false;
-            List<Runnable> localTasks = new ArrayList<>();
+            int batchSize = maxQueueCapacity > 0
+                    ? Math.min(maxQueueCapacity, MAX_DRAIN_BATCH_SIZE)
+                    : MAX_DRAIN_BATCH_SIZE;
+            Runnable[] localTasks = new Runnable[batchSize];
 
             while (state == State.Running) {
                 if (!isInitialized) {
@@ -110,25 +116,20 @@ public class SingleThreadExecutor extends 
AbstractExecutorService implements Exe
                     isInitialized = true;
                 }
 
-                int n = queue.drainTo(localTasks);
-                if (n > 0) {
-                    for (int i = 0; i < n; i++) {
-                        if (!safeRunTask(localTasks.get(i))) {
-                            return;
-                        }
-                    }
-                    localTasks.clear();
-                } else {
-                    if (!safeRunTask(queue.take())) {
+                int n = queue.takeAll(localTasks);
+                for (int i = 0; i < n; i++) {
+                    Runnable task = localTasks[i];
+                    localTasks[i] = null;
+                    if (!safeRunTask(task)) {
                         return;
                     }
                 }
             }
 
             // Clear the queue in orderly shutdown
-            int n = queue.drainTo(localTasks);
-            for (int i = 0; i < n; i++) {
-                safeRunTask(localTasks.get(i));
+            Runnable task;
+            while ((task = queue.poll()) != null) {
+                safeRunTask(task);
             }
         } catch (InterruptedException ie) {
             // Exit loop when interrupted
diff --git 
a/bookkeeper-common/src/test/java/org/apache/bookkeeper/common/collections/BatchedArrayBlockingQueueTest.java
 
b/bookkeeper-common/src/test/java/org/apache/bookkeeper/common/collections/BatchedArrayBlockingQueueTest.java
index 20e2f3723f..a20cc1b470 100644
--- 
a/bookkeeper-common/src/test/java/org/apache/bookkeeper/common/collections/BatchedArrayBlockingQueueTest.java
+++ 
b/bookkeeper-common/src/test/java/org/apache/bookkeeper/common/collections/BatchedArrayBlockingQueueTest.java
@@ -94,7 +94,7 @@ public class BatchedArrayBlockingQueueTest {
 
     @Test
     public void blockingTake() throws Exception {
-        BlockingQueue<Integer> queue = new 
GrowableMpScArrayConsumerBlockingQueue<>();
+        BlockingQueue<Integer> queue = new BatchedArrayBlockingQueue<>(100);
 
         CountDownLatch latch = new CountDownLatch(1);
 
diff --git 
a/bookkeeper-common/src/test/java/org/apache/bookkeeper/common/collections/GrowableArrayBlockingQueueTest.java
 
b/bookkeeper-common/src/test/java/org/apache/bookkeeper/common/collections/GrowableArrayBlockingQueueTest.java
deleted file mode 100644
index 7b20294d58..0000000000
--- 
a/bookkeeper-common/src/test/java/org/apache/bookkeeper/common/collections/GrowableArrayBlockingQueueTest.java
+++ /dev/null
@@ -1,273 +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.bookkeeper.common.collections;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
-
-import com.google.common.collect.Lists;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.NoSuchElementException;
-import java.util.concurrent.BlockingQueue;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicLong;
-import org.junit.Test;
-
-/**
- * Test the growable array blocking queue.
- */
-public class GrowableArrayBlockingQueueTest {
-
-    @Test
-    public void simple() throws Exception {
-        BlockingQueue<Integer> queue = new 
GrowableMpScArrayConsumerBlockingQueue<>(4);
-
-        assertEquals(null, queue.poll());
-
-        assertEquals(Integer.MAX_VALUE, queue.remainingCapacity());
-        assertEquals("[]", queue.toString());
-
-        try {
-            queue.element();
-            fail("Should have thrown exception");
-        } catch (NoSuchElementException e) {
-            // Expected
-        }
-
-        try {
-            queue.iterator();
-            fail("Should have thrown exception");
-        } catch (UnsupportedOperationException e) {
-            // Expected
-        }
-
-        // Test index rollover
-        for (int i = 0; i < 100; i++) {
-            queue.add(i);
-
-            assertEquals(i, queue.take().intValue());
-        }
-
-        queue.offer(1);
-        assertEquals("[1]", queue.toString());
-        queue.offer(2);
-        assertEquals("[1, 2]", queue.toString());
-        queue.offer(3);
-        assertEquals("[1, 2, 3]", queue.toString());
-        queue.offer(4);
-        assertEquals("[1, 2, 3, 4]", queue.toString());
-
-        assertEquals(4, queue.size());
-
-        List<Integer> list = new ArrayList<>();
-        queue.drainTo(list, 3);
-
-        assertEquals(1, queue.size());
-        assertEquals(Lists.newArrayList(1, 2, 3), list);
-        assertEquals("[4]", queue.toString());
-        assertEquals(4, queue.peek().intValue());
-
-        assertEquals(4, queue.element().intValue());
-        assertEquals(4, queue.remove().intValue());
-        try {
-            queue.remove();
-            fail("Should have thrown exception");
-        } catch (NoSuchElementException e) {
-            // Expected
-        }
-    }
-
-    @Test
-    public void blockingTake() throws Exception {
-        BlockingQueue<Integer> queue = new 
GrowableMpScArrayConsumerBlockingQueue<>();
-
-        CountDownLatch latch = new CountDownLatch(1);
-
-        new Thread(() -> {
-            try {
-                int expected = 0;
-
-                for (int i = 0; i < 100; i++) {
-                    int n = queue.take();
-
-                    assertEquals(expected++, n);
-                }
-
-                latch.countDown();
-            } catch (Exception e) {
-                e.printStackTrace();
-            }
-        }).start();
-
-        int n = 0;
-        for (int i = 0; i < 10; i++) {
-            for (int j = 0; j < 10; j++) {
-                queue.put(n);
-                ++n;
-            }
-
-            // Wait until all the entries are consumed
-            while (!queue.isEmpty()) {
-                Thread.sleep(1);
-            }
-        }
-
-        latch.await();
-    }
-
-    @Test
-    public void growArray() throws Exception {
-        BlockingQueue<Integer> queue = new 
GrowableMpScArrayConsumerBlockingQueue<>(4);
-
-        assertEquals(null, queue.poll());
-
-        assertTrue(queue.offer(1));
-        assertTrue(queue.offer(2));
-        assertTrue(queue.offer(3));
-        assertTrue(queue.offer(4));
-        assertTrue(queue.offer(5));
-
-        assertEquals(5, queue.size());
-
-        queue.clear();
-        assertEquals(0, queue.size());
-
-        assertTrue(queue.offer(1, 1, TimeUnit.SECONDS));
-        assertTrue(queue.offer(2, 1, TimeUnit.SECONDS));
-        assertTrue(queue.offer(3, 1, TimeUnit.SECONDS));
-        assertEquals(3, queue.size());
-
-        List<Integer> list = new ArrayList<>();
-        queue.drainTo(list);
-        assertEquals(0, queue.size());
-
-        assertEquals(Lists.newArrayList(1, 2, 3), list);
-    }
-
-    @Test
-    public void pollTimeout() throws Exception {
-        BlockingQueue<Integer> queue = new 
GrowableMpScArrayConsumerBlockingQueue<>(4);
-
-        assertEquals(null, queue.poll(1, TimeUnit.MILLISECONDS));
-
-        queue.put(1);
-        assertEquals(1, queue.poll(1, TimeUnit.MILLISECONDS).intValue());
-
-        // 0 timeout should not block
-        assertEquals(null, queue.poll(0, TimeUnit.HOURS));
-
-        queue.put(2);
-        queue.put(3);
-        assertEquals(2, queue.poll(1, TimeUnit.HOURS).intValue());
-        assertEquals(3, queue.poll(1, TimeUnit.HOURS).intValue());
-    }
-
-    @Test
-    public void pollTimeout2() throws Exception {
-        BlockingQueue<Integer> queue = new 
GrowableMpScArrayConsumerBlockingQueue<>();
-
-        CountDownLatch latch = new CountDownLatch(1);
-
-        new Thread(() -> {
-            try {
-                queue.poll(1, TimeUnit.HOURS);
-
-                latch.countDown();
-            } catch (Exception e) {
-                e.printStackTrace();
-            }
-        }).start();
-
-        // Make sure background thread is waiting on poll
-        Thread.sleep(100);
-        queue.put(1);
-
-        latch.await();
-    }
-
-
-    static class TestThread extends Thread {
-
-        private volatile boolean stop;
-        private final BlockingQueue<Integer> readQ;
-        private final BlockingQueue<Integer> writeQ;
-
-        private final AtomicLong counter = new AtomicLong();
-
-        TestThread(BlockingQueue<Integer> readQ, BlockingQueue<Integer> 
writeQ) {
-            this.readQ = readQ;
-            this.writeQ = writeQ;
-        }
-
-        @Override
-        public void run() {
-            ArrayList<Integer> localQ = new ArrayList<>();
-
-            while (!stop) {
-                int items = readQ.drainTo(localQ);
-                if (items > 0) {
-                    for (int i = 0; i < items; i++) {
-                        writeQ.add(localQ.get(i));
-                    }
-
-                    counter.addAndGet(items);
-                    localQ.clear();
-                } else {
-                    try {
-                        writeQ.add(readQ.take());
-                        counter.incrementAndGet();
-                    } catch (InterruptedException e) {
-                        return;
-                    }
-                }
-            }
-        }
-    }
-
-    public static void main(String[] args) throws Exception {
-        int n = 10_000;
-        BlockingQueue<Integer> q1 = new 
GrowableMpScArrayConsumerBlockingQueue<>();
-        BlockingQueue<Integer> q2 = new 
GrowableMpScArrayConsumerBlockingQueue<>();
-//        BlockingQueue<Integer> q1 = new ArrayBlockingQueue<>(N * 2);
-//        BlockingQueue<Integer> q2 = new ArrayBlockingQueue<>(N * 2);
-//        BlockingQueue<Integer> q1 = new LinkedBlockingQueue<>();
-//        BlockingQueue<Integer> q2 = new LinkedBlockingDeque<>();
-
-        TestThread t1 = new TestThread(q1, q2);
-        TestThread t2 = new TestThread(q2, q1);
-
-        for (int i = 0; i < n; i++) {
-            q1.add(i);
-        }
-
-        t1.start();
-        t2.start();
-
-        Thread.sleep(10_000);
-
-        System.out.println("Throughput " + (t1.counter.get() / 10 / 1e6) + " 
Millions items/s");
-        t1.stop = true;
-        t2.stop = true;
-    }
-}
diff --git 
a/bookkeeper-common/src/test/java/org/apache/bookkeeper/common/collections/GrowableBatchedArrayBlockingQueueTest.java
 
b/bookkeeper-common/src/test/java/org/apache/bookkeeper/common/collections/GrowableBatchedArrayBlockingQueueTest.java
new file mode 100644
index 0000000000..c1a6c96d8b
--- /dev/null
+++ 
b/bookkeeper-common/src/test/java/org/apache/bookkeeper/common/collections/GrowableBatchedArrayBlockingQueueTest.java
@@ -0,0 +1,380 @@
+/*
+ *
+ * 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.bookkeeper.common.collections;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import com.google.common.collect.Lists;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.NoSuchElementException;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import org.junit.Test;
+
+/**
+ * Test the growable batched array blocking queue.
+ */
+public class GrowableBatchedArrayBlockingQueueTest {
+
+    @Test
+    public void simple() throws Exception {
+        GrowableBatchedArrayBlockingQueue<Integer> queue = new 
GrowableBatchedArrayBlockingQueue<>(4);
+
+        assertNull(queue.poll());
+
+        assertEquals(Integer.MAX_VALUE, queue.remainingCapacity());
+
+        try {
+            queue.element();
+            fail("Should have thrown exception");
+        } catch (NoSuchElementException e) {
+            // Expected
+        }
+
+        try {
+            queue.iterator();
+            fail("Should have thrown exception");
+        } catch (UnsupportedOperationException e) {
+            // Expected
+        }
+
+        // Test index rollover
+        for (int i = 0; i < 100; i++) {
+            queue.add(i);
+
+            assertEquals(i, queue.take().intValue());
+        }
+
+        queue.offer(1);
+        queue.offer(2);
+        queue.offer(3);
+        queue.offer(4);
+
+        assertEquals(4, queue.size());
+
+        List<Integer> list = new ArrayList<>();
+        queue.drainTo(list, 3);
+
+        assertEquals(1, queue.size());
+        assertEquals(Lists.newArrayList(1, 2, 3), list);
+        assertEquals(4, queue.peek().intValue());
+
+        assertEquals(4, queue.element().intValue());
+        assertEquals(4, queue.remove().intValue());
+        try {
+            queue.remove();
+            fail("Should have thrown exception");
+        } catch (NoSuchElementException e) {
+            // Expected
+        }
+    }
+
+    @Test
+    public void blockingTake() throws Exception {
+        GrowableBatchedArrayBlockingQueue<Integer> queue = new 
GrowableBatchedArrayBlockingQueue<>();
+
+        CountDownLatch latch = new CountDownLatch(1);
+
+        new Thread(() -> {
+            try {
+                int expected = 0;
+
+                for (int i = 0; i < 100; i++) {
+                    int n = queue.take();
+
+                    assertEquals(expected++, n);
+                }
+
+                latch.countDown();
+            } catch (Exception e) {
+                e.printStackTrace();
+            }
+        }).start();
+
+        int n = 0;
+        for (int i = 0; i < 10; i++) {
+            for (int j = 0; j < 10; j++) {
+                queue.put(n);
+                ++n;
+            }
+
+            // Wait until all the entries are consumed
+            while (!queue.isEmpty()) {
+                Thread.sleep(1);
+            }
+        }
+
+        latch.await();
+    }
+
+    @Test
+    public void growArray() throws Exception {
+        GrowableBatchedArrayBlockingQueue<Integer> queue = new 
GrowableBatchedArrayBlockingQueue<>(4);
+
+        assertNull(queue.poll());
+
+        assertTrue(queue.offer(1));
+        assertTrue(queue.offer(2));
+        assertTrue(queue.offer(3));
+        assertTrue(queue.offer(4));
+        assertTrue(queue.offer(5));
+
+        assertEquals(5, queue.size());
+
+        queue.clear();
+        assertEquals(0, queue.size());
+
+        assertTrue(queue.offer(1, 1, TimeUnit.SECONDS));
+        assertTrue(queue.offer(2, 1, TimeUnit.SECONDS));
+        assertTrue(queue.offer(3, 1, TimeUnit.SECONDS));
+        assertEquals(3, queue.size());
+
+        List<Integer> list = new ArrayList<>();
+        queue.drainTo(list);
+        assertEquals(0, queue.size());
+
+        assertEquals(Lists.newArrayList(1, 2, 3), list);
+    }
+
+    @Test
+    public void growWithWrappedIndexes() throws Exception {
+        GrowableBatchedArrayBlockingQueue<Integer> queue = new 
GrowableBatchedArrayBlockingQueue<>(4);
+
+        assertTrue(queue.offer(0));
+        assertTrue(queue.offer(1));
+        assertTrue(queue.offer(2));
+
+        // Advance the consumer index so that the queue content wraps around 
the array
+        assertEquals(0, queue.poll().intValue());
+        assertEquals(1, queue.poll().intValue());
+
+        assertTrue(queue.offer(3));
+        assertTrue(queue.offer(4));
+        assertTrue(queue.offer(5));
+
+        // Next offer triggers the growth with wrapped content
+        assertTrue(queue.offer(6));
+        assertEquals(5, queue.size());
+
+        for (int i = 2; i <= 6; i++) {
+            assertEquals(i, queue.take().intValue());
+        }
+        assertEquals(0, queue.size());
+    }
+
+    @Test
+    public void putAllAndTakeAll() throws Exception {
+        GrowableBatchedArrayBlockingQueue<Integer> queue = new 
GrowableBatchedArrayBlockingQueue<>(4);
+
+        Integer[] items = new Integer[100];
+        for (int i = 0; i < 100; i++) {
+            items[i] = i;
+        }
+
+        // Insert, in one shot, more items than the current capacity
+        queue.putAll(items, 0, 100);
+        assertEquals(100, queue.size());
+
+        Integer[] local = new Integer[100];
+        int n = queue.takeAll(local);
+        assertEquals(100, n);
+        for (int i = 0; i < 100; i++) {
+            assertEquals(i, local[i].intValue());
+        }
+        assertEquals(0, queue.size());
+    }
+
+    @Test
+    public void putAllWithWrappedIndexes() throws Exception {
+        GrowableBatchedArrayBlockingQueue<Integer> queue = new 
GrowableBatchedArrayBlockingQueue<>(8);
+
+        // Advance the indexes so that the batch insert wraps around the array
+        for (int i = 0; i < 6; i++) {
+            queue.put(i);
+        }
+        for (int i = 0; i < 6; i++) {
+            assertEquals(i, queue.take().intValue());
+        }
+
+        Integer[] items = new Integer[4];
+        for (int i = 0; i < 4; i++) {
+            items[i] = 100 + i;
+        }
+
+        queue.putAll(items, 0, 4);
+        assertEquals(4, queue.size());
+
+        for (int i = 0; i < 4; i++) {
+            assertEquals(100 + i, queue.take().intValue());
+        }
+    }
+
+    @Test
+    public void takeAllBlocksUntilItemsAreAvailable() throws Exception {
+        GrowableBatchedArrayBlockingQueue<Integer> queue = new 
GrowableBatchedArrayBlockingQueue<>();
+
+        CountDownLatch latch = new CountDownLatch(1);
+
+        new Thread(() -> {
+            try {
+                Integer[] local = new Integer[10];
+                int n = queue.takeAll(local);
+                assertEquals(1, n);
+                assertEquals(1, local[0].intValue());
+
+                latch.countDown();
+            } catch (Exception e) {
+                e.printStackTrace();
+            }
+        }).start();
+
+        // Make sure the background thread is waiting on takeAll
+        Thread.sleep(100);
+        queue.put(1);
+
+        latch.await();
+    }
+
+    @Test
+    public void pollAllTimeout() throws Exception {
+        GrowableBatchedArrayBlockingQueue<Integer> queue = new 
GrowableBatchedArrayBlockingQueue<>();
+
+        Integer[] local = new Integer[10];
+        assertEquals(0, queue.pollAll(local, 1, TimeUnit.MILLISECONDS));
+
+        queue.put(1);
+        queue.put(2);
+        assertEquals(2, queue.pollAll(local, 1, TimeUnit.MILLISECONDS));
+        assertEquals(1, local[0].intValue());
+        assertEquals(2, local[1].intValue());
+    }
+
+    @Test
+    public void pollTimeout() throws Exception {
+        GrowableBatchedArrayBlockingQueue<Integer> queue = new 
GrowableBatchedArrayBlockingQueue<>(4);
+
+        assertNull(queue.poll(1, TimeUnit.MILLISECONDS));
+
+        queue.put(1);
+        assertEquals(1, queue.poll(1, TimeUnit.MILLISECONDS).intValue());
+
+        // 0 timeout should not block
+        assertNull(queue.poll(0, TimeUnit.HOURS));
+
+        queue.put(2);
+        queue.put(3);
+        assertEquals(2, queue.poll(1, TimeUnit.HOURS).intValue());
+        assertEquals(3, queue.poll(1, TimeUnit.HOURS).intValue());
+    }
+
+    @Test(timeout = 10_000)
+    public void zeroInitialCapacityIsNormalized() throws Exception {
+        GrowableBatchedArrayBlockingQueue<Integer> queue = new 
GrowableBatchedArrayBlockingQueue<>(0);
+
+        for (int i = 0; i < 10; i++) {
+            assertTrue(queue.offer(i));
+        }
+
+        for (int i = 0; i < 10; i++) {
+            assertEquals(i, queue.take().intValue());
+        }
+    }
+
+    @Test
+    public void rejectNullElements() {
+        GrowableBatchedArrayBlockingQueue<Integer> queue = new 
GrowableBatchedArrayBlockingQueue<>();
+
+        assertThrows(NullPointerException.class, () -> queue.offer(null));
+        assertThrows(NullPointerException.class, () -> queue.put(null));
+        assertThrows(NullPointerException.class, () -> queue.add(null));
+    }
+
+    @Test
+    public void drainToValidation() {
+        GrowableBatchedArrayBlockingQueue<Integer> queue = new 
GrowableBatchedArrayBlockingQueue<>();
+        queue.put(1);
+
+        assertThrows(NullPointerException.class, () -> queue.drainTo(null));
+        assertThrows(IllegalArgumentException.class, () -> 
queue.drainTo(queue));
+
+        // A non-positive maxElements does not drain and does not corrupt the 
queue state
+        List<Integer> list = new ArrayList<>();
+        assertEquals(0, queue.drainTo(list, -1));
+        assertEquals(0, queue.drainTo(list, 0));
+        assertEquals(1, queue.size());
+        assertEquals(1, queue.poll().intValue());
+    }
+
+    @Test
+    public void drainToIsExceptionSafe() {
+        GrowableBatchedArrayBlockingQueue<Integer> queue = new 
GrowableBatchedArrayBlockingQueue<>();
+        for (int i = 0; i < 5; i++) {
+            queue.put(i);
+        }
+
+        List<Integer> target = new ArrayList<Integer>() {
+            @Override
+            public boolean add(Integer item) {
+                if (size() == 2) {
+                    throw new RuntimeException("Rejecting item");
+                }
+                return super.add(item);
+            }
+        };
+
+        assertThrows(RuntimeException.class, () -> queue.drainTo(target));
+
+        // The two transferred items are committed, the rest are still in the 
queue
+        assertEquals(Lists.newArrayList(0, 1), target);
+        assertEquals(3, queue.size());
+        for (int i = 2; i < 5; i++) {
+            assertEquals(i, queue.poll().intValue());
+        }
+    }
+
+    @Test(timeout = 10_000)
+    public void takeAllWithEmptyArrayDoesNotBlock() throws Exception {
+        GrowableBatchedArrayBlockingQueue<Integer> queue = new 
GrowableBatchedArrayBlockingQueue<>();
+
+        // Even with an empty queue, a zero-length destination array returns 
immediately
+        assertEquals(0, queue.takeAll(new Integer[0]));
+        assertEquals(0, queue.pollAll(new Integer[0], 1, TimeUnit.HOURS));
+    }
+
+    @Test
+    public void putAllArgumentValidation() {
+        GrowableBatchedArrayBlockingQueue<Integer> queue = new 
GrowableBatchedArrayBlockingQueue<>();
+
+        assertThrows(NullPointerException.class, () -> queue.putAll(null, 0, 
1));
+
+        Integer[] items = { 1, 2, 3 };
+        assertThrows(IndexOutOfBoundsException.class, () -> 
queue.putAll(items, -1, 2));
+        assertThrows(IndexOutOfBoundsException.class, () -> 
queue.putAll(items, 0, 4));
+        assertThrows(IndexOutOfBoundsException.class, () -> 
queue.putAll(items, 2, 2));
+
+        assertEquals(0, queue.size());
+    }
+}
diff --git 
a/microbenchmarks/src/main/java/org/apache/bookkeeper/common/SingleThreadExecutorQueueBenchmark.java
 
b/microbenchmarks/src/main/java/org/apache/bookkeeper/common/SingleThreadExecutorQueueBenchmark.java
new file mode 100644
index 0000000000..335a7b7303
--- /dev/null
+++ 
b/microbenchmarks/src/main/java/org/apache/bookkeeper/common/SingleThreadExecutorQueueBenchmark.java
@@ -0,0 +1,139 @@
+/*
+ * 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.bookkeeper.common;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import lombok.SneakyThrows;
+import org.apache.bookkeeper.common.collections.BatchedArrayBlockingQueue;
+import org.apache.bookkeeper.common.collections.BatchedBlockingQueue;
+import 
org.apache.bookkeeper.common.collections.GrowableBatchedArrayBlockingQueue;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Fork;
+import org.openjdk.jmh.annotations.Level;
+import org.openjdk.jmh.annotations.Measurement;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.TearDown;
+import org.openjdk.jmh.annotations.Threads;
+import org.openjdk.jmh.annotations.Warmup;
+
+/**
+ * Compares the queue options for {@code SingleThreadExecutor}: the JDK {@code 
ArrayBlockingQueue}
+ * drained with {@code drainTo}, and the batched queues ({@code 
BatchedArrayBlockingQueue} and
+ * {@code GrowableBatchedArrayBlockingQueue}) drained with {@code takeAll}.
+ *
+ * <p>The consumer threads mirror the drain patterns of {@code 
SingleThreadExecutor.run()}.
+ */
+@OutputTimeUnit(TimeUnit.MICROSECONDS)
+@BenchmarkMode(Mode.Throughput)
+@Threads(8)
+@Fork(1)
+@Warmup(iterations = 2, time = 5, timeUnit = TimeUnit.SECONDS)
+@Measurement(iterations = 3, time = 5, timeUnit = TimeUnit.SECONDS)
+public class SingleThreadExecutorQueueBenchmark {
+
+    private static final int QUEUE_SIZE = 100_000;
+
+    /**
+     * State holder of the test.
+     */
+    @State(Scope.Benchmark)
+    public static class TestState {
+
+        private final ArrayBlockingQueue<Integer> arrayBlockingQueue = new 
ArrayBlockingQueue<>(QUEUE_SIZE);
+
+        private final BatchedArrayBlockingQueue<Integer> batchedQueue = new 
BatchedArrayBlockingQueue<>(QUEUE_SIZE);
+
+        private final GrowableBatchedArrayBlockingQueue<Integer> 
growableBatchedQueue =
+                new GrowableBatchedArrayBlockingQueue<>();
+
+        private final ExecutorService executor = 
Executors.newCachedThreadPool();
+
+        @Setup(Level.Trial)
+        public void setup() {
+            executor.execute(() -> consumeWithDrainTo(arrayBlockingQueue));
+            executor.execute(() -> consumeWithTakeAll(batchedQueue));
+            executor.execute(() -> consumeWithTakeAll(growableBatchedQueue));
+        }
+
+        @SneakyThrows
+        private void consumeWithDrainTo(BlockingQueue<Integer> queue) {
+            List<Integer> localTasks = new ArrayList<>();
+
+            try {
+                while (true) {
+                    int n = queue.drainTo(localTasks);
+                    if (n > 0) {
+                        localTasks.clear();
+                    } else {
+                        queue.take();
+                    }
+                }
+            } catch (InterruptedException ie) {
+            }
+        }
+
+        @SneakyThrows
+        private void consumeWithTakeAll(BatchedBlockingQueue<Integer> queue) {
+            Integer[] localTasks = new Integer[20_000];
+
+            try {
+                while (true) {
+                    queue.takeAll(localTasks);
+                }
+            } catch (InterruptedException ie) {
+            }
+        }
+
+        @TearDown(Level.Trial)
+        public void teardown() {
+            executor.shutdownNow();
+        }
+
+        @TearDown(Level.Iteration)
+        public void drainBacklog() throws InterruptedException {
+            Thread.sleep(1_000);
+        }
+    }
+
+    @Benchmark
+    public void arrayBlockingQueue(TestState s) throws Exception {
+        s.arrayBlockingQueue.put(1);
+    }
+
+    @Benchmark
+    public void batchedArrayBlockingQueue(TestState s) throws Exception {
+        s.batchedQueue.put(1);
+    }
+
+    @Benchmark
+    public void growableBatchedArrayBlockingQueue(TestState s) throws 
Exception {
+        s.growableBatchedQueue.put(1);
+    }
+}

Reply via email to