Skip to content

Commit 5946c95

Browse files
authored
Move default subscription type to factory (#818)
This commit moves the default subscription type from the `@PulsarListener` and `@ReactivePulsarListener` annotation to the associated container factory (props) which allows the Spring Boot `spring.pulsar.consumer.subscription-type` config prop to be respected. See spring-projects/spring-boot#42053
1 parent 01a95a7 commit 5946c95

File tree

10 files changed

+249
-188
lines changed

10 files changed

+249
-188
lines changed

spring-pulsar-docs/src/main/antora/modules/ROOT/pages/reference/pulsar/message-consumption.adoc

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,6 @@ Spring Boot provides this consumer factory which you can further configure by sp
1515

1616
TIP: The `spring.pulsar.consumer.subscription.name` property is ignored and is instead generated when not specified on the annotation.
1717

18-
TIP: The `spring.pulsar.consumer.subscription.type` property is ignored and is instead taken from the value on the annotation. However, you can set the `subscriptionType = {}` on the annotation to instead use the property value as the default.
19-
20-
2118
Let us revisit the `PulsarListener` code snippet we saw in the quick-tour section:
2219

2320
[source, java]

spring-pulsar-reactive/src/main/java/org/springframework/pulsar/reactive/config/DefaultReactivePulsarListenerContainerFactory.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import java.util.List;
2121

2222
import org.apache.pulsar.client.api.Schema;
23+
import org.apache.pulsar.client.api.SubscriptionType;
2324

2425
import org.springframework.core.log.LogAccessor;
2526
import org.springframework.pulsar.reactive.core.ReactivePulsarConsumerFactory;
@@ -87,6 +88,7 @@ public DefaultReactivePulsarMessageListenerContainer<T> createContainerInstance(
8788
ReactivePulsarContainerProperties<T> properties = new ReactivePulsarContainerProperties<>();
8889
properties.setSchemaResolver(this.getContainerProperties().getSchemaResolver());
8990
properties.setTopicResolver(this.getContainerProperties().getTopicResolver());
91+
properties.setSubscriptionType(this.getContainerProperties().getSubscriptionType());
9092

9193
if (!CollectionUtils.isEmpty(endpoint.getTopics())) {
9294
properties.setTopics(endpoint.getTopics());
@@ -103,8 +105,9 @@ public DefaultReactivePulsarMessageListenerContainer<T> createContainerInstance(
103105
if (endpoint.getSubscriptionType() != null) {
104106
properties.setSubscriptionType(endpoint.getSubscriptionType());
105107
}
106-
else {
107-
properties.setSubscriptionType(this.containerProperties.getSubscriptionType());
108+
// Default to Exclusive if not set on container props or endpoint
109+
if (properties.getSubscriptionType() == null) {
110+
properties.setSubscriptionType(SubscriptionType.Exclusive);
108111
}
109112

110113
if (endpoint.getSchemaType() != null) {

spring-pulsar-reactive/src/main/java/org/springframework/pulsar/reactive/config/annotation/ReactivePulsarListener.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@
7979
* @return single element array with the subscription type or empty array to indicate
8080
* no type chosen by user
8181
*/
82-
SubscriptionType[] subscriptionType() default { SubscriptionType.Exclusive };
82+
SubscriptionType[] subscriptionType() default {};
8383

8484
/**
8585
* Pulsar schema type for this listener.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/*
2+
* Copyright 2023-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.pulsar.reactive.config;
18+
19+
import static org.assertj.core.api.Assertions.assertThat;
20+
import static org.mockito.Mockito.mock;
21+
import static org.mockito.Mockito.when;
22+
23+
import org.apache.pulsar.client.api.SubscriptionType;
24+
import org.junit.jupiter.api.Nested;
25+
import org.junit.jupiter.api.Test;
26+
27+
import org.springframework.pulsar.reactive.core.ReactivePulsarConsumerFactory;
28+
import org.springframework.pulsar.reactive.listener.ReactivePulsarContainerProperties;
29+
30+
/**
31+
* Unit tests for {@link DefaultReactivePulsarListenerContainerFactory}.
32+
*/
33+
class DefaultReactivePulsarListenerContainerFactoryTests {
34+
35+
@SuppressWarnings("unchecked")
36+
@Nested
37+
class SubscriptionTypeFrom {
38+
39+
@Test
40+
void factoryPropsUsedWhenNotSetOnEndpoint() {
41+
var factoryProps = new ReactivePulsarContainerProperties<String>();
42+
factoryProps.setSubscriptionType(SubscriptionType.Shared);
43+
var containerFactory = new DefaultReactivePulsarListenerContainerFactory<String>(
44+
mock(ReactivePulsarConsumerFactory.class), factoryProps);
45+
var endpoint = mock(ReactivePulsarListenerEndpoint.class);
46+
when(endpoint.getConcurrency()).thenReturn(1);
47+
var createdContainer = containerFactory.createListenerContainer(endpoint);
48+
assertThat(createdContainer.getContainerProperties().getSubscriptionType())
49+
.isEqualTo(SubscriptionType.Shared);
50+
}
51+
52+
@Test
53+
void endpointTakesPrecedenceOverFactoryProps() {
54+
var factoryProps = new ReactivePulsarContainerProperties<String>();
55+
factoryProps.setSubscriptionType(SubscriptionType.Shared);
56+
var containerFactory = new DefaultReactivePulsarListenerContainerFactory<String>(
57+
mock(ReactivePulsarConsumerFactory.class), factoryProps);
58+
var endpoint = mock(ReactivePulsarListenerEndpoint.class);
59+
when(endpoint.getConcurrency()).thenReturn(1);
60+
when(endpoint.getSubscriptionType()).thenReturn(SubscriptionType.Failover);
61+
var createdContainer = containerFactory.createListenerContainer(endpoint);
62+
assertThat(createdContainer.getContainerProperties().getSubscriptionType())
63+
.isEqualTo(SubscriptionType.Failover);
64+
}
65+
66+
@Test
67+
void defaultUsedWhenNotSetOnEndpointNorFactoryProps() {
68+
var factoryProps = new ReactivePulsarContainerProperties<String>();
69+
var containerFactory = new DefaultReactivePulsarListenerContainerFactory<String>(
70+
mock(ReactivePulsarConsumerFactory.class), factoryProps);
71+
var endpoint = mock(ReactivePulsarListenerEndpoint.class);
72+
when(endpoint.getConcurrency()).thenReturn(1);
73+
var createdContainer = containerFactory.createListenerContainer(endpoint);
74+
assertThat(createdContainer.getContainerProperties().getSubscriptionType())
75+
.isEqualTo(SubscriptionType.Exclusive);
76+
77+
}
78+
79+
}
80+
81+
}

spring-pulsar-reactive/src/test/java/org/springframework/pulsar/reactive/listener/ReactivePulsarListenerTests.java

Lines changed: 58 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,7 @@
7272
import org.springframework.pulsar.reactive.listener.ReactivePulsarListenerTests.PulsarHeadersCustomObjectMapperTest.PulsarHeadersCustomObjectMapperTestConfig;
7373
import org.springframework.pulsar.reactive.listener.ReactivePulsarListenerTests.PulsarHeadersTest.PulsarListenerWithHeadersConfig;
7474
import org.springframework.pulsar.reactive.listener.ReactivePulsarListenerTests.StreamingListenerTestCases.StreamingListenerTestCasesConfig;
75-
import org.springframework.pulsar.reactive.listener.ReactivePulsarListenerTests.SubscriptionTypeTests.WithDefaultType.WithDefaultTypeConfig;
76-
import org.springframework.pulsar.reactive.listener.ReactivePulsarListenerTests.SubscriptionTypeTests.WithSpecificTypes.WithSpecificTypesConfig;
75+
import org.springframework.pulsar.reactive.listener.ReactivePulsarListenerTests.SubscriptionTypeTests.SubscriptionTypeTestsConfig;
7776
import org.springframework.pulsar.reactive.support.MessageUtils;
7877
import org.springframework.pulsar.support.PulsarHeaders;
7978
import org.springframework.pulsar.support.header.JsonPulsarHeaderMapper;
@@ -816,117 +815,80 @@ Mono<Void> listen2(String message) {
816815
}
817816

818817
@Nested
818+
@ContextConfiguration(classes = SubscriptionTypeTestsConfig.class)
819819
class SubscriptionTypeTests {
820820

821-
@Nested
822-
@ContextConfiguration(classes = WithDefaultTypeConfig.class)
823-
class WithDefaultType {
821+
static final CountDownLatch latchTypeNotSet = new CountDownLatch(1);
824822

825-
static final CountDownLatch latchTypeNotSet = new CountDownLatch(1);
823+
static final CountDownLatch latchTypeSetOnAnnotation = new CountDownLatch(1);
826824

827-
@Test
828-
void whenTypeNotSetAnywhereThenFallbackTypeIsUsed(
829-
@Autowired ConsumerTrackingReactivePulsarConsumerFactory<String> consumerFactory) throws Exception {
830-
assertThat(consumerFactory.topicNameToConsumerSpec).hasEntrySatisfying("rpl-typeNotSetAnywhere-topic",
831-
(consumerSpec) -> assertThat(consumerSpec.getSubscriptionType())
832-
.isEqualTo(SubscriptionType.Exclusive));
833-
pulsarTemplate.send("rpl-typeNotSetAnywhere-topic", "hello-rpl-typeNotSetAnywhere");
834-
assertThat(latchTypeNotSet.await(10, TimeUnit.SECONDS)).isTrue();
835-
}
836-
837-
@Configuration(proxyBeanMethods = false)
838-
static class WithDefaultTypeConfig {
839-
840-
@ReactivePulsarListener(topics = "rpl-typeNotSetAnywhere-topic",
841-
subscriptionName = "rpl-typeNotSetAnywhere-sub",
842-
consumerCustomizer = "subscriptionInitialPositionEarliest")
843-
Mono<Void> listenWithoutTypeSetAnywhere(String ignored) {
844-
latchTypeNotSet.countDown();
845-
return Mono.empty();
846-
}
847-
848-
}
825+
static final CountDownLatch latchTypeSetOnCustomizer = new CountDownLatch(1);
849826

827+
@Test
828+
void defaultTypeFromContainerFactoryUsedWhenTypeNotSetAnywhere(
829+
@Autowired ConsumerTrackingReactivePulsarConsumerFactory<String> consumerFactory) throws Exception {
830+
var topic = "rpl-latchTypeNotSet-topic";
831+
assertThat(consumerFactory.getSpec(topic)).extracting(ReactiveMessageConsumerSpec::getSubscriptionType)
832+
.isEqualTo(SubscriptionType.Exclusive);
833+
pulsarTemplate.send(topic, "hello-" + topic);
834+
assertThat(latchTypeNotSet.await(5, TimeUnit.SECONDS)).isTrue();
850835
}
851836

852-
@Nested
853-
@ContextConfiguration(classes = WithSpecificTypesConfig.class)
854-
class WithSpecificTypes {
855-
856-
static final CountDownLatch latchTypeSetConsumerFactory = new CountDownLatch(1);
837+
@Test
838+
void typeSetOnAnnotationOverridesDefaultTypeFromContainerFactory(
839+
@Autowired ConsumerTrackingReactivePulsarConsumerFactory<String> consumerFactory) throws Exception {
840+
var topic = "rpl-typeSetOnAnnotation-topic";
841+
assertThat(consumerFactory.getSpec(topic)).extracting(ReactiveMessageConsumerSpec::getSubscriptionType)
842+
.isEqualTo(SubscriptionType.Key_Shared);
843+
pulsarTemplate.send(topic, "hello-" + topic);
844+
assertThat(latchTypeSetOnAnnotation.await(5, TimeUnit.SECONDS)).isTrue();
845+
}
857846

858-
static final CountDownLatch latchTypeSetAnnotation = new CountDownLatch(1);
847+
@Test
848+
void typeSetOnCustomizerOverridesTypeSetOnAnnotation(
849+
@Autowired ConsumerTrackingReactivePulsarConsumerFactory<String> consumerFactory) throws Exception {
850+
var topic = "rpl-typeSetOnCustomizer-topic";
851+
assertThat(consumerFactory.getSpec(topic)).extracting(ReactiveMessageConsumerSpec::getSubscriptionType)
852+
.isEqualTo(SubscriptionType.Failover);
853+
pulsarTemplate.send(topic, "hello-" + topic);
854+
assertThat(latchTypeSetOnCustomizer.await(5, TimeUnit.SECONDS)).isTrue();
855+
}
859856

860-
static final CountDownLatch latchWithCustomizer = new CountDownLatch(1);
857+
@Configuration(proxyBeanMethods = false)
858+
static class SubscriptionTypeTestsConfig {
861859

862-
@Test
863-
void whenTypeSetOnlyInConsumerFactoryThenConsumerFactoryTypeIsUsed(
864-
@Autowired ConsumerTrackingReactivePulsarConsumerFactory<String> consumerFactory) throws Exception {
865-
assertThat(consumerFactory.getSpec("rpl-typeSetConsumerFactory-topic"))
866-
.extracting(ReactiveMessageConsumerSpec::getSubscriptionType)
867-
.isEqualTo(SubscriptionType.Shared);
868-
pulsarTemplate.send("rpl-typeSetConsumerFactory-topic", "hello-rpl-typeSetConsumerFactory");
869-
assertThat(latchTypeSetConsumerFactory.await(10, TimeUnit.SECONDS)).isTrue();
860+
@Bean
861+
ReactiveMessageConsumerBuilderCustomizer<String> consumerFactoryDefaultSubTypeCustomizer() {
862+
return (b) -> b.subscriptionType(SubscriptionType.Shared);
870863
}
871864

872-
@Test
873-
void whenTypeSetOnAnnotationThenAnnotationTypeIsUsed(
874-
@Autowired ConsumerTrackingReactivePulsarConsumerFactory<String> consumerFactory) throws Exception {
875-
assertThat(consumerFactory.getSpec("rpl-typeSetAnnotation-topic"))
876-
.extracting(ReactiveMessageConsumerSpec::getSubscriptionType)
877-
.isEqualTo(SubscriptionType.Key_Shared);
878-
pulsarTemplate.send("rpl-typeSetAnnotation-topic", "hello-rpl-typeSetAnnotation");
879-
assertThat(latchTypeSetAnnotation.await(10, TimeUnit.SECONDS)).isTrue();
865+
@ReactivePulsarListener(topics = "rpl-latchTypeNotSet-topic", subscriptionName = "rpl-latchTypeNotSet-sub",
866+
consumerCustomizer = "subscriptionInitialPositionEarliest")
867+
Mono<Void> listenWithoutTypeSetAnywhere(String ignored) {
868+
latchTypeNotSet.countDown();
869+
return Mono.empty();
880870
}
881871

882-
@Test
883-
void whenTypeSetWithCustomizerThenCustomizerTypeIsUsed(
884-
@Autowired ConsumerTrackingReactivePulsarConsumerFactory<String> consumerFactory) throws Exception {
885-
assertThat(consumerFactory.getSpec("rpl-typeSetCustomizer-topic"))
886-
.extracting(ReactiveMessageConsumerSpec::getSubscriptionType)
887-
.isEqualTo(SubscriptionType.Failover);
888-
pulsarTemplate.send("rpl-typeSetCustomizer-topic", "hello-rpl-typeSetCustomizer");
889-
assertThat(latchWithCustomizer.await(10, TimeUnit.SECONDS)).isTrue();
872+
@ReactivePulsarListener(topics = "rpl-typeSetOnAnnotation-topic",
873+
subscriptionName = "rpl-typeSetOnAnnotation-sub", subscriptionType = SubscriptionType.Key_Shared,
874+
consumerCustomizer = "subscriptionInitialPositionEarliest")
875+
Mono<Void> listenWithTypeSetOnAnnotation(String ignored) {
876+
latchTypeSetOnAnnotation.countDown();
877+
return Mono.empty();
890878
}
891879

892-
@Configuration(proxyBeanMethods = false)
893-
static class WithSpecificTypesConfig {
894-
895-
@Bean
896-
ReactiveMessageConsumerBuilderCustomizer<String> consumerFactoryDefaultSubTypeCustomizer() {
897-
return (b) -> b.subscriptionType(SubscriptionType.Shared);
898-
}
899-
900-
@ReactivePulsarListener(topics = "rpl-typeSetConsumerFactory-topic",
901-
subscriptionName = "rpl-typeSetConsumerFactory-sub", subscriptionType = {},
902-
consumerCustomizer = "subscriptionInitialPositionEarliest")
903-
Mono<Void> listenWithTypeSetOnlyOnConsumerFactory(String ignored) {
904-
latchTypeSetConsumerFactory.countDown();
905-
return Mono.empty();
906-
}
907-
908-
@ReactivePulsarListener(topics = "rpl-typeSetAnnotation-topic",
909-
subscriptionName = "rpl-typeSetAnnotation-sub", subscriptionType = SubscriptionType.Key_Shared,
910-
consumerCustomizer = "subscriptionInitialPositionEarliest")
911-
Mono<Void> listenWithTypeSetOnAnnotation(String ignored) {
912-
latchTypeSetAnnotation.countDown();
913-
return Mono.empty();
914-
}
915-
916-
@ReactivePulsarListener(topics = "rpl-typeSetCustomizer-topic",
917-
subscriptionName = "rpl-typeSetCustomizer-sub", subscriptionType = SubscriptionType.Key_Shared,
918-
consumerCustomizer = "myCustomizer")
919-
Mono<Void> listenWithTypeSetInCustomizer(String ignored) {
920-
latchWithCustomizer.countDown();
921-
return Mono.empty();
922-
}
923-
924-
@Bean
925-
public ReactivePulsarListenerMessageConsumerBuilderCustomizer<String> myCustomizer() {
926-
return cb -> cb.subscriptionInitialPosition(SubscriptionInitialPosition.Earliest)
927-
.subscriptionType(SubscriptionType.Failover);
928-
}
880+
@ReactivePulsarListener(topics = "rpl-typeSetOnCustomizer-topic",
881+
subscriptionName = "rpl-typeSetOnCustomizer-sub", subscriptionType = SubscriptionType.Key_Shared,
882+
consumerCustomizer = "myCustomizer")
883+
Mono<Void> listenWithTypeSetOnCustomizer(String ignored) {
884+
latchTypeSetOnCustomizer.countDown();
885+
return Mono.empty();
886+
}
929887

888+
@Bean
889+
public ReactivePulsarListenerMessageConsumerBuilderCustomizer<String> myCustomizer() {
890+
return cb -> cb.subscriptionInitialPosition(SubscriptionInitialPosition.Earliest)
891+
.subscriptionType(SubscriptionType.Failover);
930892
}
931893

932894
}

spring-pulsar/src/main/java/org/springframework/pulsar/annotation/PulsarListener.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@
8181
* @return single element array with the subscription type or empty array to indicate
8282
* no type chosen by user
8383
*/
84-
SubscriptionType[] subscriptionType() default { SubscriptionType.Exclusive };
84+
SubscriptionType[] subscriptionType() default {};
8585

8686
/**
8787
* Pulsar schema type for this listener.

spring-pulsar/src/main/java/org/springframework/pulsar/config/ConcurrentPulsarListenerContainerFactory.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
import java.util.Collection;
2121
import java.util.HashSet;
2222

23+
import org.apache.pulsar.client.api.SubscriptionType;
24+
2325
import org.springframework.pulsar.core.PulsarConsumerFactory;
2426
import org.springframework.pulsar.listener.ConcurrentPulsarMessageListenerContainer;
2527
import org.springframework.pulsar.listener.PulsarContainerProperties;
@@ -74,6 +76,7 @@ protected ConcurrentPulsarMessageListenerContainer<T> createContainerInstance(Pu
7476
PulsarContainerProperties properties = new PulsarContainerProperties();
7577
properties.setSchemaResolver(this.getContainerProperties().getSchemaResolver());
7678
properties.setTopicResolver(this.getContainerProperties().getTopicResolver());
79+
properties.setSubscriptionType(this.getContainerProperties().getSubscriptionType());
7780

7881
var parentTxnProps = this.getContainerProperties().transactions();
7982
var childTxnProps = properties.transactions();
@@ -102,6 +105,10 @@ protected ConcurrentPulsarMessageListenerContainer<T> createContainerInstance(Pu
102105
if (endpoint.getSubscriptionType() != null) {
103106
properties.setSubscriptionType(endpoint.getSubscriptionType());
104107
}
108+
// Default to Exclusive if not set on container props or endpoint
109+
if (properties.getSubscriptionType() == null) {
110+
properties.setSubscriptionType(SubscriptionType.Exclusive);
111+
}
105112

106113
properties.setSchemaType(endpoint.getSchemaType());
107114

spring-pulsar/src/main/java/org/springframework/pulsar/listener/DefaultPulsarMessageListenerContainer.java

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -304,11 +304,9 @@ else if (messageListener != null) {
304304
topicNames, this.containerProperties.getSubscriptionName(), properties, customizers);
305305
Assert.state(this.consumer != null, "Unable to create a consumer");
306306

307-
// If subtype is null - update it based on the actual subtype of the
308-
// underlying consumer
309-
if (this.subscriptionType == null) {
310-
updateSubscriptionTypeFromConsumer(this.consumer);
311-
}
307+
// Update sub type from underlying consumer as customizer from annotation
308+
// may have updated it
309+
updateSubscriptionTypeFromConsumer(this.consumer);
312310
}
313311
catch (PulsarException e) {
314312
DefaultPulsarMessageListenerContainer.this.logger.error(e, () -> "Pulsar exception.");

0 commit comments

Comments
 (0)