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

merlimat pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/pulsar.git


The following commit(s) were added to refs/heads/master by this push:
     new b8053046290 [improve][client] Make v5 scalable consumer receiveAsync 
truly non-blocking (#26131)
b8053046290 is described below

commit b8053046290fc0740b669b10604b052a4b3e5945
Author: Matteo Merli <[email protected]>
AuthorDate: Thu Jul 2 14:55:02 2026 -0700

    [improve][client] Make v5 scalable consumer receiveAsync truly non-blocking 
(#26131)
---
 .../client/impl/v5/AsyncCheckpointConsumerV5.java  |  11 +-
 .../client/impl/v5/AsyncStreamConsumerV5.java      |  11 +-
 .../client/impl/v5/MultiTopicQueueConsumer.java    |  30 +--
 .../client/impl/v5/MultiTopicStreamConsumer.java   |  76 +------
 .../client/impl/v5/ScalableCheckpointConsumer.java |  73 ++----
 .../client/impl/v5/ScalableQueueConsumer.java      |  34 +--
 .../client/impl/v5/ScalableStreamConsumer.java     |  68 ++----
 .../pulsar/client/impl/v5/V5ReceiveQueue.java      | 253 +++++++++++++++++++++
 .../pulsar/client/impl/v5/V5ReceiveQueueTest.java  | 248 ++++++++++++++++++++
 9 files changed, 565 insertions(+), 239 deletions(-)

diff --git 
a/pulsar-client-v5/src/main/java/org/apache/pulsar/client/impl/v5/AsyncCheckpointConsumerV5.java
 
b/pulsar-client-v5/src/main/java/org/apache/pulsar/client/impl/v5/AsyncCheckpointConsumerV5.java
index ace455e7ba9..7c8944708ea 100644
--- 
a/pulsar-client-v5/src/main/java/org/apache/pulsar/client/impl/v5/AsyncCheckpointConsumerV5.java
+++ 
b/pulsar-client-v5/src/main/java/org/apache/pulsar/client/impl/v5/AsyncCheckpointConsumerV5.java
@@ -48,16 +48,7 @@ final class AsyncCheckpointConsumerV5<T> implements 
AsyncCheckpointConsumer<T> {
 
     @Override
     public CompletableFuture<List<Message<T>>> receiveMulti(int maxMessages, 
Duration timeout) {
-        return CompletableFuture.supplyAsync(() -> {
-            try {
-                var msgs = consumer.receiveMulti(maxMessages, timeout);
-                List<Message<T>> result = new java.util.ArrayList<>();
-                msgs.forEach(result::add);
-                return result;
-            } catch (Exception e) {
-                throw new java.util.concurrent.CompletionException(e);
-            }
-        });
+        return consumer.receiveMultiAsync(maxMessages, timeout);
     }
 
     @Override
diff --git 
a/pulsar-client-v5/src/main/java/org/apache/pulsar/client/impl/v5/AsyncStreamConsumerV5.java
 
b/pulsar-client-v5/src/main/java/org/apache/pulsar/client/impl/v5/AsyncStreamConsumerV5.java
index 36afa102db0..0955e357e9b 100644
--- 
a/pulsar-client-v5/src/main/java/org/apache/pulsar/client/impl/v5/AsyncStreamConsumerV5.java
+++ 
b/pulsar-client-v5/src/main/java/org/apache/pulsar/client/impl/v5/AsyncStreamConsumerV5.java
@@ -49,16 +49,7 @@ final class AsyncStreamConsumerV5<T> implements 
AsyncStreamConsumer<T> {
 
     @Override
     public CompletableFuture<List<Message<T>>> receiveMulti(int 
maxNumMessages, Duration timeout) {
-        return CompletableFuture.supplyAsync(() -> {
-            try {
-                var msgs = consumer.receiveMulti(maxNumMessages, timeout);
-                List<Message<T>> result = new java.util.ArrayList<>();
-                msgs.forEach(result::add);
-                return result;
-            } catch (Exception e) {
-                throw new java.util.concurrent.CompletionException(e);
-            }
-        });
+        return consumer.receiveMultiAsync(maxNumMessages, timeout);
     }
 
     @Override
diff --git 
a/pulsar-client-v5/src/main/java/org/apache/pulsar/client/impl/v5/MultiTopicQueueConsumer.java
 
b/pulsar-client-v5/src/main/java/org/apache/pulsar/client/impl/v5/MultiTopicQueueConsumer.java
index 52d956e1b58..e02eb325eb8 100644
--- 
a/pulsar-client-v5/src/main/java/org/apache/pulsar/client/impl/v5/MultiTopicQueueConsumer.java
+++ 
b/pulsar-client-v5/src/main/java/org/apache/pulsar/client/impl/v5/MultiTopicQueueConsumer.java
@@ -31,7 +31,6 @@ import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.CompletionException;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ExecutionException;
-import java.util.concurrent.LinkedTransferQueue;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicLong;
 import org.apache.pulsar.client.api.v5.Message;
@@ -85,7 +84,7 @@ final class MultiTopicQueueConsumer<T> implements 
QueueConsumerImpl<T> {
 
     private final ScalableTopicsWatcher watcher;
     private final ConcurrentHashMap<String, PerTopicState<T>> perTopic = new 
ConcurrentHashMap<>();
-    private final LinkedTransferQueue<MessageV5<T>> mux = new 
LinkedTransferQueue<>();
+    private final V5ReceiveQueue<T> mux;
 
     private volatile boolean closed = false;
     private final AsyncQueueConsumerV5<T> asyncView;
@@ -103,6 +102,8 @@ final class MultiTopicQueueConsumer<T> implements 
QueueConsumerImpl<T> {
         this.propertyFilters = propertyFilters;
         this.subscriptionName = consumerConf.getSubscriptionName();
         this.watcher = watcher;
+        this.mux = new V5ReceiveQueue<>(
+                client.v4Client().externalExecutorProvider().getExecutor(), 
client.v4Client().timer());
         this.log = LOG.with()
                 .attr("namespace", namespace)
                 .attr("subscription", subscriptionName)
@@ -170,7 +171,7 @@ final class MultiTopicQueueConsumer<T> implements 
QueueConsumerImpl<T> {
         // thread; per-segment v4 receive loops fire this sink directly.
         java.util.function.Consumer<MessageV5<T>> sink = msg -> {
             if (!closed) {
-                mux.add(msg.withTopicOverride(topicName));
+                mux.offer(msg.withTopicOverride(topicName));
             }
         };
         return dagWatch.start()
@@ -299,22 +300,12 @@ final class MultiTopicQueueConsumer<T> implements 
QueueConsumerImpl<T> {
 
     @Override
     public Message<T> receive() throws PulsarClientException {
-        try {
-            return mux.take();
-        } catch (InterruptedException e) {
-            Thread.currentThread().interrupt();
-            throw new PulsarClientException("Receive interrupted", e);
-        }
+        return mux.take();
     }
 
     @Override
     public Message<T> receive(Duration timeout) throws PulsarClientException {
-        try {
-            return mux.poll(timeout.toMillis(), TimeUnit.MILLISECONDS);
-        } catch (InterruptedException e) {
-            Thread.currentThread().interrupt();
-            throw new PulsarClientException("Receive interrupted", e);
-        }
+        return mux.poll(timeout);
     }
 
     @Override
@@ -372,13 +363,7 @@ final class MultiTopicQueueConsumer<T> implements 
QueueConsumerImpl<T> {
 
     @Override
     public CompletableFuture<Message<T>> receiveAsync() {
-        return CompletableFuture.supplyAsync(() -> {
-            try {
-                return receive();
-            } catch (PulsarClientException e) {
-                throw new CompletionException(e);
-            }
-        });
+        return mux.receiveAsync();
     }
 
     @Override
@@ -388,6 +373,7 @@ final class MultiTopicQueueConsumer<T> implements 
QueueConsumerImpl<T> {
         }
         closed = true;
         watcher.close();
+        mux.close();
         // Cancel pending retries for topics that never finished subscribing 
(they're not in
         // perTopic, so the closeTopic loop below wouldn't reach them).
         retryTimeouts.values().forEach(Timeout::cancel);
diff --git 
a/pulsar-client-v5/src/main/java/org/apache/pulsar/client/impl/v5/MultiTopicStreamConsumer.java
 
b/pulsar-client-v5/src/main/java/org/apache/pulsar/client/impl/v5/MultiTopicStreamConsumer.java
index ffacdcad11d..ca189af4ea0 100644
--- 
a/pulsar-client-v5/src/main/java/org/apache/pulsar/client/impl/v5/MultiTopicStreamConsumer.java
+++ 
b/pulsar-client-v5/src/main/java/org/apache/pulsar/client/impl/v5/MultiTopicStreamConsumer.java
@@ -31,7 +31,6 @@ import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.CompletionException;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ExecutionException;
-import java.util.concurrent.LinkedTransferQueue;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicLong;
 import org.apache.pulsar.client.api.v5.Message;
@@ -81,7 +80,7 @@ final class MultiTopicStreamConsumer<T> implements 
StreamConsumer<T> {
 
     private final ScalableTopicsWatcher watcher;
     private final ConcurrentHashMap<String, PerTopic<T>> perTopic = new 
ConcurrentHashMap<>();
-    private final LinkedTransferQueue<MessageV5<T>> mux = new 
LinkedTransferQueue<>();
+    private final V5ReceiveQueue<T> mux;
 
     /**
      * Tracks the latest delivered message id per (parent topic, segment id) 
across
@@ -107,6 +106,8 @@ final class MultiTopicStreamConsumer<T> implements 
StreamConsumer<T> {
         this.propertyFilters = propertyFilters;
         this.subscriptionName = consumerConf.getSubscriptionName();
         this.watcher = watcher;
+        this.mux = new V5ReceiveQueue<>(
+                client.v4Client().externalExecutorProvider().getExecutor(), 
client.v4Client().timer());
         this.log = LOG.with()
                 .attr("namespace", namespace)
                 .attr("subscription", subscriptionName)
@@ -292,50 +293,17 @@ final class MultiTopicStreamConsumer<T> implements 
StreamConsumer<T> {
 
     @Override
     public Message<T> receive() throws PulsarClientException {
-        try {
-            return mux.take();
-        } catch (InterruptedException e) {
-            Thread.currentThread().interrupt();
-            throw new PulsarClientException("Receive interrupted", e);
-        }
+        return mux.take();
     }
 
     @Override
     public Message<T> receive(Duration timeout) throws PulsarClientException {
-        try {
-            return mux.poll(timeout.toMillis(), TimeUnit.MILLISECONDS);
-        } catch (InterruptedException e) {
-            Thread.currentThread().interrupt();
-            throw new PulsarClientException("Receive interrupted", e);
-        }
+        return mux.poll(timeout);
     }
 
     @Override
     public Messages<T> receiveMulti(int maxNumMessages, Duration timeout) 
throws PulsarClientException {
-        // Block for up to `timeout` waiting for the first message, then drain 
whatever
-        // else is immediately available up to maxNumMessages. Same shape as 
the single
-        // topic StreamConsumer.
-        long deadline = System.nanoTime() + timeout.toNanos();
-        List<Message<T>> batch = new ArrayList<>();
-        try {
-            long remaining = deadline - System.nanoTime();
-            while (batch.size() < maxNumMessages && remaining > 0) {
-                MessageV5<T> msg = mux.poll(remaining, TimeUnit.NANOSECONDS);
-                if (msg == null) {
-                    break;
-                }
-                batch.add(msg);
-                remaining = deadline - System.nanoTime();
-            }
-            // Opportunistic drain of anything else already queued.
-            List<MessageV5<T>> tail = new ArrayList<>();
-            mux.drainTo(tail, maxNumMessages - batch.size());
-            batch.addAll(tail);
-        } catch (InterruptedException e) {
-            Thread.currentThread().interrupt();
-            throw new PulsarClientException("Receive interrupted", e);
-        }
-        return new MessagesV5<>(batch);
+        return new MessagesV5<>(mux.receiveMulti(maxNumMessages, timeout));
     }
 
     @Override
@@ -398,6 +366,7 @@ final class MultiTopicStreamConsumer<T> implements 
StreamConsumer<T> {
         }
         closed = true;
         watcher.close();
+        mux.close();
         // Cancel pending retries for topics that never finished subscribing 
(they're not in
         // perTopic, so the closeTopic loop below wouldn't reach them).
         retryTimeouts.values().forEach(Timeout::cancel);
@@ -479,7 +448,7 @@ final class MultiTopicStreamConsumer<T> implements 
StreamConsumer<T> {
         MessageIdV5 newId = new MessageIdV5(
                 origId.v4MessageId(), origId.segmentId(),
                 origId.positionVector(), parentTopic, snapshot);
-        mux.add(new MessageV5<>(msg.v4Message(), newId, parentTopic));
+        mux.offer(new MessageV5<>(msg.v4Message(), newId, parentTopic));
     }
 
     // --- Per-topic state ---
@@ -503,40 +472,17 @@ final class MultiTopicStreamConsumer<T> implements 
StreamConsumer<T> {
     private final class AsyncStreamConsumerV5Multi implements 
AsyncStreamConsumer<T> {
         @Override
         public CompletableFuture<Message<T>> receive() {
-            return CompletableFuture.supplyAsync(() -> {
-                try {
-                    return MultiTopicStreamConsumer.this.receive();
-                } catch (PulsarClientException e) {
-                    throw new CompletionException(e);
-                }
-            });
+            return mux.receiveAsync();
         }
 
         @Override
         public CompletableFuture<Message<T>> receive(Duration timeout) {
-            return CompletableFuture.supplyAsync(() -> {
-                try {
-                    return MultiTopicStreamConsumer.this.receive(timeout);
-                } catch (PulsarClientException e) {
-                    throw new CompletionException(e);
-                }
-            });
+            return mux.receiveAsync(timeout);
         }
 
         @Override
         public CompletableFuture<List<Message<T>>> receiveMulti(int 
maxNumMessages, Duration timeout) {
-            return CompletableFuture.supplyAsync(() -> {
-                try {
-                    Messages<T> ms = 
MultiTopicStreamConsumer.this.receiveMulti(maxNumMessages, timeout);
-                    List<Message<T>> out = new ArrayList<>();
-                    for (Message<T> m : ms) {
-                        out.add(m);
-                    }
-                    return out;
-                } catch (PulsarClientException e) {
-                    throw new CompletionException(e);
-                }
-            });
+            return mux.receiveMultiAsync(maxNumMessages, timeout);
         }
 
         @Override
diff --git 
a/pulsar-client-v5/src/main/java/org/apache/pulsar/client/impl/v5/ScalableCheckpointConsumer.java
 
b/pulsar-client-v5/src/main/java/org/apache/pulsar/client/impl/v5/ScalableCheckpointConsumer.java
index 8f8540af4ea..8e3521af188 100644
--- 
a/pulsar-client-v5/src/main/java/org/apache/pulsar/client/impl/v5/ScalableCheckpointConsumer.java
+++ 
b/pulsar-client-v5/src/main/java/org/apache/pulsar/client/impl/v5/ScalableCheckpointConsumer.java
@@ -28,8 +28,6 @@ import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.CompletionException;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ExecutionException;
-import java.util.concurrent.LinkedTransferQueue;
-import java.util.concurrent.TimeUnit;
 import 
org.apache.pulsar.client.api.PulsarClientException.AlreadyClosedException;
 import org.apache.pulsar.client.api.Reader;
 import org.apache.pulsar.client.api.v5.Checkpoint;
@@ -81,7 +79,7 @@ final class ScalableCheckpointConsumer<T> implements 
CheckpointConsumer<T> {
     private final ConcurrentHashMap<Long, CompletableFuture<Reader<T>>> 
segmentReaders = new ConcurrentHashMap<>();
     private final ConcurrentHashMap<Long, 
org.apache.pulsar.client.api.MessageId> lastReceivedPositions =
             new ConcurrentHashMap<>();
-    private final LinkedTransferQueue<MessageV5<T>> messageQueue = new 
LinkedTransferQueue<>();
+    private final V5ReceiveQueue<T> receiveQueue;
 
     private volatile boolean closed = false;
     private final AsyncCheckpointConsumerV5<T> asyncView;
@@ -99,6 +97,8 @@ final class ScalableCheckpointConsumer<T> implements 
CheckpointConsumer<T> {
         this.topicName = topicName;
         this.startPosition = startPosition;
         this.consumerName = consumerName;
+        this.receiveQueue = new V5ReceiveQueue<>(
+                client.v4Client().externalExecutorProvider().getExecutor(), 
client.v4Client().timer());
         this.log = LOG.with().attr("topic", topicName).build();
         this.asyncView = new AsyncCheckpointConsumerV5<>(this);
     }
@@ -168,51 +168,18 @@ final class ScalableCheckpointConsumer<T> implements 
CheckpointConsumer<T> {
 
     @Override
     public Message<T> receive() throws PulsarClientException {
-        try {
-            return advanceCheckpoint(messageQueue.take());
-        } catch (InterruptedException e) {
-            Thread.currentThread().interrupt();
-            throw new PulsarClientException("Receive interrupted", e);
-        }
+        return advanceCheckpoint(receiveQueue.take());
     }
 
     @Override
     public Message<T> receive(Duration timeout) throws PulsarClientException {
-        try {
-            return advanceCheckpoint(messageQueue.poll(timeout.toMillis(), 
TimeUnit.MILLISECONDS));
-        } catch (InterruptedException e) {
-            Thread.currentThread().interrupt();
-            throw new PulsarClientException("Receive interrupted", e);
-        }
+        return advanceCheckpoint(receiveQueue.poll(timeout));
     }
 
     @Override
     public Messages<T> receiveMulti(int maxMessages, Duration timeout) throws 
PulsarClientException {
-        List<Message<T>> batch = new ArrayList<>();
-        long deadlineNanos = System.nanoTime() + timeout.toNanos();
-
-        while (batch.size() < maxMessages) {
-            long remainingNanos = deadlineNanos - System.nanoTime();
-            if (remainingNanos <= 0) {
-                break;
-            }
-            try {
-                MessageV5<T> msg = messageQueue.poll(remainingNanos, 
TimeUnit.NANOSECONDS);
-                if (msg == null) {
-                    break;
-                }
-                batch.add(advanceCheckpoint(msg));
-            } catch (InterruptedException e) {
-                Thread.currentThread().interrupt();
-                throw new PulsarClientException("Receive interrupted", e);
-            }
-            // Drain whatever else is immediately ready up to maxMessages.
-            List<Message<T>> drained = new ArrayList<>();
-            messageQueue.drainTo(drained, maxMessages - batch.size());
-            for (Message<T> drainedMsg : drained) {
-                batch.add(advanceCheckpoint(drainedMsg));
-            }
-        }
+        List<Message<T>> batch = receiveQueue.receiveMulti(maxMessages, 
timeout);
+        batch.forEach(this::advanceCheckpoint);
         return new MessagesV5<>(batch);
     }
 
@@ -258,22 +225,17 @@ final class ScalableCheckpointConsumer<T> implements 
CheckpointConsumer<T> {
     // --- Async internals ---
 
     CompletableFuture<Message<T>> receiveAsync() {
-        return CompletableFuture.supplyAsync(() -> {
-            try {
-                return receive();
-            } catch (PulsarClientException e) {
-                throw new CompletionException(e);
-            }
-        });
+        return receiveQueue.receiveAsync().thenApply(this::advanceCheckpoint);
     }
 
     CompletableFuture<Message<T>> receiveAsync(Duration timeout) {
-        return CompletableFuture.supplyAsync(() -> {
-            try {
-                return receive(timeout);
-            } catch (PulsarClientException e) {
-                throw new CompletionException(e);
-            }
+        return 
receiveQueue.receiveAsync(timeout).thenApply(this::advanceCheckpoint);
+    }
+
+    CompletableFuture<List<Message<T>>> receiveMultiAsync(int maxMessages, 
Duration timeout) {
+        return receiveQueue.receiveMultiAsync(maxMessages, 
timeout).thenApply(batch -> {
+            batch.forEach(this::advanceCheckpoint);
+            return batch;
         });
     }
 
@@ -283,6 +245,7 @@ final class ScalableCheckpointConsumer<T> implements 
CheckpointConsumer<T> {
 
     CompletableFuture<Void> closeAsync() {
         closed = true;
+        receiveQueue.close();
         try {
             sourceHandle.close();
         } catch (Exception e) {
@@ -415,7 +378,7 @@ final class ScalableCheckpointConsumer<T> implements 
CheckpointConsumer<T> {
             // taken right after the app received message N could already 
point past
             // N+1 if the read loop got ahead). The advance happens in 
receive() /
             // receiveMulti() instead, where the message crosses into 
application code.
-            messageQueue.add(new MessageV5<>(v4Msg, segmentId));
+            receiveQueue.offer(new MessageV5<>(v4Msg, segmentId));
             if (!closed) {
                 startReadLoop(reader, segmentId);
             }
@@ -431,7 +394,7 @@ final class ScalableCheckpointConsumer<T> implements 
CheckpointConsumer<T> {
                     .TopicTerminatedException) {
                 // Sealed segment fully drained server-side. Close the reader 
and drop
                 // it from the map so resources are released; the segment's 
data has
-                // already crossed into messageQueue.
+                // already crossed into receiveQueue.
                 log.info().attr("segmentId", segmentId)
                         .log("Sealed segment drained, closing reader");
                 segmentReaders.remove(segmentId);
diff --git 
a/pulsar-client-v5/src/main/java/org/apache/pulsar/client/impl/v5/ScalableQueueConsumer.java
 
b/pulsar-client-v5/src/main/java/org/apache/pulsar/client/impl/v5/ScalableQueueConsumer.java
index 3ead54e6955..fef86429bf3 100644
--- 
a/pulsar-client-v5/src/main/java/org/apache/pulsar/client/impl/v5/ScalableQueueConsumer.java
+++ 
b/pulsar-client-v5/src/main/java/org/apache/pulsar/client/impl/v5/ScalableQueueConsumer.java
@@ -29,7 +29,6 @@ import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.CompletionException;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ExecutionException;
-import java.util.concurrent.LinkedTransferQueue;
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicBoolean;
@@ -79,10 +78,10 @@ final class ScalableQueueConsumer<T> implements 
QueueConsumerImpl<T>, DagWatchCl
      */
     private final ConcurrentHashMap<Long, 
CompletableFuture<org.apache.pulsar.client.api.Consumer<T>>>
             segmentConsumers = new ConcurrentHashMap<>();
-    private final LinkedTransferQueue<MessageV5<T>> messageQueue = new 
LinkedTransferQueue<>();
+    private final V5ReceiveQueue<T> receiveQueue;
     /**
      * Where each per-segment receive loop deposits a freshly-arrived message. 
Defaults
-     * to enqueueing on {@link #messageQueue} for the user's {@link 
#receive()} to pull;
+     * to enqueueing on {@link #receiveQueue} for the user's {@link 
#receive()} to pull;
      * the multi-topic wrapper overrides this to forward directly into its 
shared
      * multiplexed queue, so no per-topic pump thread is needed.
      */
@@ -127,10 +126,12 @@ final class ScalableQueueConsumer<T> implements 
QueueConsumerImpl<T>, DagWatchCl
         this.subscriptionName = consumerConf.getSubscriptionName();
         this.dlqPolicy = dlqPolicy;
         this.dlqTopic = dlqPolicy == null ? null : resolveDlqTopic(dlqPolicy);
-        // Default sink enqueues on the local messageQueue for 
receive()/receive(timeout).
+        // Default sink enqueues on the local receiveQueue for 
receive()/receive(timeout).
         // Multi-topic mode passes a sink that forwards into the shared mux 
instead — no
         // per-topic pump thread needed.
-        this.messageSink = messageSink != null ? messageSink : 
messageQueue::add;
+        this.receiveQueue = new V5ReceiveQueue<>(
+                client.v4Client().externalExecutorProvider().getExecutor(), 
client.v4Client().timer());
+        this.messageSink = messageSink != null ? messageSink : 
receiveQueue::offer;
         this.log = LOG.with().attr("topic", topicName).attr("subscription", 
subscriptionName).build();
         this.asyncView = new AsyncQueueConsumerV5<>(this);
     }
@@ -209,22 +210,12 @@ final class ScalableQueueConsumer<T> implements 
QueueConsumerImpl<T>, DagWatchCl
 
     @Override
     public Message<T> receive() throws PulsarClientException {
-        try {
-            return messageQueue.take();
-        } catch (InterruptedException e) {
-            Thread.currentThread().interrupt();
-            throw new PulsarClientException("Receive interrupted", e);
-        }
+        return receiveQueue.take();
     }
 
     @Override
     public Message<T> receive(Duration timeout) throws PulsarClientException {
-        try {
-            return messageQueue.poll(timeout.toMillis(), 
TimeUnit.MILLISECONDS);
-        } catch (InterruptedException e) {
-            Thread.currentThread().interrupt();
-            throw new PulsarClientException("Receive interrupted", e);
-        }
+        return receiveQueue.poll(timeout);
     }
 
     @Override
@@ -281,18 +272,13 @@ final class ScalableQueueConsumer<T> implements 
QueueConsumerImpl<T>, DagWatchCl
 
     @Override
     public CompletableFuture<Message<T>> receiveAsync() {
-        return CompletableFuture.supplyAsync(() -> {
-            try {
-                return receive();
-            } catch (PulsarClientException e) {
-                throw new CompletionException(e);
-            }
-        });
+        return receiveQueue.receiveAsync();
     }
 
     @Override
     public CompletableFuture<Void> closeAsync() {
         closed = true;
+        receiveQueue.close();
         dagWatch.close();
 
         List<CompletableFuture<Void>> futures = new ArrayList<>();
diff --git 
a/pulsar-client-v5/src/main/java/org/apache/pulsar/client/impl/v5/ScalableStreamConsumer.java
 
b/pulsar-client-v5/src/main/java/org/apache/pulsar/client/impl/v5/ScalableStreamConsumer.java
index 3e622baf471..df13bd965f7 100644
--- 
a/pulsar-client-v5/src/main/java/org/apache/pulsar/client/impl/v5/ScalableStreamConsumer.java
+++ 
b/pulsar-client-v5/src/main/java/org/apache/pulsar/client/impl/v5/ScalableStreamConsumer.java
@@ -28,8 +28,6 @@ import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.CompletionException;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ExecutionException;
-import java.util.concurrent.LinkedTransferQueue;
-import java.util.concurrent.TimeUnit;
 import org.apache.pulsar.client.api.KeySharedPolicy;
 import org.apache.pulsar.client.api.Range;
 import org.apache.pulsar.client.api.SubscriptionType;
@@ -92,10 +90,10 @@ final class ScalableStreamConsumer<T>
     private final ConcurrentHashMap<Long, 
org.apache.pulsar.client.api.MessageId> latestDelivered =
             new ConcurrentHashMap<>();
 
-    private final LinkedTransferQueue<MessageV5<T>> messageQueue = new 
LinkedTransferQueue<>();
+    private final V5ReceiveQueue<T> receiveQueue;
     /**
      * Where each per-segment receive loop deposits a freshly-arrived message. 
Defaults
-     * to enqueueing on {@link #messageQueue} for the user's {@link 
#receive()} to pull;
+     * to enqueueing on {@link #receiveQueue} for the user's {@link 
#receive()} to pull;
      * the multi-topic wrapper overrides this to forward into its shared 
multiplexed
      * queue, applying its own multi-topic position-vector capture in the 
process.
      */
@@ -117,7 +115,9 @@ final class ScalableStreamConsumer<T>
         this.session = session;
         this.topicName = topicName;
         this.subscriptionName = consumerConf.getSubscriptionName();
-        this.messageSink = messageSink != null ? messageSink : 
messageQueue::add;
+        this.receiveQueue = new V5ReceiveQueue<>(
+                client.v4Client().externalExecutorProvider().getExecutor(), 
client.v4Client().timer());
+        this.messageSink = messageSink != null ? messageSink : 
receiveQueue::offer;
         this.log = LOG.with().attr("topic", topicName).attr("subscription", 
subscriptionName).build();
         this.asyncView = new AsyncStreamConsumerV5<>(this);
     }
@@ -199,48 +199,17 @@ final class ScalableStreamConsumer<T>
 
     @Override
     public Message<T> receive() throws PulsarClientException {
-        try {
-            return messageQueue.take();
-        } catch (InterruptedException e) {
-            Thread.currentThread().interrupt();
-            throw new PulsarClientException("Receive interrupted", e);
-        }
+        return receiveQueue.take();
     }
 
     @Override
     public Message<T> receive(Duration timeout) throws PulsarClientException {
-        try {
-            return messageQueue.poll(timeout.toMillis(), 
TimeUnit.MILLISECONDS);
-        } catch (InterruptedException e) {
-            Thread.currentThread().interrupt();
-            throw new PulsarClientException("Receive interrupted", e);
-        }
+        return receiveQueue.poll(timeout);
     }
 
     @Override
     public Messages<T> receiveMulti(int maxNumMessages, Duration timeout) 
throws PulsarClientException {
-        List<Message<T>> batch = new ArrayList<>();
-        long deadlineNanos = System.nanoTime() + timeout.toNanos();
-
-        while (batch.size() < maxNumMessages) {
-            long remainingNanos = deadlineNanos - System.nanoTime();
-            if (remainingNanos <= 0) {
-                break;
-            }
-            try {
-                MessageV5<T> msg = messageQueue.poll(remainingNanos, 
TimeUnit.NANOSECONDS);
-                if (msg == null) {
-                    break;
-                }
-                batch.add(msg);
-            } catch (InterruptedException e) {
-                Thread.currentThread().interrupt();
-                throw new PulsarClientException("Receive interrupted", e);
-            }
-            // Drain any immediately available messages
-            messageQueue.drainTo(batch, maxNumMessages - batch.size());
-        }
-        return new MessagesV5<>(batch);
+        return new MessagesV5<>(receiveQueue.receiveMulti(maxNumMessages, 
timeout));
     }
 
     @Override
@@ -292,27 +261,20 @@ final class ScalableStreamConsumer<T>
     // --- Async internals ---
 
     CompletableFuture<Message<T>> receiveAsync() {
-        return CompletableFuture.supplyAsync(() -> {
-            try {
-                return receive();
-            } catch (PulsarClientException e) {
-                throw new CompletionException(e);
-            }
-        });
+        return receiveQueue.receiveAsync();
     }
 
     CompletableFuture<Message<T>> receiveAsync(Duration timeout) {
-        return CompletableFuture.supplyAsync(() -> {
-            try {
-                return receive(timeout);
-            } catch (PulsarClientException e) {
-                throw new CompletionException(e);
-            }
-        });
+        return receiveQueue.receiveAsync(timeout);
+    }
+
+    CompletableFuture<List<Message<T>>> receiveMultiAsync(int maxNumMessages, 
Duration timeout) {
+        return receiveQueue.receiveMultiAsync(maxNumMessages, timeout);
     }
 
     CompletableFuture<Void> closeAsync() {
         closed = true;
+        receiveQueue.close();
         session.close();
 
         List<CompletableFuture<Void>> futures = new ArrayList<>();
diff --git 
a/pulsar-client-v5/src/main/java/org/apache/pulsar/client/impl/v5/V5ReceiveQueue.java
 
b/pulsar-client-v5/src/main/java/org/apache/pulsar/client/impl/v5/V5ReceiveQueue.java
new file mode 100644
index 00000000000..92a8e479417
--- /dev/null
+++ 
b/pulsar-client-v5/src/main/java/org/apache/pulsar/client/impl/v5/V5ReceiveQueue.java
@@ -0,0 +1,253 @@
+/*
+ * 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.pulsar.client.impl.v5;
+
+import io.netty.util.Timeout;
+import io.netty.util.Timer;
+import java.time.Duration;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.TimeUnit;
+import org.apache.pulsar.client.api.v5.Message;
+import org.apache.pulsar.client.api.v5.PulsarClientException;
+
+/**
+ * Async-native, single-consumer receive queue shared by the v5 scalable 
consumers.
+ *
+ * <p>Mirrors the v4 {@code ConsumerBase} delivery model: a buffer of ready 
messages
+ * plus a queue of pending receive futures. Both are confined to one pinned 
executor
+ * (obtained from the client's external executor provider, so one thread per 
consumer),
+ * which means a message and a waiter can never cross — no locks, and no lost 
wakeups.
+ * Every receive future is completed on that executor, so user continuations 
chained on
+ * the returned {@link CompletableFuture} never run on a netty IO thread.
+ *
+ * <p>This replaces the previous {@code LinkedTransferQueue} + {@code 
supplyAsync(take())}
+ * approach: {@link #receiveAsync()} parks no thread, is cancelable, and 
honours timeouts
+ * via the client timer instead of blocking a {@code 
ForkJoinPool.commonPool()} worker.
+ */
+final class V5ReceiveQueue<T> {
+
+    private final ExecutorService executor;
+    private final Timer timer;
+
+    // Both touched only on `executor`, so plain (non-concurrent) collections 
are safe.
+    private final ArrayDeque<Message<T>> buffer = new ArrayDeque<>();
+    private final ArrayDeque<CompletableFuture<Message<T>>> pendingReceives = 
new ArrayDeque<>();
+    private boolean closed = false;
+
+    V5ReceiveQueue(ExecutorService executor, Timer timer) {
+        this.executor = executor;
+        this.timer = timer;
+    }
+
+    /**
+     * Deposit a freshly-arrived message. Called from the per-segment receive 
loops (which
+     * run on a v4 client executor). Hands the message straight to a waiting 
receive future
+     * if there is one, otherwise buffers it.
+     */
+    void offer(Message<T> msg) {
+        executor.execute(() -> {
+            if (closed) {
+                return;
+            }
+            CompletableFuture<Message<T>> waiter = pollWaiter();
+            if (waiter != null) {
+                waiter.complete(msg);
+            } else {
+                buffer.add(msg);
+            }
+        });
+    }
+
+    /** Receive a message, completing as soon as one is available. Never 
blocks a thread. */
+    CompletableFuture<Message<T>> receiveAsync() {
+        CompletableFuture<Message<T>> result = new CompletableFuture<>();
+        executor.execute(() -> {
+            if (closed) {
+                result.completeExceptionally(alreadyClosed());
+                return;
+            }
+            Message<T> msg = buffer.poll();
+            if (msg != null) {
+                result.complete(msg);
+            } else {
+                pendingReceives.add(result);
+            }
+        });
+        return result;
+    }
+
+    /**
+     * Receive a message, completing with {@code null} if none arrives within 
{@code timeout}.
+     * The timeout is armed on the client timer; no thread is parked while 
waiting.
+     */
+    CompletableFuture<Message<T>> receiveAsync(Duration timeout) {
+        CompletableFuture<Message<T>> result = new CompletableFuture<>();
+        executor.execute(() -> {
+            if (closed) {
+                result.completeExceptionally(alreadyClosed());
+                return;
+            }
+            Message<T> msg = buffer.poll();
+            if (msg != null) {
+                result.complete(msg);
+                return;
+            }
+            long millis = timeout.toMillis();
+            if (millis <= 0) {
+                result.complete(null);
+                return;
+            }
+            pendingReceives.add(result);
+            Timeout t = timer.newTimeout(ignored -> executor.execute(() -> {
+                if (!result.isDone()) {
+                    pendingReceives.remove(result);
+                    result.complete(null);
+                }
+            }), millis, TimeUnit.MILLISECONDS);
+            // Cancel the timer when the message is handed off (or on close) 
so it doesn't linger.
+            result.whenComplete((r, e) -> t.cancel());
+        });
+        return result;
+    }
+
+    /**
+     * Receive up to {@code maxMessages}, blocking (asynchronously) up to 
{@code timeout} for
+     * the batch. Waits for the first message, then opportunistically drains 
whatever else is
+     * already buffered, repeating until the batch is full or the deadline 
passes.
+     */
+    CompletableFuture<List<Message<T>>> receiveMultiAsync(int maxMessages, 
Duration timeout) {
+        long deadlineNanos = System.nanoTime() + timeout.toNanos();
+        CompletableFuture<List<Message<T>>> result = new CompletableFuture<>();
+        collectMulti(new ArrayList<>(), maxMessages, deadlineNanos, result);
+        return result;
+    }
+
+    private void collectMulti(List<Message<T>> batch, int max, long 
deadlineNanos,
+                              CompletableFuture<List<Message<T>>> result) {
+        if (batch.size() >= max) {
+            result.complete(batch);
+            return;
+        }
+        long remainingNanos = deadlineNanos - System.nanoTime();
+        if (remainingNanos <= 0) {
+            result.complete(batch);
+            return;
+        }
+        receiveAsync(Duration.ofNanos(remainingNanos)).whenComplete((msg, ex) 
-> {
+            if (ex != null) {
+                result.completeExceptionally(ex);
+            } else if (msg == null) {
+                result.complete(batch);
+            } else {
+                batch.add(msg);
+                drainReady(batch, max).thenRun(() -> collectMulti(batch, max, 
deadlineNanos, result));
+            }
+        });
+    }
+
+    /** Move whatever is already buffered into {@code batch} (up to {@code 
max} total). */
+    private CompletableFuture<Void> drainReady(List<Message<T>> batch, int 
max) {
+        CompletableFuture<Void> done = new CompletableFuture<>();
+        executor.execute(() -> {
+            Message<T> m;
+            while (batch.size() < max && (m = buffer.poll()) != null) {
+                batch.add(m);
+            }
+            done.complete(null);
+        });
+        return done;
+    }
+
+    // --- Blocking views, for the synchronous receive() API. Block only the 
caller's thread. ---
+
+    Message<T> take() throws PulsarClientException {
+        try {
+            return receiveAsync().get();
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            throw new PulsarClientException("Receive interrupted", e);
+        } catch (ExecutionException e) {
+            throw unwrap(e);
+        }
+    }
+
+    Message<T> poll(Duration timeout) throws PulsarClientException {
+        try {
+            return receiveAsync(timeout).get();
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            throw new PulsarClientException("Receive interrupted", e);
+        } catch (ExecutionException e) {
+            throw unwrap(e);
+        }
+    }
+
+    List<Message<T>> receiveMulti(int maxMessages, Duration timeout) throws 
PulsarClientException {
+        try {
+            return receiveMultiAsync(maxMessages, timeout).get();
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            throw new PulsarClientException("Receive interrupted", e);
+        } catch (ExecutionException e) {
+            throw unwrap(e);
+        }
+    }
+
+    /** Fail any outstanding receives so blocked/awaiting callers wake instead 
of hanging forever. */
+    void close() {
+        executor.execute(() -> {
+            closed = true;
+            CompletableFuture<Message<T>> waiter;
+            while ((waiter = pendingReceives.poll()) != null) {
+                if (!waiter.isDone()) {
+                    waiter.completeExceptionally(alreadyClosed());
+                }
+            }
+            buffer.clear();
+        });
+    }
+
+    private CompletableFuture<Message<T>> pollWaiter() {
+        CompletableFuture<Message<T>> waiter;
+        // Skip futures already completed by cancellation or timeout.
+        while ((waiter = pendingReceives.poll()) != null) {
+            if (!waiter.isDone()) {
+                return waiter;
+            }
+        }
+        return null;
+    }
+
+    private static PulsarClientException alreadyClosed() {
+        return new PulsarClientException.AlreadyClosedException("Consumer 
already closed");
+    }
+
+    private static PulsarClientException unwrap(ExecutionException e) {
+        Throwable cause = e.getCause();
+        if (cause instanceof PulsarClientException pce) {
+            return pce;
+        }
+        return new PulsarClientException(cause != null ? cause : e);
+    }
+}
diff --git 
a/pulsar-client-v5/src/test/java/org/apache/pulsar/client/impl/v5/V5ReceiveQueueTest.java
 
b/pulsar-client-v5/src/test/java/org/apache/pulsar/client/impl/v5/V5ReceiveQueueTest.java
new file mode 100644
index 00000000000..61d326c18aa
--- /dev/null
+++ 
b/pulsar-client-v5/src/test/java/org/apache/pulsar/client/impl/v5/V5ReceiveQueueTest.java
@@ -0,0 +1,248 @@
+/*
+ * 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.pulsar.client.impl.v5;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNull;
+import static org.testng.Assert.assertSame;
+import static org.testng.Assert.assertTrue;
+import static org.testng.Assert.expectThrows;
+import io.netty.util.HashedWheelTimer;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import org.apache.pulsar.client.api.v5.Message;
+import org.apache.pulsar.client.api.v5.MessageId;
+import org.apache.pulsar.client.api.v5.PulsarClientException;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+public class V5ReceiveQueueTest {
+
+    private ExecutorService executor;
+    private HashedWheelTimer timer;
+    private V5ReceiveQueue<Integer> queue;
+
+    @BeforeMethod
+    public void setup() {
+        executor = Executors.newSingleThreadExecutor();
+        timer = new HashedWheelTimer();
+        queue = new V5ReceiveQueue<>(executor, timer);
+    }
+
+    @AfterMethod(alwaysRun = true)
+    public void teardown() {
+        executor.shutdownNow();
+        timer.stop();
+    }
+
+    @Test
+    public void bufferedMessageIsDeliveredToReceiveAsync() throws Exception {
+        Message<Integer> m = msg(1);
+        queue.offer(m);
+        assertSame(queue.receiveAsync().get(5, TimeUnit.SECONDS), m);
+    }
+
+    @Test
+    public void receiveAsyncCompletesWhenMessageArrivesLater() throws 
Exception {
+        CompletableFuture<Message<Integer>> f = queue.receiveAsync();
+        assertTrue(!f.isDone());
+        Message<Integer> m = msg(1);
+        queue.offer(m);
+        assertSame(f.get(5, TimeUnit.SECONDS), m);
+    }
+
+    @Test
+    public void deliversInFifoOrder() throws Exception {
+        Message<Integer> m1 = msg(1);
+        Message<Integer> m2 = msg(2);
+        Message<Integer> m3 = msg(3);
+        queue.offer(m1);
+        queue.offer(m2);
+        queue.offer(m3);
+        assertSame(queue.receiveAsync().get(5, TimeUnit.SECONDS), m1);
+        assertSame(queue.receiveAsync().get(5, TimeUnit.SECONDS), m2);
+        assertSame(queue.receiveAsync().get(5, TimeUnit.SECONDS), m3);
+    }
+
+    @Test
+    public void timedReceiveReturnsNullOnTimeout() throws Exception {
+        assertNull(queue.receiveAsync(Duration.ofMillis(150)).get(5, 
TimeUnit.SECONDS));
+    }
+
+    @Test
+    public void messageBeatsTimeout() throws Exception {
+        CompletableFuture<Message<Integer>> f = 
queue.receiveAsync(Duration.ofSeconds(30));
+        Message<Integer> m = msg(1);
+        queue.offer(m);
+        assertSame(f.get(5, TimeUnit.SECONDS), m);
+    }
+
+    @Test
+    public void blockingTakeReturnsBufferedMessage() throws Exception {
+        Message<Integer> m = msg(1);
+        queue.offer(m);
+        assertSame(queue.take(), m);
+    }
+
+    @Test
+    public void blockingPollTimesOutToNull() throws Exception {
+        assertNull(queue.poll(Duration.ofMillis(150)));
+    }
+
+    @Test
+    public void closeFailsPendingReceive() {
+        CompletableFuture<Message<Integer>> f = queue.receiveAsync();
+        queue.close();
+        ExecutionException ex = expectThrows(ExecutionException.class, () -> 
f.get(5, TimeUnit.SECONDS));
+        assertTrue(ex.getCause() instanceof 
PulsarClientException.AlreadyClosedException);
+    }
+
+    @Test
+    public void receiveAfterCloseFailsFast() {
+        queue.close();
+        ExecutionException ex = expectThrows(ExecutionException.class,
+                () -> queue.receiveAsync().get(5, TimeUnit.SECONDS));
+        assertTrue(ex.getCause() instanceof 
PulsarClientException.AlreadyClosedException);
+    }
+
+    @Test
+    public void cancelledWaiterDoesNotConsumeMessage() throws Exception {
+        CompletableFuture<Message<Integer>> cancelled = queue.receiveAsync();
+        cancelled.cancel(true);
+        Message<Integer> m = msg(1);
+        queue.offer(m);
+        // The message must reach a live receiver, not be swallowed by the 
cancelled one.
+        assertSame(queue.receiveAsync().get(5, TimeUnit.SECONDS), m);
+    }
+
+    @Test
+    public void receiveMultiReturnsUpToMax() throws Exception {
+        for (int i = 1; i <= 5; i++) {
+            queue.offer(msg(i));
+        }
+        List<Message<Integer>> batch = queue.receiveMulti(3, 
Duration.ofSeconds(5));
+        assertEquals(batch.size(), 3);
+        assertEquals(batch.get(0).value(), Integer.valueOf(1));
+        assertEquals(batch.get(1).value(), Integer.valueOf(2));
+        assertEquals(batch.get(2).value(), Integer.valueOf(3));
+    }
+
+    @Test
+    public void receiveMultiReturnsPartialOnTimeout() throws Exception {
+        queue.offer(msg(1));
+        queue.offer(msg(2));
+        List<Message<Integer>> batch = queue.receiveMultiAsync(5, 
Duration.ofMillis(300))
+                .get(5, TimeUnit.SECONDS);
+        assertEquals(batch.size(), 2);
+    }
+
+    @Test
+    public void receiveMultiEmptyOnTimeoutWithNoMessages() throws Exception {
+        List<Message<Integer>> batch = queue.receiveMultiAsync(5, 
Duration.ofMillis(150))
+                .get(5, TimeUnit.SECONDS);
+        assertTrue(batch.isEmpty());
+    }
+
+    private static Message<Integer> msg(int id) {
+        return new IntMessage(id);
+    }
+
+    /** Minimal {@link Message} whose {@link #value()} carries a test id; 
other accessors are inert. */
+    private static final class IntMessage implements Message<Integer> {
+        private final int id;
+
+        IntMessage(int id) {
+            this.id = id;
+        }
+
+        @Override
+        public Integer value() {
+            return id;
+        }
+
+        @Override
+        public byte[] data() {
+            return new byte[0];
+        }
+
+        @Override
+        public MessageId id() {
+            return null;
+        }
+
+        @Override
+        public Optional<String> key() {
+            return Optional.empty();
+        }
+
+        @Override
+        public Map<String, String> properties() {
+            return Map.of();
+        }
+
+        @Override
+        public Instant publishTime() {
+            return Instant.EPOCH;
+        }
+
+        @Override
+        public Optional<Instant> eventTime() {
+            return Optional.empty();
+        }
+
+        @Override
+        public long sequenceId() {
+            return id;
+        }
+
+        @Override
+        public Optional<String> producerName() {
+            return Optional.empty();
+        }
+
+        @Override
+        public String topic() {
+            return "test";
+        }
+
+        @Override
+        public int redeliveryCount() {
+            return 0;
+        }
+
+        @Override
+        public int size() {
+            return 0;
+        }
+
+        @Override
+        public Optional<String> replicatedFrom() {
+            return Optional.empty();
+        }
+    }
+}

Reply via email to