DATAREDIS-612 - Send and receive Pub/Sub messages using reactive connections.
We now provide sending and receiving of Redis Pub/Sub messages with reactive connections. Messages can be sent with ReactiveRedisTemplate and received as infinite stream through ReactiveRedisMessageListenerContainer. ReactiveRedisMessageListenerContainer uses a single connection to multiplex different subscriptions.
Reactive Pub/Sub supports Standalone, Sentinel and Redis Cluster setups.
ReactiveRedisConnectionFactory factory = …;
ReactiveRedisTemplate<String, String> template = new ReactiveRedisTemplate<>(factory, RedisSerializationContext.string());
Mono<Long> publish = template.convertAndSend("hello!", "world");
ReactiveRedisMessageListenerContainer container = new ReactiveRedisMessageListenerContainer(factory);
Flux<ChannelMessage<String, String>> channelMessages = container.receive(new ChannelTopic("foo"));
Flux<PatternMessage<String, String, String>> patternMessages = container.receive(new PatternTopic("foo"));
//
container.destroy();
Original Pull Request: #295
This commit is contained in:
committed by
Christoph Strobl
parent
6b247992ed
commit
4b80acf7f5
@@ -0,0 +1,233 @@
|
||||
/*
|
||||
* Copyright 2017 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
|
||||
*
|
||||
* http://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.data.redis.connection.lettuce;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.*;
|
||||
import static org.springframework.data.redis.util.ByteUtils.*;
|
||||
|
||||
import io.lettuce.core.RedisConnectionException;
|
||||
import io.lettuce.core.pubsub.api.reactive.RedisPubSubReactiveCommands;
|
||||
import reactor.core.Disposable;
|
||||
import reactor.core.publisher.DirectProcessor;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.test.StepVerifier;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.concurrent.CancellationException;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
import org.springframework.data.redis.RedisSystemException;
|
||||
import org.springframework.data.redis.connection.ReactiveSubscription.ChannelMessage;
|
||||
import org.springframework.data.redis.connection.ReactiveSubscription.PatternMessage;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link LettuceReactiveSubscription}.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class LettuceReactiveSubscriptionUnitTests {
|
||||
|
||||
LettuceReactiveSubscription subscription;
|
||||
|
||||
@Mock RedisPubSubReactiveCommands<ByteBuffer, ByteBuffer> commandsMock;
|
||||
|
||||
@Before
|
||||
public void before() {
|
||||
subscription = new LettuceReactiveSubscription(commandsMock, e -> new RedisSystemException(e.getMessage(), e));
|
||||
}
|
||||
|
||||
@Test // DATAREDIS-612
|
||||
public void shouldSubscribeChannels() {
|
||||
|
||||
when(commandsMock.subscribe(any())).thenReturn(Mono.empty());
|
||||
|
||||
Mono<Void> subscribe = subscription.subscribe(getByteBuffer("foo"), getByteBuffer("bar"));
|
||||
|
||||
assertThat(subscription.getChannels()).isEmpty();
|
||||
|
||||
StepVerifier.create(subscribe).verifyComplete();
|
||||
|
||||
assertThat(subscription.getChannels()).containsOnly(getByteBuffer("foo"), getByteBuffer("bar"));
|
||||
assertThat(subscription.getPatterns()).isEmpty();
|
||||
}
|
||||
|
||||
@Test // DATAREDIS-612
|
||||
public void shouldSubscribeChannelsShouldFail() {
|
||||
|
||||
when(commandsMock.subscribe(any())).thenReturn(Mono.error(new RedisConnectionException("Foo")));
|
||||
|
||||
Mono<Void> subscribe = subscription.subscribe(getByteBuffer("foo"), getByteBuffer("bar"));
|
||||
|
||||
StepVerifier.create(subscribe).expectError(RedisSystemException.class).verify();
|
||||
}
|
||||
|
||||
@Test // DATAREDIS-612
|
||||
public void shouldSubscribePatterns() {
|
||||
|
||||
when(commandsMock.psubscribe(any())).thenReturn(Mono.empty());
|
||||
|
||||
Mono<Void> subscribe = subscription.pSubscribe(getByteBuffer("foo"), getByteBuffer("bar"));
|
||||
|
||||
assertThat(subscription.getPatterns()).isEmpty();
|
||||
|
||||
StepVerifier.create(subscribe).verifyComplete();
|
||||
|
||||
assertThat(subscription.getPatterns()).containsOnly(getByteBuffer("foo"), getByteBuffer("bar"));
|
||||
assertThat(subscription.getChannels()).isEmpty();
|
||||
}
|
||||
|
||||
@Test // DATAREDIS-612
|
||||
public void shouldUnsubscribeChannels() {
|
||||
|
||||
when(commandsMock.subscribe(any())).thenReturn(Mono.empty());
|
||||
when(commandsMock.unsubscribe(any())).thenReturn(Mono.empty());
|
||||
StepVerifier.create(subscription.subscribe(getByteBuffer("foo"), getByteBuffer("bar"))).verifyComplete();
|
||||
|
||||
StepVerifier.create(subscription.unsubscribe()).verifyComplete();
|
||||
|
||||
assertThat(subscription.getChannels()).isEmpty();
|
||||
verify(commandsMock).unsubscribe(any());
|
||||
}
|
||||
|
||||
@Test // DATAREDIS-612
|
||||
public void shouldUnsubscribePatterns() {
|
||||
|
||||
when(commandsMock.psubscribe(any())).thenReturn(Mono.empty());
|
||||
when(commandsMock.punsubscribe(any())).thenReturn(Mono.empty());
|
||||
StepVerifier.create(subscription.pSubscribe(getByteBuffer("foo"), getByteBuffer("bar"))).verifyComplete();
|
||||
|
||||
StepVerifier.create(subscription.pUnsubscribe()).verifyComplete();
|
||||
|
||||
assertThat(subscription.getPatterns()).isEmpty();
|
||||
verify(commandsMock).punsubscribe(any());
|
||||
}
|
||||
|
||||
@Test // DATAREDIS-612
|
||||
public void shouldEmitChannelMessage() {
|
||||
|
||||
when(commandsMock.subscribe(any())).thenReturn(Mono.empty());
|
||||
StepVerifier.create(subscription.subscribe(getByteBuffer("foo"), getByteBuffer("bar"))).verifyComplete();
|
||||
|
||||
DirectProcessor<io.lettuce.core.pubsub.api.reactive.ChannelMessage<ByteBuffer, ByteBuffer>> emitter = DirectProcessor
|
||||
.create();
|
||||
when(commandsMock.observeChannels()).thenReturn(emitter);
|
||||
when(commandsMock.observePatterns()).thenReturn(Flux.empty());
|
||||
|
||||
StepVerifier.create(subscription.receive()).then(() -> {
|
||||
|
||||
emitter.onNext(createChannelMessage("other", "body"));
|
||||
emitter.onNext(createChannelMessage("foo", "body"));
|
||||
}).assertNext(msg -> {
|
||||
assertThat(msg.getChannel()).isEqualTo(getByteBuffer("foo"));
|
||||
}).thenCancel().verify();
|
||||
}
|
||||
|
||||
@Test // DATAREDIS-612
|
||||
public void shouldEmitPatternMessage() {
|
||||
|
||||
when(commandsMock.psubscribe(any())).thenReturn(Mono.empty());
|
||||
StepVerifier.create(subscription.pSubscribe(getByteBuffer("foo*"), getByteBuffer("bar*"))).verifyComplete();
|
||||
|
||||
DirectProcessor<io.lettuce.core.pubsub.api.reactive.PatternMessage<ByteBuffer, ByteBuffer>> emitter = DirectProcessor
|
||||
.create();
|
||||
when(commandsMock.observeChannels()).thenReturn(Flux.empty());
|
||||
when(commandsMock.observePatterns()).thenReturn(emitter);
|
||||
|
||||
StepVerifier.create(subscription.receive()).then(() -> {
|
||||
|
||||
emitter.onNext(createPatternMessage("other*", "channel", "body"));
|
||||
emitter.onNext(createPatternMessage("foo*", "foo", "body"));
|
||||
}).assertNext(msg -> {
|
||||
|
||||
assertThat(((PatternMessage) msg).getPattern()).isEqualTo(getByteBuffer("foo*"));
|
||||
assertThat(msg.getChannel()).isEqualTo(getByteBuffer("foo"));
|
||||
}).thenCancel().verify();
|
||||
}
|
||||
|
||||
@Test // DATAREDIS-612
|
||||
public void shouldEmitError() {
|
||||
|
||||
when(commandsMock.subscribe(any())).thenReturn(Mono.empty());
|
||||
StepVerifier.create(subscription.subscribe(getByteBuffer("foo"), getByteBuffer("bar"))).verifyComplete();
|
||||
|
||||
DirectProcessor<io.lettuce.core.pubsub.api.reactive.ChannelMessage<ByteBuffer, ByteBuffer>> emitter = DirectProcessor
|
||||
.create();
|
||||
when(commandsMock.observeChannels()).thenReturn(emitter);
|
||||
when(commandsMock.observePatterns()).thenReturn(Flux.empty());
|
||||
|
||||
StepVerifier.create(subscription.receive()).then(() -> {
|
||||
|
||||
emitter.onError(new RedisConnectionException("foo"));
|
||||
}).expectError(RedisSystemException.class).verify();
|
||||
}
|
||||
|
||||
@Test // DATAREDIS-612
|
||||
public void shouldTerminateActiveSubscriptions() {
|
||||
|
||||
when(commandsMock.psubscribe(any())).thenReturn(Mono.empty());
|
||||
when(commandsMock.punsubscribe(any())).thenReturn(Mono.empty());
|
||||
StepVerifier.create(subscription.pSubscribe(getByteBuffer("foo*"))).verifyComplete();
|
||||
|
||||
when(commandsMock.observeChannels()).thenReturn(Flux.never());
|
||||
when(commandsMock.observePatterns()).thenReturn(Flux.never());
|
||||
|
||||
StepVerifier.create(subscription.receive()).then(() -> {
|
||||
subscription.terminate().subscribe();
|
||||
}).expectError(CancellationException.class).verify();
|
||||
|
||||
assertThat(subscription.getPatterns()).isEmpty();
|
||||
}
|
||||
|
||||
@Test // DATAREDIS-612
|
||||
public void cancelledSubscriptionShouldUnregisterDownstream() {
|
||||
|
||||
DirectProcessor<io.lettuce.core.pubsub.api.reactive.PatternMessage<ByteBuffer, ByteBuffer>> emitter = DirectProcessor
|
||||
.create();
|
||||
|
||||
when(commandsMock.psubscribe(any())).thenReturn(Mono.empty());
|
||||
StepVerifier.create(subscription.pSubscribe(getByteBuffer("foo*"))).verifyComplete();
|
||||
|
||||
when(commandsMock.observeChannels()).thenReturn(Flux.never());
|
||||
when(commandsMock.observePatterns()).thenReturn(emitter);
|
||||
|
||||
Flux<ChannelMessage<ByteBuffer, ByteBuffer>> receive = subscription.receive();
|
||||
Disposable subscribe = receive.subscribe();
|
||||
|
||||
assertThat(emitter.downstreamCount()).isEqualTo(1);
|
||||
|
||||
subscribe.dispose();
|
||||
assertThat(emitter.downstreamCount()).isEqualTo(0);
|
||||
}
|
||||
|
||||
private static io.lettuce.core.pubsub.api.reactive.ChannelMessage<ByteBuffer, ByteBuffer> createChannelMessage(
|
||||
String channel, String body) {
|
||||
return new io.lettuce.core.pubsub.api.reactive.ChannelMessage<>(getByteBuffer(channel), getByteBuffer(body));
|
||||
}
|
||||
|
||||
private static io.lettuce.core.pubsub.api.reactive.PatternMessage<ByteBuffer, ByteBuffer> createPatternMessage(
|
||||
String pattern, String channel, String body) {
|
||||
return new io.lettuce.core.pubsub.api.reactive.PatternMessage<>(getByteBuffer(pattern), getByteBuffer(channel),
|
||||
getByteBuffer(body));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* Copyright 2017 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
|
||||
*
|
||||
* http://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.data.redis.listener;
|
||||
|
||||
import static org.springframework.data.redis.connection.ClusterTestVariables.*;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.runners.model.Statement;
|
||||
import org.springframework.data.redis.SettingsUtils;
|
||||
import org.springframework.data.redis.connection.RedisClusterConfiguration;
|
||||
import org.springframework.data.redis.connection.RedisClusterNode;
|
||||
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
|
||||
import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration;
|
||||
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
|
||||
import org.springframework.data.redis.connection.lettuce.LettucePoolingClientConfiguration;
|
||||
import org.springframework.data.redis.connection.lettuce.LettuceTestClientResources;
|
||||
import org.springframework.data.redis.test.util.RedisClusterRule;
|
||||
|
||||
/**
|
||||
* Parameters for testing implementations of {@link ReactiveRedisMessageListenerContainer}
|
||||
*
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
class ReactiveOperationsTestParams {
|
||||
|
||||
public static Collection<Object[]> testParams() {
|
||||
|
||||
LettuceClientConfiguration clientConfiguration = LettuceClientConfiguration.builder() //
|
||||
.shutdownTimeout(Duration.ZERO) //
|
||||
.clientResources(LettuceTestClientResources.getSharedClientResources()) //
|
||||
.build();
|
||||
|
||||
LettucePoolingClientConfiguration poolingConfiguration = LettucePoolingClientConfiguration.builder() //
|
||||
.shutdownTimeout(Duration.ZERO) //
|
||||
.clientResources(LettuceTestClientResources.getSharedClientResources()) //
|
||||
.build();
|
||||
|
||||
RedisStandaloneConfiguration standaloneConfiguration = new RedisStandaloneConfiguration(SettingsUtils.getHost(),
|
||||
SettingsUtils.getPort());
|
||||
|
||||
LettuceConnectionFactory lettuceConnectionFactory = new LettuceConnectionFactory(standaloneConfiguration,
|
||||
clientConfiguration);
|
||||
lettuceConnectionFactory.afterPropertiesSet();
|
||||
|
||||
LettuceConnectionFactory poolingConnectionFactory = new LettuceConnectionFactory(standaloneConfiguration,
|
||||
poolingConfiguration);
|
||||
poolingConnectionFactory.afterPropertiesSet();
|
||||
|
||||
List<Object[]> list = Arrays.asList(new Object[][] { //
|
||||
{ lettuceConnectionFactory, "Standalone" }, //
|
||||
{ poolingConnectionFactory, "Pooled" }, //
|
||||
});
|
||||
|
||||
if (clusterAvailable()) {
|
||||
|
||||
RedisClusterConfiguration clusterConfiguration = new RedisClusterConfiguration();
|
||||
clusterConfiguration.addClusterNode(new RedisClusterNode(CLUSTER_HOST, MASTER_NODE_1_PORT));
|
||||
|
||||
LettuceConnectionFactory lettuceClusterConnectionFactory = new LettuceConnectionFactory(clusterConfiguration,
|
||||
clientConfiguration);
|
||||
lettuceClusterConnectionFactory.afterPropertiesSet();
|
||||
|
||||
list = new ArrayList<>(list);
|
||||
list.add(new Object[] { lettuceClusterConnectionFactory, "Cluster" });
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
private static boolean clusterAvailable() {
|
||||
|
||||
try {
|
||||
new RedisClusterRule().apply(new Statement() {
|
||||
@Override
|
||||
public void evaluate() {
|
||||
|
||||
}
|
||||
}, null).evaluate();
|
||||
} catch (Throwable throwable) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
/*
|
||||
* Copyright 2017 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
|
||||
*
|
||||
* http://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.data.redis.listener;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
import reactor.core.Disposable;
|
||||
import reactor.test.StepVerifier;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.concurrent.LinkedBlockingDeque;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.AfterClass;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.Parameterized;
|
||||
import org.junit.runners.Parameterized.Parameters;
|
||||
import org.springframework.data.redis.ConnectionFactoryTracker;
|
||||
import org.springframework.data.redis.connection.ReactiveSubscription;
|
||||
import org.springframework.data.redis.connection.ReactiveSubscription.PatternMessage;
|
||||
import org.springframework.data.redis.connection.RedisConnection;
|
||||
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
|
||||
import org.springframework.data.redis.core.ReactiveRedisTemplate;
|
||||
import org.springframework.data.redis.serializer.RedisSerializationContext;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* Integration tests for {@link ReactiveRedisMessageListenerContainer} via Lettuce.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
@RunWith(Parameterized.class)
|
||||
public class ReactiveRedisMessageListenerContainerIntegrationTests {
|
||||
|
||||
static final String CHANNEL1 = "my-channel";
|
||||
static final String PATTERN1 = "my-chan*";
|
||||
public static final String MESSAGE = "hello world";
|
||||
|
||||
private final LettuceConnectionFactory connectionFactory;
|
||||
private @Nullable RedisConnection connection;
|
||||
|
||||
@Parameters(name = "{1}")
|
||||
public static Collection<Object[]> testParams() {
|
||||
return ReactiveOperationsTestParams.testParams();
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
public static void cleanUp() {
|
||||
ConnectionFactoryTracker.cleanUp();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param connectionFactory
|
||||
* @param label parameterized test label, no further use besides that.
|
||||
*/
|
||||
public ReactiveRedisMessageListenerContainerIntegrationTests(LettuceConnectionFactory connectionFactory,
|
||||
String label) {
|
||||
|
||||
this.connectionFactory = connectionFactory;
|
||||
ConnectionFactoryTracker.add(connectionFactory);
|
||||
}
|
||||
|
||||
@Before
|
||||
public void before() {
|
||||
connection = connectionFactory.getConnection();
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() {
|
||||
|
||||
if (connection != null) {
|
||||
connection.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Test // DATAREDIS-612
|
||||
public void shouldReceiveChannelMessages() {
|
||||
|
||||
ReactiveRedisMessageListenerContainer container = new ReactiveRedisMessageListenerContainer(connectionFactory);
|
||||
|
||||
StepVerifier.create(container.receive(new ChannelTopic(CHANNEL1))) //
|
||||
.then(awaitSubscription(container::getActiveSubscriptions))
|
||||
.then(() -> connection.publish(CHANNEL1.getBytes(), MESSAGE.getBytes())) //
|
||||
.assertNext(c -> {
|
||||
|
||||
assertThat(c.getChannel()).isEqualTo(CHANNEL1);
|
||||
assertThat(c.getMessage()).isEqualTo(MESSAGE);
|
||||
}) //
|
||||
.thenCancel().verify();
|
||||
|
||||
container.destroy();
|
||||
}
|
||||
|
||||
@Test // DATAREDIS-612
|
||||
public void shouldReceivePatternMessages() {
|
||||
|
||||
ReactiveRedisMessageListenerContainer container = new ReactiveRedisMessageListenerContainer(connectionFactory);
|
||||
|
||||
StepVerifier.create(container.receive(new PatternTopic(PATTERN1))) //
|
||||
.then(awaitSubscription(container::getActiveSubscriptions))
|
||||
.then(() -> connection.publish(CHANNEL1.getBytes(), MESSAGE.getBytes())) //
|
||||
.assertNext(c -> {
|
||||
|
||||
assertThat(c.getPattern()).isEqualTo(PATTERN1);
|
||||
assertThat(c.getChannel()).isEqualTo(CHANNEL1);
|
||||
assertThat(c.getMessage()).isEqualTo(MESSAGE);
|
||||
}) //
|
||||
.thenCancel().verify();
|
||||
|
||||
container.destroy();
|
||||
}
|
||||
|
||||
@Test // DATAREDIS-612
|
||||
public void shouldPublishAndReceiveMessage() throws InterruptedException {
|
||||
|
||||
ReactiveRedisMessageListenerContainer container = new ReactiveRedisMessageListenerContainer(connectionFactory);
|
||||
ReactiveRedisTemplate<String, String> template = new ReactiveRedisTemplate<>(connectionFactory,
|
||||
RedisSerializationContext.string());
|
||||
|
||||
BlockingQueue<PatternMessage<String, String, String>> messages = new LinkedBlockingDeque<>();
|
||||
Disposable subscription = container.receive(new PatternTopic(PATTERN1)).doOnNext(messages::add).subscribe();
|
||||
|
||||
StepVerifier.create(template.convertAndSend(CHANNEL1, MESSAGE), 0) //
|
||||
.then(awaitSubscription(container::getActiveSubscriptions)) //
|
||||
.thenRequest(1).expectNextCount(1) //
|
||||
.verifyComplete();
|
||||
|
||||
PatternMessage<String, String, String> message = messages.poll(1, TimeUnit.SECONDS);
|
||||
|
||||
assertThat(message).isNotNull();
|
||||
assertThat(message.getPattern()).isEqualTo(PATTERN1);
|
||||
assertThat(message.getChannel()).isEqualTo(CHANNEL1);
|
||||
assertThat(message.getMessage()).isEqualTo(MESSAGE);
|
||||
|
||||
subscription.dispose();
|
||||
container.destroy();
|
||||
}
|
||||
|
||||
private static Runnable awaitSubscription(Supplier<Collection<ReactiveSubscription>> activeSubscriptions) {
|
||||
|
||||
return () -> {
|
||||
|
||||
try {
|
||||
|
||||
while (activeSubscriptions.get().isEmpty()) {
|
||||
Thread.sleep(10);
|
||||
}
|
||||
|
||||
} catch (InterruptedException e) {
|
||||
return;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,270 @@
|
||||
/*
|
||||
* Copyright 2017 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
|
||||
*
|
||||
* http://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.data.redis.listener;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
import static org.springframework.data.redis.util.ByteUtils.*;
|
||||
|
||||
import reactor.core.Disposable;
|
||||
import reactor.core.publisher.DirectProcessor;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.publisher.MonoProcessor;
|
||||
import reactor.test.StepVerifier;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.concurrent.CancellationException;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
import org.springframework.data.redis.connection.ReactiveRedisConnection;
|
||||
import org.springframework.data.redis.connection.ReactiveRedisConnectionFactory;
|
||||
import org.springframework.data.redis.connection.ReactiveRedisPubSubCommands;
|
||||
import org.springframework.data.redis.connection.ReactiveSubscription;
|
||||
import org.springframework.data.redis.connection.ReactiveSubscription.ChannelMessage;
|
||||
import org.springframework.data.redis.connection.ReactiveSubscription.PatternMessage;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link ReactiveRedisMessageListenerContainer}.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class ReactiveRedisMessageListenerContainerUnitTests {
|
||||
|
||||
ReactiveRedisMessageListenerContainer container;
|
||||
|
||||
@Mock ReactiveRedisConnectionFactory connectionFactoryMock;
|
||||
@Mock ReactiveRedisConnection connectionMock;
|
||||
@Mock ReactiveRedisPubSubCommands commandsMock;
|
||||
@Mock ReactiveSubscription subscriptionMock;
|
||||
|
||||
@Before
|
||||
public void before() {
|
||||
|
||||
when(connectionFactoryMock.getReactiveConnection()).thenReturn(connectionMock);
|
||||
when(connectionMock.pubSubCommands()).thenReturn(commandsMock);
|
||||
when(commandsMock.createSubscription()).thenReturn(Mono.just(subscriptionMock));
|
||||
when(subscriptionMock.subscribe(any())).thenReturn(Mono.empty());
|
||||
when(subscriptionMock.pSubscribe(any())).thenReturn(Mono.empty());
|
||||
when(subscriptionMock.unsubscribe()).thenReturn(Mono.empty());
|
||||
}
|
||||
|
||||
@Test // DATAREDIS-612
|
||||
public void shouldSubscribeToPattern() {
|
||||
|
||||
when(subscriptionMock.receive()).thenReturn(Flux.never());
|
||||
|
||||
container = createContainer();
|
||||
|
||||
StepVerifier.create(container.receive(new PatternTopic("foo*"))).thenAwait().thenCancel().verify();
|
||||
|
||||
verify(subscriptionMock).pSubscribe(getByteBuffer("foo*"));
|
||||
}
|
||||
|
||||
@Test // DATAREDIS-612
|
||||
public void shouldSubscribeToMultiplePatterns() {
|
||||
|
||||
when(subscriptionMock.receive()).thenReturn(Flux.never());
|
||||
container = createContainer();
|
||||
|
||||
StepVerifier.create(container.receive(new PatternTopic("foo*"), new PatternTopic("bar*"))).thenRequest(1)
|
||||
.thenAwait().thenCancel().verify();
|
||||
|
||||
verify(subscriptionMock).pSubscribe(getByteBuffer("foo*"), getByteBuffer("bar*"));
|
||||
}
|
||||
|
||||
@Test // DATAREDIS-612
|
||||
public void shouldSubscribeToChannel() {
|
||||
|
||||
when(subscriptionMock.receive()).thenReturn(Flux.never());
|
||||
container = createContainer();
|
||||
|
||||
StepVerifier.create(container.receive(new ChannelTopic("foo"))).thenAwait().thenCancel().verify();
|
||||
|
||||
verify(subscriptionMock).subscribe(getByteBuffer("foo"));
|
||||
}
|
||||
|
||||
@Test // DATAREDIS-612
|
||||
public void shouldSubscribeToMultipleChannels() {
|
||||
|
||||
when(subscriptionMock.receive()).thenReturn(Flux.never());
|
||||
container = createContainer();
|
||||
|
||||
StepVerifier.create(container.receive(new ChannelTopic("foo"), new ChannelTopic("bar"))).thenAwait().thenCancel()
|
||||
.verify();
|
||||
|
||||
verify(subscriptionMock).subscribe(getByteBuffer("foo"), getByteBuffer("bar"));
|
||||
}
|
||||
|
||||
@Test // DATAREDIS-612
|
||||
public void shouldEmitChannelMessage() {
|
||||
|
||||
DirectProcessor<ChannelMessage<ByteBuffer, ByteBuffer>> processor = DirectProcessor.create();
|
||||
|
||||
when(subscriptionMock.receive()).thenReturn(processor);
|
||||
container = createContainer();
|
||||
|
||||
Flux<ChannelMessage<String, String>> messageStream = container.receive(new ChannelTopic("foo"));
|
||||
|
||||
StepVerifier.create(messageStream).then(() -> {
|
||||
processor.onNext(createChannelMessage("foo", "message"));
|
||||
}).assertNext(msg -> {
|
||||
|
||||
assertThat(msg.getChannel()).isEqualTo("foo");
|
||||
assertThat(msg.getMessage()).isEqualTo("message");
|
||||
}).thenCancel().verify();
|
||||
}
|
||||
|
||||
@Test // DATAREDIS-612
|
||||
public void shouldEmitPatternMessage() {
|
||||
|
||||
DirectProcessor<ChannelMessage<ByteBuffer, ByteBuffer>> processor = DirectProcessor.create();
|
||||
|
||||
when(subscriptionMock.receive()).thenReturn(processor);
|
||||
container = createContainer();
|
||||
|
||||
Flux<PatternMessage<String, String, String>> messageStream = container.receive(new PatternTopic("foo*"));
|
||||
|
||||
StepVerifier.create(messageStream).then(() -> {
|
||||
processor.onNext(createPatternMessage("foo*", "foo", "message"));
|
||||
}).assertNext(msg -> {
|
||||
|
||||
assertThat(msg.getPattern()).isEqualTo("foo*");
|
||||
assertThat(msg.getChannel()).isEqualTo("foo");
|
||||
assertThat(msg.getMessage()).isEqualTo("message");
|
||||
}).thenCancel().verify();
|
||||
}
|
||||
|
||||
@Test // DATAREDIS-612
|
||||
public void shouldRegisterSubscription() {
|
||||
|
||||
MonoProcessor<Void> subscribeMono = MonoProcessor.create();
|
||||
|
||||
reset(subscriptionMock);
|
||||
when(subscriptionMock.subscribe(any())).thenReturn(subscribeMono);
|
||||
when(subscriptionMock.unsubscribe()).thenReturn(Mono.empty());
|
||||
when(subscriptionMock.receive()).thenReturn(DirectProcessor.create());
|
||||
container = createContainer();
|
||||
|
||||
Flux<ChannelMessage<String, String>> messageStream = container.receive(new ChannelTopic("foo*"));
|
||||
|
||||
Disposable subscription = messageStream.subscribe();
|
||||
|
||||
assertThat(container.getActiveSubscriptions()).isEmpty();
|
||||
subscribeMono.onComplete();
|
||||
assertThat(container.getActiveSubscriptions()).isNotEmpty();
|
||||
subscription.dispose();
|
||||
assertThat(container.getActiveSubscriptions()).isEmpty();
|
||||
}
|
||||
|
||||
@Test // DATAREDIS-612
|
||||
public void shouldRegisterSubscriptionMultipleSubscribers() {
|
||||
|
||||
reset(subscriptionMock);
|
||||
when(subscriptionMock.subscribe(any())).thenReturn(Mono.empty());
|
||||
when(subscriptionMock.unsubscribe()).thenReturn(Mono.empty());
|
||||
when(subscriptionMock.receive()).thenReturn(DirectProcessor.create());
|
||||
container = createContainer();
|
||||
|
||||
Flux<ChannelMessage<String, String>> messageStream = container.receive(new ChannelTopic("foo*"));
|
||||
|
||||
Disposable first = messageStream.subscribe();
|
||||
Disposable second = messageStream.subscribe();
|
||||
|
||||
first.dispose();
|
||||
|
||||
verify(subscriptionMock, never()).unsubscribe();
|
||||
assertThat(container.getActiveSubscriptions()).isNotEmpty();
|
||||
|
||||
second.dispose();
|
||||
|
||||
verify(subscriptionMock).unsubscribe();
|
||||
assertThat(container.getActiveSubscriptions()).isEmpty();
|
||||
}
|
||||
|
||||
@Test // DATAREDIS-612
|
||||
public void shouldUnsubscribeOnCancel() {
|
||||
|
||||
when(subscriptionMock.receive()).thenReturn(DirectProcessor.create());
|
||||
container = createContainer();
|
||||
|
||||
Flux<PatternMessage<String, String, String>> messageStream = container.receive(new PatternTopic("foo*"));
|
||||
|
||||
StepVerifier.create(messageStream).then(() -> {
|
||||
|
||||
// Then required to trigger cancel.
|
||||
|
||||
}).thenCancel().verify();
|
||||
|
||||
verify(subscriptionMock).unsubscribe();
|
||||
}
|
||||
|
||||
@Test // DATAREDIS-612
|
||||
public void shouldTerminateSubscriptionsOnShutdown() {
|
||||
|
||||
DirectProcessor<ChannelMessage<ByteBuffer, ByteBuffer>> processor = DirectProcessor.create();
|
||||
|
||||
when(subscriptionMock.receive()).thenReturn(processor);
|
||||
when(subscriptionMock.terminate()).thenReturn(Mono.defer(() -> {
|
||||
|
||||
processor.onError(new CancellationException());
|
||||
return Mono.empty();
|
||||
}));
|
||||
container = createContainer();
|
||||
|
||||
Flux<PatternMessage<String, String, String>> messageStream = container.receive(new PatternTopic("foo*"));
|
||||
|
||||
StepVerifier.create(messageStream).then(() -> {
|
||||
container.destroy();
|
||||
}).verifyError(CancellationException.class);
|
||||
}
|
||||
|
||||
@Test // DATAREDIS-612
|
||||
public void shouldCleanupDownstream() {
|
||||
|
||||
DirectProcessor<ChannelMessage<ByteBuffer, ByteBuffer>> processor = DirectProcessor.create();
|
||||
|
||||
when(subscriptionMock.receive()).thenReturn(processor);
|
||||
container = createContainer();
|
||||
|
||||
Flux<PatternMessage<String, String, String>> messageStream = container.receive(new PatternTopic("foo*"));
|
||||
|
||||
StepVerifier.create(messageStream).then(() -> {
|
||||
assertThat(processor.hasDownstreams()).isTrue();
|
||||
processor.onNext(createPatternMessage("foo*", "foo", "message"));
|
||||
}).expectNextCount(1).thenCancel().verify();
|
||||
|
||||
assertThat(processor.hasDownstreams()).isFalse();
|
||||
}
|
||||
|
||||
private ReactiveRedisMessageListenerContainer createContainer() {
|
||||
return new ReactiveRedisMessageListenerContainer(connectionFactoryMock);
|
||||
}
|
||||
|
||||
private static ChannelMessage<ByteBuffer, ByteBuffer> createChannelMessage(String channel, String body) {
|
||||
return new ChannelMessage<>(getByteBuffer(channel), getByteBuffer(body));
|
||||
}
|
||||
|
||||
private static PatternMessage<ByteBuffer, ByteBuffer, ByteBuffer> createPatternMessage(String pattern, String channel,
|
||||
String body) {
|
||||
return new PatternMessage<>(getByteBuffer(pattern), getByteBuffer(channel), getByteBuffer(body));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user