Skip to content

Commit 36270cc

Browse files
authored
[improve][test] pulsar-perf consume: switch between V5 Queue and Stream consumer (#25981)
1 parent 083fd4c commit 36270cc

2 files changed

Lines changed: 187 additions & 44 deletions

File tree

pulsar-testclient/src/main/java/org/apache/pulsar/testclient/PerformanceConsumer.java

Lines changed: 134 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,9 @@
3030
import java.util.ArrayList;
3131
import java.util.Collections;
3232
import java.util.List;
33+
import java.util.concurrent.CompletableFuture;
3334
import java.util.concurrent.ExecutorService;
3435
import java.util.concurrent.Executors;
35-
import java.util.concurrent.Future;
3636
import java.util.concurrent.Semaphore;
3737
import java.util.concurrent.TimeUnit;
3838
import java.util.concurrent.atomic.AtomicLong;
@@ -43,10 +43,13 @@
4343
import org.HdrHistogram.HistogramLogWriter;
4444
import org.HdrHistogram.Recorder;
4545
import org.apache.pulsar.client.api.v5.Message;
46+
import org.apache.pulsar.client.api.v5.MessageId;
4647
import org.apache.pulsar.client.api.v5.PulsarClient;
4748
import org.apache.pulsar.client.api.v5.PulsarClientBuilder;
4849
import org.apache.pulsar.client.api.v5.QueueConsumer;
4950
import org.apache.pulsar.client.api.v5.QueueConsumerBuilder;
51+
import org.apache.pulsar.client.api.v5.StreamConsumer;
52+
import org.apache.pulsar.client.api.v5.StreamConsumerBuilder;
5053
import org.apache.pulsar.client.api.v5.Transaction;
5154
import org.apache.pulsar.client.api.v5.auth.PemFileKeyProvider;
5255
import org.apache.pulsar.client.api.v5.config.ConsumerEncryptionPolicy;
@@ -75,6 +78,18 @@ public enum SubscriptionType {
7578
Key_Shared
7679
}
7780

81+
/**
82+
* Which V5 scalable-topic consumer API to drive. {@code Queue} gives unordered,
83+
* individually-acked work distribution; {@code Stream} gives ordered, cumulatively-acked
84+
* consumption with broker-coordinated 1:1 segment-to-consumer assignment. Switching to
85+
* {@code Stream} with more consumers than segments is the handle for exercising the
86+
* auto-split feature (PIP-483).
87+
*/
88+
public enum ScalableConsumerType {
89+
Queue,
90+
Stream
91+
}
92+
7893
private static final LongAdder messagesReceived = new LongAdder();
7994
private static final LongAdder bytesReceived = new LongAdder();
8095

@@ -117,6 +132,12 @@ public enum SubscriptionType {
117132
@Option(names = { "-st", "--subscription-type" }, description = "Subscription type")
118133
public SubscriptionType subscriptionType = SubscriptionType.Exclusive;
119134

135+
@Option(names = { "-sct", "--scalable-consumer-type" },
136+
description = "V5 scalable-topic consumer API to use: Queue (unordered, individual ack) "
137+
+ "or Stream (ordered, cumulative ack, 1:1 segment assignment). Use Stream with "
138+
+ "more consumers than segments to drive auto-split (PIP-483).")
139+
public ScalableConsumerType scalableConsumerType = ScalableConsumerType.Queue;
140+
120141
@Option(names = { "-sp", "--subscription-position" }, description = "Subscription position")
121142
private SubscriptionInitialPosition subscriptionInitialPosition = SubscriptionInitialPosition.LATEST;
122143

@@ -297,60 +318,28 @@ public void run() throws Exception {
297318
Semaphore messageReceiveLimiter = new Semaphore(this.numMessagesPerTransaction);
298319
Thread thread = Thread.currentThread();
299320

300-
QueueConsumerBuilder<byte[]> consumerBuilder = pulsarClient.newQueueConsumer(Schema.bytes())
301-
.receiverQueueSize(this.receiverQueueSize)
302-
.acknowledgmentGroupTime(Duration.ofMillis(this.acknowledgmentsGroupingDelayMillis))
303-
.subscriptionInitialPosition(this.subscriptionInitialPosition);
304-
305-
if (isNotBlank(this.encKeyFile)) {
306-
// We do not know the key name from --encryption-key-value-file alone; PemFileKeyProvider
307-
// expects a name → path mapping. The encryption test path uses subscribers that name keys
308-
// explicitly; here we register the file under the same name the producer side used
309-
// (defaults to the file path's last component if unset upstream).
310-
String keyName = Path.of(this.encKeyFile).getFileName().toString();
311-
PemFileKeyProvider keys = PemFileKeyProvider.builder()
312-
.privateKey(keyName, Path.of(this.encKeyFile))
313-
.build();
314-
consumerBuilder.encryptionPolicy(ConsumerEncryptionPolicy.builder()
315-
.privateKeyProvider(keys)
316-
.build());
317-
}
321+
final ConsumerEncryptionPolicy encryptionPolicy = buildEncryptionPolicyOrNull();
318322

319-
List<Future<QueueConsumer<byte[]>>> futures = new ArrayList<>();
323+
List<CompletableFuture<PerfConsumer>> futures = new ArrayList<>();
320324
for (int i = 0; i < this.numTopics; i++) {
321325
final TopicName topicName = TopicName.get(this.topics.get(i));
322326

323327
log.info()
324328
.attr("adding", this.numConsumers)
325329
.attr("topic", topicName)
330+
.attr("consumerType", this.scalableConsumerType)
326331
.log("Adding consumers per subscription on topic");
327332

328333
for (int j = 0; j < this.numSubscriptions; j++) {
329334
String subscriberName = this.subscriptions.get(j);
330335
for (int k = 0; k < this.numConsumers; k++) {
331-
// V5 QueueConsumerBuilder has no clone(); build per-consumer to set topic+sub.
332-
QueueConsumerBuilder<byte[]> b = pulsarClient.newQueueConsumer(Schema.bytes())
333-
.receiverQueueSize(this.receiverQueueSize)
334-
.acknowledgmentGroupTime(Duration.ofMillis(this.acknowledgmentsGroupingDelayMillis))
335-
.subscriptionInitialPosition(this.subscriptionInitialPosition)
336-
.replicateSubscriptionState(this.replicatedSubscription)
337-
.topic(topicName.toString())
338-
.subscriptionName(subscriberName);
339-
if (isNotBlank(this.encKeyFile)) {
340-
String keyName = Path.of(this.encKeyFile).getFileName().toString();
341-
PemFileKeyProvider keys = PemFileKeyProvider.builder()
342-
.privateKey(keyName, Path.of(this.encKeyFile))
343-
.build();
344-
b.encryptionPolicy(ConsumerEncryptionPolicy.builder()
345-
.privateKeyProvider(keys)
346-
.build());
347-
}
348-
futures.add(b.subscribeAsync());
336+
futures.add(subscribeAsync(pulsarClient, topicName.toString(), subscriberName,
337+
encryptionPolicy));
349338
}
350339
}
351340
}
352-
final List<QueueConsumer<byte[]>> consumers = new ArrayList<>(futures.size());
353-
for (Future<QueueConsumer<byte[]>> future : futures) {
341+
final List<PerfConsumer> consumers = new ArrayList<>(futures.size());
342+
for (CompletableFuture<PerfConsumer> future : futures) {
354343
consumers.add(future.get());
355344
}
356345

@@ -359,7 +348,7 @@ public void run() throws Exception {
359348
// per consumer mirrors the v4 dispatch concurrency closely enough for the perf workload.
360349
ExecutorService consumerExec = Executors.newCachedThreadPool(
361350
new DefaultThreadFactory("pulsar-perf-consumer-poll"));
362-
for (QueueConsumer<byte[]> consumer : consumers) {
351+
for (PerfConsumer consumer : consumers) {
363352
consumerExec.submit(() -> pollLoop(consumer, atomicReference, messageAckedCount,
364353
messageReceiveLimiter, limiter, testEndTime, thread, pulsarClient));
365354
}
@@ -466,7 +455,7 @@ public void run() throws Exception {
466455
* dedicated thread that drives {@code receive(timeout)} and runs the same per-message
467456
* handler the v4 listener did (latency record, rate-limit, ack, transaction commit/rollover).
468457
*/
469-
private void pollLoop(QueueConsumer<byte[]> consumer,
458+
private void pollLoop(PerfConsumer consumer,
470459
AtomicReference<Transaction> atomicReference,
471460
AtomicLong messageAckedCount,
472461
Semaphore messageReceiveLimiter,
@@ -543,7 +532,7 @@ private void pollLoop(QueueConsumer<byte[]> consumer,
543532
}
544533
Transaction txn = atomicReference.get();
545534
try {
546-
consumer.acknowledge(msg.id(), txn);
535+
consumer.ackTxn(msg.id(), txn);
547536
totalMessageAck.increment();
548537
messageAck.increment();
549538
} catch (Exception e) {
@@ -556,7 +545,7 @@ private void pollLoop(QueueConsumer<byte[]> consumer,
556545
}
557546
} else {
558547
try {
559-
consumer.acknowledge(msg.id());
548+
consumer.ack(msg.id());
560549
totalMessageAck.increment();
561550
messageAck.increment();
562551
} catch (Exception e) {
@@ -628,6 +617,107 @@ private void pollLoop(QueueConsumer<byte[]> consumer,
628617
}
629618
}
630619

620+
/**
621+
* Minimal common view over the V5 {@link QueueConsumer} / {@link StreamConsumer} APIs so the
622+
* poll loop is independent of which scalable-topic consumer type was selected. The ack methods
623+
* map to {@code acknowledge} for Queue and {@code acknowledgeCumulative} for Stream.
624+
*/
625+
private interface PerfConsumer {
626+
Message<byte[]> receive(Duration timeout) throws Exception;
627+
628+
void ack(MessageId messageId) throws Exception;
629+
630+
void ackTxn(MessageId messageId, Transaction txn) throws Exception;
631+
}
632+
633+
private CompletableFuture<PerfConsumer> subscribeAsync(PulsarClient client, String topic,
634+
String subscription,
635+
ConsumerEncryptionPolicy encryptionPolicy) {
636+
if (this.scalableConsumerType == ScalableConsumerType.Stream) {
637+
// StreamConsumer has no receiverQueueSize knob; the rest carries over. Deliberately
638+
// do NOT set a consumerName: the controller keys group membership by consumer name,
639+
// so the V5 client's auto-generated unique name keeps every consumer — within one
640+
// process and across separate `pulsar-perf consume` invocations — a distinct member.
641+
// (Setting a deterministic name would make two processes collide and the second be
642+
// treated as a reconnect of the first.)
643+
StreamConsumerBuilder<byte[]> b = client.newStreamConsumer(Schema.bytes())
644+
.acknowledgmentGroupTime(Duration.ofMillis(this.acknowledgmentsGroupingDelayMillis))
645+
.subscriptionInitialPosition(this.subscriptionInitialPosition)
646+
.replicateSubscriptionState(this.replicatedSubscription)
647+
.topic(topic)
648+
.subscriptionName(subscription);
649+
if (encryptionPolicy != null) {
650+
b.encryptionPolicy(encryptionPolicy);
651+
}
652+
return b.subscribeAsync().thenApply(PerformanceConsumer::wrap);
653+
}
654+
QueueConsumerBuilder<byte[]> b = client.newQueueConsumer(Schema.bytes())
655+
.receiverQueueSize(this.receiverQueueSize)
656+
.acknowledgmentGroupTime(Duration.ofMillis(this.acknowledgmentsGroupingDelayMillis))
657+
.subscriptionInitialPosition(this.subscriptionInitialPosition)
658+
.replicateSubscriptionState(this.replicatedSubscription)
659+
.topic(topic)
660+
.subscriptionName(subscription);
661+
if (encryptionPolicy != null) {
662+
b.encryptionPolicy(encryptionPolicy);
663+
}
664+
return b.subscribeAsync().thenApply(PerformanceConsumer::wrap);
665+
}
666+
667+
private ConsumerEncryptionPolicy buildEncryptionPolicyOrNull() {
668+
if (!isNotBlank(this.encKeyFile)) {
669+
return null;
670+
}
671+
// We do not know the key name from --encryption-key-value-file alone; PemFileKeyProvider
672+
// expects a name → path mapping. Register the file under the same name the producer side
673+
// used (defaults to the file path's last component if unset upstream).
674+
String keyName = Path.of(this.encKeyFile).getFileName().toString();
675+
PemFileKeyProvider keys = PemFileKeyProvider.builder()
676+
.privateKey(keyName, Path.of(this.encKeyFile))
677+
.build();
678+
return ConsumerEncryptionPolicy.builder()
679+
.privateKeyProvider(keys)
680+
.build();
681+
}
682+
683+
private static PerfConsumer wrap(QueueConsumer<byte[]> consumer) {
684+
return new PerfConsumer() {
685+
@Override
686+
public Message<byte[]> receive(Duration timeout) throws Exception {
687+
return consumer.receive(timeout);
688+
}
689+
690+
@Override
691+
public void ack(MessageId messageId) throws Exception {
692+
consumer.acknowledge(messageId);
693+
}
694+
695+
@Override
696+
public void ackTxn(MessageId messageId, Transaction txn) throws Exception {
697+
consumer.acknowledge(messageId, txn);
698+
}
699+
};
700+
}
701+
702+
private static PerfConsumer wrap(StreamConsumer<byte[]> consumer) {
703+
return new PerfConsumer() {
704+
@Override
705+
public Message<byte[]> receive(Duration timeout) throws Exception {
706+
return consumer.receive(timeout);
707+
}
708+
709+
@Override
710+
public void ack(MessageId messageId) throws Exception {
711+
consumer.acknowledgeCumulative(messageId);
712+
}
713+
714+
@Override
715+
public void ackTxn(MessageId messageId, Transaction txn) throws Exception {
716+
consumer.acknowledgeCumulative(messageId, txn);
717+
}
718+
};
719+
}
720+
631721
private void printAggregatedThroughput(long start) {
632722
double elapsed = (System.nanoTime() - start) / 1e9;
633723
double rate = totalMessagesReceived.sum() / elapsed;
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.pulsar.testclient;
20+
21+
import static org.testng.Assert.assertEquals;
22+
import org.testng.annotations.Test;
23+
import picocli.CommandLine;
24+
25+
public class PerformanceConsumerArgsTest {
26+
27+
private static PerformanceConsumer parse(String... args) {
28+
PerformanceConsumer consumer = new PerformanceConsumer();
29+
new CommandLine(consumer).parseArgs(args);
30+
return consumer;
31+
}
32+
33+
@Test
34+
public void testScalableConsumerTypeDefaultsToQueue() {
35+
PerformanceConsumer consumer = parse("my-topic");
36+
assertEquals(consumer.scalableConsumerType,
37+
PerformanceConsumer.ScalableConsumerType.Queue);
38+
}
39+
40+
@Test
41+
public void testScalableConsumerTypeStreamLongOption() {
42+
PerformanceConsumer consumer = parse("--scalable-consumer-type", "Stream", "my-topic");
43+
assertEquals(consumer.scalableConsumerType,
44+
PerformanceConsumer.ScalableConsumerType.Stream);
45+
}
46+
47+
@Test
48+
public void testScalableConsumerTypeShortOption() {
49+
PerformanceConsumer consumer = parse("-sct", "Queue", "my-topic");
50+
assertEquals(consumer.scalableConsumerType,
51+
PerformanceConsumer.ScalableConsumerType.Queue);
52+
}
53+
}

0 commit comments

Comments
 (0)