package com.lambdaworks.redis.pubsub; import java.lang.reflect.Array; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Set; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.TimeUnit; import com.lambdaworks.redis.*; import com.lambdaworks.redis.api.sync.RedisCommands; import com.lambdaworks.redis.cluster.api.sync.RedisClusterCommands; import com.lambdaworks.redis.codec.RedisCodec; import com.lambdaworks.redis.protocol.ConnectionWatchdog; import com.lambdaworks.redis.pubsub.api.async.RedisPubSubAsyncCommands; import com.lambdaworks.redis.pubsub.api.rx.RedisPubSubReactiveCommands; import com.lambdaworks.redis.pubsub.api.sync.RedisPubSubCommands; import io.netty.channel.ChannelHandler; import io.netty.util.internal.ConcurrentSet; /** * An thread-safe pub/sub connection to a Redis server. Multiple threads may share one {@link StatefulRedisPubSubConnectionImpl} * * A {@link ConnectionWatchdog} monitors each connection and reconnects automatically until {@link #close} is called. All * pending commands will be (re)sent after successful reconnection. * * @param <K> Key type. * @param <V> Value type. * @author Mark Paluch */ @ChannelHandler.Sharable public class StatefulRedisPubSubConnectionImpl<K, V> extends StatefulRedisConnectionImpl<K, V> implements StatefulRedisPubSubConnection<K, V> { protected final List<RedisPubSubListener<K, V>> listeners; protected final Set<K> channels; protected final Set<K> patterns; /** * Initialize a new connection. * * @param writer the channel writer * @param codec Codec used to encode/decode keys and values. * @param timeout Maximum time to wait for a response. * @param unit Unit of time for the timeout. */ public StatefulRedisPubSubConnectionImpl(RedisChannelWriter<K, V> writer, RedisCodec<K, V> codec, long timeout, TimeUnit unit) { super(writer, codec, timeout, unit); listeners = new CopyOnWriteArrayList<>(); channels = new ConcurrentSet<>(); patterns = new ConcurrentSet<>(); } /** * Add a new listener. * * @param listener Listener. */ @Override public void addListener(RedisPubSubListener<K, V> listener) { listeners.add(listener); } /** * Remove an existing listener. * * @param listener Listener. */ @Override public void removeListener(RedisPubSubListener<K, V> listener) { listeners.remove(listener); } @Override public RedisPubSubAsyncCommands<K, V> async() { return (RedisPubSubAsyncCommands<K, V>) async; } @Override protected RedisPubSubAsyncCommandsImpl<K, V> newRedisAsyncCommandsImpl() { return new RedisPubSubAsyncCommandsImpl<>(this, codec); } @Override public RedisPubSubCommands<K, V> sync() { return (RedisPubSubCommands<K, V>) sync; } @Override protected RedisPubSubCommands<K, V> newRedisSyncCommandsImpl() { return syncHandler(async(), RedisConnection.class, RedisClusterConnection.class, RedisPubSubCommands.class); } @Override public RedisPubSubReactiveCommands<K, V> reactive() { return (RedisPubSubReactiveCommands<K, V>) reactive; } @Override protected RedisPubSubReactiveCommandsImpl<K, V> newRedisReactiveCommandsImpl() { return new RedisPubSubReactiveCommandsImpl<>(this, codec); } @Override @SuppressWarnings("unchecked") public void channelRead(Object msg) { PubSubOutput<K, V, V> output = (PubSubOutput<K, V, V>) msg; // drop empty messages if (output.type() == null || (output.pattern() == null && output.channel() == null && output.get() == null)) { return; } updateInternalState(output); notifyListeners(output); } private void notifyListeners(PubSubOutput<K, V, V> output) { // update listeners for (RedisPubSubListener<K, V> listener : listeners) { switch (output.type()) { case message: listener.message(output.channel(), output.get()); break; case pmessage: listener.message(output.pattern(), output.channel(), output.get()); break; case psubscribe: listener.psubscribed(output.pattern(), output.count()); break; case punsubscribe: listener.punsubscribed(output.pattern(), output.count()); break; case subscribe: listener.subscribed(output.channel(), output.count()); break; case unsubscribe: listener.unsubscribed(output.channel(), output.count()); break; default: throw new UnsupportedOperationException("Operation " + output.type() + " not supported"); } } } /** * Re-subscribe to all previously subscribed channels and patterns. * * @return list of the futures of the {@literal subscribe} and {@literal psubscribe} commands. */ protected List<RedisFuture<Void>> resubscribe() { List<RedisFuture<Void>> result = new ArrayList<>(); if (!channels.isEmpty()) { result.add(async().subscribe(toArray(channels))); } if (!patterns.isEmpty()) { result.add(async().psubscribe(toArray(patterns))); } return result; } @SuppressWarnings("unchecked") private <T> T[] toArray(Collection<T> c) { Class<T> cls = (Class<T>) c.iterator().next().getClass(); T[] array = (T[]) Array.newInstance(cls, c.size()); return c.toArray(array); } private void updateInternalState(PubSubOutput<K, V, V> output) { // update internal state switch (output.type()) { case psubscribe: patterns.add(output.pattern()); break; case punsubscribe: patterns.remove(output.pattern()); break; case subscribe: channels.add(output.channel()); break; case unsubscribe: channels.remove(output.channel()); break; default: break; } } @Override public void activated() { super.activated(); resubscribe(); } }