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 f1dc6391d2ce [improve][client] Avoid per-message cross-topic vector 
deep copy in v5 multi-topic stream consumer (#26162)
f1dc6391d2ce is described below

commit f1dc6391d2ce9825ee9963c5a5f4c7521ae772e3
Author: Matteo Merli <[email protected]>
AuthorDate: Wed Jul 8 07:46:44 2026 -0700

    [improve][client] Avoid per-message cross-topic vector deep copy in v5 
multi-topic stream consumer (#26162)
---
 .../client/impl/v5/MultiTopicStreamConsumer.java   | 48 ++++++++++------------
 1 file changed, 22 insertions(+), 26 deletions(-)

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 7e0925692141..a743d1882173 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
@@ -83,11 +83,12 @@ final class MultiTopicStreamConsumer<T> implements 
StreamConsumer<T> {
     private final V5ReceiveQueue<T> mux;
 
     /**
-     * Tracks the latest delivered message id per (parent topic, segment id) 
across
-     * every per-topic consumer. Snapshotted at enqueue time for each delivered
-     * message so cumulative ack covers everything visible up to that message.
+     * Latest-delivered position per parent topic, each an <em>immutable</em> 
per-segment slice
+     * (the message's own position vector). Snapshotted — shallowly, sharing 
those immutable
+     * slices — at enqueue time for each delivered message so cumulative ack 
covers everything
+     * visible up to that message.
      */
-    private final ConcurrentHashMap<String, ConcurrentHashMap<Long, 
org.apache.pulsar.client.api.MessageId>>
+    private final ConcurrentHashMap<String, Map<Long, 
org.apache.pulsar.client.api.MessageId>>
             latestDeliveredPerTopicSegment = new ConcurrentHashMap<>();
 
     private volatile boolean closed = false;
@@ -264,10 +265,10 @@ final class MultiTopicStreamConsumer<T> implements 
StreamConsumer<T> {
             return CompletableFuture.completedFuture(null);
         }
         // Flush: ack everything we delivered for this topic.
-        ConcurrentHashMap<Long, org.apache.pulsar.client.api.MessageId> latest 
=
+        Map<Long, org.apache.pulsar.client.api.MessageId> latest =
                 latestDeliveredPerTopicSegment.remove(topicName);
         if (latest != null && !latest.isEmpty()) {
-            state.consumer.ackUpToVector(new HashMap<>(latest));
+            state.consumer.ackUpToVector(latest);
         }
         return state.consumer.closeAsync()
                 .thenRun(() -> log.info().attr("topic", topicName)
@@ -417,9 +418,12 @@ final class MultiTopicStreamConsumer<T> implements 
StreamConsumer<T> {
      * message id; we adopt that as the per-topic slice of our cross-topic
      * vector, snapshot the full map, and forward into the shared mux.
      *
-     * <p>Runs on the netty IO thread that delivered the per-segment message —
-     * the only contention is the synchronized snapshot block which guards
-     * against torn cross-topic views during concurrent deliveries.
+     * <p>Runs on the netty IO thread that delivered the per-segment message. 
No lock: the
+     * message's own {@code positionVector} is already an immutable, complete 
snapshot of this
+     * topic's live segments, so we adopt it as the topic's slice and take a 
<em>shallow</em>
+     * copy of the cross-topic map. Because every slice is immutable, a 
concurrent delivery to
+     * another topic only ever swaps in a whole new slice — the snapshot can 
never observe a
+     * torn per-topic view — which also removes the per-message 
O(topics×segments) deep copy.
      */
     private CompletableFuture<Void> onPerTopicMessage(String parentTopic, 
MessageV5<T> msg) {
         if (closed) {
@@ -427,23 +431,15 @@ final class MultiTopicStreamConsumer<T> implements 
StreamConsumer<T> {
         }
         MessageIdV5 origId = (MessageIdV5) msg.id();
 
-        // Adopt the message's own positionVector as our per-topic 
latest-delivered
-        // slice. ScalableStreamConsumer maintained the increasing invariant on
-        // each segment id; merging via putAll keeps the property cross-topic.
-        ConcurrentHashMap<Long, org.apache.pulsar.client.api.MessageId> ours =
-                latestDeliveredPerTopicSegment.computeIfAbsent(parentTopic,
-                        k -> new ConcurrentHashMap<>());
-        ours.putAll(origId.positionVector());
-
-        // Snapshot the cross-topic vector under lock so concurrent deliveries
-        // can't observe a torn view.
-        Map<String, Map<Long, org.apache.pulsar.client.api.MessageId>> 
snapshot;
-        synchronized (latestDeliveredPerTopicSegment) {
-            snapshot = new HashMap<>(latestDeliveredPerTopicSegment.size());
-            for (var e : latestDeliveredPerTopicSegment.entrySet()) {
-                snapshot.put(e.getKey(), new HashMap<>(e.getValue()));
-            }
-        }
+        // Adopt the message's own (immutable) position vector as this topic's 
latest-delivered
+        // slice. It already covers every live segment of the topic up to this 
message; segments
+        // that sealed or rebalanced away are gone from segmentConsumers, so 
acking them is a
+        // no-op — dropping them here is observationally identical to the old 
accumulate-forever.
+        latestDeliveredPerTopicSegment.put(parentTopic, 
origId.positionVector());
+
+        // Shallow snapshot: O(topics), values are the shared immutable slices 
above.
+        Map<String, Map<Long, org.apache.pulsar.client.api.MessageId>> 
snapshot =
+                new HashMap<>(latestDeliveredPerTopicSegment);
 
         MessageIdV5 newId = new MessageIdV5(
                 origId.v4MessageId(), origId.segmentId(),

Reply via email to