3030import java .util .concurrent .TimeUnit ;
3131import org .apache .pulsar .client .api .v5 .Message ;
3232import org .apache .pulsar .client .api .v5 .PulsarClientException ;
33+ import org .apache .pulsar .common .util .collections .GrowableArrayBlockingQueue ;
3334
3435/**
35- * Async-native, single-consumer receive queue shared by the v5 scalable consumers.
36+ * Async-native receive queue shared by the v5 scalable consumers.
3637 *
37- * <p>Mirrors the v4 {@code ConsumerBase} delivery model: a buffer of ready messages
38- * plus a queue of pending receive futures. Both are confined to one pinned executor
39- * (obtained from the client's external executor provider, so one thread per consumer),
40- * which means a message and a waiter can never cross — no locks, and no lost wakeups.
41- * Every receive future is completed on that executor, so user continuations chained on
42- * the returned {@link CompletableFuture} never run on a netty IO thread.
38+ * <p>Mirrors the v4 {@code ConsumerBase} delivery model. The buffer of ready messages is a
39+ * thread-safe blocking queue: producers (the per-segment receive loops, on a v4 client executor)
40+ * append to it directly, and the blocking {@link #take()}/{@link #poll(Duration)} pull from it on
41+ * the caller's own thread, so in steady state a message never has to cross to another thread just
42+ * to be handed over. Only the bookkeeping that needs ordering — the pending {@link #receiveAsync()}
43+ * futures and the paused producers — is confined to one pinned executor (obtained from the
44+ * client's external executor provider, so one thread per consumer), and every async receive
45+ * future is completed there, so user continuations chained on it never run on a netty IO thread.
4346 *
44- * <p>This replaces the previous {@code LinkedTransferQueue} + {@code supplyAsync(take())}
45- * approach: {@link #receiveAsync()} parks no thread, is cancelable, and honours timeouts
46- * via the client timer instead of blocking a {@code ForkJoinPool.commonPool()} worker.
47- *
48- * <p>Backpressure: {@link #offer} returns a future that completes only when the buffer has
49- * room. Producers gate re-arming on it, so the buffer is bounded by {@code receiverQueueSize}
50- * plus one in-flight message per producer (each producer offers at most one message before
51- * observing the pause) — modelled on v4 {@code MultiTopicsConsumerImpl}'s pause/resume of
52- * sub-consumers, including its fairness rule: once any producer is paused, further offers
53- * pause at the half-way mark too, so active producers can't hold the buffer above the resume
54- * threshold and starve the paused ones.
47+ * <p>Backpressure: {@link #offer} returns a future that completes only when the buffer has room.
48+ * Producers gate re-arming on it, so the buffer is bounded by {@code receiverQueueSize} plus one
49+ * in-flight message per producer (each producer offers at most one message before observing the
50+ * pause) — modelled on v4 {@code MultiTopicsConsumerImpl}'s pause/resume of sub-consumers,
51+ * including its fairness rule: once any producer is paused, further offers pause at the half-way
52+ * mark too, so active producers can't hold the buffer above the resume threshold and starve the
53+ * paused ones.
5554 */
5655final class V5ReceiveQueue <T > {
5756
@@ -64,18 +63,23 @@ final class V5ReceiveQueue<T> {
6463 private final int highWatermark ;
6564 private final int lowWatermark ;
6665
67- // All three touched only on `executor`, so plain (non-concurrent) collections are safe.
68- private final ArrayDeque <Message <T >> buffer = new ArrayDeque <>();
66+ /**
67+ * Ready messages. Thread-safe: producers append from their own threads, blocking receives
68+ * poll from the caller thread, and the executor drains it for pending async receives.
69+ */
70+ private final GrowableArrayBlockingQueue <Message <T >> buffer = new GrowableArrayBlockingQueue <>();
71+
72+ // Both touched only on `executor`, so plain (non-concurrent) collections are safe.
6973 private final ArrayDeque <CompletableFuture <Message <T >>> pendingReceives = new ArrayDeque <>();
7074 // Capacity futures handed back to producers that were paused because the buffer was full.
7175 private final ArrayDeque <CompletableFuture <Void >> capacityWaiters = new ArrayDeque <>();
72- private boolean closed = false ;
7376
74- // Snapshots of executor-confined state, readable from producer threads so the offer()
75- // fast path can decide without a hop. Written only on `executor`; may lag by the offers
76- // still queued, so each producer (one in-flight offer at a time) can overshoot by one.
77- private volatile int approxBufferSize = 0 ;
77+ // Snapshots of the executor-confined state, readable from producer and receiver threads so
78+ // the hot paths can decide without a hop. Written only on `executor` (except `closed`).
79+ /** True while {@link #pendingReceives} may hold a future that the next message belongs to. */
80+ private volatile boolean hasPendingReceives = false ;
7881 private volatile boolean producersPaused = false ;
82+ private volatile boolean closed = false ;
7983
8084 V5ReceiveQueue (ExecutorService executor , Timer timer , int receiverQueueSize ) {
8185 this .executor = executor ;
@@ -86,54 +90,44 @@ final class V5ReceiveQueue<T> {
8690
8791 /**
8892 * Deposit a freshly-arrived message. Called from the per-segment receive loops (which
89- * run on a v4 client executor). Hands the message straight to a waiting receive future
90- * if there is one, otherwise buffers it.
93+ * run on a v4 client executor). Appends straight to the buffer — waking a blocked
94+ * {@link #take()}/{@link #poll(Duration)} if it was empty — and hands it over on the
95+ * executor only if an async receive is waiting.
9196 *
9297 * @return a future that completes when the sink is ready for the next message — right
93- * away unless the buffer is filling up , in which case it defers until the consumer
94- * drains it below the low watermark (backpressure).
98+ * away unless the buffer is full , in which case it defers until the consumer drains it
99+ * below the low watermark (backpressure).
95100 */
96101 CompletableFuture <Void > offer (Message <T > msg ) {
97- // Fast path, decided on the caller thread: while the buffer is comfortably below the
98- // watermarks and nobody is paused, grant capacity with a shared completed future so
99- // the (fast-consumer) hot path pays no allocation and no serialized hop through our
100- // executor before the segment loop re-arms.
101- if (!producersPaused && approxBufferSize < lowWatermark ) {
102- executor .execute (() -> doOffer (msg , null ));
102+ if (closed ) {
103+ // Dropped, like a message arriving on an already-closed v4 consumer.
104+ return READY ;
105+ }
106+ buffer .put (msg );
107+ if (hasPendingReceives ) {
108+ // A receiver registering concurrently re-checks the buffer after publishing the
109+ // flag, and we read the flag after appending, so one side always sees the other.
110+ executor .execute (this ::drainToPendingReceives );
111+ }
112+ // Fast path, decided on the caller thread: while the buffer is below the high watermark
113+ // and nobody is paused, grant capacity with a shared completed future so the hot path
114+ // pays no allocation and no serialized hop through our executor before the segment
115+ // loop re-arms.
116+ if (!producersPaused && buffer .size () < highWatermark ) {
103117 return READY ;
104118 }
105119 CompletableFuture <Void > capacity = new CompletableFuture <>();
106- executor .execute (() -> doOffer ( msg , capacity ));
120+ executor .execute (() -> grantCapacity ( capacity ));
107121 return capacity ;
108122 }
109123
110- /** Runs on {@code executor}. {@code capacity} is null when the fast path already granted it. */
111- private void doOffer (Message <T > msg , CompletableFuture <Void > capacity ) {
112- if (closed ) {
113- if (capacity != null ) {
114- capacity .complete (null );
115- }
116- return ;
117- }
118- CompletableFuture <Message <T >> waiter = pollWaiter ();
119- if (waiter != null ) {
120- // Handed straight to a waiting receiver; the buffer didn't grow.
121- waiter .complete (msg );
122- if (capacity != null ) {
123- capacity .complete (null );
124- }
125- return ;
126- }
127- buffer .add (msg );
128- approxBufferSize = buffer .size ();
129- if (capacity == null ) {
130- return ;
131- }
124+ /** Runs on {@code executor}: park the producer while the buffer is full, else grant now. */
125+ private void grantCapacity (CompletableFuture <Void > capacity ) {
132126 // Pause when full — or, once any producer is paused, already at the half-way mark, so
133127 // active producers can't keep the buffer hovering above the resume threshold while the
134128 // paused ones starve (v4 MultiTopicsConsumerImpl's fairness clause).
135- if (buffer .size () >= highWatermark
136- || (!capacityWaiters .isEmpty () && buffer .size () > lowWatermark )) {
129+ if (! closed && ( buffer .size () >= highWatermark
130+ || (!capacityWaiters .isEmpty () && buffer .size () > lowWatermark ))) {
137131 capacityWaiters .add (capacity );
138132 producersPaused = true ;
139133 } else {
@@ -156,6 +150,13 @@ private void maybeResumeProducers() {
156150 }
157151 }
158152
153+ /** After a receive pulled straight from the buffer: resume producers once it drained enough. */
154+ private void afterDirectPoll () {
155+ if (producersPaused && buffer .size () <= lowWatermark ) {
156+ executor .execute (this ::maybeResumeProducers );
157+ }
158+ }
159+
159160 /** Receive a message, completing as soon as one is available. Never blocks a thread. */
160161 CompletableFuture <Message <T >> receiveAsync () {
161162 CompletableFuture <Message <T >> result = new CompletableFuture <>();
@@ -164,13 +165,17 @@ CompletableFuture<Message<T>> receiveAsync() {
164165 result .completeExceptionally (alreadyClosed ());
165166 return ;
166167 }
168+ if (result .isDone ()) {
169+ // Cancelled before we got here: it must not consume a message that has since
170+ // been appended straight to the buffer.
171+ return ;
172+ }
167173 Message <T > msg = buffer .poll ();
168174 if (msg != null ) {
169- approxBufferSize = buffer .size ();
170175 result .complete (msg );
171176 maybeResumeProducers ();
172177 } else {
173- pendingReceives . add (result );
178+ addPendingReceive (result );
174179 }
175180 });
176181 return result ;
@@ -187,9 +192,13 @@ CompletableFuture<Message<T>> receiveAsync(Duration timeout) {
187192 result .completeExceptionally (alreadyClosed ());
188193 return ;
189194 }
195+ if (result .isDone ()) {
196+ // Cancelled before we got here: it must not consume a message that has since
197+ // been appended straight to the buffer.
198+ return ;
199+ }
190200 Message <T > msg = buffer .poll ();
191201 if (msg != null ) {
192- approxBufferSize = buffer .size ();
193202 result .complete (msg );
194203 maybeResumeProducers ();
195204 return ;
@@ -199,19 +208,44 @@ CompletableFuture<Message<T>> receiveAsync(Duration timeout) {
199208 result .complete (null );
200209 return ;
201210 }
202- pendingReceives .add (result );
203211 Timeout t = timer .newTimeout (ignored -> executor .execute (() -> {
204212 if (!result .isDone ()) {
205213 pendingReceives .remove (result );
214+ hasPendingReceives = !pendingReceives .isEmpty ();
206215 result .complete (null );
207216 }
208217 }), millis , TimeUnit .MILLISECONDS );
209218 // Cancel the timer when the message is handed off (or on close) so it doesn't linger.
210219 result .whenComplete ((r , e ) -> t .cancel ());
220+ addPendingReceive (result );
211221 });
212222 return result ;
213223 }
214224
225+ /** Runs on {@code executor}: park an async receive until the next message arrives. */
226+ private void addPendingReceive (CompletableFuture <Message <T >> result ) {
227+ pendingReceives .add (result );
228+ hasPendingReceives = true ;
229+ // A producer that appended between our poll and this flag store saw the flag clear and
230+ // posted no drain, so look again now that the flag is published (both are volatile).
231+ drainToPendingReceives ();
232+ }
233+
234+ /** Runs on {@code executor}: hand buffered messages to pending async receives, in order. */
235+ private void drainToPendingReceives () {
236+ CompletableFuture <Message <T >> waiter ;
237+ while ((waiter = pollWaiter ()) != null ) {
238+ Message <T > msg = buffer .poll ();
239+ if (msg == null ) {
240+ pendingReceives .addFirst (waiter );
241+ break ;
242+ }
243+ waiter .complete (msg );
244+ }
245+ hasPendingReceives = !pendingReceives .isEmpty ();
246+ maybeResumeProducers ();
247+ }
248+
215249 /**
216250 * Receive up to {@code maxMessages}, blocking (asynchronously) up to {@code timeout} for
217251 * the batch. Waits for the first message, then opportunistically drains whatever else is
@@ -255,35 +289,53 @@ private CompletableFuture<Void> drainReady(List<Message<T>> batch, int max) {
255289 while (batch .size () < max && (m = buffer .poll ()) != null ) {
256290 batch .add (m );
257291 }
258- approxBufferSize = buffer .size ();
259292 maybeResumeProducers ();
260293 done .complete (null );
261294 });
262295 return done ;
263296 }
264297
265- // --- Blocking views, for the synchronous receive() API. Block only the caller's thread. ---
298+ // --- Blocking views, for the synchronous receive() API. Pull straight from the buffer on the
299+ // caller's thread; it parks only while the buffer is empty. ---
266300
267301 Message <T > take () throws PulsarClientException {
302+ if (closed ) {
303+ throw alreadyClosed ();
304+ }
305+ Message <T > msg ;
268306 try {
269- return receiveAsync (). get ();
307+ msg = buffer . take ();
270308 } catch (InterruptedException e ) {
309+ if (closed ) {
310+ // close() terminates the buffer, which wakes blocked takers this way.
311+ throw alreadyClosed ();
312+ }
271313 Thread .currentThread ().interrupt ();
272314 throw new PulsarClientException ("Receive interrupted" , e );
273- } catch (ExecutionException e ) {
274- throw unwrap (e );
275315 }
316+ afterDirectPoll ();
317+ return msg ;
276318 }
277319
278320 Message <T > poll (Duration timeout ) throws PulsarClientException {
321+ if (closed ) {
322+ throw alreadyClosed ();
323+ }
324+ Message <T > msg ;
279325 try {
280- return receiveAsync (timeout ). get ( );
326+ msg = buffer . poll (timeout . toNanos (), TimeUnit . NANOSECONDS );
281327 } catch (InterruptedException e ) {
282328 Thread .currentThread ().interrupt ();
283329 throw new PulsarClientException ("Receive interrupted" , e );
284- } catch (ExecutionException e ) {
285- throw unwrap (e );
286330 }
331+ if (msg == null ) {
332+ if (closed ) {
333+ throw alreadyClosed ();
334+ }
335+ return null ;
336+ }
337+ afterDirectPoll ();
338+ return msg ;
287339 }
288340
289341 List <Message <T >> receiveMulti (int maxMessages , Duration timeout ) throws PulsarClientException {
@@ -299,22 +351,24 @@ List<Message<T>> receiveMulti(int maxMessages, Duration timeout) throws PulsarCl
299351
300352 /** Fail any outstanding receives so blocked/awaiting callers wake instead of hanging forever. */
301353 void close () {
354+ closed = true ;
355+ // Wake blocking receivers parked on the buffer; anything offered from now on is dropped.
356+ buffer .terminate (null );
302357 executor .execute (() -> {
303- closed = true ;
304358 CompletableFuture <Message <T >> waiter ;
305359 while ((waiter = pendingReceives .poll ()) != null ) {
306360 if (!waiter .isDone ()) {
307361 waiter .completeExceptionally (alreadyClosed ());
308362 }
309363 }
364+ hasPendingReceives = false ;
310365 // Release any paused producers so their receive loops re-arm and observe the close.
311366 CompletableFuture <Void > capacity ;
312367 while ((capacity = capacityWaiters .poll ()) != null ) {
313368 capacity .complete (null );
314369 }
315370 producersPaused = false ;
316371 buffer .clear ();
317- approxBufferSize = 0 ;
318372 });
319373 }
320374
0 commit comments