org.springframework.data.redis.listener.ReactiveRedisMessageListenerContainer Maven / Gradle / Ivy
/*
* Copyright 2018-2022 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.data.redis.listener;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.publisher.Sinks;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLongFieldUpdater;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.data.redis.connection.ReactivePubSubCommands;
import org.springframework.data.redis.connection.ReactiveRedisConnection;
import org.springframework.data.redis.connection.ReactiveRedisConnectionFactory;
import org.springframework.data.redis.connection.ReactiveSubscription;
import org.springframework.data.redis.connection.ReactiveSubscription.ChannelMessage;
import org.springframework.data.redis.connection.ReactiveSubscription.Message;
import org.springframework.data.redis.connection.ReactiveSubscription.PatternMessage;
import org.springframework.data.redis.connection.SubscriptionListener;
import org.springframework.data.redis.connection.util.ByteArrayWrapper;
import org.springframework.data.redis.serializer.RedisElementReader;
import org.springframework.data.redis.serializer.RedisSerializationContext.SerializationPair;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.util.ByteUtils;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
/**
* Container providing a stream of {@link ChannelMessage} for messages received via Redis Pub/Sub listeners. The stream
* is infinite and registers Redis subscriptions. Handles the low level details of listening, converting and message
* dispatching.
*
* Note the container allocates a single connection when it is created and releases the connection on
* {@link #destroy()}. Connections are allocated eagerly to not interfere with non-blocking use during application
* operations. Using reactive infrastructure allows usage of a single connection due to channel multiplexing.
*
* This class is thread-safe and allows subscription by multiple concurrent threads.
*
* @author Mark Paluch
* @author Christoph Strobl
* @since 2.1
* @see ReactiveSubscription
* @see ReactivePubSubCommands
*/
public class ReactiveRedisMessageListenerContainer implements DisposableBean {
private final SerializationPair stringSerializationPair = SerializationPair
.fromSerializer(RedisSerializer.string());
private final Map subscriptions = new ConcurrentHashMap<>();
private volatile @Nullable ReactiveRedisConnection connection;
/**
* Create a new {@link ReactiveRedisMessageListenerContainer} given {@link ReactiveRedisConnectionFactory}.
*
* @param connectionFactory must not be {@literal null}.
*/
public ReactiveRedisMessageListenerContainer(ReactiveRedisConnectionFactory connectionFactory) {
Assert.notNull(connectionFactory, "ReactiveRedisConnectionFactory must not be null!");
this.connection = connectionFactory.getReactiveConnection();
}
/*
* (non-Javadoc)
* @see org.springframework.beans.factory.DisposableBean#destroy()
*/
@Override
public void destroy() {
destroyLater().block();
}
/**
* @return the {@link Mono} signalling container termination.
*/
public Mono destroyLater() {
return Mono.defer(this::doDestroy);
}
private Mono doDestroy() {
if (this.connection == null) {
return Mono.empty();
}
ReactiveRedisConnection connection = getRequiredConnection();
Flux terminationSignals = null;
while (!subscriptions.isEmpty()) {
Map local = new HashMap<>(subscriptions);
List> monos = local.keySet().stream() //
.peek(subscriptions::remove) //
.map(ReactiveSubscription::cancel) //
.collect(Collectors.toList());
if (terminationSignals == null) {
terminationSignals = Flux.concat(monos);
} else {
terminationSignals = terminationSignals.mergeWith(Flux.concat(monos));
}
}
this.connection = null;
return terminationSignals != null ? terminationSignals.then(connection.closeLater()) : connection.closeLater();
}
/**
* Return the currently active {@link ReactiveSubscription subscriptions}.
*
* @return {@link Set} of active {@link ReactiveSubscription}
*/
public Collection getActiveSubscriptions() {
return subscriptions.entrySet().stream().filter(entry -> entry.getValue().hasRegistration())
.map(Map.Entry::getKey).collect(Collectors.toList());
}
/**
* Subscribe to one or more {@link ChannelTopic}s and receive a stream of {@link ChannelMessage}. Messages and channel
* names are treated as {@link String}. The message stream subscribes lazily to the Redis channels and unsubscribes if
* the {@link org.reactivestreams.Subscription} is {@link org.reactivestreams.Subscription#cancel() cancelled}.
*
* @param channelTopics the channels to subscribe.
* @return the message stream.
* @throws InvalidDataAccessApiUsageException if {@code patternTopics} is empty.
* @see #receive(Iterable, SerializationPair, SerializationPair)
*/
public Flux> receive(ChannelTopic... channelTopics) {
Assert.notNull(channelTopics, "ChannelTopics must not be null!");
Assert.noNullElements(channelTopics, "ChannelTopics must not contain null elements!");
return receive(Arrays.asList(channelTopics), stringSerializationPair, stringSerializationPair);
}
/**
* Subscribe to one or more {@link ChannelTopic}s and receive a stream of {@link ChannelMessage} once the returned
* {@link Mono} completes. Messages and channel names are treated as {@link String}. The message stream subscribes
* lazily to the Redis channels and unsubscribes if the inner {@link org.reactivestreams.Subscription} is
* {@link org.reactivestreams.Subscription#cancel() cancelled}.
*
* The returned {@link Mono} completes once the connection has been subscribed to the given {@link Topic topics}. Note
* that cancelling the returned {@link Mono} can leave the connection in a subscribed state.
*
* @param channelTopics the channels to subscribe.
* @return the message stream.
* @throws InvalidDataAccessApiUsageException if {@code patternTopics} is empty.
* @since 2.6
*/
public Mono>> receiveLater(ChannelTopic... channelTopics) {
Assert.notNull(channelTopics, "ChannelTopics must not be null!");
Assert.noNullElements(channelTopics, "ChannelTopics must not contain null elements!");
return receiveLater(Arrays.asList(channelTopics), stringSerializationPair, stringSerializationPair);
}
/**
* Subscribe to one or more {@link PatternTopic}s and receive a stream of {@link PatternMessage}. Messages, pattern,
* and channel names are treated as {@link String}. The message stream subscribes lazily to the Redis channels and
* unsubscribes if the {@link org.reactivestreams.Subscription} is {@link org.reactivestreams.Subscription#cancel()
* cancelled}.
*
* @param patternTopics the channels to subscribe.
* @return the message stream.
* @throws InvalidDataAccessApiUsageException if {@code patternTopics} is empty.
* @see #receive(Iterable, SerializationPair, SerializationPair)
*/
@SuppressWarnings("unchecked")
public Flux> receive(PatternTopic... patternTopics) {
Assert.notNull(patternTopics, "PatternTopic must not be null!");
Assert.noNullElements(patternTopics, "PatternTopic must not contain null elements!");
return receive(Arrays.asList(patternTopics), stringSerializationPair, stringSerializationPair)
.map(m -> (PatternMessage) m);
}
/**
* Subscribe to one or more {@link PatternTopic}s and receive a stream of {@link PatternMessage} once the returned
* {@link Mono} completes. Messages, pattern, and channel names are treated as {@link String}. The message stream
* subscribes lazily to the Redis channels and unsubscribes if the inner {@link org.reactivestreams.Subscription} is
* {@link org.reactivestreams.Subscription#cancel() cancelled}.
*
* The returned {@link Mono} completes once the connection has been subscribed to the given {@link Topic topics}. Note
* that cancelling the returned {@link Mono} can leave the connection in a subscribed state.
*
* @param patternTopics the channels to subscribe.
* @return the message stream.
* @throws InvalidDataAccessApiUsageException if {@code patternTopics} is empty.
* @since 2.6
*/
@SuppressWarnings("unchecked")
public Mono>> receiveLater(PatternTopic... patternTopics) {
Assert.notNull(patternTopics, "PatternTopic must not be null!");
Assert.noNullElements(patternTopics, "PatternTopic must not contain null elements!");
return receiveLater(Arrays.asList(patternTopics), stringSerializationPair, stringSerializationPair)
.map(it -> it.map(m -> (PatternMessage) m));
}
/**
* Subscribe to one or more {@link Topic}s and receive a stream of {@link ChannelMessage}. The stream may contain
* {@link PatternMessage} if subscribed to patterns. Messages, and channel names are serialized/deserialized using the
* given {@code channelSerializer} and {@code messageSerializer}. The message stream subscribes lazily to the Redis
* channels and unsubscribes if the {@link org.reactivestreams.Subscription} is
* {@link org.reactivestreams.Subscription#cancel() cancelled}.
*
* @param topics the channels/patterns to subscribe.
* @param subscriptionListener listener to receive subscription/unsubscription notifications.
* @return the message stream.
* @throws InvalidDataAccessApiUsageException if {@code patternTopics} is empty.
* @see #receive(Iterable, SerializationPair, SerializationPair)
* @since 2.6
*/
public Flux> receive(Iterable topics,
SubscriptionListener subscriptionListener) {
return receive(topics, stringSerializationPair, stringSerializationPair, subscriptionListener);
}
/**
* Subscribe to one or more {@link Topic}s and receive a stream of {@link ChannelMessage}. The stream may contain
* {@link PatternMessage} if subscribed to patterns. Messages, and channel names are serialized/deserialized using the
* given {@code channelSerializer} and {@code messageSerializer}. The message stream subscribes lazily to the Redis
* channels and unsubscribes if the {@link org.reactivestreams.Subscription} is
* {@link org.reactivestreams.Subscription#cancel() cancelled}.
*
* @param topics the channels/patterns to subscribe.
* @return the message stream.
* @see #receive(Iterable, SerializationPair, SerializationPair)
* @throws InvalidDataAccessApiUsageException if {@code topics} is empty.
*/
public Flux> receive(Iterable topics, SerializationPair channelSerializer,
SerializationPair messageSerializer) {
return receive(topics, channelSerializer, messageSerializer, SubscriptionListener.NO_OP_SUBSCRIPTION_LISTENER);
}
/**
* Subscribe to one or more {@link Topic}s and receive a stream of {@link ChannelMessage}. The stream may contain
* {@link PatternMessage} if subscribed to patterns. Messages, and channel names are serialized/deserialized using the
* given {@code channelSerializer} and {@code messageSerializer}. The message stream subscribes lazily to the Redis
* channels and unsubscribes if the {@link org.reactivestreams.Subscription} is
* {@link org.reactivestreams.Subscription#cancel() cancelled}. {@link SubscriptionListener} is notified upon
* subscription/unsubscription and can be used for synchronization.
*
* @param topics the channels to subscribe.
* @param channelSerializer serialization pair to decode the channel/pattern name.
* @param messageSerializer serialization pair to decode the message body.
* @param subscriptionListener listener to receive subscription/unsubscription notifications.
* @return the message stream.
* @see #receive(Iterable, SerializationPair, SerializationPair)
* @throws InvalidDataAccessApiUsageException if {@code topics} is empty.
* @since 2.6
*/
public Flux> receive(Iterable topics, SerializationPair channelSerializer,
SerializationPair messageSerializer, SubscriptionListener subscriptionListener) {
Assert.notNull(topics, "Topics must not be null!");
Assert.notNull(channelSerializer, "Channel serializer must not be null!");
Assert.notNull(messageSerializer, "Message serializer must not be null!");
Assert.notNull(subscriptionListener, "SubscriptionListener must not be null!");
verifyConnection();
ByteBuffer[] patterns = getTargets(topics, PatternTopic.class);
ByteBuffer[] channels = getTargets(topics, ChannelTopic.class);
if (ObjectUtils.isEmpty(patterns) && ObjectUtils.isEmpty(channels)) {
throw new InvalidDataAccessApiUsageException("No channels or patterns to subscribe to.");
}
return doReceive(channelSerializer, messageSerializer,
getRequiredConnection().pubSubCommands().createSubscription(subscriptionListener), patterns,
channels);
}
private Flux> doReceive(SerializationPair channelSerializer,
SerializationPair messageSerializer, Mono subscription, ByteBuffer[] patterns,
ByteBuffer[] channels) {
Flux> messageStream = subscription.flatMapMany(it -> {
Mono subscribe = subscribe(patterns, channels, it);
Sinks.One> terminalSink = Sinks.one();
return it.receive().mergeWith(subscribe.then(Mono.defer(() -> {
getSubscribers(it).registered();
return Mono.empty();
}))).doOnCancel(() -> {
Subscribers subscribers = getSubscribers(it);
if (subscribers.unregister()) {
subscriptions.remove(it);
it.cancel().subscribe(v -> terminalSink.tryEmitEmpty(), terminalSink::tryEmitError);
}
}).mergeWith(terminalSink.asMono());
});
return messageStream
.map(message -> readMessage(channelSerializer.getReader(), messageSerializer.getReader(), message));
}
/**
* Subscribe to one or more {@link Topic}s and receive a stream of {@link ChannelMessage}. The returned {@link Mono}
* completes once the connection has been subscribed to the given {@link Topic topics}. Note that cancelling the
* returned {@link Mono} can leave the connection in a subscribed state.
*
* @param topics the channels to subscribe.
* @param channelSerializer serialization pair to decode the channel/pattern name.
* @param messageSerializer serialization pair to decode the message body.
* @return the message stream.
* @throws InvalidDataAccessApiUsageException if {@code topics} is empty.
* @since 2.6
*/
public Mono>> receiveLater(Iterable topics,
SerializationPair channelSerializer, SerializationPair messageSerializer) {
Assert.notNull(topics, "Topics must not be null!");
Assert.notNull(channelSerializer, "Channel serializer must not be null!");
Assert.notNull(messageSerializer, "Message serializer must not be null!");
verifyConnection();
ByteBuffer[] patterns = getTargets(topics, PatternTopic.class);
ByteBuffer[] channels = getTargets(topics, ChannelTopic.class);
if (ObjectUtils.isEmpty(patterns) && ObjectUtils.isEmpty(channels)) {
throw new InvalidDataAccessApiUsageException("No channels or patterns to subscribe to.");
}
return Mono.defer(() -> {
SubscriptionReadyListener readyListener = SubscriptionReadyListener.create(topics, stringSerializationPair);
return doReceiveLater(channelSerializer, messageSerializer,
getRequiredConnection().pubSubCommands().createSubscription(readyListener), patterns, channels)
.delayUntil(it -> readyListener.getTrigger());
});
}
private Mono>> doReceiveLater(SerializationPair channelSerializer,
SerializationPair messageSerializer, Mono subscription, ByteBuffer[] patterns,
ByteBuffer[] channels) {
return subscription.flatMap(it -> {
Mono subscribe = subscribe(patterns, channels, it).doOnSuccess(v -> getSubscribers(it).registered());
Sinks.One> terminalSink = Sinks.one();
Flux> receiver = it.receive().doOnCancel(() -> {
Subscribers subscribers = getSubscribers(it);
if (subscribers.unregister()) {
subscriptions.remove(it);
it.cancel().subscribe(v -> terminalSink.tryEmitEmpty(), terminalSink::tryEmitError);
}
}).mergeWith(terminalSink.asMono())
.map(message -> readMessage(channelSerializer.getReader(), messageSerializer.getReader(), message));
return subscribe.then(Mono.just(receiver));
});
}
private static Mono subscribe(ByteBuffer[] patterns, ByteBuffer[] channels, ReactiveSubscription it) {
Assert.isTrue(!ObjectUtils.isEmpty(channels) || !ObjectUtils.isEmpty(patterns),
"Must provide either channels or patterns!");
Mono subscribe = null;
if (!ObjectUtils.isEmpty(patterns)) {
subscribe = it.pSubscribe(patterns);
}
if (!ObjectUtils.isEmpty(channels)) {
Mono channelsSubscribe = it.subscribe(channels);
if (subscribe == null) {
subscribe = channelsSubscribe;
} else {
subscribe = subscribe.and(channelsSubscribe);
}
}
return subscribe == null ? Mono.empty() : subscribe;
}
private boolean isActive() {
return connection != null;
}
private void verifyConnection() {
if (!isActive()) {
throw new IllegalStateException("ReactiveRedisMessageListenerContainer is already disposed!");
}
}
private Subscribers getSubscribers(ReactiveSubscription it) {
return subscriptions.computeIfAbsent(it, key -> new Subscribers());
}
private ByteBuffer[] getTargets(Iterable topics, Class classFilter) {
return StreamSupport.stream(topics.spliterator(), false) //
.filter(classFilter::isInstance) //
.map(Topic::getTopic) //
.map(stringSerializationPair::write) //
.toArray(ByteBuffer[]::new);
}
private Message readMessage(RedisElementReader channelSerializer,
RedisElementReader messageSerializer, Message message) {
if (message instanceof PatternMessage) {
PatternMessage patternMessage = (PatternMessage) message;
String pattern = read(stringSerializationPair.getReader(), patternMessage.getPattern());
C channel = read(channelSerializer, patternMessage.getChannel());
B body = read(messageSerializer, patternMessage.getMessage());
return new PatternMessage<>(pattern, channel, body);
}
C channel = read(channelSerializer, message.getChannel());
B body = read(messageSerializer, message.getMessage());
return new ChannelMessage<>(channel, body);
}
private ReactiveRedisConnection getRequiredConnection() {
ReactiveRedisConnection connection = this.connection;
if (connection == null) {
throw new IllegalStateException("Connection no longer available");
}
return connection;
}
private static C read(RedisElementReader reader, ByteBuffer buffer) {
try {
buffer.mark();
return reader.read(buffer);
} finally {
buffer.reset();
}
}
/**
* Object to track subscriber count and to determine the last unsubscribed subscriber.
*
* @author Mark Paluch
*/
static class Subscribers {
private static final AtomicLongFieldUpdater SUBSCRIBERS = AtomicLongFieldUpdater
.newUpdater(Subscribers.class, "subscribers");
// accessed via SUBSCRIBERS
@SuppressWarnings("unused") private volatile long subscribers;
/**
* Register a subscriber and increment subscriber count.
*/
void registered() {
SUBSCRIBERS.incrementAndGet(this);
}
/**
* @return {@literal true} if at least one subscriber registered via {@link #registered()}.
*/
boolean hasRegistration() {
return SUBSCRIBERS.get(this) > 0;
}
/**
* Unregister a subscriber and decrement subscriber count.
*
* @return {@literal true} if this was the last unregistered subscriber.
*/
boolean unregister() {
long value = SUBSCRIBERS.get(this);
if (value <= 0) {
return false;
}
if (SUBSCRIBERS.compareAndSet(this, value, value - 1) && value == 1) {
return true;
}
return false;
}
}
static class SubscriptionReadyListener extends AtomicBoolean implements SubscriptionListener {
private final Set toSubscribe;
private final Sinks.Empty sink = Sinks.empty();
private SubscriptionReadyListener(Set topics) {
this.toSubscribe = topics;
}
public static SubscriptionReadyListener create(Iterable topics,
SerializationPair serializationPair) {
Set wrappers = new HashSet<>();
for (Topic topic : topics) {
wrappers.add(new ByteArrayWrapper(ByteUtils.getBytes(serializationPair.getWriter().write(topic.getTopic()))));
}
return new SubscriptionReadyListener(wrappers);
}
@Override
public void onChannelSubscribed(byte[] channel, long count) {
removeRemaining(channel);
}
@Override
public void onPatternSubscribed(byte[] pattern, long count) {
removeRemaining(pattern);
}
private void removeRemaining(byte[] channel) {
boolean done;
synchronized (toSubscribe) {
toSubscribe.remove(new ByteArrayWrapper(channel));
done = toSubscribe.isEmpty();
}
if (done && compareAndSet(false, true)) {
sink.emitEmpty(Sinks.EmitFailureHandler.FAIL_FAST);
}
}
public Mono getTrigger() {
return sink.asMono();
}
}
}