
io.lettuce.core.cluster.RedisClusterClient Maven / Gradle / Ivy
Show all versions of lettuce-core Show documentation
/*
* Copyright 2011-Present, Redis Ltd. and Contributors
* All rights reserved.
*
* Licensed under the MIT License.
*
* This file contains contributions from third-party contributors
* licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.lettuce.core.cluster;
import java.io.Closeable;
import java.net.SocketAddress;
import java.net.URI;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
import io.lettuce.core.*;
import io.lettuce.core.api.StatefulRedisConnection;
import io.lettuce.core.cluster.api.NodeSelectionSupport;
import io.lettuce.core.cluster.api.StatefulRedisClusterConnection;
import io.lettuce.core.cluster.api.async.RedisAdvancedClusterAsyncCommands;
import io.lettuce.core.cluster.api.sync.RedisAdvancedClusterCommands;
import io.lettuce.core.cluster.event.ClusterTopologyChangedEvent;
import io.lettuce.core.cluster.event.TopologyRefreshEvent;
import io.lettuce.core.cluster.models.partitions.Partitions;
import io.lettuce.core.cluster.models.partitions.RedisClusterNode;
import io.lettuce.core.cluster.pubsub.StatefulRedisClusterPubSubConnection;
import io.lettuce.core.cluster.topology.ClusterTopologyRefresh;
import io.lettuce.core.cluster.topology.NodeConnectionFactory;
import io.lettuce.core.cluster.topology.TopologyComparators;
import io.lettuce.core.codec.RedisCodec;
import io.lettuce.core.codec.StringCodec;
import io.lettuce.core.event.jfr.EventRecorder;
import io.lettuce.core.internal.Exceptions;
import io.lettuce.core.internal.Futures;
import io.lettuce.core.internal.LettuceAssert;
import io.lettuce.core.internal.LettuceLists;
import io.lettuce.core.json.JsonParser;
import io.lettuce.core.output.KeyValueStreamingChannel;
import io.lettuce.core.protocol.CommandExpiryWriter;
import io.lettuce.core.protocol.CommandHandler;
import io.lettuce.core.protocol.DefaultEndpoint;
import io.lettuce.core.protocol.PushHandler;
import io.lettuce.core.pubsub.PubSubCommandHandler;
import io.lettuce.core.pubsub.PubSubEndpoint;
import io.lettuce.core.pubsub.StatefulRedisPubSubConnection;
import io.lettuce.core.pubsub.StatefulRedisPubSubConnectionImpl;
import io.lettuce.core.resource.ClientResources;
import io.netty.util.internal.logging.InternalLogger;
import io.netty.util.internal.logging.InternalLoggerFactory;
import reactor.core.publisher.Mono;
import static io.lettuce.core.RedisAuthenticationHandler.createHandler;
/**
* A scalable and thread-safe Redis cluster client supporting synchronous, asynchronous and
* reactive execution models. Multiple threads may share one connection. The cluster client handles command routing based on the
* first key of the command and maintains a view of the cluster that is available when calling the {@link #getPartitions()}
* method.
*
*
* Connections to the cluster members are opened on the first access to the cluster node and managed by the
* {@link StatefulRedisClusterConnection}. You should not use transactional commands on cluster connections since {@code MULTI},
* {@code EXEC} and {@code DISCARD} have no key and cannot be assigned to a particular node. A cluster connection uses a default
* connection to run non-keyed commands.
*
*
*
* The Redis cluster client provides a {@link RedisAdvancedClusterCommands sync}, {@link RedisAdvancedClusterAsyncCommands
* async} and {@link io.lettuce.core.cluster.api.reactive.RedisAdvancedClusterReactiveCommands reactive} API.
*
*
*
* Connections to particular nodes can be obtained by {@link StatefulRedisClusterConnection#getConnection(String)} providing the
* node id or {@link StatefulRedisClusterConnection#getConnection(String, int)} by host and port.
*
*
*
* Multiple keys operations have to operate on a key
* that hashes to the same slot. Following commands do not need to follow that rule since they are pipelined according to its
* hash value to multiple nodes in parallel on the sync, async and, reactive API:
*
*
* - {@link RedisAdvancedClusterAsyncCommands#del(Object[]) DEL}
* - {@link RedisAdvancedClusterAsyncCommands#unlink(Object[]) UNLINK}
* - {@link RedisAdvancedClusterAsyncCommands#mget(Object[]) MGET}
* - {@link RedisAdvancedClusterAsyncCommands#mget(KeyValueStreamingChannel, Object[])} ) MGET with streaming}
* - {@link RedisAdvancedClusterAsyncCommands#mset(Map) MSET}
* - {@link RedisAdvancedClusterAsyncCommands#msetnx(Map) MSETNX}
*
*
*
* Following commands on the Cluster sync, async and, reactive API are implemented with a Cluster-flavor:
*
*
* - {@link RedisAdvancedClusterAsyncCommands#clientSetname(Object)} Executes {@code CLIENT SET} on all connections and
* initializes new connections with the {@code clientName}.
* - {@link RedisAdvancedClusterAsyncCommands#flushall()} Run {@code FLUSHALL} on all upstream nodes.
* - {@link RedisAdvancedClusterAsyncCommands#flushdb()} Executes {@code FLUSHDB} on all upstream nodes.
* - {@link RedisAdvancedClusterAsyncCommands#keys(Object)} Executes {@code KEYS} on all.
* - {@link RedisAdvancedClusterAsyncCommands#randomkey()} Returns a random key from a random upstream node.
* - {@link RedisAdvancedClusterAsyncCommands#scriptFlush()} Executes {@code SCRIPT FLUSH} on all nodes.
* - {@link RedisAdvancedClusterAsyncCommands#scriptKill()} Executes {@code SCRIPT KILL} on all nodes.
* - {@link RedisAdvancedClusterAsyncCommands#shutdown(boolean)} Executes {@code SHUTDOWN} on all nodes.
* - {@link RedisAdvancedClusterAsyncCommands#scan()} Executes a {@code SCAN} on all nodes according to {@link ReadFrom}. The
* resulting cursor must be reused across the {@code SCAN} to scan iteratively across the whole cluster.
*
*
*
* Cluster commands can be issued to multiple hosts in parallel by using the {@link NodeSelectionSupport} API. A set of nodes is
* selected using a {@link java.util.function.Predicate} and commands can be issued to the node selection
*
*
* AsyncExecutions<String> ping = commands.upstream().commands().ping();
* Collection<RedisClusterNode> nodes = ping.nodes();
* nodes.stream().forEach(redisClusterNode -> ping.get(redisClusterNode));
*
*
*
* Connection timeouts are initialized from the first provided {@link RedisURI}.
*
*
* {@link RedisClusterClient} is an expensive resource. Reuse this instance or share external {@link ClientResources} as much as
* possible.
*
* @author Mark Paluch
* @since 3.0
* @see RedisURI
* @see StatefulRedisClusterConnection
* @see RedisCodec
* @see ClusterClientOptions
* @see ClientResources
*/
public class RedisClusterClient extends AbstractRedisClient {
private static final InternalLogger logger = InternalLoggerFactory.getInstance(RedisClusterClient.class);
private final ClusterTopologyRefresh refresh;
private final ClusterTopologyRefreshScheduler topologyRefreshScheduler = new ClusterTopologyRefreshScheduler(
this::getClusterClientOptions, this::getPartitions, this::refreshPartitionsAsync, getResources());
private final Iterable initialUris;
private volatile Partitions partitions;
/**
* Non-private constructor to make {@link RedisClusterClient} proxyable.
*/
protected RedisClusterClient() {
super(null);
this.initialUris = Collections.emptyList();
this.refresh = createTopologyRefresh();
}
/**
* Initialize the client with a list of cluster URI's. All uris are tried in sequence for connecting initially to the
* cluster. If any uri is successful for connection, the others are not tried anymore. The initial uri is needed to discover
* the cluster structure for distributing the requests.
*
* @param clientResources the client resources. If {@code null}, the client will create a new dedicated instance of client
* resources and keep track of them.
* @param redisURIs iterable of initial {@link RedisURI cluster URIs}. Must not be {@code null} and not empty.
*/
protected RedisClusterClient(ClientResources clientResources, Iterable redisURIs) {
super(clientResources);
assertNotEmpty(redisURIs);
assertSameOptions(redisURIs);
this.initialUris = Collections.unmodifiableList(LettuceLists.newList(redisURIs));
this.refresh = createTopologyRefresh();
setDefaultTimeout(getFirstUri().getTimeout());
setOptions(ClusterClientOptions.create());
}
private static void assertSameOptions(Iterable redisURIs) {
Boolean ssl = null;
Boolean startTls = null;
Boolean verifyPeer = null;
for (RedisURI redisURI : redisURIs) {
if (ssl == null) {
ssl = redisURI.isSsl();
}
if (startTls == null) {
startTls = redisURI.isStartTls();
}
if (verifyPeer == null) {
verifyPeer = redisURI.isVerifyPeer();
}
if (ssl.booleanValue() != redisURI.isSsl()) {
throw new IllegalArgumentException(
"RedisURI " + redisURI + " SSL is not consistent with the other seed URI SSL settings");
}
if (startTls.booleanValue() != redisURI.isStartTls()) {
throw new IllegalArgumentException(
"RedisURI " + redisURI + " StartTLS is not consistent with the other seed URI StartTLS settings");
}
if (verifyPeer.booleanValue() != redisURI.isVerifyPeer()) {
throw new IllegalArgumentException(
"RedisURI " + redisURI + " VerifyPeer is not consistent with the other seed URI VerifyPeer settings");
}
}
}
/**
* Create a new client that connects to the supplied {@link RedisURI uri} with default {@link ClientResources}. You can
* connect to different Redis servers but you must supply a {@link RedisURI} on connecting.
*
* @param redisURI the Redis URI, must not be {@code null}
* @return a new instance of {@link RedisClusterClient}
*/
public static RedisClusterClient create(RedisURI redisURI) {
assertNotNull(redisURI);
return create(Collections.singleton(redisURI));
}
/**
* Create a new client that connects to the supplied {@link RedisURI uri} with default {@link ClientResources}. You can
* connect to different Redis servers but you must supply a {@link RedisURI} on connecting.
*
* @param redisURIs one or more Redis URI, must not be {@code null} and not empty.
* @return a new instance of {@link RedisClusterClient}
*/
public static RedisClusterClient create(Iterable redisURIs) {
assertNotEmpty(redisURIs);
assertSameOptions(redisURIs);
return new RedisClusterClient(null, redisURIs);
}
/**
* Create a new client that connects to the supplied uri with default {@link ClientResources}. You can connect to different
* Redis servers but you must supply a {@link RedisURI} on connecting.
*
* @param uri the Redis URI, must not be empty or {@code null}.
* @return a new instance of {@link RedisClusterClient}
*/
public static RedisClusterClient create(String uri) {
LettuceAssert.notEmpty(uri, "URI must not be empty");
return create(RedisClusterURIUtil.toRedisURIs(URI.create(uri)));
}
/**
* Create a new client that connects to the supplied {@link RedisURI uri} with shared {@link ClientResources}. You need to
* shut down the {@link ClientResources} upon shutting down your application.You can connect to different Redis servers but
* you must supply a {@link RedisURI} on connecting.
*
* @param clientResources the client resources, must not be {@code null}
* @param redisURI the Redis URI, must not be {@code null}
* @return a new instance of {@link RedisClusterClient}
*/
public static RedisClusterClient create(ClientResources clientResources, RedisURI redisURI) {
assertNotNull(clientResources);
assertNotNull(redisURI);
return create(clientResources, Collections.singleton(redisURI));
}
/**
* Create a new client that connects to the supplied uri with shared {@link ClientResources}.You need to shut down the
* {@link ClientResources} upon shutting down your application. You can connect to different Redis servers but you must
* supply a {@link RedisURI} on connecting.
*
* @param clientResources the client resources, must not be {@code null}
* @param uri the Redis URI, must not be empty or {@code null}.
* @return a new instance of {@link RedisClusterClient}
*/
public static RedisClusterClient create(ClientResources clientResources, String uri) {
assertNotNull(clientResources);
LettuceAssert.notEmpty(uri, "URI must not be empty");
return create(clientResources, RedisClusterURIUtil.toRedisURIs(URI.create(uri)));
}
/**
* Create a new client that connects to the supplied {@link RedisURI uri} with shared {@link ClientResources}. You need to
* shut down the {@link ClientResources} upon shutting down your application.You can connect to different Redis servers but
* you must supply a {@link RedisURI} on connecting.
*
* @param clientResources the client resources, must not be {@code null}
* @param redisURIs one or more Redis URI, must not be {@code null} and not empty
* @return a new instance of {@link RedisClusterClient}
*/
public static RedisClusterClient create(ClientResources clientResources, Iterable redisURIs) {
assertNotNull(clientResources);
assertNotEmpty(redisURIs);
assertSameOptions(redisURIs);
return new RedisClusterClient(clientResources, redisURIs);
}
/**
* Set the {@link ClusterClientOptions} for the client.
*
* @param clientOptions client options for the client and connections that are created after setting the options
*/
public void setOptions(ClusterClientOptions clientOptions) {
super.setOptions(clientOptions);
}
/**
* Retrieve the cluster view. Partitions are shared amongst all connections opened by this client instance.
*
* @return the partitions.
*/
public Partitions getPartitions() {
if (partitions == null) {
get(initializePartitions(), e -> new RedisException("Cannot obtain initial Redis Cluster topology", e));
}
return partitions;
}
/**
* Returns the seed {@link RedisURI} for the topology refreshing. This method is called before each topology refresh to
* provide an {@link Iterable} of {@link RedisURI} that is used to perform the next topology refresh.
*
* Subclasses of {@link RedisClusterClient} may override that method.
*
* @return {@link Iterable} of {@link RedisURI} for the next topology refresh.
*/
protected Iterable getTopologyRefreshSource() {
boolean initialSeedNodes = !useDynamicRefreshSources();
Iterable seed;
if (initialSeedNodes || partitions == null || partitions.isEmpty()) {
seed = this.initialUris;
} else {
List uris = new ArrayList<>();
for (RedisClusterNode partition : TopologyComparators.sortByUri(partitions)) {
uris.add(partition.getUri());
}
seed = uris;
}
return seed;
}
/**
* Connect to a Redis Cluster and treat keys and values as UTF-8 strings.
*
* What to expect from this connection:
*
*
* - A default connection is created to the node with the lowest latency
* - Keyless commands are send to the default connection
* - Single-key keyspace commands are routed to the appropriate node
* - Multi-key keyspace commands require the same slot-hash and are routed to the appropriate node
* - Pub/sub commands are sent to the node that handles the slot derived from the pub/sub channel
*
*
* @return A new stateful Redis Cluster connection
*/
public StatefulRedisClusterConnection connect() {
return connect(newStringStringCodec());
}
/**
* Connect to a Redis Cluster. Use the supplied {@link RedisCodec codec} to encode/decode keys and values.
*
* What to expect from this connection:
*
*
* - A default connection is created to the node with the lowest latency
* - Keyless commands are send to the default connection
* - Single-key keyspace commands are routed to the appropriate node
* - Multi-key keyspace commands require the same slot-hash and are routed to the appropriate node
* - Pub/sub commands are sent to the node that handles the slot derived from the pub/sub channel
*
*
* @param codec Use this codec to encode/decode keys and values, must not be {@code null}
* @param Key type
* @param Value type
* @return A new stateful Redis Cluster connection
*/
public StatefulRedisClusterConnection connect(RedisCodec codec) {
assertInitialPartitions();
return getConnection(connectClusterAsync(codec));
}
/**
* Connect asynchronously to a Redis Cluster. Use the supplied {@link RedisCodec codec} to encode/decode keys and values.
* Connecting asynchronously requires an initialized topology. Call {@link #getPartitions()} first, otherwise the connect
* will fail with a{@link IllegalStateException}.
*
* What to expect from this connection:
*
*
* - A default connection is created to the node with the lowest latency
* - Keyless commands are send to the default connection
* - Single-key keyspace commands are routed to the appropriate node
* - Multi-key keyspace commands require the same slot-hash and are routed to the appropriate node
* - Pub/sub commands are sent to the node that handles the slot derived from the pub/sub channel
*
*
* @param codec Use this codec to encode/decode keys and values, must not be {@code null}
* @param Key type
* @param Value type
* @return a {@link CompletableFuture} that is notified with the connection progress.
* @since 5.1
*/
public CompletableFuture> connectAsync(RedisCodec codec) {
return transformAsyncConnectionException(connectClusterAsync(codec), getInitialUris());
}
/**
* Connect to a Redis Cluster using pub/sub connections and treat keys and values as UTF-8 strings.
*
* What to expect from this connection:
*
*
* - A default connection is created to the node with the least number of clients
* - Pub/sub commands are sent to the node with the least number of clients
* - Keyless commands are send to the default connection
* - Single-key keyspace commands are routed to the appropriate node
* - Multi-key keyspace commands require the same slot-hash and are routed to the appropriate node
*
*
* @return A new stateful Redis Cluster connection
*/
public StatefulRedisClusterPubSubConnection connectPubSub() {
return connectPubSub(newStringStringCodec());
}
/**
* Connect to a Redis Cluster using pub/sub connections. Use the supplied {@link RedisCodec codec} to encode/decode keys and
* values.
*
* What to expect from this connection:
*
*
* - A default connection is created to the node with the least number of clients
* - Pub/sub commands are sent to the node with the least number of clients
* - Keyless commands are send to the default connection
* - Single-key keyspace commands are routed to the appropriate node
* - Multi-key keyspace commands require the same slot-hash and are routed to the appropriate node
*
*
* @param codec Use this codec to encode/decode keys and values, must not be {@code null}
* @param Key type
* @param Value type
* @return A new stateful Redis Cluster connection
*/
public StatefulRedisClusterPubSubConnection connectPubSub(RedisCodec codec) {
assertInitialPartitions();
return getConnection(connectClusterPubSubAsync(codec));
}
/**
* Connect asynchronously to a Redis Cluster using pub/sub connections. Use the supplied {@link RedisCodec codec} to
* encode/decode keys and values. Connecting asynchronously requires an initialized topology. Call {@link #getPartitions()}
* first, otherwise the connect will fail with a{@link IllegalStateException}.
*
* What to expect from this connection:
*
*
* - A default connection is created to the node with the least number of clients
* - Pub/sub commands are sent to the node with the least number of clients
* - Keyless commands are send to the default connection
* - Single-key keyspace commands are routed to the appropriate node
* - Multi-key keyspace commands require the same slot-hash and are routed to the appropriate node
*
*
* @param codec Use this codec to encode/decode keys and values, must not be {@code null}
* @param Key type
* @param Value type
* @return a {@link CompletableFuture} that is notified with the connection progress.
* @since 5.1
*/
public CompletableFuture> connectPubSubAsync(RedisCodec codec) {
return transformAsyncConnectionException(connectClusterPubSubAsync(codec), getInitialUris());
}
StatefulRedisConnection connectToNode(SocketAddress socketAddress) {
return connectToNode(newStringStringCodec(), socketAddress.toString(), null, Mono.just(socketAddress));
}
/**
* Create a connection to a redis socket address.
*
* @param codec Use this codec to encode/decode keys and values, must not be {@code null}
* @param nodeId the nodeId
* @param clusterWriter global cluster writer
* @param socketAddressSupplier supplier for the socket address
* @param Key type
* @param Value type
* @return A new connection
*/
StatefulRedisConnection connectToNode(RedisCodec codec, String nodeId, RedisChannelWriter clusterWriter,
Mono socketAddressSupplier) {
return getConnection(connectToNodeAsync(codec, nodeId, clusterWriter, socketAddressSupplier));
}
/**
* Create a connection to a redis socket address.
*
* @param codec Use this codec to encode/decode keys and values, must not be {@code null}
* @param nodeId the nodeId
* @param clusterWriter global cluster writer
* @param socketAddressSupplier supplier for the socket address
* @param Key type
* @param Value type
* @return A new connection
*/
ConnectionFuture> connectToNodeAsync(RedisCodec codec, String nodeId,
RedisChannelWriter clusterWriter, Mono socketAddressSupplier) {
assertNotNull(codec);
assertNotEmpty(initialUris);
LettuceAssert.notNull(socketAddressSupplier, "SocketAddressSupplier must not be null");
ClusterNodeEndpoint endpoint = new ClusterNodeEndpoint(getClusterClientOptions(), getResources(), clusterWriter);
RedisChannelWriter writer = endpoint;
if (CommandExpiryWriter.isSupported(getClusterClientOptions())) {
writer = new CommandExpiryWriter(writer, getClusterClientOptions(), getResources());
}
if (CommandListenerWriter.isSupported(getCommandListeners())) {
writer = new CommandListenerWriter(writer, getCommandListeners());
}
StatefulRedisConnectionImpl connection = newStatefulRedisConnection(writer, endpoint, codec,
getFirstUri().getTimeout(), getClusterClientOptions().getJsonParser());
connection.setAuthenticationHandler(
createHandler(connection, getFirstUri().getCredentialsProvider(), false, getOptions()));
ConnectionFuture> connectionFuture = connectStatefulAsync(connection, endpoint,
getFirstUri(), socketAddressSupplier,
() -> new CommandHandler(getClusterClientOptions(), getResources(), endpoint));
return connectionFuture.whenComplete((conn, throwable) -> {
if (throwable != null) {
connection.closeAsync();
}
});
}
/**
* Create a new instance of {@link StatefulRedisConnectionImpl} or a subclass.
*
* Subclasses of {@link RedisClusterClient} may override that method.
*
* @param channelWriter the channel writer
* @param pushHandler the handler for push notifications
* @param codec codec
* @param timeout default timeout
* @param parser the JSON parser to be used
* @param Key-Type
* @param Value Type
* @return new instance of StatefulRedisConnectionImpl
*/
protected StatefulRedisConnectionImpl newStatefulRedisConnection(RedisChannelWriter channelWriter,
PushHandler pushHandler, RedisCodec codec, Duration timeout, Mono parser) {
return new StatefulRedisConnectionImpl<>(channelWriter, pushHandler, codec, timeout, parser);
}
/**
* Create a pub/sub connection to a redis socket address.
*
* @param codec Use this codec to encode/decode keys and values, must not be {@code null}
* @param nodeId the nodeId
* @param socketAddressSupplier supplier for the socket address
* @param Key type
* @param Value type
* @return A new connection
*/
ConnectionFuture> connectPubSubToNodeAsync(RedisCodec codec, String nodeId,
Mono socketAddressSupplier) {
assertNotNull(codec);
assertNotEmpty(initialUris);
LettuceAssert.notNull(socketAddressSupplier, "SocketAddressSupplier must not be null");
logger.debug("connectPubSubToNode(" + nodeId + ")");
PubSubEndpoint endpoint = new PubSubEndpoint<>(getClusterClientOptions(), getResources());
RedisChannelWriter writer = endpoint;
if (CommandExpiryWriter.isSupported(getClusterClientOptions())) {
writer = new CommandExpiryWriter(writer, getClusterClientOptions(), getResources());
}
if (CommandListenerWriter.isSupported(getCommandListeners())) {
writer = new CommandListenerWriter(writer, getCommandListeners());
}
StatefulRedisPubSubConnectionImpl connection = new StatefulRedisPubSubConnectionImpl<>(endpoint, writer, codec,
getFirstUri().getTimeout());
connection.setAuthenticationHandler(
createHandler(connection, getFirstUri().getCredentialsProvider(), true, getOptions()));
ConnectionFuture> connectionFuture = connectStatefulAsync(connection, endpoint,
getFirstUri(), socketAddressSupplier,
() -> new PubSubCommandHandler<>(getClusterClientOptions(), getResources(), codec, endpoint));
return connectionFuture.whenComplete((conn, throwable) -> {
if (throwable != null) {
connection.closeAsync();
}
});
}
/**
* Create a clustered pub/sub connection with command distributor.
*
* @param codec Use this codec to encode/decode keys and values, must not be {@code null}
* @param Key type
* @param Value type
* @return a new connection
*/
private CompletableFuture> connectClusterAsync(RedisCodec codec) {
if (partitions == null) {
return Futures.failed(new IllegalStateException(
"Partitions not initialized. Initialize via RedisClusterClient.getPartitions()."));
}
topologyRefreshScheduler.activateTopologyRefreshIfNeeded();
logger.debug("connectCluster(" + initialUris + ")");
DefaultEndpoint endpoint = new DefaultEndpoint(getClusterClientOptions(), getResources());
RedisChannelWriter writer = endpoint;
if (CommandExpiryWriter.isSupported(getClusterClientOptions())) {
writer = new CommandExpiryWriter(writer, getClusterClientOptions(), getResources());
}
if (CommandListenerWriter.isSupported(getCommandListeners())) {
writer = new CommandListenerWriter(writer, getCommandListeners());
}
ClusterDistributionChannelWriter clusterWriter = new ClusterDistributionChannelWriter(writer, getClusterClientOptions(),
topologyRefreshScheduler);
PooledClusterConnectionProvider pooledClusterConnectionProvider = new PooledClusterConnectionProvider<>(this,
clusterWriter, codec, topologyRefreshScheduler);
clusterWriter.setClusterConnectionProvider(pooledClusterConnectionProvider);
StatefulRedisClusterConnectionImpl connection = newStatefulRedisClusterConnection(clusterWriter,
pooledClusterConnectionProvider, codec, getFirstUri().getTimeout(), getClusterClientOptions().getJsonParser());
connection.setReadFrom(ReadFrom.UPSTREAM);
connection.setPartitions(partitions);
Supplier commandHandlerSupplier = () -> new CommandHandler(getClusterClientOptions(), getResources(),
endpoint);
Mono socketAddressSupplier = getSocketAddressSupplier(connection::getPartitions,
TopologyComparators::sortByClientCount);
Mono> connectionMono = Mono
.defer(() -> connect(socketAddressSupplier, endpoint, connection, commandHandlerSupplier));
for (int i = 1; i < getConnectionAttempts(); i++) {
connectionMono = connectionMono
.onErrorResume(t -> connect(socketAddressSupplier, endpoint, connection, commandHandlerSupplier));
}
return connectionMono
.doOnNext(
c -> connection.registerCloseables(closeableResources, clusterWriter, pooledClusterConnectionProvider))
.map(it -> (StatefulRedisClusterConnection) it).toFuture();
}
/**
* Create a new instance of {@link StatefulRedisClusterConnectionImpl} or a subclass.
*
* Subclasses of {@link RedisClusterClient} may override that method.
*
* @param channelWriter the channel writer
* @param pushHandler the handler for push notifications
* @param codec codec
* @param timeout default timeout
* @param parser the Json parser to be used
* @param Key-Type
* @param Value Type
* @return new instance of StatefulRedisClusterConnectionImpl
*/
protected StatefulRedisClusterConnectionImpl newStatefulRedisClusterConnection(
RedisChannelWriter channelWriter, ClusterPushHandler pushHandler, RedisCodec codec, Duration timeout,
Mono parser) {
return new StatefulRedisClusterConnectionImpl(channelWriter, pushHandler, codec, timeout, parser);
}
private Mono connect(Mono socketAddressSupplier, DefaultEndpoint endpoint,
StatefulRedisClusterConnectionImpl connection, Supplier commandHandlerSupplier) {
ConnectionFuture future = connectStatefulAsync(connection, endpoint, getFirstUri(), socketAddressSupplier,
commandHandlerSupplier);
return Mono.fromCompletionStage(future).doOnError(t -> logger.warn(t.getMessage()));
}
private Mono connect(Mono socketAddressSupplier, DefaultEndpoint endpoint,
StatefulRedisConnectionImpl connection, Supplier commandHandlerSupplier) {
ConnectionFuture future = connectStatefulAsync(connection, endpoint, getFirstUri(), socketAddressSupplier,
commandHandlerSupplier);
return Mono.fromCompletionStage(future).doOnError(t -> logger.warn(t.getMessage()));
}
/**
* Create a clustered connection with command distributor.
*
* @param codec Use this codec to encode/decode keys and values, must not be {@code null}
* @param Key type
* @param Value type
* @return a new connection
*/
private CompletableFuture> connectClusterPubSubAsync(
RedisCodec codec) {
if (partitions == null) {
return Futures.failed(new IllegalStateException(
"Partitions not initialized. Initialize via RedisClusterClient.getPartitions()."));
}
topologyRefreshScheduler.activateTopologyRefreshIfNeeded();
logger.debug("connectClusterPubSub(" + initialUris + ")");
PubSubClusterEndpoint endpoint = new PubSubClusterEndpoint<>(getClusterClientOptions(), getResources());
RedisChannelWriter writer = endpoint;
if (CommandExpiryWriter.isSupported(getClusterClientOptions())) {
writer = new CommandExpiryWriter(writer, getClusterClientOptions(), getResources());
}
if (CommandListenerWriter.isSupported(getCommandListeners())) {
writer = new CommandListenerWriter(writer, getCommandListeners());
}
ClusterDistributionChannelWriter clusterWriter = new ClusterDistributionChannelWriter(writer, getClusterClientOptions(),
topologyRefreshScheduler);
ClusterPubSubConnectionProvider pooledClusterConnectionProvider = new ClusterPubSubConnectionProvider<>(this,
clusterWriter, codec, endpoint.getUpstreamListener(), topologyRefreshScheduler);
StatefulRedisClusterPubSubConnectionImpl connection = new StatefulRedisClusterPubSubConnectionImpl<>(endpoint,
pooledClusterConnectionProvider, clusterWriter, codec, getFirstUri().getTimeout());
clusterWriter.setClusterConnectionProvider(pooledClusterConnectionProvider);
connection.setPartitions(partitions);
connection.setAuthenticationHandler(
createHandler(connection, getFirstUri().getCredentialsProvider(), true, getOptions()));
Supplier commandHandlerSupplier = () -> new PubSubCommandHandler<>(getClusterClientOptions(),
getResources(), codec, endpoint);
Mono socketAddressSupplier = getSocketAddressSupplier(connection::getPartitions,
TopologyComparators::sortByClientCount);
Mono> connectionMono = Mono
.defer(() -> connect(socketAddressSupplier, endpoint, connection, commandHandlerSupplier));
for (int i = 1; i < getConnectionAttempts(); i++) {
connectionMono = connectionMono
.onErrorResume(t -> connect(socketAddressSupplier, endpoint, connection, commandHandlerSupplier));
}
return connectionMono
.doOnNext(
c -> connection.registerCloseables(closeableResources, clusterWriter, pooledClusterConnectionProvider))
.map(it -> (StatefulRedisClusterPubSubConnection) it).toFuture();
}
private int getConnectionAttempts() {
return Math.max(1, partitions.size());
}
/**
* Initiates a channel connection considering {@link ClientOptions} initialization options, authentication and client name
* options.
*/
@SuppressWarnings("unchecked")
private , S> ConnectionFuture connectStatefulAsync(T connection,
DefaultEndpoint endpoint, RedisURI connectionSettings, Mono socketAddressSupplier,
Supplier commandHandlerSupplier) {
ConnectionBuilder connectionBuilder = createConnectionBuilder(connection, connection.getConnectionState(), endpoint,
connectionSettings, socketAddressSupplier, commandHandlerSupplier);
ConnectionFuture> future = initializeChannelAsync(connectionBuilder);
return future.thenApply(channelHandler -> (S) connection);
}
/**
* Initiates a channel connection considering {@link ClientOptions} initialization options, authentication and client name
* options.
*/
@SuppressWarnings("unchecked")
private , S> ConnectionFuture connectStatefulAsync(T connection,
DefaultEndpoint endpoint, RedisURI connectionSettings, Mono socketAddressSupplier,
Supplier commandHandlerSupplier) {
ConnectionBuilder connectionBuilder = createConnectionBuilder(connection, connection.getConnectionState(), endpoint,
connectionSettings, socketAddressSupplier, commandHandlerSupplier);
ConnectionFuture> future = initializeChannelAsync(connectionBuilder);
return future.thenApply(channelHandler -> (S) connection);
}
private ConnectionBuilder createConnectionBuilder(RedisChannelHandler connection, ConnectionState state,
DefaultEndpoint endpoint, RedisURI connectionSettings, Mono socketAddressSupplier,
Supplier commandHandlerSupplier) {
ConnectionBuilder connectionBuilder;
if (connectionSettings.isSsl()) {
SslConnectionBuilder sslConnectionBuilder = SslConnectionBuilder.sslConnectionBuilder();
sslConnectionBuilder.ssl(connectionSettings);
connectionBuilder = sslConnectionBuilder;
} else {
connectionBuilder = ConnectionBuilder.connectionBuilder();
}
state.apply(connectionSettings);
connectionBuilder.connectionInitializer(createHandshake(state));
connectionBuilder.reconnectionListener(new ReconnectEventListener(topologyRefreshScheduler));
connectionBuilder.clientOptions(getClusterClientOptions());
connectionBuilder.connection(connection);
connectionBuilder.clientResources(getResources());
connectionBuilder.endpoint(endpoint);
connectionBuilder.commandHandler(commandHandlerSupplier);
connectionBuilder(socketAddressSupplier, connectionBuilder, connection.getConnectionEvents(), connectionSettings);
return connectionBuilder;
}
/**
* Refresh partitions and re-initialize the routing table.
*
* @deprecated since 6.0. Renamed to {@link #refreshPartitions()}.
*/
@Deprecated
public void reloadPartitions() {
refreshPartitions();
}
/**
* Refresh partitions and re-initialize the routing table.
*
* @since 6.0
*/
public void refreshPartitions() {
get(refreshPartitionsAsync().toCompletableFuture(), e -> new RedisException("Cannot reload Redis Cluster topology", e));
}
/**
* Asynchronously reload partitions and re-initialize the distribution table.
*
* @return a {@link CompletionStage} that signals completion.
* @since 6.0
*/
public CompletionStage refreshPartitionsAsync() {
List sources = new ArrayList<>();
Iterable topologyRefreshSource = getTopologyRefreshSource();
for (RedisURI redisURI : topologyRefreshSource) {
sources.add(redisURI);
}
EventRecorder.RecordableEvent event = EventRecorder.getInstance().start(new TopologyRefreshEvent(sources));
if (partitions == null) {
return initializePartitions().thenAccept(Partitions::updateCache)
.whenComplete((unused, throwable) -> event.record());
}
return loadPartitionsAsync().thenAccept(loadedPartitions -> {
if (TopologyComparators.isChanged(getPartitions(), loadedPartitions)) {
logger.debug("Using a new cluster topology");
List before = new ArrayList<>(getPartitions());
List after = new ArrayList<>(loadedPartitions);
getResources().eventBus().publish(new ClusterTopologyChangedEvent(before, after));
}
this.partitions.reload(loadedPartitions.getPartitions());
updatePartitionsInConnections();
}).whenComplete((unused, throwable) -> event.record());
}
/**
* Suspend periodic topology refresh if it was activated previously. Suspending cancels the periodic schedule without
* interrupting any running topology refresh. Suspension is in place until obtaining a new {@link #connect connection}.
*
* @since 6.3
*/
public void suspendTopologyRefresh() {
topologyRefreshScheduler.suspendTopologyRefresh();
}
/**
* Return whether a scheduled or adaptive topology refresh is in progress.
*
* @return {@code true} if a topology refresh is in progress.
* @since 6.3
*/
public boolean isTopologyRefreshInProgress() {
return topologyRefreshScheduler.isTopologyRefreshInProgress();
}
protected void updatePartitionsInConnections() {
forEachClusterConnection(input -> {
input.setPartitions(partitions);
});
forEachClusterPubSubConnection(input -> {
input.setPartitions(partitions);
});
}
protected CompletableFuture initializePartitions() {
return loadPartitionsAsync().thenApply(it -> this.partitions = it);
}
private void assertInitialPartitions() {
if (partitions == null) {
get(initializePartitions(),
e -> new RedisConnectionException("Unable to establish a connection to Redis Cluster", e));
}
}
/**
* Retrieve partitions. Nodes within {@link Partitions} are ordered by latency. Lower latency nodes come first.
*
* @return Partitions
*/
protected Partitions loadPartitions() {
return get(loadPartitionsAsync(), Function.identity());
}
private static T get(CompletableFuture future, Function mapper) {
try {
return future.get();
} catch (ExecutionException e) {
if (e.getCause() instanceof RedisException) {
throw mapper.apply((RedisException) e.getCause());
}
throw Exceptions.bubble(e);
} catch (Exception e) {
throw Exceptions.bubble(e);
}
}
/**
* Retrieve partitions. Nodes within {@link Partitions} are ordered by latency. Lower latency nodes come first.
*
* @return future that emits {@link Partitions} upon a successful topology lookup.
* @since 6.0
*/
protected CompletableFuture loadPartitionsAsync() {
Iterable topologyRefreshSource = getTopologyRefreshSource();
CompletableFuture future = new CompletableFuture<>();
fetchPartitions(topologyRefreshSource).whenComplete((nodes, throwable) -> {
if (throwable == null) {
future.complete(nodes);
return;
}
// Attempt recovery using initial seed nodes
if (useDynamicRefreshSources() && topologyRefreshSource != initialUris) {
fetchPartitions(initialUris).whenComplete((nextNodes, nextThrowable) -> {
if (nextThrowable != null) {
Throwable exception = Exceptions.unwrap(nextThrowable);
exception.addSuppressed(Exceptions.unwrap(throwable));
future.completeExceptionally(exception);
} else {
future.complete(nextNodes);
}
});
} else {
future.completeExceptionally(Exceptions.unwrap(throwable));
}
});
Predicate nodeFilter = getClusterClientOptions().getNodeFilter();
if (nodeFilter != ClusterClientOptions.DEFAULT_NODE_FILTER) {
return future.thenApply(partitions -> {
List toRemove = new ArrayList<>();
for (RedisClusterNode partition : partitions) {
if (!nodeFilter.test(partition)) {
toRemove.add(partition);
}
}
partitions.removeAll(toRemove);
return partitions;
});
}
return future;
}
private CompletionStage fetchPartitions(Iterable topologyRefreshSource) {
CompletionStage