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:
Artem Bilan
2020-08-11 15:04:02 -04:00
committed by GitHub
parent 217e43b194
commit a76bb24965
12 changed files with 1040 additions and 3 deletions

View File

@@ -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();
}
}

View 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>