All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.springframework.data.redis.listener.ReactiveRedisMessageListenerContainer Maven / Gradle / Ivy

There is a newer version: 3.2.5
Show newest version
/*
 * Copyright 2018 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 reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.publisher.MonoProcessor;
import reactor.core.scheduler.Schedulers;

import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
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.serializer.RedisElementReader;
import org.springframework.data.redis.serializer.RedisSerializationContext.SerializationPair;
import org.springframework.data.redis.serializer.RedisSerializer;
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() { if (connection != null) { 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)); } } if (terminationSignals != null) { return terminationSignals.collectList() .doFinally(signalType -> connection.closeLater().subscribeOn(Schedulers.immediate())) .flatMap(all -> Mono.empty()); } this.connection = null; } return Mono.empty(); } /** * 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(entry -> 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 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 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 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) { Assert.notNull(topics, "Topics 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, connection.pubSubCommands().createSubscription(), 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); MonoProcessor> terminalProcessor = MonoProcessor.create(); 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.unsubscribe().subscribe(v -> terminalProcessor.onComplete(), terminalProcessor::onError); } }).mergeWith(terminalProcessor); }); return messageStream .map(message -> readMessage(channelSerializer.getReader(), messageSerializer.getReader(), message)); } 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; } 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); } @SuppressWarnings("unchecked") 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 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; } } }