package com.lambdaworks.redis.masterslave; import java.util.*; import com.lambdaworks.redis.RedisClient; import com.lambdaworks.redis.RedisException; import com.lambdaworks.redis.RedisURI; import com.lambdaworks.redis.api.StatefulRedisConnection; import com.lambdaworks.redis.codec.RedisCodec; import com.lambdaworks.redis.internal.LettuceAssert; import com.lambdaworks.redis.internal.LettuceLists; import com.lambdaworks.redis.models.role.RedisInstance; import com.lambdaworks.redis.models.role.RedisNodeDescription; import io.netty.util.internal.logging.InternalLogger; import io.netty.util.internal.logging.InternalLoggerFactory; /** * Master-Slave connection API. * <p> * This API allows connections to Redis Master/Slave setups which run either Redis Standalone or are managed by Redis Sentinel. * Master-Slave connections can discover topologies and select a source for read operations using * {@link com.lambdaworks.redis.ReadFrom}. * </p> * <p> * * Connections can be obtained by providing the {@link RedisClient}, a {@link RedisURI} and a {@link RedisCodec}. * * <pre> * @code * RedisClient client = RedisClient.create(); * StatefulRedisMasterSlaveConnection<String, String> connection = MasterSlave.connect(client, * RedisURI.create("redis://localhost"), * new Utf8StringCodec()); * // ... * * connection.close(); * client.shutdown(); * } * </pre> * </p> * <h3>Topology Discovery</h3> * <p> * Master-Slave topologies are either static or semi-static. Redis Standalone instances with attached slaves provide no * failover/HA mechanism. Redis Sentinel managed instances are controlled by Redis Sentinel and allow failover (which include * master promotion). The {@link MasterSlave} API supports both mechanisms. The topology is provided by a * {@link TopologyProvider}: * * <ul> * <li>{@link MasterSlaveTopologyProvider}: Dynamic topology lookup using the {@code INFO REPLICATION} output. Slaves are listed * as {@code slaveN=...} entries. The initial connection can either point to a master or a slave and the topology provider will * discover nodes. The connection needs to be re-established outside of lettuce in a case of Master/Slave failover or topology * changes.</li> * <li>{@link StaticMasterSlaveTopologyProvider}: Topology is defined by the list of {@link RedisURI URIs} and the {@code ROLE} * output. MasterSlave uses only the supplied nodes and won't discover additional nodes in the setup. The connection needs to be * re-established outside of lettuce in a case of Master/Slave failover or topology changes.</li> * <li>{@link SentinelTopologyProvider}: Dynamic topology lookup using the Redis Sentinel API. In particular, * {@code SENTINEL MASTER} and {@code SENTINEL SLAVES} output. Master/Slave failover is handled by lettuce.</li> * </ul> * * <p> * Topology Updates * </p> * <ul> * <li>Standalone Master/Slave: Performs a one-time topology lookup which remains static afterward</li> * <li>Redis Sentinel: Subscribes to all Sentinels and listens for Pub/Sub messages to trigger topology refreshing</li> * </ul> * </p> * * @author Mark Paluch * @since 4.1 */ public class MasterSlave { private static final InternalLogger LOG = InternalLoggerFactory.getInstance(MasterSlave.class); /** * Open a new connection to a Redis Master-Slave server/servers using the supplied {@link RedisURI} and the supplied * {@link RedisCodec codec} to encode/decode keys. * <p> * This {@link MasterSlave} performs auto-discovery of nodes using either Redis Sentinel or Master/Slave. A {@link RedisURI} * can point to either a master or a slave host. * </p> * * @param redisClient the Redis client * @param codec Use this codec to encode/decode keys and values, must not be {@literal null} * @param redisURI the Redis server to connect to, must not be {@literal null} * @param <K> Key type * @param <V> Value type * @return A new connection */ public static <K, V> StatefulRedisMasterSlaveConnection<K, V> connect(RedisClient redisClient, RedisCodec<K, V> codec, RedisURI redisURI) { LettuceAssert.notNull(redisClient, "RedisClient must not be null"); LettuceAssert.notNull(codec, "RedisCodec must not be null"); LettuceAssert.notNull(redisURI, "RedisURI must not be null"); if (isSentinel(redisURI)) { return connectSentinel(redisClient, codec, redisURI); } else { return connectMasterSlave(redisClient, codec, redisURI); } } /** * Open a new connection to a Redis Master-Slave server/servers using the supplied {@link RedisURI} and the supplied * {@link RedisCodec codec} to encode/decode keys. * <p> * This {@link MasterSlave} performs auto-discovery of nodes if the URI is a Redis Sentinel URI. Master/Slave URIs will be * treated as static topology and no additional hosts are discovered in such case. Redis Standalone Master/Slave will * discover the roles of the supplied {@link RedisURI URIs} and issue commands to the appropriate node. * </p> * * @param redisClient the Redis client * @param codec Use this codec to encode/decode keys and values, must not be {@literal null} * @param redisURIs the Redis server to connect to, must not be {@literal null} * @param <K> Key type * @param <V> Value type * @return A new connection */ public static <K, V> StatefulRedisMasterSlaveConnection<K, V> connect(RedisClient redisClient, RedisCodec<K, V> codec, Iterable<RedisURI> redisURIs) { LettuceAssert.notNull(redisClient, "RedisClient must not be null"); LettuceAssert.notNull(codec, "RedisCodec must not be null"); LettuceAssert.notNull(redisURIs, "RedisURIs must not be null"); List<RedisURI> uriList = LettuceLists.newList(redisURIs); LettuceAssert.isTrue(!uriList.isEmpty(), "RedisURIs must not be empty"); if (isSentinel(uriList.get(0))) { return connectSentinel(redisClient, codec, uriList.get(0)); } else { return connectStaticMasterSlave(redisClient, codec, uriList); } } private static <K, V> StatefulRedisMasterSlaveConnection<K, V> connectSentinel(RedisClient redisClient, RedisCodec<K, V> codec, RedisURI redisURI) { TopologyProvider topologyProvider = new SentinelTopologyProvider(redisURI.getSentinelMasterId(), redisClient, redisURI); SentinelTopologyRefresh sentinelTopologyRefresh = new SentinelTopologyRefresh(redisClient, redisURI.getSentinelMasterId(), redisURI.getSentinels()); MasterSlaveTopologyRefresh refresh = new MasterSlaveTopologyRefresh(redisClient, topologyProvider); MasterSlaveConnectionProvider<K, V> connectionProvider = new MasterSlaveConnectionProvider<>(redisClient, codec, redisURI, Collections.emptyMap()); connectionProvider.setKnownNodes(refresh.getNodes(redisURI)); MasterSlaveChannelWriter<K, V> channelWriter = new MasterSlaveChannelWriter<>(connectionProvider); StatefulRedisMasterSlaveConnectionImpl<K, V> connection = new StatefulRedisMasterSlaveConnectionImpl<>(channelWriter, codec, redisURI.getTimeout(), redisURI.getUnit()); Runnable runnable = () -> { try { LOG.debug("Refreshing topology"); List<RedisNodeDescription> nodes = refresh.getNodes(redisURI); LOG.debug("New topology: {}", nodes); connectionProvider.setKnownNodes(nodes); } catch (Exception e) { LOG.error("Error during background refresh", e); } }; try { connection.registerCloseables(new ArrayList<>(), sentinelTopologyRefresh); sentinelTopologyRefresh.bind(runnable); } catch (RuntimeException e) { connection.close(); throw e; } return connection; } private static <K, V> StatefulRedisMasterSlaveConnection<K, V> connectMasterSlave(RedisClient redisClient, RedisCodec<K, V> codec, RedisURI redisURI) { Map<RedisURI, StatefulRedisConnection<K, V>> initialConnections = new HashMap<>(); try { StatefulRedisConnection<K, V> nodeConnection = redisClient.connect(codec, redisURI); initialConnections.put(redisURI, nodeConnection); TopologyProvider topologyProvider = new MasterSlaveTopologyProvider(nodeConnection, redisURI); List<RedisNodeDescription> nodes = topologyProvider.getNodes(); RedisNodeDescription node = getConnectedNode(redisURI, nodes); if (node.getRole() != RedisInstance.Role.MASTER) { RedisNodeDescription master = lookupMaster(nodes); nodeConnection = redisClient.connect(codec, master.getUri()); initialConnections.put(master.getUri(), nodeConnection); topologyProvider = new MasterSlaveTopologyProvider(nodeConnection, master.getUri()); } MasterSlaveTopologyRefresh refresh = new MasterSlaveTopologyRefresh(redisClient, topologyProvider); MasterSlaveConnectionProvider<K, V> connectionProvider = new MasterSlaveConnectionProvider<>(redisClient, codec, redisURI, initialConnections); connectionProvider.setKnownNodes(refresh.getNodes(redisURI)); MasterSlaveChannelWriter<K, V> channelWriter = new MasterSlaveChannelWriter<>(connectionProvider); StatefulRedisMasterSlaveConnectionImpl<K, V> connection = new StatefulRedisMasterSlaveConnectionImpl<>( channelWriter, codec, redisURI.getTimeout(), redisURI.getUnit()); return connection; } catch (RuntimeException e) { for (StatefulRedisConnection<K, V> connection : initialConnections.values()) { connection.close(); } throw e; } } private static <K, V> StatefulRedisMasterSlaveConnection<K, V> connectStaticMasterSlave(RedisClient redisClient, RedisCodec<K, V> codec, Iterable<RedisURI> redisURIs) { Map<RedisURI, StatefulRedisConnection<K, V>> initialConnections = new HashMap<>(); try { TopologyProvider topologyProvider = new StaticMasterSlaveTopologyProvider(redisClient, redisURIs); RedisURI seedNode = redisURIs.iterator().next(); MasterSlaveTopologyRefresh refresh = new MasterSlaveTopologyRefresh(redisClient, topologyProvider); MasterSlaveConnectionProvider<K, V> connectionProvider = new MasterSlaveConnectionProvider<>(redisClient, codec, seedNode, initialConnections); List<RedisNodeDescription> nodes = refresh.getNodes(seedNode); if (nodes.isEmpty()) { throw new RedisException(String.format("Cannot determine topology from %s", redisURIs)); } connectionProvider.setKnownNodes(nodes); MasterSlaveChannelWriter<K, V> channelWriter = new MasterSlaveChannelWriter<>(connectionProvider); StatefulRedisMasterSlaveConnectionImpl<K, V> connection = new StatefulRedisMasterSlaveConnectionImpl<>( channelWriter, codec, seedNode.getTimeout(), seedNode.getUnit()); return connection; } catch (RuntimeException e) { for (StatefulRedisConnection<K, V> connection : initialConnections.values()) { connection.close(); } throw e; } } private static RedisNodeDescription lookupMaster(List<RedisNodeDescription> nodes) { Optional<RedisNodeDescription> first = nodes.stream().filter(n -> n.getRole() == RedisInstance.Role.MASTER).findFirst(); return first.orElseThrow(() -> new IllegalStateException("Cannot lookup master from " + nodes)); } private static RedisNodeDescription getConnectedNode(RedisURI redisURI, List<RedisNodeDescription> nodes) { Optional<RedisNodeDescription> first = nodes.stream().filter(n -> equals(redisURI, n)).findFirst(); return first.orElseThrow( () -> new IllegalStateException("Cannot lookup node descriptor for connected node at " + redisURI)); } private static boolean equals(RedisURI redisURI, RedisNodeDescription node) { return node.getUri().getHost().equals(redisURI.getHost()) && node.getUri().getPort() == redisURI.getPort(); } private static boolean isSentinel(RedisURI redisURI) { return !redisURI.getSentinels().isEmpty(); } }