Skip to content

Commit cb82d6c

Browse files
author
Dave Maughan
committed
[feature][client] PIP-184: Topic specific consumer priorityLevel
Resolves apache#16481
1 parent 6f1f6aa commit cb82d6c

13 files changed

+446
-6
lines changed

pulsar-client-api/src/main/java/org/apache/pulsar/client/api/ConsumerBuilder.java

+17
Original file line numberDiff line numberDiff line change
@@ -813,4 +813,21 @@ public interface ConsumerBuilder<T> extends Cloneable {
813813
* @param enabled whether to enable AutoScaledReceiverQueueSize.
814814
*/
815815
ConsumerBuilder<T> autoScaledReceiverQueueSizeEnabled(boolean enabled);
816+
817+
/**
818+
* Configure topic specific options to override those set at the {@link ConsumerBuilder} level.
819+
*
820+
* @param topicNameOrPattern a topic name or a regular expression to match a topic name
821+
* @return a {@link TopicConsumerBuilder} instance
822+
*/
823+
TopicConsumerBuilder<T> topicConfiguration(String topicNameOrPattern);
824+
825+
/**
826+
* Configure topic specific options to override those set at the {@link ConsumerBuilder} level.
827+
*
828+
* @param topicNameOrPattern a topic name or a regular expression to match a topic name
829+
* @param builderConsumer a consumer to allow the configuration of the {@link TopicConsumerBuilder} instance
830+
*/
831+
ConsumerBuilder<T> topicConfiguration(String topicNameOrPattern,
832+
java.util.function.Consumer<TopicConsumerBuilder<T>> builderConsumer);
816833
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
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.client.api;
20+
21+
/**
22+
* {@link TopicConsumerBuilder} is used to configure topic specific options to override those set at the
23+
* {@link ConsumerBuilder} level.
24+
*
25+
* @see ConsumerBuilder#topicConfiguration(String)
26+
*
27+
* @param <T> the type of the value in the {@link ConsumerBuilder}
28+
*/
29+
public interface TopicConsumerBuilder<T> {
30+
/**
31+
* Configure the priority level of this topic.
32+
*
33+
* @see ConsumerBuilder#priorityLevel(int)
34+
*
35+
* @param priorityLevel the priority of this topic
36+
* @return the {@link TopicConsumerBuilder} instance
37+
*/
38+
TopicConsumerBuilder<T> priorityLevel(int priorityLevel);
39+
40+
/**
41+
* Complete the configuration of the topic specific options and return control back to the
42+
* {@link ConsumerBuilder} instance.
43+
*
44+
* @return the {@link ConsumerBuilder} instance
45+
*/
46+
ConsumerBuilder<T> build();
47+
}

pulsar-client/src/main/java/org/apache/pulsar/client/impl/ConsumerBuilderImpl.java

+17
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,10 @@
5252
import org.apache.pulsar.client.api.SubscriptionInitialPosition;
5353
import org.apache.pulsar.client.api.SubscriptionMode;
5454
import org.apache.pulsar.client.api.SubscriptionType;
55+
import org.apache.pulsar.client.api.TopicConsumerBuilder;
5556
import org.apache.pulsar.client.impl.conf.ConfigurationDataUtils;
5657
import org.apache.pulsar.client.impl.conf.ConsumerConfigurationData;
58+
import org.apache.pulsar.client.impl.conf.TopicConsumerConfigurationData;
5759
import org.apache.pulsar.client.util.RetryMessageUtil;
5860
import org.apache.pulsar.common.naming.TopicName;
5961
import org.apache.pulsar.common.partition.PartitionedTopicMetadata;
@@ -537,4 +539,19 @@ public ConsumerBuilder<T> autoScaledReceiverQueueSizeEnabled(boolean enabled) {
537539
conf.setAutoScaledReceiverQueueSizeEnabled(enabled);
538540
return this;
539541
}
542+
543+
@Override
544+
public TopicConsumerBuilder<T> topicConfiguration(String topicNameOrPattern) {
545+
checkArgument(StringUtils.isNotBlank(topicNameOrPattern), "topicNameOrPattern cannot be blank");
546+
TopicConsumerConfigurationData topicConf = TopicConsumerConfigurationData.of(topicNameOrPattern, conf);
547+
conf.getTopicConfigurations().add(topicConf);
548+
return new TopicConsumerBuilderImpl<>(this, topicConf);
549+
}
550+
551+
@Override
552+
public ConsumerBuilder<T> topicConfiguration(String topicNameOrPattern,
553+
java.util.function.Consumer<TopicConsumerBuilder<T>> builderConsumer) {
554+
builderConsumer.accept(topicConfiguration(topicNameOrPattern));
555+
return this;
556+
}
540557
}

pulsar-client/src/main/java/org/apache/pulsar/client/impl/ConsumerImpl.java

+4-1
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@
6161
import java.util.concurrent.locks.ReentrantReadWriteLock;
6262
import java.util.function.Function;
6363
import java.util.stream.Collectors;
64+
import lombok.AccessLevel;
65+
import lombok.Getter;
6466
import org.apache.commons.lang3.StringUtils;
6567
import org.apache.pulsar.client.api.Consumer;
6668
import org.apache.pulsar.client.api.ConsumerCryptoFailureAction;
@@ -146,6 +148,7 @@ public class ConsumerImpl<T> extends ConsumerBase<T> implements ConnectionHandle
146148
private final NegativeAcksTracker negativeAcksTracker;
147149

148150
protected final ConsumerStatsRecorder stats;
151+
@Getter(AccessLevel.PACKAGE)
149152
private final int priorityLevel;
150153
private final SubscriptionMode subscriptionMode;
151154
private volatile BatchMessageIdImpl startMessageId;
@@ -266,7 +269,7 @@ protected ConsumerImpl(PulsarClientImpl client, String topic, ConsumerConfigurat
266269
this.partitionIndex = partitionIndex;
267270
this.hasParentConsumer = hasParentConsumer;
268271
this.parentConsumerHasListener = parentConsumerHasListener;
269-
this.priorityLevel = conf.getPriorityLevel();
272+
this.priorityLevel = conf.getMatchingTopicConfiguration(topic).getPriorityLevel();
270273
this.readCompacted = conf.isReadCompacted();
271274
this.subscriptionInitialPosition = conf.getSubscriptionInitialPosition();
272275
this.negativeAcksTracker = new NegativeAcksTracker(this, conf);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
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.client.impl;
20+
21+
import static com.google.common.base.Preconditions.checkArgument;
22+
import lombok.RequiredArgsConstructor;
23+
import org.apache.pulsar.client.api.ConsumerBuilder;
24+
import org.apache.pulsar.client.api.TopicConsumerBuilder;
25+
import org.apache.pulsar.client.impl.conf.TopicConsumerConfigurationData;
26+
27+
@RequiredArgsConstructor
28+
class TopicConsumerBuilderImpl<T> implements TopicConsumerBuilder<T> {
29+
private final ConsumerBuilder<T> consumerBuilder;
30+
private final TopicConsumerConfigurationData topicConf;
31+
32+
@Override
33+
public TopicConsumerBuilder<T> priorityLevel(int priorityLevel) {
34+
checkArgument(priorityLevel >= 0, "priorityLevel needs to be >= 0");
35+
topicConf.setPriorityLevel(priorityLevel);
36+
return this;
37+
}
38+
39+
@Override
40+
public ConsumerBuilder<T> build() {
41+
return consumerBuilder;
42+
}
43+
}

pulsar-client/src/main/java/org/apache/pulsar/client/impl/conf/ConsumerConfigurationData.java

+16
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
import com.fasterxml.jackson.annotation.JsonIgnore;
2323
import com.google.common.collect.Sets;
2424
import java.io.Serializable;
25+
import java.util.ArrayList;
26+
import java.util.List;
2527
import java.util.Map;
2628
import java.util.Set;
2729
import java.util.SortedMap;
@@ -164,6 +166,20 @@ public int getMaxPendingChuckedMessage() {
164166

165167
private boolean autoScaledReceiverQueueSizeEnabled = false;
166168

169+
private List<TopicConsumerConfigurationData> topicConfigurations = new ArrayList<>();
170+
171+
public TopicConsumerConfigurationData getMatchingTopicConfiguration(String topicName) {
172+
return topicConfigurations.stream()
173+
.filter(topicConf -> topicConf.matchesTopicName(topicName))
174+
.findFirst()
175+
.orElseGet(() -> TopicConsumerConfigurationData.of(topicName, this));
176+
}
177+
178+
public void setTopicConfigurations(List<TopicConsumerConfigurationData> topicConfigurations) {
179+
checkArgument(topicConfigurations != null, "topicConfigurations should not be null.");
180+
this.topicConfigurations = topicConfigurations;
181+
}
182+
167183
public void setAutoUpdatePartitionsIntervalSeconds(int interval, TimeUnit timeUnit) {
168184
checkArgument(interval > 0, "interval needs to be > 0");
169185
this.autoUpdatePartitionsIntervalSeconds = timeUnit.toSeconds(interval);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
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.client.impl.conf;
20+
21+
import java.io.Serializable;
22+
import java.util.regex.Pattern;
23+
import lombok.AllArgsConstructor;
24+
import lombok.Data;
25+
import lombok.NoArgsConstructor;
26+
import lombok.NonNull;
27+
28+
@Data
29+
@NoArgsConstructor
30+
@AllArgsConstructor
31+
public class TopicConsumerConfigurationData implements Serializable {
32+
private static final long serialVersionUID = 1L;
33+
34+
@NonNull
35+
private Pattern topicsPattern;
36+
private int priorityLevel;
37+
38+
public boolean matchesTopicName(String topicName) {
39+
if (topicsPattern == null || topicName == null) {
40+
return false;
41+
}
42+
return topicsPattern.matcher(topicName).matches();
43+
}
44+
45+
public static TopicConsumerConfigurationData of(@NonNull String topicNameOrPattern,
46+
ConsumerConfigurationData<?> conf) {
47+
return of(topicNameOrPattern, conf.getPriorityLevel());
48+
}
49+
50+
public static TopicConsumerConfigurationData of(@NonNull String topicNameOrPattern, int priorityLevel) {
51+
Pattern topicsPattern = Pattern.compile(topicNameOrPattern);
52+
return new TopicConsumerConfigurationData(topicsPattern, priorityLevel);
53+
}
54+
55+
public static void main(String[] args) {
56+
57+
}
58+
}

pulsar-client/src/test/java/org/apache/pulsar/client/impl/ConsumerBuilderImplTest.java

+31
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,13 @@
1818
*/
1919
package org.apache.pulsar.client.impl;
2020

21+
import static org.assertj.core.api.Assertions.assertThat;
22+
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
2123
import static org.mockito.Mockito.mock;
2224
import static org.mockito.Mockito.verify;
2325
import static org.mockito.Mockito.when;
2426
import static org.testng.Assert.assertNotNull;
27+
import java.util.ArrayList;
2528
import java.util.Arrays;
2629
import java.util.HashMap;
2730
import java.util.List;
@@ -31,13 +34,16 @@
3134
import java.util.regex.Pattern;
3235
import org.apache.pulsar.client.api.BatchReceivePolicy;
3336
import org.apache.pulsar.client.api.Consumer;
37+
import org.apache.pulsar.client.api.ConsumerBuilder;
3438
import org.apache.pulsar.client.api.DeadLetterPolicy;
3539
import org.apache.pulsar.client.api.PulsarClientException;
3640
import org.apache.pulsar.client.api.Schema;
3741
import org.apache.pulsar.client.api.SubscriptionInitialPosition;
3842
import org.apache.pulsar.client.api.SubscriptionMode;
3943
import org.apache.pulsar.client.impl.conf.ConsumerConfigurationData;
44+
import org.apache.pulsar.client.impl.conf.TopicConsumerConfigurationData;
4045
import org.testng.annotations.BeforeMethod;
46+
import org.testng.annotations.DataProvider;
4147
import org.testng.annotations.Test;
4248

4349
/**
@@ -338,4 +344,29 @@ public void testStartPaused() {
338344
consumerBuilderImpl.startPaused(true);
339345
verify(consumerBuilderImpl.getConf()).setStartPaused(true);
340346
}
347+
348+
@Test
349+
public void testTopicConsumerBuilder() {
350+
List<TopicConsumerConfigurationData> topicConsumerConfigurationDataList = new ArrayList<>();
351+
when(consumerBuilderImpl.getConf().getTopicConfigurations()).thenReturn(topicConsumerConfigurationDataList);
352+
353+
ConsumerBuilder<?> consumerBuilder = consumerBuilderImpl.topicConfiguration("foo").priorityLevel(1).build();
354+
355+
assertThat(consumerBuilder).isSameAs(consumerBuilderImpl);
356+
assertThat(topicConsumerConfigurationDataList).hasSize(1);
357+
TopicConsumerConfigurationData topicConsumerConfigurationData = topicConsumerConfigurationDataList.get(0);
358+
assertThat(topicConsumerConfigurationData.getTopicsPattern().pattern()).isEqualTo("foo");
359+
assertThat(topicConsumerConfigurationData.getPriorityLevel()).isEqualTo(1);
360+
}
361+
362+
@DataProvider(name = "nullOrBlankTopicPatterns")
363+
public Object[] nullOrBlankTopicPatterns() {
364+
return new Object[]{" ", "", null};
365+
}
366+
367+
@Test(dataProvider = "nullOrBlankTopicPatterns")
368+
public void testTopicConsumerBuilderBlankPattern(String topicNameOrPattern) {
369+
assertThatIllegalArgumentException()
370+
.isThrownBy(() -> consumerBuilderImpl.topicConfiguration(topicNameOrPattern));
371+
}
341372
}

pulsar-client/src/test/java/org/apache/pulsar/client/impl/ConsumerImplTest.java

+24-4
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
*/
1919
package org.apache.pulsar.client.impl;
2020

21+
import static org.assertj.core.api.Assertions.assertThat;
2122
import static org.mockito.Mockito.any;
2223
import static org.mockito.Mockito.doNothing;
2324
import static org.mockito.Mockito.doReturn;
@@ -26,6 +27,8 @@
2627
import static org.mockito.Mockito.times;
2728
import static org.mockito.Mockito.verify;
2829

30+
import java.util.Collections;
31+
import java.util.List;
2932
import java.util.concurrent.CompletableFuture;
3033
import java.util.concurrent.CompletionException;
3134
import java.util.concurrent.ExecutorService;
@@ -39,6 +42,7 @@
3942
import org.apache.pulsar.client.api.PulsarClientException;
4043
import org.apache.pulsar.client.impl.conf.ClientConfigurationData;
4144
import org.apache.pulsar.client.impl.conf.ConsumerConfigurationData;
45+
import org.apache.pulsar.client.impl.conf.TopicConsumerConfigurationData;
4246
import org.apache.pulsar.client.util.ExecutorProvider;
4347
import org.awaitility.Awaitility;
4448
import org.testng.Assert;
@@ -47,23 +51,28 @@
4751
import org.testng.annotations.Test;
4852

4953
public class ConsumerImplTest {
54+
private final String topic = "non-persistent://tenant/ns1/my-topic";
5055

5156
private ExecutorProvider executorProvider;
5257
private ExecutorService internalExecutor;
5358
private ConsumerImpl<byte[]> consumer;
54-
private ConsumerConfigurationData consumerConf;
59+
private ConsumerConfigurationData<byte[]> consumerConf;
5560

5661
@BeforeMethod(alwaysRun = true)
5762
public void setUp() {
63+
consumerConf = new ConsumerConfigurationData<>();
64+
createConsumer(consumerConf);
65+
}
66+
67+
private void createConsumer(ConsumerConfigurationData consumerConf) {
5868
executorProvider = new ExecutorProvider(1, "ConsumerImplTest");
5969
internalExecutor = Executors.newSingleThreadScheduledExecutor();
60-
consumerConf = new ConsumerConfigurationData<>();
70+
6171
PulsarClientImpl client = ClientTestFixtures.createPulsarClientMock(executorProvider, internalExecutor);
6272
ClientConfigurationData clientConf = client.getConfiguration();
6373
clientConf.setOperationTimeoutMs(100);
6474
clientConf.setStatsIntervalSeconds(0);
65-
CompletableFuture<Consumer<ConsumerImpl>> subscribeFuture = new CompletableFuture<>();
66-
String topic = "non-persistent://tenant/ns1/my-topic";
75+
CompletableFuture<Consumer<byte[]>> subscribeFuture = new CompletableFuture<>();
6776

6877
consumerConf.setSubscriptionName("test-sub");
6978
consumer = ConsumerImpl.newConsumerImpl(client, topic, consumerConf,
@@ -239,4 +248,15 @@ public void testMaxReceiverQueueSize() {
239248
Assert.assertEquals(consumer.getCurrentReceiverQueueSize(), size + 100);
240249
Assert.assertEquals(consumer.getAvailablePermits(), permits + 100);
241250
}
251+
252+
@Test
253+
public void testTopicPriorityLevel() {
254+
ConsumerConfigurationData<Object> consumerConf = new ConsumerConfigurationData<>();
255+
consumerConf.getTopicConfigurations().add(
256+
TopicConsumerConfigurationData.of(topic, 1));
257+
258+
createConsumer(consumerConf);
259+
260+
assertThat(consumer.getPriorityLevel()).isEqualTo(1);
261+
}
242262
}

0 commit comments

Comments
 (0)