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:
Mark Paluch
2018-04-26 09:17:19 +02:00
committed by Christoph Strobl
parent 6b247992ed
commit 4b80acf7f5
14 changed files with 1871 additions and 12 deletions

View File

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

View File

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

View File

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

View File

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