Migrating zeromq components from the 3.1 WIP branch to main

zeromq-supplier
zeromq-consumer
zeromq-source
zeromq-sink
This commit is contained in:
Soby Chacko
2021-06-04 16:34:11 -04:00
parent 2751122bcd
commit 50949ad3de
12 changed files with 750 additions and 0 deletions

View File

@@ -0,0 +1,30 @@
# ZeroMQ Consumer
A consumer that allows you to send messages through a ZeroMQ socker.
## Beans for injection
You can import the `ZeroMqConsumerConfiguration` in the application and then inject the following bean.
`Function<Flux<Message<?>>, Mono<Void>> zeromqConsumer`
You need to inject this as `Function<Flux<Message<?>>, Mono<Void>> zeromqConsumer`.
You can use `zeromqConsumer` as a qualifier when injecting.
**NOTE:** This is a functional endpoint. One will need to subscribe to this endpoint in order to start accepting data
on it.
## Configuration Options
All configuration properties are prefixed with `zeromq.consumer`.
For more information on the various options available, please see link:src/main/java/org/springframework/cloud/fn/consumer/zeromq/ZeroMqConsumerProperties.java[ZeroMqConsumerProperties].
## Tests
See this link:src/test/java/org/springframework/cloud/fn/consumer/zeromq/[test suite] for the various ways, this consumer is used.
## Other usage
See this https://github.com/spring-cloud/stream-applications/blob/master/applications/sink/zeromq-sink/README.adoc[README] where this consumer is used to create a Spring Cloud Stream application where it makes a ZeroMQ Sink.

View File

@@ -0,0 +1,59 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.cloud.fn</groupId>
<artifactId>spring-functions-parent</artifactId>
<version>${revision}</version>
<relativePath>../../spring-functions-parent</relativePath>
</parent>
<artifactId>zeromq-consumer</artifactId>
<name>zeromq-consumer</name>
<description>ZeroMQ consumer</description>
<dependencies>
<dependency>
<groupId>org.springframework.cloud.fn</groupId>
<artifactId>config-common</artifactId>
<version>${project.version}</version>
</dependency>
<!-- Spring Integration dependencies -->
<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-zeromq</artifactId>
</dependency>
<!-- Spring Boot dependencies -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-json</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<scope>provided</scope>
</dependency>
<!-- Test dependencies -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,77 @@
/*
* Copyright 2016-2020 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.cloud.fn.consumer.zeromq;
import java.util.function.Consumer;
import java.util.function.Function;
import org.zeromq.ZContext;
import org.zeromq.ZMQ;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.integration.mapping.OutboundMessageMapper;
import org.springframework.integration.zeromq.outbound.ZeroMqMessageHandler;
import org.springframework.messaging.Message;
/**
*
* @author Daniel Frey
* @since 3.1.0
*/
@Configuration
@EnableConfigurationProperties(ZeroMqConsumerProperties.class)
public class ZeroMqConsumerConfiguration {
@Bean
public ZContext zContext() {
return new ZContext();
}
@Bean
public ZeroMqMessageHandler zeromqMessageHandler(ZeroMqConsumerProperties properties, ZContext zContext,
@Autowired(required = false) Consumer<ZMQ.Socket> socketConfigurer,
@Autowired(required = false) OutboundMessageMapper<byte[]> messageMapper) {
ZeroMqMessageHandler zeroMqMessageHandler = new ZeroMqMessageHandler(zContext, properties.getConnectUrl(),
properties.getSocketType());
if (properties.getTopic() != null) {
zeroMqMessageHandler.setTopicExpression(properties.getTopic());
}
if (socketConfigurer != null) {
zeroMqMessageHandler.setSocketConfigurer(socketConfigurer);
}
if (messageMapper != null) {
zeroMqMessageHandler.setMessageMapper(messageMapper);
}
return zeroMqMessageHandler;
}
@Bean
public Function<Flux<Message<?>>, Mono<Void>> zeromqConsumer(ZeroMqMessageHandler zeromqMessageHandler) {
return input -> input.flatMap(zeromqMessageHandler::handleMessage)
.ignoreElements();
}
}

View File

@@ -0,0 +1,87 @@
/*
* Copyright 2016-2020 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.cloud.fn.consumer.zeromq;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import org.zeromq.SocketType;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.expression.Expression;
import org.springframework.validation.annotation.Validated;
/**
*
* @author Daniel Frey
* @since 3.1.0
*/
@ConfigurationProperties("zeromq.consumer")
@Validated
public class ZeroMqConsumerProperties {
/**
* The Socket Type the connection should establish.
*/
private SocketType socketType = SocketType.PUB;
/**
* Connection URL for connecting to the ZeroMQ Socket.
*/
private String connectUrl;
/**
* A Topic SpEL expression to evaluate a topic before sending messages to subscribers.
*/
private Expression topic;
@NotNull(message = "'socketType' is required")
public SocketType getSocketType() {
return socketType;
}
/**
* @param socketType the {@link SocketType} to establish.
*/
public void setSocketType(SocketType socketType) {
this.socketType = socketType;
}
@NotEmpty(message = "connectUrl is required like protocol://server:port")
public String getConnectUrl() {
return connectUrl;
}
/**
* @param connectUrl The ZeroMQ socket to expose
*/
public void setConnectUrl(String connectUrl) {
this.connectUrl = connectUrl;
}
public Expression getTopic() {
return topic;
}
/**
* @param topic The 'topic' SpEL expression to set
*/
public void setTopic(Expression topic) {
this.topic = topic;
}
}

View File

@@ -0,0 +1,93 @@
/*
* Copyright 2016-2020 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.cloud.fn.consumer.zeromq;
import java.util.function.Function;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.zeromq.SocketType;
import org.zeromq.ZContext;
import org.zeromq.ZMQ;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.test.annotation.DirtiesContext;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Daniel Frey since 3.1.0
*/
@SpringBootTest(properties = {
"zeromq.consumer.topic='test-topic'"
})
@DirtiesContext
public class ZeroMqConsumerConfigurationTests {
private static final ZContext CONTEXT = new ZContext();
private static ZMQ.Socket socket;
@Autowired
Function<Flux<Message<?>>, Mono<Void>> subject;
@BeforeAll
static void setup() {
socket = CONTEXT.createSocket(SocketType.SUB);
socket.setReceiveTimeOut(10_000);
int bindPort = socket.bindToRandomPort("tcp://*");
socket.subscribe("test-topic");
System.setProperty("zeromq.consumer.connectUrl", "tcp://localhost:" + bindPort);
}
@AfterAll
static void tearDown() {
socket.close();
CONTEXT.close();
}
@Test
void testMessageHandlerConfiguration() throws InterruptedException {
Thread.sleep(2000);
Message<?> testMessage = MessageBuilder.withPayload("test").setHeader("topic", "test-topic").build();
subject.apply(Flux.just(testMessage))
.subscribe();
String topic = socket.recvStr();
assertThat(topic).isEqualTo("test-topic");
assertThat(socket.recvStr()).isEmpty();
assertThat(socket.recvStr()).isEqualTo("test");
}
@SpringBootApplication
public static class ZeroMqConsumerTestApplication {
}
}

View File

@@ -106,6 +106,11 @@
<artifactId>websocket-supplier</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud.fn</groupId>
<artifactId>zeromq-supplier</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud.fn</groupId>
<artifactId>analytics-consumer</artifactId>
@@ -201,6 +206,11 @@
<artifactId>websocket-consumer</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud.fn</groupId>
<artifactId>zeromq-consumer</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud.fn</groupId>
<artifactId>aggregator-function</artifactId>

View File

@@ -55,6 +55,7 @@
<module>consumer/twitter-consumer</module>
<module>consumer/wavefront-consumer</module>
<module>consumer/rsocket-consumer</module>
<module>consumer/zeromq-consumer</module>
<module>function/aggregator-function</module>
<module>function/filter-function</module>
@@ -88,6 +89,7 @@
<module>supplier/twitter-supplier</module>
<module>supplier/cdc-debezium-supplier</module>
<module>supplier/syslog-supplier</module>
<module>supplier/zeromq-supplier</module>
<module>spring-functions-parent</module>
<module>function-dependencies</module>

View File

@@ -0,0 +1,32 @@
# ZeroMQ Supplier
A basic ZeroMQ supplier that produced messages through TCP connection.
The `Supplier` uses the `ZeroMqMessageProducer` from Spring Integration.
This supplier gives you a reactive stream of messages and the supplier has a signature of `Supplier<Flux<Message<?>>>`.
Users have to subscribe to this `Flux` and receive the data.
## Beans for injection
You can import the `ZeroMqSupplierConfiguration` in the application and then inject the following bean.
`zeromqSupplier`
You need to inject this as `Supplier<Flux<Message<?>>>`.
You can use `zeromqSupplier` as a qualifier when injecting.
Once injected, you can use the `get` method of the `Supplier` to invoke it and then subscribe to the returned `Flux`.
## Configuration Options
All configuration properties are prefixed with `zeromq.supplier`.
For more information on the various options available, please see link:src/main/java/org/springframework/cloud/fn/supplier/zeromq/ZeroMqSupplierProperties.java[ZeroMqSupplierProperties].
## Tests
See this link:src/test/java/org/springframework/cloud/fn/supplier/zeromq/ZeroMqSupplierTests.java[test suite] for the various ways, this supplier is used.
## Other usage
See this https://github.com/spring-cloud/stream-applications/blob/master/applications/source/zeromq-source/README.adoc[README] where this supplier is used to create a Spring Cloud Stream application where it makes a ZeroMQ Source.

View File

@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.cloud.fn</groupId>
<artifactId>spring-functions-parent</artifactId>
<version>${revision}</version>
<relativePath>../../spring-functions-parent</relativePath>
</parent>
<artifactId>zeromq-supplier</artifactId>
<name>zeromq-supplier</name>
<description>ZeroMQ supplier</description>
<dependencies>
<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-zeromq</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,84 @@
/*
* Copyright 2016-2020 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.cloud.fn.supplier.zeromq;
import java.util.function.Consumer;
import java.util.function.Supplier;
import org.zeromq.SocketType;
import org.zeromq.ZContext;
import org.zeromq.ZMQ;
import reactor.core.publisher.Flux;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.integration.channel.FluxMessageChannel;
import org.springframework.integration.zeromq.inbound.ZeroMqMessageProducer;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.GenericMessage;
/**
* A source module that receives data from ZeroMQ.
*
* @author Daniel Frey
* @since 3.1.0
*/
@Configuration
@EnableConfigurationProperties(ZeroMqSupplierProperties.class)
public class ZeroMqSupplierConfiguration {
private FluxMessageChannel output = new FluxMessageChannel();
@Bean
public ZContext zContext() {
return new ZContext();
}
@Bean
public ZeroMqMessageProducer adapter(ZeroMqSupplierProperties properties, ZContext zContext,
@Autowired(required = false) Consumer<ZMQ.Socket> socketConfigurer) {
ZeroMqMessageProducer zeroMqMessageProducer = new ZeroMqMessageProducer(zContext, properties.getSocketType());
if (properties.getConnectUrl() != null) {
zeroMqMessageProducer.setConnectUrl(properties.getConnectUrl());
}
else if (properties.getBindPort() > 0) {
zeroMqMessageProducer.setBindPort(properties.getBindPort());
}
zeroMqMessageProducer.setConsumeDelay(properties.getConsumeDelay());
if (SocketType.SUB.equals(properties.getSocketType())) {
zeroMqMessageProducer.setTopics(properties.getTopics());
}
zeroMqMessageProducer.setMessageMapper(GenericMessage::new);
if (socketConfigurer != null) {
zeroMqMessageProducer.setSocketConfigurer(socketConfigurer);
}
zeroMqMessageProducer.setOutputChannel(output);
zeroMqMessageProducer.setAutoStartup(false);
return zeroMqMessageProducer;
}
@Bean
public Supplier<Flux<Message<?>>> zeromqSupplier(ZeroMqMessageProducer adapter) {
return () -> Flux.from(output).doOnSubscribe(subscription -> adapter.start());
}
}

View File

@@ -0,0 +1,132 @@
/*
* Copyright 2016-2020 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.cloud.fn.supplier.zeromq;
import java.time.Duration;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import org.hibernate.validator.constraints.Range;
import org.zeromq.SocketType;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.validation.annotation.Validated;
/**
*
* @author Daniel Frey
* @since 3.1.0
*/
@ConfigurationProperties("zeromq.supplier")
@Validated
public class ZeroMqSupplierProperties {
/**
* The Socket Type the connection should make.
*/
private SocketType socketType = SocketType.SUB;
/**
* Connection URL for to the ZeroMQ Socket.
*/
private String connectUrl;
/**
* Bind Port for creating a ZeroMQ Socket; 0 selects a random port.
*/
private int bindPort;
/**
* The delay to consume from the ZeroMQ Socket when no data received.
*/
private Duration consumeDelay = Duration.ofSeconds(1);
/**
* The Topics to subscribe to.
*/
private String[] topics = {""};
/**
* @param socketType the {@link SocketType} to establish.
*/
public void setSocketType(SocketType socketType) {
this.socketType = socketType;
}
@NotNull(message = "'socketType' is required")
public SocketType getSocketType() {
return socketType;
}
@NotEmpty(message = "connectUrl is required like tcp://server:port")
public String getConnectUrl() {
return connectUrl;
}
/**
*
* @param connectUrl The ZeroMQ server connect url
*
* @see org.springframework.integration.zeromq.inbound.ZeroMqMessageProducer#setConnectUrl(String)
*/
public void setConnectUrl(String connectUrl) {
this.connectUrl = connectUrl;
}
@Range(min = 0, message = "'bindPort' must not be negative")
public int getBindPort() {
return bindPort;
}
/**
* @param bindPort The Port to bind to on all interfaces
*
* @see org.springframework.integration.zeromq.inbound.ZeroMqMessageProducer#setBindPort(int)
*/
public void setBindPort(int bindPort) {
this.bindPort = bindPort;
}
@NotNull(message = "'consumeDelay' is required")
public Duration getConsumeDelay() {
return consumeDelay;
}
/**
* Specify a {@link Duration} to delay consumption when no data received.
* @param consumeDelay the {@link Duration} to delay consumption when empty.
*/
public void setConsumeDelay(Duration consumeDelay) {
this.consumeDelay = consumeDelay;
}
public String[] getTopics() {
return topics;
}
/**
*
* @param topics The ZeroMQ Topics to subscribe to
*
* @see org.springframework.integration.zeromq.inbound.ZeroMqMessageProducer#setTopics(String...)
*/
public void setTopics(String... topics) {
this.topics = topics;
}
}

View File

@@ -0,0 +1,99 @@
/*
* Copyright 2016-2020 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.cloud.fn.supplier.zeromq;
import java.time.Duration;
import java.util.function.Supplier;
import org.assertj.core.api.InstanceOfAssertFactories;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.zeromq.SocketType;
import org.zeromq.ZContext;
import org.zeromq.ZFrame;
import org.zeromq.ZMQ;
import org.zeromq.ZMsg;
import reactor.core.publisher.Flux;
import reactor.test.StepVerifier;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.messaging.Message;
import org.springframework.test.annotation.DirtiesContext;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Daniel Frey
* since 3.1.0
*/
@SpringBootTest(properties = {"zeromq.supplier.topics=test-topic"})
@DirtiesContext
public class ZeroMqSupplierConfigurationTests {
private static final ZContext CONTEXT = new ZContext();
private static ZMQ.Socket socket;
@Autowired
Supplier<Flux<Message<?>>> subject;
@BeforeAll
static void setup() {
String socketAddress = "tcp://*";
socket = CONTEXT.createSocket(SocketType.PUB);
int bindPort = socket.bindToRandomPort(socketAddress);
System.setProperty("zeromq.supplier.connectUrl", "tcp://localhost:" + bindPort);
}
@AfterAll
static void tearDown() {
socket.close();
CONTEXT.close();
}
@Test
void testSubscriptionConfiguration() throws InterruptedException {
StepVerifier stepVerifier =
StepVerifier.create(subject.get())
.assertNext((message) ->
assertThat(message.getPayload())
.asInstanceOf(InstanceOfAssertFactories.type(byte[].class))
.isEqualTo("test".getBytes(ZMQ.CHARSET))
)
.thenCancel()
.verifyLater();
Thread.sleep(2000);
ZMsg msg = ZMsg.newStringMsg("test");
msg.wrap(new ZFrame("test-topic"));
msg.send(socket);
stepVerifier.verify(Duration.ofSeconds(10));
}
@SpringBootApplication
public static class ZeroMqSourceTestApplication { }
}