package com.lambdaworks.redis.masterslave; import java.io.Closeable; import java.io.IOException; import java.util.*; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import com.lambdaworks.redis.RedisClient; import com.lambdaworks.redis.RedisConnectionException; import com.lambdaworks.redis.RedisURI; import com.lambdaworks.redis.api.StatefulConnection; import com.lambdaworks.redis.codec.Utf8StringCodec; import com.lambdaworks.redis.pubsub.RedisPubSubAdapter; import com.lambdaworks.redis.pubsub.StatefulRedisPubSubConnection; import io.netty.util.concurrent.EventExecutorGroup; import io.netty.util.internal.logging.InternalLogger; import io.netty.util.internal.logging.InternalLoggerFactory; /** * Sentinel Pub/Sub listener-enabled topology refresh. * * @author Mark Paluch * @since 4.2 */ class SentinelTopologyRefresh implements Closeable { private static final InternalLogger LOG = InternalLoggerFactory.getInstance(SentinelTopologyRefresh.class); private final List<StatefulRedisPubSubConnection<String, String>> pubSubConnections = new ArrayList<>(); private final RedisClient redisClient; private final String masterId; private final List<RedisURI> sentinels; private final AtomicReference<Timeout> timeoutRef = new AtomicReference<Timeout>(); private final Set<String> PROCESSING_CHANNELS = new HashSet<>(Arrays.asList("failover-end", "failover-end-for-timeout")); private int timeout = 5; private TimeUnit timeUnit = TimeUnit.SECONDS; private RedisPubSubAdapter<String, String> adapter; SentinelTopologyRefresh(RedisClient redisClient, String masterId, List<RedisURI> sentinels) { this.redisClient = redisClient; this.masterId = masterId; this.sentinels = sentinels; } @Override public void close() throws IOException { pubSubConnections.forEach(c -> c.removeListener(adapter)); pubSubConnections.forEach(StatefulConnection::close); } void bind(Runnable runnable) { Utf8StringCodec codec = new Utf8StringCodec(); AtomicReference<RedisConnectionException> ref = new AtomicReference<>(); sentinels.forEach(redisURI -> { try { StatefulRedisPubSubConnection<String, String> pubSubConnection = redisClient.connectPubSub(codec, redisURI); pubSubConnections.add(pubSubConnection); } catch (RedisConnectionException e) { if (ref.get() == null) { ref.set(e); } else { ref.get().addSuppressed(e); } } }); if (sentinels.isEmpty() && ref.get() != null) { throw ref.get(); } adapter = new RedisPubSubAdapter<String, String>() { @Override public void message(String pattern, String channel, String message) { if (processingAllowed(channel, message)) { LOG.debug("Received topology changed signal from Redis Sentinel, scheduling topology update"); Timeout timeout = timeoutRef.get(); if (timeout == null) { getEventExecutor().submit(runnable); } else { getEventExecutor().schedule(runnable, timeout.remaining(), TimeUnit.MILLISECONDS); } } } }; pubSubConnections.forEach(c -> { c.addListener(adapter); c.async().psubscribe("*"); }); } private boolean processingAllowed(String channel, String message) { if (getEventExecutor().isShuttingDown()) { return false; } if (!messageMatches(channel, message)) { return false; } Timeout existingTimeout = timeoutRef.get(); if (existingTimeout != null) { if (!existingTimeout.isExpired()) { return false; } } Timeout timeout = new Timeout(this.timeout, this.timeUnit); return timeoutRef.compareAndSet(existingTimeout, timeout); } protected EventExecutorGroup getEventExecutor() { return redisClient.getResources().eventExecutorGroup(); } private boolean messageMatches(String channel, String message) { // trailing spaces after the master name are not bugs if (channel.equals("+elected-leader")) { if (message.startsWith(String.format("master %s ", masterId))) { return true; } } if (channel.equals("+switch-master")) { if (message.startsWith(String.format("%s ", masterId))) { return true; } } if (channel.equals("fix-slave-config")) { if (message.contains(String.format("@ %s ", masterId))) { return true; } } if (PROCESSING_CHANNELS.contains(channel)) { return true; } return false; } }