Add support for Apache Pulsar

Add support for Apache Pulsar using the Spring for Apache Pulsar
project.

See gh-34763

Co-authored-by: Phillip Webb <pwebb@vmware.com>
This commit is contained in:
Chris Bono
2023-08-30 16:39:37 -07:00
committed by Phillip Webb
parent 8f78acd548
commit 6e7b845bdf
56 changed files with 5261 additions and 3 deletions

View File

@@ -0,0 +1,15 @@
plugins {
id "java"
id "org.springframework.boot.conventions"
}
description = "Spring Boot Pulsar smoke test"
dependencies {
implementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-pulsar-reactive"))
testImplementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-test"))
testImplementation(project(":spring-boot-project:spring-boot-tools:spring-boot-test-support"))
testImplementation("org.awaitility:awaitility")
testImplementation("org.testcontainers:junit-jupiter")
testImplementation("org.testcontainers:pulsar")
}

View File

@@ -0,0 +1,20 @@
/*
* 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 smoketest.pulsar.reactive;
record SampleMessage(Integer id, String content) {
}

View File

@@ -0,0 +1,43 @@
/*
* 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 smoketest.pulsar.reactive;
import java.util.ArrayList;
import java.util.List;
import reactor.core.publisher.Mono;
import org.springframework.pulsar.reactive.config.annotation.ReactivePulsarListener;
import org.springframework.stereotype.Component;
@Component
class SampleMessageConsumer {
private List<SampleMessage> consumed = new ArrayList<>();
List<SampleMessage> getConsumed() {
return this.consumed;
}
@ReactivePulsarListener(topics = SampleReactivePulsarApplication.TOPIC)
Mono<Void> consumeMessagesFromPulsarTopic(SampleMessage msg) {
System.out.println("**** CONSUME: " + msg);
this.consumed.add(msg);
return Mono.empty();
}
}

View File

@@ -0,0 +1,53 @@
/*
* 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 smoketest.pulsar.reactive;
import org.apache.pulsar.reactive.client.api.MessageSpec;
import reactor.core.publisher.Flux;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.pulsar.core.PulsarTopic;
import org.springframework.pulsar.reactive.core.ReactivePulsarTemplate;
@SpringBootApplication
public class SampleReactivePulsarApplication {
static final String TOPIC = "pulsar-reactive-smoke-test-topic";
@Bean
PulsarTopic pulsarTestTopic() {
return PulsarTopic.builder(TOPIC).numberOfPartitions(1).build();
}
@Bean
ApplicationRunner sendMessagesToPulsarTopic(ReactivePulsarTemplate<SampleMessage> template) {
return (args) -> Flux.range(0, 10)
.map((i) -> new SampleMessage(i, "message:" + i))
.map(MessageSpec::of)
.as((msgs) -> template.send(TOPIC, msgs))
.doOnNext((sendResult) -> System.out.println("*** PRODUCE: " + sendResult.getMessageId()))
.subscribe();
}
public static void main(String[] args) {
SpringApplication.run(SampleReactivePulsarApplication.class, args);
}
}

View File

@@ -0,0 +1 @@
spring.pulsar.reactive.consumer.subscription-initial-position=earliest

View File

@@ -0,0 +1,60 @@
/*
* 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 smoketest.pulsar.reactive;
import java.time.Duration;
import java.util.stream.IntStream;
import org.awaitility.Awaitility;
import org.junit.jupiter.api.Test;
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.testsupport.testcontainers.DockerImageNames;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest
@Testcontainers(disabledWithoutDocker = true)
class SampleReactivePulsarApplicationTests {
@Container
private static final PulsarContainer PULSAR_CONTAINER = new PulsarContainer(DockerImageNames.pulsar())
.withStartupAttempts(2)
.withStartupTimeout(Duration.ofMinutes(3));
@DynamicPropertySource
static void pulsarProperties(DynamicPropertyRegistry registry) {
registry.add("spring.pulsar.client.service-url", PULSAR_CONTAINER::getPulsarBrokerUrl);
registry.add("spring.pulsar.admin.service-url", PULSAR_CONTAINER::getHttpServiceUrl);
}
@Test
void appProducesAndConsumesSampleMessages(@Autowired SampleMessageConsumer consumer) {
Integer[] expectedIds = IntStream.range(0, 10).boxed().toArray(Integer[]::new);
Awaitility.await()
.atMost(Duration.ofSeconds(20))
.untilAsserted(() -> assertThat(consumer.getConsumed()).extracting(SampleMessage::id)
.containsExactly(expectedIds));
}
}

View File

@@ -0,0 +1,15 @@
plugins {
id "java"
id "org.springframework.boot.conventions"
}
description = "Spring Boot Pulsar smoke test"
dependencies {
implementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-pulsar"))
testImplementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-test"))
testImplementation(project(":spring-boot-project:spring-boot-tools:spring-boot-test-support"))
testImplementation("org.awaitility:awaitility")
testImplementation("org.testcontainers:junit-jupiter")
testImplementation("org.testcontainers:pulsar")
}

View File

@@ -0,0 +1,20 @@
/*
* 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 smoketest.pulsar;
record SampleMessage(Integer id, String content) {
}

View File

@@ -0,0 +1,40 @@
/*
* 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 smoketest.pulsar;
import java.util.ArrayList;
import java.util.List;
import org.springframework.pulsar.annotation.PulsarListener;
import org.springframework.stereotype.Component;
@Component
class SampleMessageConsumer {
private List<SampleMessage> consumed = new ArrayList<>();
List<SampleMessage> getConsumed() {
return this.consumed;
}
@PulsarListener(topics = SamplePulsarApplication.TOPIC)
void consumeMessagesFromPulsarTopic(SampleMessage msg) {
System.out.println("**** CONSUME: " + msg);
this.consumed.add(msg);
}
}

View File

@@ -0,0 +1,52 @@
/*
* 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 smoketest.pulsar;
import org.apache.pulsar.client.api.MessageId;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.pulsar.core.PulsarTemplate;
import org.springframework.pulsar.core.PulsarTopic;
@SpringBootApplication
public class SamplePulsarApplication {
static final String TOPIC = "pulsar-smoke-test-topic";
@Bean
PulsarTopic pulsarTestTopic() {
return PulsarTopic.builder(TOPIC).numberOfPartitions(1).build();
}
@Bean
ApplicationRunner sendMessagesToPulsarTopic(PulsarTemplate<SampleMessage> template) {
return (args) -> {
for (int i = 0; i < 10; i++) {
MessageId msgId = template.send(TOPIC, new SampleMessage(i, "message:" + i));
System.out.println("*** PRODUCE: " + msgId);
}
};
}
public static void main(String[] args) {
SpringApplication.run(SamplePulsarApplication.class, args);
}
}

View File

@@ -0,0 +1 @@
spring.pulsar.consumer.subscription-initial-position=earliest

View File

@@ -0,0 +1,60 @@
/*
* 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 smoketest.pulsar;
import java.time.Duration;
import java.util.stream.IntStream;
import org.awaitility.Awaitility;
import org.junit.jupiter.api.Test;
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.testsupport.testcontainers.DockerImageNames;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest
@Testcontainers(disabledWithoutDocker = true)
class SamplePulsarApplicationTests {
@Container
private static final PulsarContainer PULSAR_CONTAINER = new PulsarContainer(DockerImageNames.pulsar())
.withStartupAttempts(2)
.withStartupTimeout(Duration.ofMinutes(3));
@DynamicPropertySource
static void pulsarProperties(DynamicPropertyRegistry registry) {
registry.add("spring.pulsar.client.service-url", PULSAR_CONTAINER::getPulsarBrokerUrl);
registry.add("spring.pulsar.admin.service-url", PULSAR_CONTAINER::getHttpServiceUrl);
}
@Test
void appProducesAndConsumesSampleMessages(@Autowired SampleMessageConsumer consumer) {
Integer[] expectedIds = IntStream.range(0, 10).boxed().toArray(Integer[]::new);
Awaitility.await()
.atMost(Duration.ofSeconds(20))
.untilAsserted(() -> assertThat(consumer.getConsumed()).extracting(SampleMessage::id)
.containsExactly(expectedIds));
}
}