
io.lettuce.core.masterreplica.MasterReplica Maven / Gradle / Ivy
Show all versions of lettuce-core Show documentation
package io.lettuce.core.masterreplica;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.ExecutionException;
import io.lettuce.core.ConnectionFuture;
import io.lettuce.core.RedisClient;
import io.lettuce.core.RedisConnectionException;
import io.lettuce.core.RedisURI;
import io.lettuce.core.codec.RedisCodec;
import io.lettuce.core.internal.Futures;
import io.lettuce.core.internal.LettuceAssert;
import io.lettuce.core.internal.LettuceLists;
import io.netty.util.internal.logging.InternalLogger;
import io.netty.util.internal.logging.InternalLoggerFactory;
/**
* Master-Replica connection API.
*
* This API allows connections to Redis Master/Replica setups which run either in a static Master/Replica setup or are managed
* by Redis Sentinel. Master-Replica connections can discover topologies and select a source for read operations using
* {@link io.lettuce.core.ReadFrom}.
*
*
*
* Connections can be obtained by providing the {@link RedisClient}, a {@link RedisURI} and a {@link RedisCodec}.
*
*
* RedisClient client = RedisClient.create();
* StatefulRedisMasterReplicaConnection<String, String> connection = MasterReplica.connect(client,
* RedisURI.create("redis://localhost"), StringCodec.UTF8);
* // ...
*
* connection.close();
* client.shutdown();
*
*
*
* Topology Discovery
*
* Master-Replica topologies are either static or semi-static. Redis Standalone instances with attached replicas provide no
* failover/HA mechanism. Redis Sentinel managed instances are controlled by Redis Sentinel and allow failover (which include
* master promotion). The {@link MasterReplica} API supports both mechanisms. The topology is provided by a
* {@link TopologyProvider}:
*
*
* - {@link ReplicaTopologyProvider}: Dynamic topology lookup using the {@code INFO REPLICATION} output. Replicas are listed
* as {@code replicaN=...} entries. The initial connection can either point to a master or a replica and the topology provider
* will discover nodes. The connection needs to be re-established outside of lettuce in a case of Master/Replica failover or
* topology changes.
* - {@link StaticMasterReplicaTopologyProvider}: Topology is defined by the list of {@link RedisURI URIs} and the
* {@code ROLE} output. MasterReplica 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/Replica failover or topology changes.
* - {@link SentinelTopologyProvider}: Dynamic topology lookup using the Redis Sentinel API. In particular,
* {@code SENTINEL MASTER} and {@code SENTINEL SLAVES} output. Master/Replica failover is handled by lettuce.
*
*
* Topology Updates
*
* - Standalone Master/Replica: Performs a one-time topology lookup which remains static afterward
* - Redis Sentinel: Subscribes to all Sentinels and listens for Pub/Sub messages to trigger topology refreshing
*
*
* Connection Fault-Tolerance
Connecting to Master/Replica bears the possibility that individual nodes are not
* reachable. {@link MasterReplica} can still connect to a partially-available set of nodes.
*
*
* - Redis Sentinel: At least one Sentinel must be reachable, the masterId must be registered and at least one host must be
* available (master or replica). Allows for runtime-recovery based on Sentinel Events.
* - Static Setup (auto-discovery): The initial endpoint must be reachable. No recovery/reconfiguration during runtime.
* - Static Setup (provided hosts): All endpoints must be reachable. No recovery/reconfiguration during runtime.
*
*
* @author Mark Paluch
* @since 5.2
*/
public class MasterReplica {
/**
* Open a new connection to a Redis Master-Replica server/servers using the supplied {@link RedisURI} and the supplied
* {@link RedisCodec codec} to encode/decode keys.
*
* This {@link MasterReplica} performs auto-discovery of nodes using either Redis Sentinel or Master/Replica. A
* {@link RedisURI} can point to either a master or a replica host.
*
*
* @param redisClient the Redis client.
* @param codec Use this codec to encode/decode keys and values, must not be {@code null}.
* @param redisURI the Redis server to connect to, must not be {@code null}.
* @param Key type.
* @param Value type.
* @return a new connection.
*/
public static StatefulRedisMasterReplicaConnection connect(RedisClient redisClient, RedisCodec codec,
RedisURI redisURI) {
return getConnection(connectAsyncSentinelOrAutodiscovery(redisClient, codec, redisURI), redisURI);
}
/**
* Open asynchronously a new connection to a Redis Master-Replica server/servers using the supplied {@link RedisURI} and the
* supplied {@link RedisCodec codec} to encode/decode keys.
*
* This {@link MasterReplica} performs auto-discovery of nodes using either Redis Sentinel or Master/Replica. A
* {@link RedisURI} can point to either a master or a replica host.
*
*
* @param redisClient the Redis client.
* @param codec Use this codec to encode/decode keys and values, must not be {@code null}.
* @param redisURI the Redis server to connect to, must not be {@code null}.
* @param Key type.
* @param Value type.
* @return {@link CompletableFuture} that is notified once the connect is finished.
* @since 6.0
*/
public static CompletableFuture> connectAsync(RedisClient redisClient,
RedisCodec codec, RedisURI redisURI) {
return transformAsyncConnectionException(connectAsyncSentinelOrAutodiscovery(redisClient, codec, redisURI), redisURI);
}
private static CompletableFuture> connectAsyncSentinelOrAutodiscovery(
RedisClient redisClient, RedisCodec 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 new SentinelConnector<>(redisClient, codec, redisURI).connectAsync();
}
return new AutodiscoveryConnector<>(redisClient, codec, redisURI).connectAsync();
}
/**
* Open a new connection to a Redis Master-Replica server/servers using the supplied {@link RedisURI} and the supplied
* {@link RedisCodec codec} to encode/decode keys.
*
* This {@link MasterReplica} performs auto-discovery of nodes if the URI is a Redis Sentinel URI. Master/Replica URIs will
* be treated as static topology and no additional hosts are discovered in such case. Redis Standalone Master/Replica will
* discover the roles of the supplied {@link RedisURI URIs} and issue commands to the appropriate node.
*
*
* When using Redis Sentinel, ensure that {@link Iterable redisURIs} contains only a single entry as only the first URI is
* considered. {@link RedisURI} pointing to multiple Sentinels can be configured through
* {@link RedisURI.Builder#withSentinel}.
*
*
* @param redisClient the Redis client.
* @param codec Use this codec to encode/decode keys and values, must not be {@code null}.
* @param redisURIs the Redis server(s) to connect to, must not be {@code null}.
* @param Key type.
* @param Value type.
* @return a new connection.
* @since 6.0
*/
public static StatefulRedisMasterReplicaConnection connect(RedisClient redisClient, RedisCodec codec,
Iterable redisURIs) {
return getConnection(connectAsyncSentinelOrStaticSetup(redisClient, codec, redisURIs), redisURIs);
}
/**
* Open asynchronously a new connection to a Redis Master-Replica server/servers using the supplied {@link RedisURI} and the
* supplied {@link RedisCodec codec} to encode/decode keys.
*
* This {@link MasterReplica} performs auto-discovery of nodes if the URI is a Redis Sentinel URI. Master/Replica URIs will
* be treated as static topology and no additional hosts are discovered in such case. Redis Standalone Master/Replica will
* discover the roles of the supplied {@link RedisURI URIs} and issue commands to the appropriate node.
*
*
* When using Redis Sentinel, ensure that {@link Iterable redisURIs} contains only a single entry as only the first URI is
* considered. {@link RedisURI} pointing to multiple Sentinels can be configured through
* {@link RedisURI.Builder#withSentinel}.
*
*
* @param redisClient the Redis client.
* @param codec Use this codec to encode/decode keys and values, must not be {@code null}.
* @param redisURIs the Redis server(s) to connect to, must not be {@code null}.
* @param Key type.
* @param Value type.
* @return {@link CompletableFuture} that is notified once the connect is finished.
* @since 6.0
*/
public static CompletableFuture> connectAsync(RedisClient redisClient,
RedisCodec codec, Iterable redisURIs) {
return transformAsyncConnectionException(connectAsyncSentinelOrStaticSetup(redisClient, codec, redisURIs), redisURIs);
}
private static CompletableFuture> connectAsyncSentinelOrStaticSetup(
RedisClient redisClient, RedisCodec codec, Iterable 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 uriList = LettuceLists.newList(redisURIs);
LettuceAssert.isTrue(!uriList.isEmpty(), "RedisURIs must not be empty");
RedisURI first = uriList.get(0);
if (isSentinel(first)) {
if (uriList.size() > 1) {
InternalLogger logger = InternalLoggerFactory.getInstance(MasterReplica.class);
logger.warn(
"RedisURIs contains multiple endpoints of which the first is configured for Sentinel usage. Using only the first URI [{}] without considering the remaining URIs. Make sure to include all Sentinel endpoints in a single RedisURI.",
first);
}
return new SentinelConnector<>(redisClient, codec, first).connectAsync();
}
return new StaticMasterReplicaConnector<>(redisClient, codec, uriList).connectAsync();
}
private static boolean isSentinel(RedisURI redisURI) {
return !redisURI.getSentinels().isEmpty();
}
/**
* Retrieve the connection from {@link ConnectionFuture}. Performs a blocking {@link ConnectionFuture#get()} to synchronize
* the channel/connection initialization. Any exception is rethrown as {@link RedisConnectionException}.
*
* @param connectionFuture must not be null.
* @param context context information (single RedisURI, multiple URIs), used as connection target in the reported exception.
* @param Connection type.
* @return the connection.
* @throws RedisConnectionException in case of connection failures.
*/
private static T getConnection(CompletableFuture connectionFuture, Object context) {
try {
return connectionFuture.get();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw RedisConnectionException.create(context.toString(), e);
} catch (Exception e) {
if (e instanceof ExecutionException) {
// filter intermediate RedisConnectionException exceptions that bloat the stack trace
if (e.getCause() instanceof RedisConnectionException
&& e.getCause().getCause() instanceof RedisConnectionException) {
throw RedisConnectionException.create(context.toString(), e.getCause().getCause());
}
throw RedisConnectionException.create(context.toString(), e.getCause());
}
throw RedisConnectionException.create(context.toString(), e);
}
}
private static CompletableFuture transformAsyncConnectionException(CompletionStage future, Object context) {
return ConnectionFuture.from(null, future.toCompletableFuture()).thenCompose((v, e) -> {
if (e != null) {
// filter intermediate RedisConnectionException exceptions that bloat the stack trace
if (e.getCause() instanceof RedisConnectionException
&& e.getCause().getCause() instanceof RedisConnectionException) {
return Futures.failed(RedisConnectionException.create(context.toString(), e.getCause()));
}
return Futures.failed(RedisConnectionException.create(context.toString(), e));
}
return CompletableFuture.completedFuture(v);
}).toCompletableFuture();
}
}