INT-3045: Add ZeroMqChannel support (#3355)
* INT-3045: Add `ZeroMqChannel` support JIRA: https://jira.spring.io/browse/INT-3045 Provide a `SubscribableChannel` implementation for ZeroMQ The general idea is to let to have a distributed channel implementation where every client can connect to a single server backed by the channel. The logic in the channel is fully transparent for end-user and there is just enough to send message to it and subscribe for receiving on the other side. If PUB/SUB model is used, all the subscribes (even over the network) going to receive the same published message. In case of PUSH/PULL only one subscriber in the whole cluster is going to get the published message * Use Reactor for better threading control * JeroMQ is not interruptible-friendly: use control sockets to stop proxy loop * Name Reactor's schedulers to avoid daemon threads * * Use try-catch-with-resource to close sockets automatically * Fix Checkstyle violations * Use `Mono.handle()` to receive data from the socket * * Optimize local for just a couple of PAIR sockets * Implement TCP binding * Add PUB/SUB tests * * Fix subscriber scheduler name * Optimize socket create logic * Add PUSH/PULL over TCP test * * Fix subscriber scheduler name * Optimize socket create logic * Add PUSH/PULL over TCP test * Implement PUB/SUB over TCP * * Introduce `ZeroMqProxy` - Spring-friendly component to configure and manage ZeroMq proxy * Use this `ZeroMqProxy` logic as an external component for `ZeroMqChannel` testing * * Fix Checkstyle * Apply docs polishing * Expose a capture socket on the proxy * Implement `DisposableBean` in the `ZeroMqProxy` to destroy an internal executor service * Add JavaDocs to `ZeroMqChannel` * Add one more `ZeroMqChannel` to TCP test to be sure that proxy distribution works well * * Add `hamcrest-core` dependency for Awatility * * Add more JavaDocs to `ZeroMqProxy` and `ZeroMqChannel` * Expose `ZeroMqChannel.setZeroMqProxy()` option for easier configuration within the same application context * Make `ZeroMqChannel` sockets configuration and connection dependant on provided `ZeroMqProxy` (if any) * Add `Consumer<ZMQ.Socket>` configuration callbacks to the `ZeroMqChannel` * Expose `ZeroMqChannel.consumeDelay` option * * Add docs for ZeroMQ * Some additions into a `reactive-streams.adoc` * Fix typo in the `xmpp.adoc` * * Add `optional` `jackson-databind` since `ZeroMqChannel` uses it by default * More words into docs * * Fix language in docs according review * Fix language in docs according review Co-authored-by: Gary Russell <grussell@vmware.com> * Apply suggestions from code review Co-authored-by: Oliver <oli-ver@users.noreply.github.com> * * Fix threading using a `publishOn()` for specific scheduler after `cache()` * * Remove unused import * * Change proxy port check from static `Mono.just()` to `Mono.fromCallable()` to really evaluate the current port state on every repeat * Add finite `100` repeat number to avoid infinite blocking when proxy is not started at all * Add `doOnError()` for proxy `Mono` to log `ERROR` when repeat is exhausted * * Fix Checkstyle violation Co-authored-by: Gary Russell <grussell@vmware.com> Co-authored-by: Oliver <oli-ver@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,193 @@
|
||||
/*
|
||||
* Copyright 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.integration.zeromq.channel;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.awaitility.Awaitility.await;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.zeromq.SocketType;
|
||||
import org.zeromq.ZContext;
|
||||
import org.zeromq.ZMQ;
|
||||
|
||||
import org.springframework.integration.support.json.EmbeddedJsonHeadersMessageMapper;
|
||||
import org.springframework.integration.zeromq.ZeroMqProxy;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.support.GenericMessage;
|
||||
|
||||
/**
|
||||
* @author Artem Bilan
|
||||
*
|
||||
* @since 5.4
|
||||
*/
|
||||
public class ZeroMqChannelTests {
|
||||
|
||||
private static final ZContext CONTEXT = new ZContext();
|
||||
|
||||
@AfterAll
|
||||
static void tearDown() {
|
||||
CONTEXT.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSimpleSendAndReceive() throws InterruptedException {
|
||||
ZeroMqChannel channel = new ZeroMqChannel(CONTEXT);
|
||||
channel.setBeanName("testChannel1");
|
||||
channel.setConsumeDelay(Duration.ofMillis(10));
|
||||
channel.afterPropertiesSet();
|
||||
|
||||
BlockingQueue<Message<?>> received = new LinkedBlockingQueue<>();
|
||||
|
||||
channel.subscribe(received::offer);
|
||||
|
||||
assertThat(channel.send(new GenericMessage<>("test1"), 1000)).isTrue();
|
||||
assertThat(channel.send(new GenericMessage<>("test2"), 500)).isTrue();
|
||||
|
||||
Message<?> message = received.poll(10, TimeUnit.SECONDS);
|
||||
assertThat(message).isNotNull().extracting(Message::getPayload).isEqualTo("test1");
|
||||
message = received.poll(10, TimeUnit.SECONDS);
|
||||
assertThat(message).isNotNull().extracting(Message::getPayload).isEqualTo("test2");
|
||||
|
||||
// Ensure that second subscriber doesn't make it as pub-sub
|
||||
channel.subscribe(received::offer);
|
||||
|
||||
assertThat(channel.send(new GenericMessage<>("test3"))).isTrue();
|
||||
|
||||
message = received.poll(10, TimeUnit.SECONDS);
|
||||
assertThat(message).isNotNull().extracting(Message::getPayload).isEqualTo("test3");
|
||||
assertThat(received.poll(100, TimeUnit.MILLISECONDS)).isNull();
|
||||
|
||||
channel.destroy();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPubSubLocal() throws InterruptedException {
|
||||
ZeroMqChannel channel = new ZeroMqChannel(CONTEXT, true);
|
||||
channel.setBeanName("testChannel2");
|
||||
channel.setConsumeDelay(Duration.ofMillis(10));
|
||||
channel.afterPropertiesSet();
|
||||
|
||||
BlockingQueue<Message<?>> received = new LinkedBlockingQueue<>();
|
||||
|
||||
channel.subscribe(received::offer);
|
||||
channel.subscribe(received::offer);
|
||||
|
||||
GenericMessage<String> testMessage = new GenericMessage<>("test1");
|
||||
assertThat(channel.send(testMessage)).isTrue();
|
||||
|
||||
Message<?> message = received.poll(10, TimeUnit.SECONDS);
|
||||
assertThat(message).isNotNull().isEqualTo(testMessage);
|
||||
message = received.poll(10, TimeUnit.SECONDS);
|
||||
assertThat(message).isNotNull().isEqualTo(testMessage);
|
||||
|
||||
channel.destroy();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPushPullBind() throws InterruptedException {
|
||||
ZeroMqProxy proxy = new ZeroMqProxy(CONTEXT);
|
||||
proxy.setBeanName("pullPushProxy");
|
||||
proxy.setExposeCaptureSocket(true);
|
||||
proxy.afterPropertiesSet();
|
||||
proxy.start();
|
||||
|
||||
await().until(() -> proxy.getBackendPort() > 0);
|
||||
|
||||
ZMQ.Socket captureSocket = CONTEXT.createSocket(SocketType.SUB);
|
||||
captureSocket.connect(proxy.getCaptureAddress());
|
||||
captureSocket.subscribe(ZMQ.SUBSCRIPTION_ALL);
|
||||
|
||||
ZeroMqChannel channel = new ZeroMqChannel(CONTEXT);
|
||||
channel.setConnectUrl("tcp://localhost:" + proxy.getFrontendPort() + ':' + proxy.getBackendPort());
|
||||
channel.setBeanName("testChannel3");
|
||||
channel.setConsumeDelay(Duration.ofMillis(10));
|
||||
channel.afterPropertiesSet();
|
||||
|
||||
BlockingQueue<Message<?>> received = new LinkedBlockingQueue<>();
|
||||
|
||||
channel.subscribe(received::offer);
|
||||
channel.subscribe(received::offer);
|
||||
|
||||
GenericMessage<String> testMessage = new GenericMessage<>("test1");
|
||||
assertThat(channel.send(testMessage)).isTrue();
|
||||
|
||||
Message<?> message = received.poll(10, TimeUnit.SECONDS);
|
||||
assertThat(message).isNotNull().isEqualTo(testMessage);
|
||||
assertThat(received.poll(100, TimeUnit.MILLISECONDS)).isNull();
|
||||
|
||||
channel.destroy();
|
||||
|
||||
byte[] recv = captureSocket.recv();
|
||||
assertThat(recv).isNotNull();
|
||||
Message<?> capturedMessage = new EmbeddedJsonHeadersMessageMapper().toMessage(recv);
|
||||
assertThat(capturedMessage).isEqualTo(testMessage);
|
||||
captureSocket.close();
|
||||
|
||||
proxy.stop();
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void testPubSubBind() throws InterruptedException {
|
||||
ZeroMqProxy proxy = new ZeroMqProxy(CONTEXT, ZeroMqProxy.Type.SUB_PUB);
|
||||
proxy.setBeanName("subPubProxy");
|
||||
proxy.afterPropertiesSet();
|
||||
proxy.start();
|
||||
|
||||
ZeroMqChannel channel = new ZeroMqChannel(CONTEXT, true);
|
||||
channel.setZeroMqProxy(proxy);
|
||||
channel.setBeanName("testChannel4");
|
||||
channel.setConsumeDelay(Duration.ofMillis(10));
|
||||
channel.afterPropertiesSet();
|
||||
|
||||
BlockingQueue<Message<?>> received = new LinkedBlockingQueue<>();
|
||||
|
||||
channel.subscribe(received::offer);
|
||||
channel.subscribe(received::offer);
|
||||
|
||||
await().until(() -> proxy.getBackendPort() > 0);
|
||||
|
||||
ZeroMqChannel channel2 = new ZeroMqChannel(CONTEXT, true);
|
||||
channel2.setConnectUrl("tcp://localhost:" + proxy.getFrontendPort() + ':' + proxy.getBackendPort());
|
||||
channel2.setBeanName("testChannel5");
|
||||
channel.setConsumeDelay(Duration.ofMillis(10));
|
||||
channel2.afterPropertiesSet();
|
||||
|
||||
channel.subscribe(received::offer);
|
||||
|
||||
GenericMessage<String> testMessage = new GenericMessage<>("test1");
|
||||
assertThat(channel.send(testMessage)).isTrue();
|
||||
|
||||
Message<?> message = received.poll(10, TimeUnit.SECONDS);
|
||||
assertThat(message).isNotNull().isEqualTo(testMessage);
|
||||
message = received.poll(10, TimeUnit.SECONDS);
|
||||
assertThat(message).isNotNull().isEqualTo(testMessage);
|
||||
message = received.poll(10, TimeUnit.SECONDS);
|
||||
assertThat(message).isNotNull().isEqualTo(testMessage);
|
||||
assertThat(received.poll(100, TimeUnit.MILLISECONDS)).isNull();
|
||||
|
||||
channel.destroy();
|
||||
proxy.stop();
|
||||
}
|
||||
|
||||
}
|
||||
15
spring-integration-zeromq/src/test/resources/log4j2-test.xml
Normal file
15
spring-integration-zeromq/src/test/resources/log4j2-test.xml
Normal file
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Configuration status="WARN">
|
||||
<Appenders>
|
||||
<Console name="STDOUT" target="SYSTEM_OUT">
|
||||
<PatternLayout pattern="%d %p [%t] [%c] - %m%n" />
|
||||
</Console>
|
||||
</Appenders>
|
||||
<Loggers>
|
||||
<Logger name="org.springframework.integration" level="warn"/>
|
||||
<Logger name="org.springframework.integration.zeromq" level="warn"/>
|
||||
<Root level="warn">
|
||||
<AppenderRef ref="STDOUT" />
|
||||
</Root>
|
||||
</Loggers>
|
||||
</Configuration>
|
||||
Reference in New Issue
Block a user