package com.lambdaworks.redis.masterslave; import static com.lambdaworks.redis.masterslave.MasterSlaveUtils.findNodeByHostAndPort; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Function; import com.lambdaworks.redis.ReadFrom; import com.lambdaworks.redis.RedisClient; import com.lambdaworks.redis.RedisException; import com.lambdaworks.redis.RedisURI; import com.lambdaworks.redis.api.StatefulConnection; import com.lambdaworks.redis.api.StatefulRedisConnection; import com.lambdaworks.redis.cluster.models.partitions.Partitions; import com.lambdaworks.redis.codec.RedisCodec; import com.lambdaworks.redis.internal.LettuceSets; 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; /** * Connection provider for master/slave setups. The connection provider * * @author Mark Paluch * @since 4.1 */ public class MasterSlaveConnectionProvider<K, V> { private static final InternalLogger logger = InternalLoggerFactory.getInstance(MasterSlaveConnectionProvider.class); private final boolean debugEnabled = logger.isDebugEnabled(); // Contains HostAndPort-identified connections. private final Map<ConnectionKey, StatefulRedisConnection<K, V>> connections = new ConcurrentHashMap<>(); private final ConnectionFactory<K, V> connectionFactory; private final RedisURI initialRedisUri; private List<RedisNodeDescription> knownNodes = new ArrayList<>(); private boolean autoFlushCommands = true; private Object stateLock = new Object(); private ReadFrom readFrom; @Deprecated public MasterSlaveConnectionProvider(RedisClient redisClient, RedisCodec<K, V> redisCodec, StatefulRedisConnection<K, V> masterConnection, RedisURI initialRedisUri) { this.initialRedisUri = initialRedisUri; this.connectionFactory = new ConnectionFactory<>(redisClient, redisCodec); connections.put(toConnectionKey(initialRedisUri), masterConnection); } MasterSlaveConnectionProvider(RedisClient redisClient, RedisCodec<K, V> redisCodec, RedisURI initialRedisUri, Map<RedisURI, StatefulRedisConnection<K, V>> initialConnections) { this.initialRedisUri = initialRedisUri; this.connectionFactory = new ConnectionFactory<>(redisClient, redisCodec); for (Map.Entry<RedisURI, StatefulRedisConnection<K, V>> entry : initialConnections.entrySet()) { connections.put(toConnectionKey(entry.getKey()), entry.getValue()); } } /** * Retrieve a {@link StatefulRedisConnection} by the intent. * {@link com.lambdaworks.redis.masterslave.MasterSlaveConnectionProvider.Intent#WRITE} intentions use the master * connection, {@link com.lambdaworks.redis.masterslave.MasterSlaveConnectionProvider.Intent#READ} intentions lookup one or * more read candidates using the {@link ReadFrom} setting. * * @param intent command intent * @return the connection. */ public StatefulRedisConnection<K, V> getConnection(Intent intent) { if (debugEnabled) { logger.debug("getConnection(" + intent + ")"); } if (readFrom != null && intent == Intent.READ) { List<RedisNodeDescription> selection = readFrom.select(new ReadFrom.Nodes() { @Override public List<RedisNodeDescription> getNodes() { return knownNodes; } @Override public Iterator<RedisNodeDescription> iterator() { return knownNodes.iterator(); } }); if (selection.isEmpty()) { throw new RedisException(String.format("Cannot determine a node to read (Known nodes: %s) with setting %s", knownNodes, readFrom)); } try { for (RedisNodeDescription redisNodeDescription : selection) { StatefulRedisConnection<K, V> readerCandidate = getConnection(redisNodeDescription); if (!readerCandidate.isOpen()) { continue; } return readerCandidate; } return getConnection(selection.get(0)); } catch (RuntimeException e) { throw new RedisException(e); } } return getConnection(getMaster()); } protected StatefulRedisConnection<K, V> getConnection(RedisNodeDescription redisNodeDescription) { return connections.computeIfAbsent( new ConnectionKey(redisNodeDescription.getUri().getHost(), redisNodeDescription.getUri().getPort()), connectionFactory); } /** * * @return number of connections. */ protected long getConnectionCount() { return connections.size(); } /** * Retrieve a set of PoolKey's for all pooled connections that are within the pool but not within the {@link Partitions}. * * @return Set of {@link ConnectionKey}s */ private Set<ConnectionKey> getStaleConnectionKeys() { Map<ConnectionKey, StatefulRedisConnection<K, V>> map = new HashMap<>(connections); Set<ConnectionKey> stale = new HashSet<>(); for (ConnectionKey connectionKey : map.keySet()) { if (connectionKey.host != null && findNodeByHostAndPort(knownNodes, connectionKey.host, connectionKey.port) != null) { continue; } stale.add(connectionKey); } return stale; } /** * Close stale connections. */ public void closeStaleConnections() { logger.debug("closeStaleConnections() count before expiring: {}", getConnectionCount()); Set<ConnectionKey> stale = getStaleConnectionKeys(); for (ConnectionKey connectionKey : stale) { StatefulRedisConnection<K, V> connection = connections.get(connectionKey); if (connection != null) { connections.remove(connectionKey); connection.close(); } } logger.debug("closeStaleConnections() count after expiring: {}", getConnectionCount()); } public void reset() { allConnections().forEach(StatefulRedisConnection::reset); } /** * Close all connections. */ public void close() { allConnections().forEach(StatefulRedisConnection::close); connections.clear(); } public void flushCommands() { allConnections().forEach(StatefulConnection::flushCommands); } public void setAutoFlushCommands(boolean autoFlushCommands) { synchronized (stateLock) { } allConnections().forEach(connection -> connection.setAutoFlushCommands(autoFlushCommands)); } protected Collection<StatefulRedisConnection<K, V>> allConnections() { Set<StatefulRedisConnection<K, V>> connections = LettuceSets.newHashSet(this.connections.values()); return (Collection) connections; } /** * * @param knownNodes */ public void setKnownNodes(Collection<RedisNodeDescription> knownNodes) { synchronized (stateLock) { this.knownNodes.clear(); this.knownNodes.addAll(knownNodes); closeStaleConnections(); } } public ReadFrom getReadFrom() { return readFrom; } public void setReadFrom(ReadFrom readFrom) { synchronized (stateLock) { this.readFrom = readFrom; } } public RedisNodeDescription getMaster() { for (RedisNodeDescription knownNode : knownNodes) { if (knownNode.getRole() == RedisInstance.Role.MASTER) { return knownNode; } } throw new RedisException(String.format("Master is currently unknown: %s", knownNodes)); } private class ConnectionFactory<K, V> implements Function<ConnectionKey, StatefulRedisConnection<K, V>> { private final RedisClient redisClient; private final RedisCodec<K, V> redisCodec; public ConnectionFactory(RedisClient redisClient, RedisCodec<K, V> redisCodec) { this.redisClient = redisClient; this.redisCodec = redisCodec; } @Override public StatefulRedisConnection<K, V> apply(ConnectionKey key) { RedisURI.Builder builder = RedisURI.Builder.redis(key.host, key.port); if (initialRedisUri.getPassword() != null && initialRedisUri.getPassword().length != 0) { builder.withPassword(new String(initialRedisUri.getPassword())); } builder.withDatabase(initialRedisUri.getDatabase()); StatefulRedisConnection<K, V> connection = redisClient.connect(redisCodec, builder.build()); synchronized (stateLock) { connection.setAutoFlushCommands(autoFlushCommands); } return connection; } } private ConnectionKey toConnectionKey(RedisURI redisURI) { return new ConnectionKey(redisURI.getHost(), redisURI.getPort()); } /** * Connection to identify a connection by host/port. */ private static class ConnectionKey { private final String host; private final int port; public ConnectionKey(String host, int port) { this.host = host; this.port = port; } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof ConnectionKey)) return false; ConnectionKey that = (ConnectionKey) o; if (port != that.port) return false; return !(host != null ? !host.equals(that.host) : that.host != null); } @Override public int hashCode() { int result = (host != null ? host.hashCode() : 0); result = 31 * result + port; return result; } } enum Intent { READ, WRITE; } }