@@ -27,6 +27,7 @@ import org.springframework.context.annotation.Profile;
|
||||
import org.springframework.pulsar.annotation.PulsarListener;
|
||||
import org.springframework.pulsar.core.PulsarTemplate;
|
||||
import org.springframework.pulsar.core.PulsarTopic;
|
||||
import org.springframework.pulsar.core.PulsarTopicBuilder;
|
||||
|
||||
@SpringBootConfiguration
|
||||
@EnableAutoConfiguration
|
||||
@@ -39,7 +40,7 @@ class ImperativeAppConfig {
|
||||
|
||||
@Bean
|
||||
PulsarTopic pulsarTestTopic() {
|
||||
return PulsarTopic.builder(TOPIC).numberOfPartitions(1).build();
|
||||
return new PulsarTopicBuilder().name(TOPIC).numberOfPartitions(1).build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
|
||||
@@ -26,6 +26,7 @@ import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Profile;
|
||||
import org.springframework.pulsar.core.PulsarTopic;
|
||||
import org.springframework.pulsar.core.PulsarTopicBuilder;
|
||||
import org.springframework.pulsar.reactive.config.annotation.ReactivePulsarListener;
|
||||
import org.springframework.pulsar.reactive.core.ReactivePulsarTemplate;
|
||||
|
||||
@@ -43,7 +44,7 @@ class ReactiveAppConfig {
|
||||
|
||||
@Bean
|
||||
PulsarTopic pulsarTestTopic() {
|
||||
return PulsarTopic.builder(TOPIC).numberOfPartitions(1).build();
|
||||
return new PulsarTopicBuilder().name(TOPIC).numberOfPartitions(1).build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
* Copyright 2012-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.pulsar.inttest.config;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
import org.awaitility.Awaitility;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.testcontainers.containers.PulsarContainer;
|
||||
import org.testcontainers.junit.jupiter.Container;
|
||||
import org.testcontainers.junit.jupiter.Testcontainers;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.test.system.CapturedOutput;
|
||||
import org.springframework.boot.test.system.OutputCaptureExtension;
|
||||
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
|
||||
import org.springframework.pulsar.core.PulsarAdministration;
|
||||
import org.springframework.pulsar.test.support.PulsarTestContainerSupport;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
|
||||
@Testcontainers(disabledWithoutDocker = true)
|
||||
@ExtendWith(OutputCaptureExtension.class)
|
||||
class DefaultTenantAndNamespaceTests {
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Container
|
||||
@ServiceConnection
|
||||
static PulsarContainer PULSAR_CONTAINER = new PulsarContainer(PulsarTestContainerSupport.getPulsarImage());
|
||||
|
||||
@Nested
|
||||
@SpringBootTest(classes = ImperativeAppConfig.class)
|
||||
@ExtendWith(OutputCaptureExtension.class)
|
||||
@ActiveProfiles("inttest.pulsar.imperative")
|
||||
class WithImperativeApp {
|
||||
|
||||
@Test
|
||||
void produceConsumeWithDefaultTenantNamespace(CapturedOutput output,
|
||||
@Autowired PulsarAdministration pulsarAdmin) {
|
||||
TestVerifyUtils.verifyProduceConsume(output, 10, (i) -> ImperativeAppConfig.MSG_PREFIX + i);
|
||||
TestVerifyUtils.verifyTopicsLocatedInTenantAndNamespace(pulsarAdmin, ImperativeAppConfig.TENANT,
|
||||
ImperativeAppConfig.NAMESPACE, ImperativeAppConfig.NFQ_TOPIC);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
@SpringBootTest(classes = ReactiveAppConfig.class)
|
||||
@ExtendWith(OutputCaptureExtension.class)
|
||||
@ActiveProfiles("inttest.pulsar.reactive")
|
||||
class WithReactiveApp {
|
||||
|
||||
@Test
|
||||
void produceConsumeWithDefaultTenantNamespace(CapturedOutput output,
|
||||
@Autowired PulsarAdministration pulsarAdmin) {
|
||||
TestVerifyUtils.verifyProduceConsume(output, 10, (i) -> ReactiveAppConfig.MSG_PREFIX + i);
|
||||
TestVerifyUtils.verifyTopicsLocatedInTenantAndNamespace(pulsarAdmin, ReactiveAppConfig.TENANT,
|
||||
ReactiveAppConfig.NAMESPACE, ReactiveAppConfig.NFQ_TOPIC);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class TestVerifyUtils {
|
||||
|
||||
static void verifyProduceConsume(CapturedOutput output, int numExpectedMessages,
|
||||
Function<Integer, Object> expectedMessageFactory) {
|
||||
var expectedOutput = new ArrayList<String>();
|
||||
IntStream.range(0, numExpectedMessages).forEachOrdered((i) -> {
|
||||
var expectedMsg = expectedMessageFactory.apply(i);
|
||||
expectedOutput.add("++++++PRODUCE %s------".formatted(expectedMsg));
|
||||
expectedOutput.add("++++++CONSUME %s------".formatted(expectedMsg));
|
||||
});
|
||||
Awaitility.waitAtMost(Duration.ofSeconds(15))
|
||||
.untilAsserted(() -> assertThat(output).contains(expectedOutput));
|
||||
}
|
||||
|
||||
static void verifyTopicsLocatedInTenantAndNamespace(PulsarAdministration pulsarAdmin, String tenant,
|
||||
String namespace, String topic) {
|
||||
// verify topics created in expected tenant/namespace and not in
|
||||
// public/default
|
||||
try (var admin = pulsarAdmin.createAdminClient()) {
|
||||
var fqTopic = "persistent://%s/%s/%s".formatted(tenant, namespace, topic);
|
||||
assertThat(admin.topics().getList("%s/%s".formatted(tenant, namespace))).containsExactly(fqTopic);
|
||||
assertThat(admin.topics().getList("public/default"))
|
||||
.noneSatisfy(t -> assertThat(t).doesNotEndWith("/" + topic));
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
* Copyright 2023-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.pulsar.inttest.config;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.apache.pulsar.client.api.PulsarClient;
|
||||
import org.apache.pulsar.common.naming.TopicDomain;
|
||||
import org.apache.pulsar.common.policies.data.TenantInfoImpl;
|
||||
|
||||
import org.springframework.boot.ApplicationRunner;
|
||||
import org.springframework.boot.SpringBootConfiguration;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Profile;
|
||||
import org.springframework.pulsar.annotation.PulsarListener;
|
||||
import org.springframework.pulsar.core.DefaultPulsarConsumerFactory;
|
||||
import org.springframework.pulsar.core.DefaultPulsarProducerFactory;
|
||||
import org.springframework.pulsar.core.PulsarAdministration;
|
||||
import org.springframework.pulsar.core.PulsarConsumerFactory;
|
||||
import org.springframework.pulsar.core.PulsarProducerFactory;
|
||||
import org.springframework.pulsar.core.PulsarTemplate;
|
||||
import org.springframework.pulsar.core.PulsarTopicBuilder;
|
||||
import org.springframework.pulsar.core.TopicResolver;
|
||||
|
||||
@SpringBootConfiguration
|
||||
@EnableAutoConfiguration
|
||||
@Profile("inttest.pulsar.imperative")
|
||||
class ImperativeAppConfig {
|
||||
|
||||
private static final Log LOG = LogFactory.getLog(ImperativeAppConfig.class);
|
||||
static final String TENANT = "my-tenant-i";
|
||||
static final String NAMESPACE = "my-namespace-i";
|
||||
static final String NFQ_TOPIC = "dtant-topic-i";
|
||||
static final String FQ_TOPIC = "persistent://my-tenant-i/my-namespace-i/dtant-topic-i";
|
||||
static final String MSG_PREFIX = "DefaultTenantNamespace-i:";
|
||||
|
||||
@Bean
|
||||
PulsarTopicBuilder topicBuilder() {
|
||||
return new PulsarTopicBuilder(TopicDomain.persistent, TENANT, NAMESPACE);
|
||||
}
|
||||
|
||||
@Bean
|
||||
PulsarProducerFactory<Object> pulsarProducerFactory(PulsarClient pulsarClient, TopicResolver topicResolver,
|
||||
PulsarTopicBuilder topicBuilder) {
|
||||
var producerFactory = new DefaultPulsarProducerFactory<>(pulsarClient, null, null, topicResolver);
|
||||
producerFactory.setTopicBuilder(topicBuilder);
|
||||
return producerFactory;
|
||||
}
|
||||
|
||||
@Bean
|
||||
PulsarConsumerFactory<Object> pulsarConsumerFactory(PulsarClient pulsarClient, PulsarTopicBuilder topicBuilder) {
|
||||
var consumerFactory = new DefaultPulsarConsumerFactory<>(pulsarClient, null);
|
||||
consumerFactory.setTopicBuilder(topicBuilder);
|
||||
return consumerFactory;
|
||||
}
|
||||
|
||||
@PulsarListener(topics = NFQ_TOPIC)
|
||||
void consumeFromNonFullyQualifiedTopic(String msg) {
|
||||
LOG.info("++++++CONSUME %s------".formatted(msg));
|
||||
}
|
||||
|
||||
@PulsarListener(topics = FQ_TOPIC)
|
||||
void consumeFromFullyQualifiedTopic(String msg) {
|
||||
LOG.info("++++++CONSUME %s------".formatted(msg));
|
||||
}
|
||||
|
||||
@Bean
|
||||
ApplicationRunner produceWithDefaultTenantAndNamespace(PulsarAdministration pulsarAdmin,
|
||||
PulsarTemplate<String> template) {
|
||||
createTenantAndNamespace(pulsarAdmin);
|
||||
return (args) -> {
|
||||
for (int i = 0; i < 10; i++) {
|
||||
var msg = MSG_PREFIX + i;
|
||||
template.send((i < 5) ? FQ_TOPIC : NFQ_TOPIC, msg);
|
||||
LOG.info("++++++PRODUCE %s------".formatted(msg));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private void createTenantAndNamespace(PulsarAdministration pulsarAdmin) {
|
||||
try (var admin = pulsarAdmin.createAdminClient()) {
|
||||
admin.tenants()
|
||||
.createTenant(TENANT, TenantInfoImpl.builder().allowedClusters(Set.of("standalone")).build());
|
||||
LOG.info("Created tenant -> %s".formatted(admin.tenants().getTenantInfo(TENANT)));
|
||||
admin.namespaces().createNamespace("%s/%s".formatted(TENANT, NAMESPACE));
|
||||
LOG.info("Created namespace -> %s".formatted(admin.namespaces().getNamespaces(TENANT)));
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
* Copyright 2023-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.pulsar.inttest.config;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.apache.pulsar.common.naming.TopicDomain;
|
||||
import org.apache.pulsar.common.policies.data.TenantInfoImpl;
|
||||
import org.apache.pulsar.reactive.client.api.ReactivePulsarClient;
|
||||
|
||||
import org.springframework.boot.ApplicationRunner;
|
||||
import org.springframework.boot.SpringBootConfiguration;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Profile;
|
||||
import org.springframework.pulsar.core.PulsarAdministration;
|
||||
import org.springframework.pulsar.core.PulsarTopicBuilder;
|
||||
import org.springframework.pulsar.reactive.config.annotation.ReactivePulsarListener;
|
||||
import org.springframework.pulsar.reactive.core.DefaultReactivePulsarConsumerFactory;
|
||||
import org.springframework.pulsar.reactive.core.DefaultReactivePulsarSenderFactory;
|
||||
import org.springframework.pulsar.reactive.core.ReactivePulsarConsumerFactory;
|
||||
import org.springframework.pulsar.reactive.core.ReactivePulsarSenderFactory;
|
||||
import org.springframework.pulsar.reactive.core.ReactivePulsarTemplate;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
@SpringBootConfiguration
|
||||
@EnableAutoConfiguration
|
||||
@Profile("inttest.pulsar.reactive")
|
||||
class ReactiveAppConfig {
|
||||
|
||||
private static final Log LOG = LogFactory.getLog(ReactiveAppConfig.class);
|
||||
static final String TENANT = "my-tenant-r";
|
||||
static final String NAMESPACE = "my-namespace-r";
|
||||
static final String NFQ_TOPIC = "dtant-topic-r";
|
||||
static final String FQ_TOPIC = "persistent://my-tenant-r/my-namespace-r/dtant-topic-r";
|
||||
static final String MSG_PREFIX = "DefaultTenantNamespace-r:";
|
||||
|
||||
@Bean
|
||||
PulsarTopicBuilder topicBuilder() {
|
||||
return new PulsarTopicBuilder(TopicDomain.persistent, TENANT, NAMESPACE);
|
||||
}
|
||||
|
||||
@Bean
|
||||
ReactivePulsarSenderFactory<Object> reactivePulsarSenderFactory(ReactivePulsarClient reactivePulsarClient,
|
||||
PulsarTopicBuilder topicBuilder) {
|
||||
return DefaultReactivePulsarSenderFactory.builderFor(reactivePulsarClient)
|
||||
.withTopicBuilder(topicBuilder)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
ReactivePulsarConsumerFactory<Object> reactivePulsarConsumerFactory(ReactivePulsarClient reactivePulsarClient,
|
||||
PulsarTopicBuilder topicBuilder) {
|
||||
var consumerFactory = new DefaultReactivePulsarConsumerFactory<>(reactivePulsarClient, List.of());
|
||||
consumerFactory.setTopicBuilder(topicBuilder);
|
||||
return consumerFactory;
|
||||
}
|
||||
|
||||
@ReactivePulsarListener(topics = NFQ_TOPIC)
|
||||
Mono<Void> consumeFromNonFullyQualifiedTopic(String msg) {
|
||||
LOG.info("++++++CONSUME %s------".formatted(msg));
|
||||
return Mono.empty();
|
||||
}
|
||||
|
||||
@ReactivePulsarListener(topics = FQ_TOPIC)
|
||||
Mono<Void> consumeFromFullyQualifiedTopic(String msg) {
|
||||
LOG.info("++++++CONSUME %s------".formatted(msg));
|
||||
return Mono.empty();
|
||||
}
|
||||
|
||||
@Bean
|
||||
ApplicationRunner produceWithDefaultTenantAndNamespace(PulsarAdministration pulsarAdmin,
|
||||
ReactivePulsarTemplate<String> template) {
|
||||
createTenantAndNamespace(pulsarAdmin);
|
||||
return (args) -> {
|
||||
for (int i = 0; i < 10; i++) {
|
||||
var msg = MSG_PREFIX + i;
|
||||
template.send((i < 5) ? FQ_TOPIC : NFQ_TOPIC, msg).subscribe();
|
||||
LOG.info("++++++PRODUCE %s------".formatted(msg));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private void createTenantAndNamespace(PulsarAdministration pulsarAdmin) {
|
||||
try (var admin = pulsarAdmin.createAdminClient()) {
|
||||
admin.tenants()
|
||||
.createTenant(TENANT, TenantInfoImpl.builder().allowedClusters(Set.of("standalone")).build());
|
||||
LOG.info("Created tenant -> %s".formatted(admin.tenants().getTenantInfo(TENANT)));
|
||||
admin.namespaces().createNamespace("%s/%s".formatted(TENANT, NAMESPACE));
|
||||
LOG.info("Created namespace -> %s".formatted(admin.namespaces().getNamespaces(TENANT)));
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -11,4 +11,5 @@
|
||||
<logger name="com.github.dockerjava" level="ERROR"/>
|
||||
<logger name="org.springframework.pulsar.function" level="INFO"/>
|
||||
<logger name="org.springframework.pulsar.inttest.app" level="INFO"/>
|
||||
<logger name="org.springframework.pulsar.inttest.config" level="INFO"/>
|
||||
</configuration>
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
[[default-tenant-namespace]]
|
||||
= Default Tenant / Namespace
|
||||
include::../attributes/attributes.adoc[]
|
||||
|
||||
Pulsar has built-in support for {apache-pulsar-docs}/concepts-multi-tenancy/[multi-tenancy].
|
||||
When producing or consuming messages in Pulsar, the specified topic is actually a topic URL of the following format:
|
||||
```
|
||||
(persistent|non-persistent)://tenant/namespace/topic
|
||||
```
|
||||
The URL dictates which tenant and namespace the operation is targeted against.
|
||||
However, when not fully-qualified (i.e. only topic name is specified), the default tenant of `public` and namespace of `default` is used.
|
||||
|
||||
Spring for Apache Pulsar allows you to specify a default tenant and/or namespace to use when producing or consuming messages against a non-fully-qualified topic URL.
|
||||
|
||||
[discrete]
|
||||
== Configuration
|
||||
|
||||
[discrete]
|
||||
=== With Spring Boot
|
||||
When using the Spring Boot you can simply set the {spring-boot-pulsar-config-props}[`spring.pulsar.defaults.tenant`] and {spring-boot-pulsar-config-props}[`spring.pulsar.defaults.namespace`] application properties to specify these defaults.
|
||||
|
||||
[discrete]
|
||||
=== Without Spring Boot
|
||||
However, if you are instead manually configuring the components, you can provide a `PulsarTopicBuilder` configured with the desired default topic and namespace when constructing the corresponding producer or consumer factory.
|
||||
All default consumer/reader/producer factory implementations (imperative and reactive) allow a topic builder to be specified.
|
||||
|
||||
[NOTE]
|
||||
You will need to specify the topic builder on each manually configured factory that you want to use the default tenant/namespace
|
||||
@@ -28,17 +28,20 @@ For all such beans, the `PulsarAdministration` either creates the corresponding
|
||||
|
||||
The following example shows how to add `PulsarTopic` beans to let the `PulsarAdministration` auto-create topics for you:
|
||||
|
||||
[source,java]
|
||||
[source,java,indent=0,subs="verbatim"]
|
||||
----
|
||||
@Bean
|
||||
PulsarTopic simpleTopic {
|
||||
// This will create a non-partitioned topic in the public/default namespace
|
||||
return PulsarTopic.builder("simple-topic").build();
|
||||
// This will create a non-partitioned persistent topic in the 'public/default' tenant/namespace
|
||||
return new PulsarTopicBuilder().name("my-topic").build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
PulsarTopic partitionedTopic {
|
||||
// This will create a partitioned topic with 3 partitions in the provided tenant and namespace
|
||||
return PulsarTopic.builder("persistent://my-tenant/my-namespace/partitioned-topic", 3).build();
|
||||
// This will create a persistent topic with 3 partitions in the provided tenant and namespace
|
||||
return new PulsarTopicBuilder()
|
||||
.name("persistent://my-tenant/my-namespace/partitioned-topic")
|
||||
.numberOfPartitions(3)
|
||||
.build();
|
||||
}
|
||||
----
|
||||
|
||||
@@ -10,11 +10,28 @@ This section covers the changes made from version 1.1 to version 1.2.
|
||||
You can provide your own Jackson `ObjectMapper` that Pulsar will use when producing and consuming JSON messages.
|
||||
See xref:./reference/custom-object-mapper.adoc[Custom Object Mapper] for more details.
|
||||
|
||||
=== Default Tenant and Namespace
|
||||
You can specify a default tenant and/or namespace to use when producing or consuming messages against a non-fully-qualified topic URL.
|
||||
See xref:./reference/default-tenant-namespace.adoc[Default Tenant / Namespace] for more details.
|
||||
|
||||
=== Deprecations
|
||||
|
||||
==== PulsarClient#getPartitionsForTopic(java.lang.String)
|
||||
Version `3.3.1` of the Pulsar client deprecates the `getPartitionsForTopic(java.lang.String)` in favor of `getPartitionsForTopic(java.lang.String, boolean metadataAutoCreationEnabled)`.
|
||||
|
||||
==== PulsarTopic#builder
|
||||
The `PulsarTopicBuilder` is now a registered bean that is configured with default values for domain, tenant, and namespace.
|
||||
As such, this convenience method is no longer needed.
|
||||
Instead, inject the builder bean where needed.
|
||||
|
||||
=== Breaking Changes
|
||||
|
||||
==== PulsarTopic#<init>
|
||||
The `PulsarTopic` constructor now requires a fully qualified topic name (`domain://tenant/namespace/name`).
|
||||
If you are invoking the constructor you will need to be sure the topic you pass in is fully-qualified.
|
||||
A better alternative is to instead use the `PulsarTopicBuilder` as it does not require fully qualified names and will add default values for the missing components in the specified name.
|
||||
|
||||
|
||||
[[what-s-new-in-1-1-since-1-0]]
|
||||
== What's New in 1.1 Since 1.0
|
||||
:page-section-summary-toc: 1
|
||||
|
||||
@@ -25,6 +25,7 @@ import org.apache.pulsar.reactive.client.api.ReactiveMessageConsumerBuilder;
|
||||
import org.apache.pulsar.reactive.client.api.ReactivePulsarClient;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.pulsar.core.PulsarTopicBuilder;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
/**
|
||||
@@ -41,6 +42,9 @@ public class DefaultReactivePulsarConsumerFactory<T> implements ReactivePulsarCo
|
||||
@Nullable
|
||||
private final List<ReactiveMessageConsumerBuilderCustomizer<T>> defaultConfigCustomizers;
|
||||
|
||||
@Nullable
|
||||
private PulsarTopicBuilder topicBuilder;
|
||||
|
||||
/**
|
||||
* Construct an instance.
|
||||
* @param reactivePulsarClient the reactive client
|
||||
@@ -53,6 +57,18 @@ public class DefaultReactivePulsarConsumerFactory<T> implements ReactivePulsarCo
|
||||
this.defaultConfigCustomizers = defaultConfigCustomizers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Non-fully-qualified topic names specified on the created consumers will be
|
||||
* automatically fully-qualified with a default prefix
|
||||
* ({@code domain://tenant/namespace}) according to the specified topic builder.
|
||||
* @param topicBuilder the topic builder used to fully qualify topic names or null to
|
||||
* not fully qualify topic names
|
||||
* @since 1.2.0
|
||||
*/
|
||||
public void setTopicBuilder(@Nullable PulsarTopicBuilder topicBuilder) {
|
||||
this.topicBuilder = topicBuilder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReactiveMessageConsumer<T> createConsumer(Schema<T> schema) {
|
||||
return createConsumer(schema, Collections.emptyList());
|
||||
@@ -61,20 +77,28 @@ public class DefaultReactivePulsarConsumerFactory<T> implements ReactivePulsarCo
|
||||
@Override
|
||||
public ReactiveMessageConsumer<T> createConsumer(Schema<T> schema,
|
||||
List<ReactiveMessageConsumerBuilderCustomizer<T>> customizers) {
|
||||
|
||||
ReactiveMessageConsumerBuilder<T> consumerBuilder = this.reactivePulsarClient.messageConsumer(schema);
|
||||
|
||||
// Apply the default customizers
|
||||
if (!CollectionUtils.isEmpty(this.defaultConfigCustomizers)) {
|
||||
this.defaultConfigCustomizers.forEach((customizer -> customizer.customize(consumerBuilder)));
|
||||
}
|
||||
|
||||
// Apply the user specified customizers
|
||||
if (!CollectionUtils.isEmpty(customizers)) {
|
||||
customizers.forEach((c) -> c.customize(consumerBuilder));
|
||||
}
|
||||
|
||||
if (this.topicBuilder != null) {
|
||||
this.ensureTopicNamesFullyQualified(consumerBuilder);
|
||||
}
|
||||
return consumerBuilder.build();
|
||||
}
|
||||
|
||||
protected void ensureTopicNamesFullyQualified(ReactiveMessageConsumerBuilder<T> consumerBuilder) {
|
||||
var mutableSpec = consumerBuilder.getMutableSpec();
|
||||
var topics = mutableSpec.getTopicNames();
|
||||
if (!CollectionUtils.isEmpty(topics)) {
|
||||
var fullyQualifiedTopics = topics.stream().map(this.topicBuilder::getFullyQualifiedNameForTopic).toList();
|
||||
mutableSpec.setTopicNames(fullyQualifiedTopics);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ import org.apache.pulsar.reactive.client.api.ReactiveMessageReaderBuilder;
|
||||
import org.apache.pulsar.reactive.client.api.ReactivePulsarClient;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.pulsar.core.PulsarTopicBuilder;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
/**
|
||||
@@ -41,6 +42,9 @@ public class DefaultReactivePulsarReaderFactory<T> implements ReactivePulsarRead
|
||||
@Nullable
|
||||
private final List<ReactiveMessageReaderBuilderCustomizer<T>> defaultConfigCustomizers;
|
||||
|
||||
@Nullable
|
||||
private PulsarTopicBuilder topicBuilder;
|
||||
|
||||
/**
|
||||
* Construct an instance.
|
||||
* @param reactivePulsarClient the reactive client
|
||||
@@ -53,6 +57,18 @@ public class DefaultReactivePulsarReaderFactory<T> implements ReactivePulsarRead
|
||||
this.defaultConfigCustomizers = defaultConfigCustomizers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Non-fully-qualified topic names specified on the created readers will be
|
||||
* automatically fully-qualified with a default prefix
|
||||
* ({@code domain://tenant/namespace}) according to the specified topic builder.
|
||||
* @param topicBuilder the topic builder used to fully qualify topic names or null to
|
||||
* not fully qualify topic names
|
||||
* @since 1.2.0
|
||||
*/
|
||||
public void setTopicBuilder(@Nullable PulsarTopicBuilder topicBuilder) {
|
||||
this.topicBuilder = topicBuilder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReactiveMessageReader<T> createReader(Schema<T> schema) {
|
||||
return createReader(schema, Collections.emptyList());
|
||||
@@ -61,20 +77,28 @@ public class DefaultReactivePulsarReaderFactory<T> implements ReactivePulsarRead
|
||||
@Override
|
||||
public ReactiveMessageReader<T> createReader(Schema<T> schema,
|
||||
List<ReactiveMessageReaderBuilderCustomizer<T>> customizers) {
|
||||
|
||||
ReactiveMessageReaderBuilder<T> readerBuilder = this.reactivePulsarClient.messageReader(schema);
|
||||
|
||||
// Apply the default customizers
|
||||
if (!CollectionUtils.isEmpty(this.defaultConfigCustomizers)) {
|
||||
this.defaultConfigCustomizers.forEach((customizer -> customizer.customize(readerBuilder)));
|
||||
}
|
||||
|
||||
// Apply the user specified customizers
|
||||
if (!CollectionUtils.isEmpty(customizers)) {
|
||||
customizers.forEach((c) -> c.customize(readerBuilder));
|
||||
}
|
||||
|
||||
if (this.topicBuilder != null) {
|
||||
this.ensureTopicNamesFullyQualified(readerBuilder);
|
||||
}
|
||||
return readerBuilder.build();
|
||||
}
|
||||
|
||||
protected void ensureTopicNamesFullyQualified(ReactiveMessageReaderBuilder<T> readerBuilder) {
|
||||
var mutableSpec = readerBuilder.getMutableSpec();
|
||||
var topics = mutableSpec.getTopicNames();
|
||||
if (!CollectionUtils.isEmpty(topics)) {
|
||||
var fullyQualifiedTopics = topics.stream().map(this.topicBuilder::getFullyQualifiedNameForTopic).toList();
|
||||
mutableSpec.setTopicNames(fullyQualifiedTopics);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ import org.apache.pulsar.reactive.client.api.ReactivePulsarClient;
|
||||
import org.springframework.core.log.LogAccessor;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.pulsar.core.DefaultTopicResolver;
|
||||
import org.springframework.pulsar.core.PulsarTopicBuilder;
|
||||
import org.springframework.pulsar.core.TopicResolver;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
@@ -65,14 +66,19 @@ public final class DefaultReactivePulsarSenderFactory<T>
|
||||
@Nullable
|
||||
private final List<ReactiveMessageSenderBuilderCustomizer<T>> defaultConfigCustomizers;
|
||||
|
||||
@Nullable
|
||||
private final PulsarTopicBuilder topicBuilder;
|
||||
|
||||
private DefaultReactivePulsarSenderFactory(ReactivePulsarClient reactivePulsarClient, TopicResolver topicResolver,
|
||||
@Nullable ReactiveMessageSenderCache reactiveMessageSenderCache, @Nullable String defaultTopic,
|
||||
@Nullable List<ReactiveMessageSenderBuilderCustomizer<T>> defaultConfigCustomizers) {
|
||||
@Nullable List<ReactiveMessageSenderBuilderCustomizer<T>> defaultConfigCustomizers,
|
||||
@Nullable PulsarTopicBuilder topicBuilder) {
|
||||
this.reactivePulsarClient = reactivePulsarClient;
|
||||
this.topicResolver = topicResolver;
|
||||
this.reactiveMessageSenderCache = reactiveMessageSenderCache;
|
||||
this.defaultTopic = defaultTopic;
|
||||
this.defaultConfigCustomizers = defaultConfigCustomizers;
|
||||
this.topicBuilder = topicBuilder;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -116,7 +122,7 @@ public final class DefaultReactivePulsarSenderFactory<T>
|
||||
private ReactiveMessageSender<T> doCreateReactiveMessageSender(Schema<T> schema, @Nullable String topic,
|
||||
@Nullable List<ReactiveMessageSenderBuilderCustomizer<T>> customizers) {
|
||||
Objects.requireNonNull(schema, "Schema must be specified");
|
||||
String resolvedTopic = this.topicResolver.resolveTopic(topic, () -> getDefaultTopic()).orElseThrow();
|
||||
String resolvedTopic = this.resolveTopicName(topic);
|
||||
this.logger.trace(() -> "Creating reactive message sender for '%s' topic".formatted(resolvedTopic));
|
||||
|
||||
ReactiveMessageSenderBuilder<T> sender = this.reactivePulsarClient.messageSender(schema);
|
||||
@@ -140,6 +146,12 @@ public final class DefaultReactivePulsarSenderFactory<T>
|
||||
return sender.build();
|
||||
}
|
||||
|
||||
protected String resolveTopicName(String userSpecifiedTopic) {
|
||||
var resolvedTopic = this.topicResolver.resolveTopic(userSpecifiedTopic, this::getDefaultTopic).orElseThrow();
|
||||
return this.topicBuilder != null ? this.topicBuilder.getFullyQualifiedNameForTopic(resolvedTopic)
|
||||
: resolvedTopic;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDefaultTopic() {
|
||||
return this.defaultTopic;
|
||||
@@ -192,6 +204,9 @@ public final class DefaultReactivePulsarSenderFactory<T>
|
||||
|
||||
private TopicResolver topicResolver = new DefaultTopicResolver();
|
||||
|
||||
@Nullable
|
||||
private PulsarTopicBuilder topicBuilder;
|
||||
|
||||
@Nullable
|
||||
private ReactiveMessageSenderCache messageSenderCache;
|
||||
|
||||
@@ -216,6 +231,20 @@ public final class DefaultReactivePulsarSenderFactory<T>
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide the topic builder to use to fully qualify topic names.
|
||||
* Non-fully-qualified topic names specified on the created senders will be
|
||||
* automatically fully-qualified with a default prefix
|
||||
* ({@code domain://tenant/namespace}) according to the topic builder.
|
||||
* @param topicBuilder the topic builder to use
|
||||
* @return this same builder instance
|
||||
* @since 1.2.0
|
||||
*/
|
||||
public Builder<T> withTopicBuilder(PulsarTopicBuilder topicBuilder) {
|
||||
this.topicBuilder = topicBuilder;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide the message sender cache to use.
|
||||
* @param messageSenderCache the message sender cache to use
|
||||
@@ -266,7 +295,7 @@ public final class DefaultReactivePulsarSenderFactory<T>
|
||||
public DefaultReactivePulsarSenderFactory<T> build() {
|
||||
Assert.notNull(this.topicResolver, "Topic resolver is required");
|
||||
return new DefaultReactivePulsarSenderFactory<>(this.reactivePulsarClient, this.topicResolver,
|
||||
this.messageSenderCache, this.defaultTopic, this.defaultConfigCustomizers);
|
||||
this.messageSenderCache, this.defaultTopic, this.defaultConfigCustomizers, this.topicBuilder);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
package org.springframework.pulsar.reactive.core;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
@@ -31,6 +33,8 @@ import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.pulsar.core.PulsarTopicBuilder;
|
||||
|
||||
/**
|
||||
* Tests for {@link DefaultReactivePulsarConsumerFactory}.
|
||||
*
|
||||
@@ -104,4 +108,25 @@ class DefaultReactivePulsarConsumerFactoryTests {
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
class FactoryCreatedWithTopicBuilder {
|
||||
|
||||
@Test
|
||||
void createConsumer() {
|
||||
var topicBuilder = spy(new PulsarTopicBuilder());
|
||||
var consumerFactory = new DefaultReactivePulsarConsumerFactory<String>(
|
||||
AdaptedReactivePulsarClientFactory.create((PulsarClient) null), null);
|
||||
consumerFactory.setTopicBuilder(topicBuilder);
|
||||
var inputTopic = "my-topic";
|
||||
var fullyQualifiedTopic = "persistent://public/default/my-topic";
|
||||
var consumer = consumerFactory.createConsumer(SCHEMA,
|
||||
Collections.singletonList(builder -> builder.topic(inputTopic)));
|
||||
assertThat(consumer)
|
||||
.extracting("consumerSpec", InstanceOfAssertFactories.type(ReactiveMessageConsumerSpec.class))
|
||||
.hasFieldOrPropertyWithValue("topicNames", List.of(fullyQualifiedTopic));
|
||||
verify(topicBuilder).getFullyQualifiedNameForTopic(inputTopic);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
package org.springframework.pulsar.reactive.core;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
@@ -29,6 +31,8 @@ import org.apache.pulsar.reactive.client.api.ReactiveMessageReaderSpec;
|
||||
import org.assertj.core.api.InstanceOfAssertFactories;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.pulsar.core.PulsarTopicBuilder;
|
||||
|
||||
/**
|
||||
* Tests for {@link DefaultReactivePulsarReaderFactory}.
|
||||
*
|
||||
@@ -66,4 +70,20 @@ class DefaultReactivePulsarReaderFactoryTests {
|
||||
.isEqualTo("new-test-reader");
|
||||
}
|
||||
|
||||
@Test
|
||||
void createReaderUsingTopicBuilder() {
|
||||
var inputTopic = "my-topic";
|
||||
var fullyQualifiedTopic = "persistent://public/default/my-topic";
|
||||
var topicBuilder = spy(new PulsarTopicBuilder());
|
||||
var readerFactory = new DefaultReactivePulsarReaderFactory<String>(
|
||||
AdaptedReactivePulsarClientFactory.create((PulsarClient) null), null);
|
||||
readerFactory.setTopicBuilder(topicBuilder);
|
||||
var reader = readerFactory.createReader(schema,
|
||||
Collections.singletonList(builder -> builder.topic(inputTopic)));
|
||||
assertThat(reader).extracting("readerSpec", InstanceOfAssertFactories.type(ReactiveMessageReaderSpec.class))
|
||||
.extracting(ReactiveMessageReaderSpec::getTopicNames)
|
||||
.isEqualTo(List.of(fullyQualifiedTopic));
|
||||
verify(topicBuilder).getFullyQualifiedNameForTopic(inputTopic);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.InOrder;
|
||||
|
||||
import org.springframework.pulsar.core.PulsarTopicBuilder;
|
||||
import org.springframework.pulsar.core.TopicResolver;
|
||||
|
||||
/**
|
||||
@@ -74,6 +75,20 @@ class DefaultReactivePulsarSenderFactoryTests {
|
||||
assertThat(senderFactory).hasFieldOrPropertyWithValue("topicResolver", customTopicResolver);
|
||||
}
|
||||
|
||||
@Test
|
||||
void createSenderWithTopicBuilder() {
|
||||
var inputTopic = "my-topic";
|
||||
var fullyQualifiedTopic = "persistent://public/default/my-topic";
|
||||
var topicBuilder = spy(new PulsarTopicBuilder());
|
||||
var senderFactory = DefaultReactivePulsarSenderFactory.<String>builderFor(mock(PulsarClient.class))
|
||||
.withTopicBuilder(topicBuilder)
|
||||
.build();
|
||||
assertThat(senderFactory).hasFieldOrPropertyWithValue("topicBuilder", topicBuilder);
|
||||
var sender = senderFactory.createSender(schema, inputTopic);
|
||||
assertThatSenderHasTopic(sender, fullyQualifiedTopic);
|
||||
verify(topicBuilder).getFullyQualifiedNameForTopic(inputTopic);
|
||||
}
|
||||
|
||||
private void assertThatSenderHasTopic(ReactiveMessageSender<String> sender, String expectedTopic) {
|
||||
assertThatSenderSpecSatisfies(sender,
|
||||
(senderSpec) -> assertThat(senderSpec).extracting(ReactiveMessageSenderSpec::getTopicName)
|
||||
|
||||
@@ -32,6 +32,7 @@ import org.springframework.pulsar.core.PulsarAdministration;
|
||||
import org.springframework.pulsar.core.PulsarProducerFactory;
|
||||
import org.springframework.pulsar.core.PulsarTemplate;
|
||||
import org.springframework.pulsar.core.PulsarTopic;
|
||||
import org.springframework.pulsar.core.PulsarTopicBuilder;
|
||||
import org.springframework.pulsar.reactive.config.DefaultReactivePulsarListenerContainerFactory;
|
||||
import org.springframework.pulsar.reactive.config.ReactivePulsarListenerContainerFactory;
|
||||
import org.springframework.pulsar.reactive.config.annotation.EnableReactivePulsar;
|
||||
@@ -107,7 +108,9 @@ abstract class ReactivePulsarListenerTestsBase implements PulsarTestContainerSup
|
||||
|
||||
@Bean
|
||||
PulsarTopic partitionedTopic() {
|
||||
return PulsarTopic.builder("persistent://public/default/concurrency-on-pl").numberOfPartitions(3).build();
|
||||
return new PulsarTopicBuilder().name("persistent://public/default/concurrency-on-pl")
|
||||
.numberOfPartitions(3)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
|
||||
@@ -25,7 +25,8 @@ dependencies {
|
||||
implementation 'io.zipkin.reporter2:zipkin-reporter-brave'
|
||||
implementation 'io.zipkin.reporter2:zipkin-sender-urlconnection'
|
||||
developmentOnly 'org.springframework.boot:spring-boot-docker-compose'
|
||||
|
||||
// TODO remove when new PulsarTopicBuilder published
|
||||
implementation project(':spring-pulsar')
|
||||
testImplementation project(':spring-pulsar-test')
|
||||
testRuntimeOnly 'ch.qos.logback:logback-classic'
|
||||
testImplementation "org.springframework.boot:spring-boot-starter-test"
|
||||
|
||||
@@ -31,6 +31,7 @@ import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.pulsar.annotation.PulsarListener;
|
||||
import org.springframework.pulsar.core.PulsarTemplate;
|
||||
import org.springframework.pulsar.core.PulsarTopic;
|
||||
import org.springframework.pulsar.core.PulsarTopicBuilder;
|
||||
|
||||
@SpringBootApplication
|
||||
public class FailoverConsumerApp {
|
||||
@@ -45,7 +46,7 @@ public class FailoverConsumerApp {
|
||||
|
||||
@Bean
|
||||
PulsarTopic failoverDemoTopic() {
|
||||
return PulsarTopic.builder(TOPIC).numberOfPartitions(3).build();
|
||||
return new PulsarTopicBuilder().name(TOPIC).numberOfPartitions(3).build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
|
||||
@@ -21,7 +21,7 @@ ext['pulsar.version'] = "${pulsarVersion}"
|
||||
dependencies {
|
||||
implementation 'org.springframework.boot:spring-boot-starter-pulsar'
|
||||
developmentOnly 'org.springframework.boot:spring-boot-docker-compose'
|
||||
// temporary until JsonSchemaUtil published
|
||||
// TODO remove when new PulsarTopicBuilder published
|
||||
implementation project(':spring-pulsar')
|
||||
implementation(testFixtures(project(":spring-pulsar")))
|
||||
implementation project(':spring-pulsar-test')
|
||||
|
||||
@@ -30,6 +30,7 @@ import org.springframework.pulsar.annotation.PulsarListener;
|
||||
import org.springframework.pulsar.core.DefaultSchemaResolver;
|
||||
import org.springframework.pulsar.core.PulsarTemplate;
|
||||
import org.springframework.pulsar.core.PulsarTopic;
|
||||
import org.springframework.pulsar.core.PulsarTopicBuilder;
|
||||
import org.springframework.pulsar.core.SchemaResolver;
|
||||
import org.springframework.pulsar.test.model.UserRecord;
|
||||
import org.springframework.pulsar.test.model.json.UserRecordObjectMapper;
|
||||
@@ -98,7 +99,7 @@ public class ImperativeProduceAndConsumeApp {
|
||||
|
||||
@Bean
|
||||
PulsarTopic partitionedTopic() {
|
||||
return PulsarTopic.builder(TOPIC).numberOfPartitions(3).build();
|
||||
return new PulsarTopicBuilder().name(TOPIC).numberOfPartitions(3).build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
|
||||
@@ -52,6 +52,9 @@ public class DefaultPulsarConsumerFactory<T> implements PulsarConsumerFactory<T>
|
||||
@Nullable
|
||||
private final List<ConsumerBuilderCustomizer<T>> defaultConfigCustomizers;
|
||||
|
||||
@Nullable
|
||||
private PulsarTopicBuilder topicBuilder;
|
||||
|
||||
/**
|
||||
* Construct a consumer factory instance.
|
||||
* @param pulsarClient the client used to consume
|
||||
@@ -64,6 +67,18 @@ public class DefaultPulsarConsumerFactory<T> implements PulsarConsumerFactory<T>
|
||||
this.defaultConfigCustomizers = defaultConfigCustomizers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Non-fully-qualified topic names specified on the created consumers will be
|
||||
* automatically fully-qualified with a default prefix
|
||||
* ({@code domain://tenant/namespace}) according to the specified topic builder.
|
||||
* @param topicBuilder the topic builder used to fully qualify topic names or null to
|
||||
* not fully qualify topic names
|
||||
* @since 1.2.0
|
||||
*/
|
||||
public void setTopicBuilder(@Nullable PulsarTopicBuilder topicBuilder) {
|
||||
this.topicBuilder = topicBuilder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Consumer<T> createConsumer(Schema<T> schema, @Nullable Collection<String> topics,
|
||||
@Nullable String subscriptionName, ConsumerBuilderCustomizer<T> customizer) {
|
||||
@@ -111,6 +126,9 @@ public class DefaultPulsarConsumerFactory<T> implements PulsarConsumerFactory<T>
|
||||
}
|
||||
|
||||
private void replaceTopicsOnBuilder(ConsumerBuilder<T> builder, Collection<String> topics) {
|
||||
if (this.topicBuilder != null) {
|
||||
topics = topics.stream().map(this.topicBuilder::getFullyQualifiedNameForTopic).toList();
|
||||
}
|
||||
var builderImpl = (ConsumerBuilderImpl<T>) builder;
|
||||
builderImpl.getConf().setTopicNames(new HashSet<>(topics));
|
||||
}
|
||||
|
||||
@@ -57,6 +57,9 @@ public class DefaultPulsarProducerFactory<T> implements PulsarProducerFactory<T>
|
||||
|
||||
private final TopicResolver topicResolver;
|
||||
|
||||
@Nullable
|
||||
private PulsarTopicBuilder topicBuilder;
|
||||
|
||||
/**
|
||||
* Construct a producer factory that uses a default topic resolver.
|
||||
* @param pulsarClient the client used to create the producers
|
||||
@@ -102,6 +105,18 @@ public class DefaultPulsarProducerFactory<T> implements PulsarProducerFactory<T>
|
||||
this.topicResolver = Objects.requireNonNull(topicResolver, "topicResolver must not be null");
|
||||
}
|
||||
|
||||
/**
|
||||
* Non-fully-qualified topic names specified on the created producers will be
|
||||
* automatically fully-qualified with a default prefix
|
||||
* ({@code domain://tenant/namespace}) according to the specified topic builder.
|
||||
* @param topicBuilder the topic builder used to fully qualify topic names or null to
|
||||
* not fully qualify topic names
|
||||
* @since 1.2.0
|
||||
*/
|
||||
public void setTopicBuilder(@Nullable PulsarTopicBuilder topicBuilder) {
|
||||
this.topicBuilder = topicBuilder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Producer<T> createProducer(Schema<T> schema, @Nullable String topic) {
|
||||
return doCreateProducer(schema, topic, null, null);
|
||||
@@ -169,7 +184,9 @@ public class DefaultPulsarProducerFactory<T> implements PulsarProducerFactory<T>
|
||||
}
|
||||
|
||||
protected String resolveTopicName(String userSpecifiedTopic) {
|
||||
return this.topicResolver.resolveTopic(userSpecifiedTopic, this::getDefaultTopic).orElseThrow();
|
||||
var resolvedTopic = this.topicResolver.resolveTopic(userSpecifiedTopic, this::getDefaultTopic).orElseThrow();
|
||||
return this.topicBuilder != null ? this.topicBuilder.getFullyQualifiedNameForTopic(resolvedTopic)
|
||||
: resolvedTopic;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -45,6 +45,9 @@ public class DefaultPulsarReaderFactory<T> implements PulsarReaderFactory<T> {
|
||||
@Nullable
|
||||
private final List<ReaderBuilderCustomizer<T>> defaultConfigCustomizers;
|
||||
|
||||
@Nullable
|
||||
private PulsarTopicBuilder topicBuilder;
|
||||
|
||||
/**
|
||||
* Construct a reader factory instance with no default configuration.
|
||||
* @param pulsarClient the client used to consume
|
||||
@@ -65,6 +68,18 @@ public class DefaultPulsarReaderFactory<T> implements PulsarReaderFactory<T> {
|
||||
this.defaultConfigCustomizers = defaultConfigCustomizers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Non-fully-qualified topic names specified on the created readers will be
|
||||
* automatically fully-qualified with a default prefix
|
||||
* ({@code domain://tenant/namespace}) according to the specified topic builder.
|
||||
* @param topicBuilder the topic builder used to fully qualify topic names or null to
|
||||
* not fully qualify topic names
|
||||
* @since 1.2.0
|
||||
*/
|
||||
public void setTopicBuilder(@Nullable PulsarTopicBuilder topicBuilder) {
|
||||
this.topicBuilder = topicBuilder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Reader<T> createReader(@Nullable List<String> topics, @Nullable MessageId messageId, Schema<T> schema,
|
||||
@Nullable List<ReaderBuilderCustomizer<T>> customizers) throws PulsarClientException {
|
||||
@@ -92,6 +107,9 @@ public class DefaultPulsarReaderFactory<T> implements PulsarReaderFactory<T> {
|
||||
}
|
||||
|
||||
private void replaceTopicsOnBuilder(ReaderBuilder<T> builder, Collection<String> topics) {
|
||||
if (this.topicBuilder != null) {
|
||||
topics = topics.stream().map(this.topicBuilder::getFullyQualifiedNameForTopic).toList();
|
||||
}
|
||||
var builderImpl = (ReaderBuilderImpl<T>) builder;
|
||||
builderImpl.getConf().setTopicNames(new HashSet<>(topics));
|
||||
}
|
||||
|
||||
@@ -130,12 +130,13 @@ public class PulsarAdministration
|
||||
}
|
||||
|
||||
private String getTopicNamespaceIdentifier(PulsarTopic topic) {
|
||||
return topic.getComponents().tenant() + "/" + topic.getComponents().namespace();
|
||||
var components = topic.getComponents();
|
||||
return components.tenant() + "/" + components.namespace();
|
||||
}
|
||||
|
||||
private List<String> getMatchingTopicPartitions(PulsarTopic topic, List<String> existingTopics) {
|
||||
return existingTopics.stream()
|
||||
.filter(existing -> existing.startsWith(topic.getFullyQualifiedTopicName() + "-partition-"))
|
||||
.filter(existing -> existing.startsWith(topic.topicName() + "-partition-"))
|
||||
.toList();
|
||||
}
|
||||
|
||||
@@ -143,7 +144,6 @@ public class PulsarAdministration
|
||||
if (CollectionUtils.isEmpty(topics)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try (PulsarAdmin admin = createAdminClient()) {
|
||||
doCreateOrModifyTopicsIfNeeded(admin, topics);
|
||||
}
|
||||
@@ -163,7 +163,7 @@ public class PulsarAdministration
|
||||
var existingTopicsInNamespace = admin.topics().getList(namespace);
|
||||
|
||||
for (var topic : requestedTopics) {
|
||||
var topicName = topic.getFullyQualifiedTopicName();
|
||||
var topicName = topic.topicName();
|
||||
if (topic.isPartitioned()) {
|
||||
if (existingTopicsInNamespace.contains(topicName)) {
|
||||
throw new IllegalStateException(
|
||||
@@ -214,9 +214,8 @@ public class PulsarAdministration
|
||||
}
|
||||
|
||||
private void createTopics(PulsarAdmin admin, Set<PulsarTopic> topicsToCreate) throws PulsarAdminException {
|
||||
this.logger.debug(() -> "Creating topics: " + topicsToCreate.stream()
|
||||
.map(PulsarTopic::getFullyQualifiedTopicName)
|
||||
.collect(Collectors.joining(",")));
|
||||
this.logger.debug(() -> "Creating topics: "
|
||||
+ topicsToCreate.stream().map(PulsarTopic::topicName).collect(Collectors.joining(",")));
|
||||
for (var topic : topicsToCreate) {
|
||||
if (topic.isPartitioned()) {
|
||||
admin.topics().createPartitionedTopic(topic.topicName(), topic.numberOfPartitions());
|
||||
@@ -228,9 +227,8 @@ public class PulsarAdministration
|
||||
}
|
||||
|
||||
private void modifyTopics(PulsarAdmin admin, Set<PulsarTopic> topicsToModify) throws PulsarAdminException {
|
||||
this.logger.debug(() -> "Modifying topics: " + topicsToModify.stream()
|
||||
.map(PulsarTopic::getFullyQualifiedTopicName)
|
||||
.collect(Collectors.joining(",")));
|
||||
this.logger.debug(() -> "Modifying topics: "
|
||||
+ topicsToModify.stream().map(PulsarTopic::topicName).collect(Collectors.joining(",")));
|
||||
for (var topic : topicsToModify) {
|
||||
admin.topics().updatePartitionedTopic(topic.topicName(), topic.numberOfPartitions());
|
||||
}
|
||||
|
||||
@@ -16,25 +16,57 @@
|
||||
|
||||
package org.springframework.pulsar.core;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.apache.pulsar.common.naming.TopicDomain;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Model class for a Pulsar topic.
|
||||
* Represents a Pulsar topic.
|
||||
* <p>
|
||||
* The input {@code topicName} must be fully-qualified. As such, it is recommended to use
|
||||
* the {@link PulsarTopicBuilder} to create instances like this: <pre>{@code
|
||||
* PulsarTopic topic = new PulsarTopicBuilder().name("my-topic").build();
|
||||
* }</pre> The builder is more lenient and allows non-fully-qualified topic names to be
|
||||
* input and fully qualifies the output name using its configured default tenant and
|
||||
* namepsace.
|
||||
*
|
||||
* Use the {@link PulsarTopicBuilder} to create instances like this:
|
||||
*
|
||||
* <pre>{@code
|
||||
* PulsarTopic topic = PulsarTopic.builder("topic-name").build();
|
||||
* }</pre>
|
||||
*
|
||||
* @param topicName the topic name
|
||||
* @param topicName the fully qualified topic name in the format
|
||||
* {@code 'domain://tenant/namespace/name'}
|
||||
* @param numberOfPartitions the number of partitions, or 0 for non-partitioned topics
|
||||
* @author Alexander Preuß
|
||||
* @author Chris Bono
|
||||
* @see PulsarTopicBuilder
|
||||
*/
|
||||
public record PulsarTopic(String topicName, int numberOfPartitions) {
|
||||
|
||||
// Pulsar allows (a-zA-Z_0-9) and special chars -=:. for names
|
||||
private static final String NAME_PATTERN_STR = "[-=:\\.\\w]*";
|
||||
|
||||
private static Pattern TOPIC_NAME_PATTERN = Pattern.compile("(persistent|non-persistent)\\:\\/\\/(%s)\\/(%s)\\/(%s)"
|
||||
.formatted(NAME_PATTERN_STR, NAME_PATTERN_STR, NAME_PATTERN_STR));
|
||||
|
||||
private static final String INVALID_NAME_MSG = "topicName %s must be fully-qualified "
|
||||
+ "in the format 'domain://tenant/namespace/name' where "
|
||||
+ "domain is one of ('persistent', 'non-persistent') and the other components must be "
|
||||
+ "composed of one or more letters, digits, or special characters ('-', '=', ':', or '.')";
|
||||
|
||||
public PulsarTopic {
|
||||
Assert.state(TOPIC_NAME_PATTERN.matcher(topicName).matches(), INVALID_NAME_MSG.formatted(topicName));
|
||||
Assert.state(numberOfPartitions >= 0, "numberOfPartitions must be >= 0");
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience method to create a topic builder with the specified topic name.
|
||||
* @param topicName the name of the topic
|
||||
* @return the topic builder instance
|
||||
* @deprecated As of version 1.2.0 topic builder is a registered bean - instead use an
|
||||
* injected instance where needed
|
||||
*/
|
||||
@Deprecated(since = "1.2.0", forRemoval = true)
|
||||
public static PulsarTopicBuilder builder(String topicName) {
|
||||
return new PulsarTopicBuilder(topicName);
|
||||
return new PulsarTopicBuilder().name(topicName);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -50,28 +82,27 @@ public record PulsarTopic(String topicName, int numberOfPartitions) {
|
||||
* @return {@link TopicComponents}
|
||||
*/
|
||||
public TopicComponents getComponents() {
|
||||
String[] splitTopic = this.topicName().split("/");
|
||||
if (splitTopic.length == 1) { // e.g. 'my-topic'
|
||||
return new TopicComponents(TopicDomain.persistent, "public", "default", splitTopic[0]);
|
||||
}
|
||||
else if (splitTopic.length == 3) { // e.g. 'public/default/my-topic'
|
||||
return new TopicComponents(TopicDomain.persistent, splitTopic[0], splitTopic[1], splitTopic[2]);
|
||||
}
|
||||
else if (splitTopic.length == 5) { // e.g. 'persistent://public/default/my-topic'
|
||||
String type = splitTopic[0].replace(":", "");
|
||||
return new TopicComponents(TopicDomain.getEnum(type), splitTopic[2], splitTopic[3], splitTopic[4]);
|
||||
}
|
||||
throw new IllegalArgumentException("Topic name '" + this + "' has unexpected components.");
|
||||
|
||||
var splitTopic = this.topicName().split("/");
|
||||
var type = splitTopic[0].replace(":", "");
|
||||
return new TopicComponents(TopicDomain.getEnum(type), splitTopic[2], splitTopic[3], splitTopic[4]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the fully-qualified name of the topic.
|
||||
* Get the fully-qualified name of this topic in the format
|
||||
* {@code domain://tenant/namespace/name} where the components have the following
|
||||
* defaults when not specified in the original topic name used to build this topic.
|
||||
* <pre>
|
||||
* - {@code domain} is one of ('persistent', 'non-persistent') with a default of 'persistent'
|
||||
* - {@code tenant} has default of 'public'
|
||||
* - {@code namespace} has default of 'default'
|
||||
* </pre>
|
||||
* @return the fully-qualified topic name
|
||||
* @deprecated As of version 1.2.0 topicName must always be fully qualified, use
|
||||
* {@link #topicName()} instead.
|
||||
*/
|
||||
@Deprecated(since = "1.2.0", forRemoval = true)
|
||||
public String getFullyQualifiedTopicName() {
|
||||
TopicComponents components = this.getComponents();
|
||||
return components.domain + "://" + components.tenant + "/" + components.namespace + "/" + components.name;
|
||||
return this.topicName();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -16,23 +16,110 @@
|
||||
|
||||
package org.springframework.pulsar.core;
|
||||
|
||||
import org.apache.pulsar.common.naming.TopicDomain;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Builder class to create {@link PulsarTopic} instances.
|
||||
*
|
||||
* @author Alexander Preuß
|
||||
* @author Chris Bono
|
||||
*/
|
||||
public class PulsarTopicBuilder {
|
||||
|
||||
private final String topicName;
|
||||
private static final String FQ_TOPIC_NAME_FORMAT = "%s://%s/%s/%s";
|
||||
|
||||
private static final String DEFAULT_TENANT = "public";
|
||||
|
||||
private static final String DEFAULT_NAMESPACE = "default";
|
||||
|
||||
private final TopicDomain defaultDomain;
|
||||
|
||||
private final String defaultTenant;
|
||||
|
||||
private final String defaultNamespace;
|
||||
|
||||
@Nullable
|
||||
private String name;
|
||||
|
||||
@Nullable
|
||||
private int numberOfPartitions;
|
||||
|
||||
protected PulsarTopicBuilder(String topicName) {
|
||||
this.topicName = topicName;
|
||||
/**
|
||||
* Create a builder instance that uses the following defaults. <pre>
|
||||
* - {@code domain -> 'persistent'}
|
||||
* - {@code tenant -> 'public'}
|
||||
* - {@code namespace -> 'default'}
|
||||
* </pre>
|
||||
*/
|
||||
public PulsarTopicBuilder() {
|
||||
this(TopicDomain.persistent, DEFAULT_TENANT, DEFAULT_NAMESPACE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the number of topic partitions.
|
||||
* Create a builder instance that uses the specified defaults.
|
||||
* @param defaultDomain domain to use for the topic when not present in the name
|
||||
* @param defaultTenant tentant to use for the topic when not present in the name
|
||||
* @param defaultNamespace namespace to use for the topic when not present in the name
|
||||
*/
|
||||
public PulsarTopicBuilder(TopicDomain defaultDomain, String defaultTenant, String defaultNamespace) {
|
||||
Assert.notNull(defaultDomain, "defaultDomain must not be null");
|
||||
Assert.hasText(defaultTenant, "defaultTenant must be specified");
|
||||
Assert.hasText(defaultNamespace, "defaultNamespace must be specified");
|
||||
this.defaultDomain = defaultDomain;
|
||||
this.defaultTenant = defaultTenant;
|
||||
this.defaultNamespace = defaultNamespace;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the fully-qualified name of the specified topic in the format
|
||||
* {@code domain://tenant/namespace/name}.
|
||||
* @param topicName the topic name to fully qualify
|
||||
* @return the fully-qualified topic name
|
||||
*/
|
||||
public String getFullyQualifiedNameForTopic(String topicName) {
|
||||
return this.fullyQualifiedName(topicName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the name of the topic under construction. The following formats are accepted:
|
||||
* <pre>
|
||||
* - {@code 'name'}
|
||||
* - {@code 'tenant/namespace/name'}
|
||||
* - {@code 'domain://tenant/namespace/name'}
|
||||
* </pre> When the name is not fully-qualified the missing components are populated
|
||||
* with the corresponding default configured on the builder.
|
||||
* @param name the topic name
|
||||
* @return this builder
|
||||
*/
|
||||
public PulsarTopicBuilder name(String name) {
|
||||
this.name = fullyQualifiedName(name);
|
||||
return this;
|
||||
}
|
||||
|
||||
private String fullyQualifiedName(String name) {
|
||||
Assert.notNull(name, "name must not be null");
|
||||
String[] splitTopic = name.split("/");
|
||||
if (splitTopic.length == 1) { // e.g. 'my-topic'
|
||||
return FQ_TOPIC_NAME_FORMAT.formatted(this.defaultDomain, this.defaultTenant, this.defaultNamespace,
|
||||
splitTopic[0]);
|
||||
}
|
||||
if (splitTopic.length == 3) { // e.g. 'public/default/my-topic'
|
||||
return FQ_TOPIC_NAME_FORMAT.formatted(this.defaultDomain, splitTopic[0], splitTopic[1], splitTopic[2]);
|
||||
}
|
||||
if (splitTopic.length == 5) { // e.g. 'persistent://public/default/my-topic'
|
||||
String type = splitTopic[0].replace(":", "");
|
||||
return FQ_TOPIC_NAME_FORMAT.formatted(TopicDomain.getEnum(type), splitTopic[2], splitTopic[3],
|
||||
splitTopic[4]);
|
||||
}
|
||||
throw new IllegalArgumentException("Topic name '" + name + "' must be in one of "
|
||||
+ "the following formats ('name', 'tenant/namespace/name', 'domain://tenant/namespace/name')");
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the number of topic partitions for the topic under construction.
|
||||
* @param numberOfPartitions the number of topic partitions
|
||||
* @return this builder
|
||||
*/
|
||||
@@ -46,7 +133,7 @@ public class PulsarTopicBuilder {
|
||||
* @return {@link PulsarTopic}
|
||||
*/
|
||||
public PulsarTopic build() {
|
||||
return new PulsarTopic(this.topicName, this.numberOfPartitions);
|
||||
return new PulsarTopic(this.name, this.numberOfPartitions);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -89,6 +89,23 @@ class CachingPulsarProducerFactoryTests extends PulsarProducerFactoryTests {
|
||||
assertThat(cachedProducerWrapper).isSameAs(producer1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void createProducerWithTopicBuilderAndMultipleCalls() {
|
||||
var inputTopic = "topic1";
|
||||
var fullyQualifiedTopic = "persistent://public/default/topic1";
|
||||
var topicBuilder = spy(new PulsarTopicBuilder());
|
||||
var producerFactory = producerFactory(pulsarClient, null, null, topicBuilder);
|
||||
var cacheKey = new ProducerCacheKey<>(schema, fullyQualifiedTopic, null, null);
|
||||
var producer1 = producerFactory.createProducer(schema, inputTopic);
|
||||
var producer2 = producerFactory.createProducer(new StringSchema(), inputTopic);
|
||||
var producer3 = producerFactory.createProducer(new StringSchema(), fullyQualifiedTopic);
|
||||
assertThat(producer1).isSameAs(producer2).isSameAs(producer3);
|
||||
CacheProvider<ProducerCacheKey<String>, Producer<String>> producerCache = getAssertedProducerCache(
|
||||
producerFactory, Collections.singletonList(cacheKey));
|
||||
Producer<String> cachedProducerWrapper = producerCache.asMap().get(cacheKey);
|
||||
assertThat(cachedProducerWrapper).isSameAs(producer1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void cachedProducerIsCloseSafeWrapper() throws PulsarClientException {
|
||||
var producerFactory = newProducerFactory();
|
||||
@@ -169,7 +186,7 @@ class CachingPulsarProducerFactoryTests extends PulsarProducerFactoryTests {
|
||||
|
||||
@Test
|
||||
void factoryDestroyCleansUpCacheAndClosesProducers() {
|
||||
CachingPulsarProducerFactory<String> producerFactory = producerFactory(pulsarClient, null, null);
|
||||
CachingPulsarProducerFactory<String> producerFactory = producerFactory(pulsarClient, null, null, null);
|
||||
var actualProducer1 = actualProducer(producerFactory.createProducer(schema, "topic1"));
|
||||
var actualProducer2 = actualProducer(producerFactory.createProducer(schema, "topic2"));
|
||||
var cacheKey1 = new ProducerCacheKey<>(schema, "topic1", null, null);
|
||||
@@ -200,7 +217,7 @@ class CachingPulsarProducerFactoryTests extends PulsarProducerFactoryTests {
|
||||
void createProducerEncountersException() {
|
||||
pulsarClient = spy(pulsarClient);
|
||||
when(this.pulsarClient.newProducer(schema)).thenThrow(new RuntimeException("5150"));
|
||||
var producerFactory = producerFactory(pulsarClient, null, null);
|
||||
var producerFactory = producerFactory(pulsarClient, null, null, null);
|
||||
assertThatThrownBy(() -> producerFactory.createProducer(schema, "topic1")).isInstanceOf(RuntimeException.class)
|
||||
.hasMessage("5150");
|
||||
getAssertedProducerCache(producerFactory, Collections.emptyList());
|
||||
@@ -230,9 +247,11 @@ class CachingPulsarProducerFactoryTests extends PulsarProducerFactoryTests {
|
||||
|
||||
@Override
|
||||
protected CachingPulsarProducerFactory<String> producerFactory(PulsarClient pulsarClient,
|
||||
@Nullable String defaultTopic, @Nullable List<ProducerBuilderCustomizer<String>> defaultConfigCustomizers) {
|
||||
@Nullable String defaultTopic, @Nullable List<ProducerBuilderCustomizer<String>> defaultConfigCustomizers,
|
||||
@Nullable PulsarTopicBuilder topicBuilder) {
|
||||
var producerFactory = new CachingPulsarProducerFactory<>(pulsarClient, defaultTopic, defaultConfigCustomizers,
|
||||
new DefaultTopicResolver(), Duration.ofMinutes(5L), 30L, 2);
|
||||
producerFactory.setTopicBuilder(topicBuilder);
|
||||
producerFactories.add(producerFactory);
|
||||
return producerFactory;
|
||||
}
|
||||
@@ -331,7 +350,8 @@ class CachingPulsarProducerFactoryTests extends PulsarProducerFactoryTests {
|
||||
|
||||
@Test
|
||||
void restartLifecycle() {
|
||||
var producerFactory = (CachingPulsarProducerFactory<String>) producerFactory(pulsarClient, null, null);
|
||||
var producerFactory = (CachingPulsarProducerFactory<String>) producerFactory(pulsarClient, null, null,
|
||||
null);
|
||||
producerFactory.start();
|
||||
var producer1 = producerFactory.createProducer(schema, "topic1");
|
||||
var producer2 = producerFactory.createProducer(schema, "topic2");
|
||||
|
||||
@@ -21,6 +21,8 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.inOrder;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
@@ -267,4 +269,29 @@ class DefaultPulsarConsumerFactoryTests implements PulsarTestContainerSupport {
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
class CreateConsumerUsingPulsarTopicBuilder {
|
||||
|
||||
private DefaultPulsarConsumerFactory<String> consumerFactory;
|
||||
|
||||
private PulsarTopicBuilder pulsarTopicBuilder;
|
||||
|
||||
@BeforeEach
|
||||
void createConsumerFactory() {
|
||||
pulsarTopicBuilder = spy(new PulsarTopicBuilder());
|
||||
consumerFactory = new DefaultPulsarConsumerFactory<>(pulsarClient, null);
|
||||
consumerFactory.setTopicBuilder(pulsarTopicBuilder);
|
||||
}
|
||||
|
||||
@Test
|
||||
void withPulsarTopicBuilder() throws PulsarClientException {
|
||||
try (var consumer = consumerFactory.createConsumer(SCHEMA, Collections.singletonList("topic1"),
|
||||
"with-pulsar-topic-builder-sub", null, null)) {
|
||||
assertThat(consumer.getTopic()).isEqualTo("persistent://public/default/topic1");
|
||||
verify(pulsarTopicBuilder).getFullyQualifiedNameForTopic("topic1");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -54,8 +54,12 @@ class DefaultPulsarProducerFactoryTests extends PulsarProducerFactoryTests {
|
||||
|
||||
@Override
|
||||
protected PulsarProducerFactory<String> producerFactory(PulsarClient pulsarClient, @Nullable String defaultTopic,
|
||||
@Nullable List<ProducerBuilderCustomizer<String>> defaultConfigCustomizers) {
|
||||
return new DefaultPulsarProducerFactory<>(pulsarClient, defaultTopic, defaultConfigCustomizers);
|
||||
@Nullable List<ProducerBuilderCustomizer<String>> defaultConfigCustomizers,
|
||||
@Nullable PulsarTopicBuilder topicBuilder) {
|
||||
var producerFactory = new DefaultPulsarProducerFactory<>(pulsarClient, defaultTopic, defaultConfigCustomizers,
|
||||
new DefaultTopicResolver());
|
||||
producerFactory.setTopicBuilder(topicBuilder);
|
||||
return producerFactory;
|
||||
}
|
||||
|
||||
@Nested
|
||||
|
||||
@@ -21,6 +21,8 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.inOrder;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
@@ -192,6 +194,32 @@ public class DefaultPulsarReaderFactoryTests implements PulsarTestContainerSuppo
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
class WithPulsarTopicBuilder {
|
||||
|
||||
private DefaultPulsarReaderFactory<String> pulsarReaderFactory;
|
||||
|
||||
private PulsarTopicBuilder pulsarTopicBuilder;
|
||||
|
||||
@BeforeEach
|
||||
void createReaderFactory() {
|
||||
pulsarTopicBuilder = spy(new PulsarTopicBuilder());
|
||||
pulsarReaderFactory = new DefaultPulsarReaderFactory<>(pulsarClient, null);
|
||||
pulsarReaderFactory.setTopicBuilder(pulsarTopicBuilder);
|
||||
}
|
||||
|
||||
@Test
|
||||
void topicIsFullyQualified() throws Exception {
|
||||
var topic = "wptb-reader-topic";
|
||||
try (var reader = pulsarReaderFactory.createReader(List.of(topic), MessageId.earliest, Schema.STRING,
|
||||
Collections.emptyList())) {
|
||||
assertThat(reader.getTopic()).isEqualTo("persistent://public/default/" + topic);
|
||||
verify(pulsarTopicBuilder).getFullyQualifiedNameForTopic(topic);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
@SuppressWarnings("unchecked")
|
||||
class DefaultConfigCustomizerApi {
|
||||
|
||||
@@ -66,11 +66,11 @@ public class PulsarAdministrationIntegrationTests implements PulsarTestContainer
|
||||
List<String> expectedFullyQualifiedTopicNames = expectedTopics.stream().<String>mapMulti((topic, consumer) -> {
|
||||
if (topic.isPartitioned()) {
|
||||
for (int i = 0; i < topic.numberOfPartitions(); i++) {
|
||||
consumer.accept(topic.getFullyQualifiedTopicName() + "-partition-" + i);
|
||||
consumer.accept(topic.topicName() + "-partition-" + i);
|
||||
}
|
||||
}
|
||||
else {
|
||||
consumer.accept(topic.getFullyQualifiedTopicName());
|
||||
consumer.accept(topic.topicName());
|
||||
}
|
||||
|
||||
}).toList();
|
||||
@@ -106,17 +106,17 @@ public class PulsarAdministrationIntegrationTests implements PulsarTestContainer
|
||||
|
||||
@Bean
|
||||
PulsarTopic nonPartitionedTopic() {
|
||||
return PulsarTopic.builder("cmt-non-partitioned-1").build();
|
||||
return new PulsarTopicBuilder().name("cmt-non-partitioned-1").build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
PulsarTopic nonPartitionedTopic2() {
|
||||
return PulsarTopic.builder("cmt-non-partitioned-2").build();
|
||||
return new PulsarTopicBuilder().name("cmt-non-partitioned-2").build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
PulsarTopic partitionedTopic() {
|
||||
return PulsarTopic.builder("cmt-partitioned-1").numberOfPartitions(4).build();
|
||||
return new PulsarTopicBuilder().name("cmt-partitioned-1").numberOfPartitions(4).build();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -157,14 +157,14 @@ public class PulsarAdministrationIntegrationTests implements PulsarTestContainer
|
||||
|
||||
@Bean
|
||||
PulsarTopic partitionedGreenTopic() {
|
||||
return PulsarTopic.builder("persistent://%s/partitioned-1".formatted(PUBLIC_GREEN_NAMESPACE))
|
||||
return new PulsarTopicBuilder().name("persistent://%s/partitioned-1".formatted(PUBLIC_GREEN_NAMESPACE))
|
||||
.numberOfPartitions(2)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
PulsarTopic partitionedBlueTopic() {
|
||||
return PulsarTopic.builder("persistent://%s/partitioned-1".formatted(PUBLIC_BLUE_NAMESPACE))
|
||||
return new PulsarTopicBuilder().name("persistent://%s/partitioned-1".formatted(PUBLIC_BLUE_NAMESPACE))
|
||||
.numberOfPartitions(2)
|
||||
.build();
|
||||
}
|
||||
@@ -180,7 +180,7 @@ public class PulsarAdministrationIntegrationTests implements PulsarTestContainer
|
||||
@Test
|
||||
void topicsExist(@Autowired ObjectProvider<PulsarTopic> expectedTopics) throws Exception {
|
||||
assertThatTopicsExist(expectedTopics.stream().toList());
|
||||
PulsarTopic biggerTopic = PulsarTopic.builder("ipc-partitioned-1").numberOfPartitions(4).build();
|
||||
PulsarTopic biggerTopic = new PulsarTopicBuilder().name("ipc-partitioned-1").numberOfPartitions(4).build();
|
||||
pulsarAdministration.createOrModifyTopics(biggerTopic);
|
||||
assertThatTopicsExist(Collections.singletonList(biggerTopic));
|
||||
}
|
||||
@@ -190,7 +190,7 @@ public class PulsarAdministrationIntegrationTests implements PulsarTestContainer
|
||||
|
||||
@Bean
|
||||
PulsarTopic smallerTopic() {
|
||||
return PulsarTopic.builder("ipc-partitioned-1").numberOfPartitions(1).build();
|
||||
return new PulsarTopicBuilder().name("ipc-partitioned-1").numberOfPartitions(1).build();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -204,7 +204,7 @@ public class PulsarAdministrationIntegrationTests implements PulsarTestContainer
|
||||
@Test
|
||||
void topicModificationThrows(@Autowired ObjectProvider<PulsarTopic> expectedTopics) throws Exception {
|
||||
assertThatTopicsExist(expectedTopics.stream().toList());
|
||||
PulsarTopic smallerTopic = PulsarTopic.builder("dpc-partitioned-1").numberOfPartitions(4).build();
|
||||
PulsarTopic smallerTopic = new PulsarTopicBuilder().name("dpc-partitioned-1").numberOfPartitions(4).build();
|
||||
assertThatIllegalStateException().isThrownBy(() -> pulsarAdministration.createOrModifyTopics(smallerTopic))
|
||||
.withMessage(
|
||||
"Topic 'persistent://public/default/dpc-partitioned-1' found w/ 8 partitions but can't shrink to 4 - needs to be deleted first");
|
||||
@@ -216,7 +216,7 @@ public class PulsarAdministrationIntegrationTests implements PulsarTestContainer
|
||||
|
||||
@Bean
|
||||
PulsarTopic biggerTopic() {
|
||||
return PulsarTopic.builder("dpc-partitioned-1").numberOfPartitions(8).build();
|
||||
return new PulsarTopicBuilder().name("dpc-partitioned-1").numberOfPartitions(8).build();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -229,7 +229,7 @@ public class PulsarAdministrationIntegrationTests implements PulsarTestContainer
|
||||
|
||||
@Test
|
||||
void unpartitionedTopicExists() throws PulsarAdminException {
|
||||
var topic = PulsarTopic.builder("taet-foo").numberOfPartitions(0).build();
|
||||
var topic = new PulsarTopicBuilder().name("taet-foo").numberOfPartitions(0).build();
|
||||
pulsarAdministration.createOrModifyTopics(topic);
|
||||
assertThatTopicsExist(List.of(topic));
|
||||
// subsequent call should short circuit and not fail
|
||||
@@ -238,7 +238,7 @@ public class PulsarAdministrationIntegrationTests implements PulsarTestContainer
|
||||
|
||||
@Test
|
||||
void partitionedTopicExists() throws PulsarAdminException {
|
||||
var topic = PulsarTopic.builder("taet-bar").numberOfPartitions(3).build();
|
||||
var topic = new PulsarTopicBuilder().name("taet-bar").numberOfPartitions(3).build();
|
||||
pulsarAdministration.createOrModifyTopics(topic);
|
||||
assertThatTopicsExist(List.of(topic));
|
||||
// subsequent call should short circuit and not fail
|
||||
@@ -253,8 +253,8 @@ public class PulsarAdministrationIntegrationTests implements PulsarTestContainer
|
||||
|
||||
@Test
|
||||
void unpartitionedTopicAlreadyExists() {
|
||||
var unpartitionedTopic = PulsarTopic.builder("ctt-foo").numberOfPartitions(0).build();
|
||||
var partitionedTopic = PulsarTopic.builder("ctt-foo").numberOfPartitions(3).build();
|
||||
var unpartitionedTopic = new PulsarTopicBuilder().name("ctt-foo").numberOfPartitions(0).build();
|
||||
var partitionedTopic = new PulsarTopicBuilder().name("ctt-foo").numberOfPartitions(3).build();
|
||||
pulsarAdministration.createOrModifyTopics(unpartitionedTopic);
|
||||
assertThatIllegalStateException()
|
||||
.isThrownBy(() -> pulsarAdministration.createOrModifyTopics(partitionedTopic))
|
||||
@@ -264,8 +264,8 @@ public class PulsarAdministrationIntegrationTests implements PulsarTestContainer
|
||||
|
||||
@Test
|
||||
void partitionedTopicAlreadyExists() {
|
||||
var unpartitionedTopic = PulsarTopic.builder("ctt-bar").numberOfPartitions(0).build();
|
||||
var partitionedTopic = PulsarTopic.builder("ctt-bar").numberOfPartitions(3).build();
|
||||
var unpartitionedTopic = new PulsarTopicBuilder().name("ctt-bar").numberOfPartitions(0).build();
|
||||
var partitionedTopic = new PulsarTopicBuilder().name("ctt-bar").numberOfPartitions(3).build();
|
||||
pulsarAdministration.createOrModifyTopics(partitionedTopic);
|
||||
assertThatIllegalStateException()
|
||||
.isThrownBy(() -> pulsarAdministration.createOrModifyTopics(unpartitionedTopic))
|
||||
|
||||
@@ -22,6 +22,7 @@ import static org.assertj.core.api.Assertions.assertThatNullPointerException;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.inOrder;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
import java.util.Arrays;
|
||||
@@ -93,20 +94,20 @@ abstract class PulsarProducerFactoryTests implements PulsarTestContainerSupport
|
||||
}
|
||||
|
||||
protected PulsarProducerFactory<String> newProducerFactory() {
|
||||
return producerFactory(pulsarClient, null, null);
|
||||
return producerFactory(pulsarClient, null, null, null);
|
||||
}
|
||||
|
||||
protected PulsarProducerFactory<String> newProducerFactoryWithDefaultTopic(String defaultTopic) {
|
||||
return producerFactory(pulsarClient, defaultTopic, null);
|
||||
return producerFactory(pulsarClient, defaultTopic, null, null);
|
||||
}
|
||||
|
||||
private PulsarProducerFactory<String> newProducerFactoryWithDefaultKeys(Set<String> defaultKeys) {
|
||||
return producerFactory(pulsarClient, null, List.of((pb) -> defaultKeys.forEach(pb::addEncryptionKey)));
|
||||
return producerFactory(pulsarClient, null, List.of((pb) -> defaultKeys.forEach(pb::addEncryptionKey)), null);
|
||||
}
|
||||
|
||||
protected PulsarProducerFactory<String> newProducerFactoryWithDefaultConfigCustomizers(
|
||||
List<ProducerBuilderCustomizer<String>> customizers) {
|
||||
return producerFactory(pulsarClient, null, customizers);
|
||||
return producerFactory(pulsarClient, null, customizers, null);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -124,10 +125,13 @@ abstract class PulsarProducerFactoryTests implements PulsarTestContainerSupport
|
||||
* @param defaultTopic the default topic to use for the producers
|
||||
* @param defaultConfigCustomizers the optional list of customizers to apply to the
|
||||
* created producers
|
||||
* @param topicBuilder the optional topic builder to use for fully qualifying topic
|
||||
* names
|
||||
* @return a Pulsar producer factory instance to use for the tests
|
||||
*/
|
||||
protected abstract PulsarProducerFactory<String> producerFactory(PulsarClient pulsarClient,
|
||||
@Nullable String defaultTopic, @Nullable List<ProducerBuilderCustomizer<String>> defaultConfigCustomizers);
|
||||
@Nullable String defaultTopic, @Nullable List<ProducerBuilderCustomizer<String>> defaultConfigCustomizers,
|
||||
@Nullable PulsarTopicBuilder topicBuilder);
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
@@ -251,4 +255,19 @@ abstract class PulsarProducerFactoryTests implements PulsarTestContainerSupport
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
class CreateProducerWithTopicBuilder {
|
||||
|
||||
@Test
|
||||
void topicIsFullyQualified() throws PulsarClientException {
|
||||
var topicBuilder = spy(new PulsarTopicBuilder());
|
||||
var producerFactory = producerFactory(pulsarClient, null, null, topicBuilder);
|
||||
try (var producer = producerFactory.createProducer(schema, "topic1")) {
|
||||
assertThatProducerHasSchemaAndTopic(producer, schema, "persistent://public/default/topic1");
|
||||
verify(topicBuilder).getFullyQualifiedNameForTopic("topic1");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* Copyright 2022-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.pulsar.core;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.apache.pulsar.common.naming.TopicDomain;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
|
||||
/**
|
||||
* Tests for {@link PulsarTopicBuilder}.
|
||||
*
|
||||
* @author Chris Bono
|
||||
*/
|
||||
class PulsarTopicBuilderTests {
|
||||
|
||||
private PulsarTopicBuilder builder = new PulsarTopicBuilder();
|
||||
|
||||
@Test
|
||||
void whenNumPartitionsNotSpecifiedThenTopicIsNotPartitioned() {
|
||||
var topicName = "persistent://my-tenant/my-namespace/my-topic";
|
||||
var topic = builder.name(topicName).build();
|
||||
assertThat(topic.topicName()).isEqualTo(topicName);
|
||||
assertThat(topic.numberOfPartitions()).isEqualTo(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenNumPartitionsSpecifiedThenTopicIsPartitioned() {
|
||||
var topicName = "persistent://my-tenant/my-namespace/my-topic";
|
||||
var topic = builder.name(topicName).numberOfPartitions(5).build();
|
||||
assertThat(topic.topicName()).isEqualTo(topicName);
|
||||
assertThat(topic.numberOfPartitions()).isEqualTo(5);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = { "persistent://my-namespace/my-topic", "my-namespace/my-topic" })
|
||||
void whenNameIsInvalidThenExceptionIsThrown(String invalidName) {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> builder.name(invalidName))
|
||||
.withMessage("Topic name '" + invalidName + "' must be in one of the following formats "
|
||||
+ "('name', 'tenant/namespace/name', 'domain://tenant/namespace/name')");
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("nameIsAlwaysFullyQualifiedProvider")
|
||||
void nameIsAlwaysFullyQualified(PulsarTopicBuilder topicBuilder, String inputTopic, String expectedTopic) {
|
||||
assertThat(topicBuilder.getFullyQualifiedNameForTopic(inputTopic)).isEqualTo(expectedTopic);
|
||||
var topic = topicBuilder.name(inputTopic).build();
|
||||
assertThat(topic.topicName()).isEqualTo(expectedTopic);
|
||||
}
|
||||
|
||||
private static Stream<Arguments> nameIsAlwaysFullyQualifiedProvider() {
|
||||
var defaultBuilder = new PulsarTopicBuilder();
|
||||
var customBuilder = new PulsarTopicBuilder(TopicDomain.non_persistent, "my-tenant", "my-namespace");
|
||||
return Stream.of(Arguments.of(defaultBuilder, "my-topic", "persistent://public/default/my-topic"),
|
||||
Arguments.of(defaultBuilder, "foo/bar/my-topic", "persistent://foo/bar/my-topic"),
|
||||
Arguments.of(defaultBuilder, "non-persistent://foo/bar/my-topic", "non-persistent://foo/bar/my-topic"),
|
||||
Arguments.of(customBuilder, "my-topic", "non-persistent://my-tenant/my-namespace/my-topic"),
|
||||
Arguments.of(customBuilder, "foo/bar/my-topic", "non-persistent://foo/bar/my-topic"),
|
||||
Arguments.of(customBuilder, "persistent://foo/bar/my-topic", "persistent://foo/bar/my-topic"));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2022-2023 the original author or authors.
|
||||
* Copyright 2022-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -17,63 +17,95 @@
|
||||
package org.springframework.pulsar.core;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import java.util.stream.Stream;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||
|
||||
import org.apache.pulsar.common.naming.TopicDomain;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
|
||||
/**
|
||||
* Tests for {@link PulsarTopic}.
|
||||
*
|
||||
* @author Alexander Preuß
|
||||
* @@author Chris Bono
|
||||
*/
|
||||
public class PulsarTopicTests {
|
||||
|
||||
@Test
|
||||
void builderDefaultValues() {
|
||||
String topicName = "test-default-values";
|
||||
PulsarTopicBuilder builder = PulsarTopic.builder(topicName);
|
||||
PulsarTopic topic = builder.build();
|
||||
private static final String FULLY_QUALIFIED_TOPIC = "persistent://public/default/my-topic";
|
||||
|
||||
assertThat(topic.topicName()).isEqualTo(topicName);
|
||||
assertThat(topic.numberOfPartitions()).isEqualTo(0);
|
||||
@Test
|
||||
void whenNegativeNumPartitionsThenExceptionIsThrown() {
|
||||
assertThatIllegalStateException().isThrownBy(() -> new PulsarTopic(FULLY_QUALIFIED_TOPIC, -1))
|
||||
.withMessage("numberOfPartitions must be >= 0");
|
||||
}
|
||||
|
||||
@Test
|
||||
void fakeTestToVerifyReleasePipeline() {
|
||||
void whenZeroNumPartitionsThenTopicIsNotPartitioned() {
|
||||
var topic = new PulsarTopic(FULLY_QUALIFIED_TOPIC, 0);
|
||||
assertThat(topic.numberOfPartitions()).isEqualTo(0);
|
||||
assertThat(topic.isPartitioned()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenPosititveNumPartitionsThenTopicIsPartitioned() {
|
||||
var topic = new PulsarTopic(FULLY_QUALIFIED_TOPIC, 2);
|
||||
assertThat(topic.numberOfPartitions()).isEqualTo(2);
|
||||
assertThat(topic.isPartitioned()).isTrue();
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("topicComponentsProvider")
|
||||
void topicComponents(PulsarTopic topic, TopicDomain domain, String tenant, String namespace, String topicName) {
|
||||
PulsarTopic.TopicComponents components = topic.getComponents();
|
||||
assertThat(components.domain()).isEqualTo(domain);
|
||||
assertThat(components.tenant()).isEqualTo(tenant);
|
||||
assertThat(components.namespace()).isEqualTo(namespace);
|
||||
assertThat(components.name()).isEqualTo(topicName);
|
||||
// @formatter:off
|
||||
@ValueSource(strings = {
|
||||
"my-domain://public/default/my-topic",
|
||||
"public/default/my-topic", "my-topic",
|
||||
"persistent://public/cluster/default/my-topic",
|
||||
"persistent://publ@c/default/my-topic",
|
||||
"persistent://public/def@ult/my-topic",
|
||||
"persistent://public/default/my-t@pic"
|
||||
})
|
||||
// @formatter:on
|
||||
void whenNameIsInvalidThenExceptionIsThrown(String invalidTopicName) {
|
||||
var msg = "topicName %s must be fully-qualified in the format".formatted(invalidTopicName);
|
||||
assertThatIllegalStateException().isThrownBy(() -> new PulsarTopic(invalidTopicName, 0))
|
||||
.withMessageStartingWith(msg);
|
||||
}
|
||||
|
||||
private static Stream<Arguments> topicComponentsProvider() {
|
||||
return Stream.of(
|
||||
Arguments.of(PulsarTopic.builder("topic-1").build(), TopicDomain.persistent, "public", "default",
|
||||
"topic-1"),
|
||||
Arguments.of(PulsarTopic.builder("public/default/topic-2").build(), TopicDomain.persistent, "public",
|
||||
"default", "topic-2"),
|
||||
Arguments.of(PulsarTopic.builder("persistent://public/default/topic-3").build(), TopicDomain.persistent,
|
||||
"public", "default", "topic-3"),
|
||||
Arguments.of(PulsarTopic.builder("public/my-namespace/topic-4").build(), TopicDomain.persistent,
|
||||
"public", "my-namespace", "topic-4"),
|
||||
Arguments.of(PulsarTopic.builder("my-tenant/my-namespace/topic-5").build(), TopicDomain.persistent,
|
||||
"my-tenant", "my-namespace", "topic-5"),
|
||||
Arguments.of(PulsarTopic.builder("non-persistent://public/my-namespace/topic-6").build(),
|
||||
TopicDomain.non_persistent, "public", "my-namespace", "topic-6"),
|
||||
Arguments.of(PulsarTopic.builder("non-persistent://my-tenant/my-namespace/topic-7").build(),
|
||||
TopicDomain.non_persistent, "my-tenant", "my-namespace", "topic-7"));
|
||||
@ParameterizedTest
|
||||
// @formatter:off
|
||||
@ValueSource(strings = {
|
||||
"persistent://public/default/my-topic",
|
||||
"non-persistent://public/default/my-topic",
|
||||
"persistent://PUB-=:.7lic/DE-=:.7fault/MY-=:.7topic"
|
||||
})
|
||||
// @formatter:on
|
||||
void whenNameIsValidThenTopicCreated(String validTopicName) {
|
||||
var topic = new PulsarTopic(validTopicName, 0);
|
||||
assertThat(topic.topicName()).isEqualTo(validTopicName);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getComponentsReturnsProperComponents() {
|
||||
var topic = new PulsarTopic("persistent://public/default/my-topic", 0);
|
||||
var components = topic.getComponents();
|
||||
assertThat(components.domain()).isEqualTo(TopicDomain.persistent);
|
||||
assertThat(components.tenant()).isEqualTo("public");
|
||||
assertThat(components.namespace()).isEqualTo("default");
|
||||
assertThat(components.name()).isEqualTo("my-topic");
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings({ "deprecation", "removal" })
|
||||
void deprecatedBuilderMethodReturnsValidBuilder() {
|
||||
var fullyQualifiedName = "persistent://public/default/my-topic";
|
||||
assertThat(PulsarTopic.builder("my-topic").build().topicName()).isEqualTo(fullyQualifiedName);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings({ "deprecation", "removal" })
|
||||
void deprecatedGetFullyQualifiedTopicNameReturnsValidName() {
|
||||
var topic = new PulsarTopic(FULLY_QUALIFIED_TOPIC, 0);
|
||||
assertThat(topic.getFullyQualifiedTopicName()).isEqualTo(FULLY_QUALIFIED_TOPIC);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ import org.springframework.pulsar.core.PulsarConsumerFactory;
|
||||
import org.springframework.pulsar.core.PulsarProducerFactory;
|
||||
import org.springframework.pulsar.core.PulsarTemplate;
|
||||
import org.springframework.pulsar.core.PulsarTopic;
|
||||
import org.springframework.pulsar.core.PulsarTopicBuilder;
|
||||
import org.springframework.pulsar.test.support.PulsarTestContainerSupport;
|
||||
import org.springframework.test.annotation.DirtiesContext;
|
||||
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
|
||||
@@ -96,7 +97,9 @@ abstract class PulsarListenerTestsBase implements PulsarTestContainerSupport {
|
||||
|
||||
@Bean
|
||||
PulsarTopic partitionedTopic() {
|
||||
return PulsarTopic.builder("persistent://public/default/concurrency-on-pl").numberOfPartitions(3).build();
|
||||
return new PulsarTopicBuilder().name("persistent://public/default/concurrency-on-pl")
|
||||
.numberOfPartitions(3)
|
||||
.build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
<suppress files="Proto" checks=".*"/>
|
||||
<suppress files="ReactiveSpringPulsarBootApp" checks="HideUtilityClassConstructor"/>
|
||||
<suppress files="SamplePulsarApplicationTests" checks="HideUtilityClassConstructor" />
|
||||
<suppress files="DefaultTenantAndNamespaceTests" checks="HideUtilityClassConstructor" />
|
||||
<suppress files="[\\/]spring-pulsar-docs[\\/]" checks="JavadocPackage|JavadocType|JavadocVariable|SpringDeprecatedCheck" />
|
||||
<suppress files="[\\/]spring-pulsar-docs[\\/]" checks="SpringJavadoc" message="\@since" />
|
||||
<suppress files="[\\/]spring-pulsar-docs[\\/].*jooq" checks="AvoidStaticImport" />
|
||||
|
||||
Reference in New Issue
Block a user