/*
* Copyright 2015-2017 the original author or authors.
*
* 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
*
* http://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 org.springframework.data.redis.connection.lettuce;
import io.lettuce.core.RedisException;
import io.lettuce.core.api.StatefulConnection;
import io.lettuce.core.api.sync.BaseRedisCommands;
import io.lettuce.core.cluster.RedisClusterClient;
import io.lettuce.core.cluster.SlotHash;
import io.lettuce.core.cluster.api.StatefulRedisClusterConnection;
import io.lettuce.core.cluster.api.sync.RedisClusterCommands;
import io.lettuce.core.cluster.models.partitions.Partitions;
import io.lettuce.core.codec.ByteArrayCodec;
import io.lettuce.core.codec.RedisCodec;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.DirectFieldAccessor;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.dao.DataAccessException;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.data.redis.ExceptionTranslationStrategy;
import org.springframework.data.redis.PassThroughExceptionTranslationStrategy;
import org.springframework.data.redis.connection.*;
import org.springframework.data.redis.connection.ClusterCommandExecutor.ClusterCommandCallback;
import org.springframework.data.redis.connection.ClusterCommandExecutor.MultiKeyClusterCommandCallback;
import org.springframework.data.redis.connection.ClusterCommandExecutor.NodeResult;
import org.springframework.data.redis.connection.RedisClusterNode.SlotRange;
import org.springframework.data.redis.connection.convert.Converters;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
/**
* @author Christoph Strobl
* @author Mark Paluch
* @since 1.7
*/
public class LettuceClusterConnection extends LettuceConnection implements DefaultedRedisClusterConnection {
static final ExceptionTranslationStrategy exceptionConverter = new PassThroughExceptionTranslationStrategy(
new LettuceExceptionConverter());
static final RedisCodec<byte[], byte[]> CODEC = ByteArrayCodec.INSTANCE;
private final Log log = LogFactory.getLog(getClass());
private final RedisClusterClient clusterClient;
private ClusterCommandExecutor clusterCommandExecutor;
private ClusterTopologyProvider topologyProvider;
private final boolean disposeClusterCommandExecutorOnClose;
/**
* Creates new {@link LettuceClusterConnection} using {@link RedisClusterClient}.
*
* @param clusterClient must not be {@literal null}.
*/
public LettuceClusterConnection(RedisClusterClient clusterClient) {
super(null, 100, clusterClient, null, 0);
Assert.notNull(clusterClient, "RedisClusterClient must not be null.");
this.clusterClient = clusterClient;
topologyProvider = new LettuceClusterTopologyProvider(clusterClient);
clusterCommandExecutor = new ClusterCommandExecutor(topologyProvider,
new LettuceClusterNodeResourceProvider(clusterClient), exceptionConverter);
disposeClusterCommandExecutorOnClose = true;
}
/**
* Creates new {@link LettuceClusterConnection} using {@link RedisClusterClient} running commands across the cluster
* via given {@link ClusterCommandExecutor}.
*
* @param clusterClient must not be {@literal null}.
* @param executor must not be {@literal null}.
*/
public LettuceClusterConnection(RedisClusterClient clusterClient, ClusterCommandExecutor executor) {
super(null, 100, clusterClient, null, 0);
Assert.notNull(clusterClient, "RedisClusterClient must not be null.");
Assert.notNull(executor, "ClusterCommandExecutor must not be null.");
this.clusterClient = clusterClient;
topologyProvider = new LettuceClusterTopologyProvider(clusterClient);
clusterCommandExecutor = executor;
disposeClusterCommandExecutorOnClose = false;
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.lettuce.LettuceConnection#geoCommands()
*/
@Override
public RedisGeoCommands geoCommands() {
return new LettuceClusterGeoCommands(this);
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.lettuce.LettuceConnection#hashCommands()
*/
@Override
public RedisHashCommands hashCommands() {
return new LettuceClusterHashCommands(this);
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.lettuce.LettuceConnection#hyperLogLogCommands()
*/
@Override
public RedisHyperLogLogCommands hyperLogLogCommands() {
return new LettuceClusterHyperLogLogCommands(this);
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.lettuce.LettuceConnection#keyCommands()
*/
@Override
public RedisKeyCommands keyCommands() {
return doGetClusterKeyCommands();
}
private LettuceClusterKeyCommands doGetClusterKeyCommands() {
return new LettuceClusterKeyCommands(this);
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.lettuce.LettuceConnection#listCommands()
*/
@Override
public RedisListCommands listCommands() {
return new LettuceClusterListCommands(this);
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.lettuce.LettuceConnection#stringCommands()
*/
@Override
public RedisStringCommands stringCommands() {
return new LettuceClusterStringCommands(this);
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.lettuce.LettuceConnection#setCommands()
*/
@Override
public RedisSetCommands setCommands() {
return new LettuceClusterSetCommands(this);
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.lettuce.LettuceConnection#zSetCommands()
*/
@Override
public RedisZSetCommands zSetCommands() {
return new LettuceClusterZSetCommands(this);
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.lettuce.LettuceConnection#serverCommands()
*/
@Override
public RedisClusterServerCommands serverCommands() {
return new LettuceClusterServerCommands(this);
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisClusterCommands#getClusterSlaves(org.springframework.data.redis.connection.RedisClusterNode)
*/
@Override
public Set<RedisClusterNode> clusterGetSlaves(final RedisClusterNode master) {
Assert.notNull(master, "Master must not be null!");
final RedisClusterNode nodeToUse = topologyProvider.getTopology().lookup(master);
return clusterCommandExecutor
.executeCommandOnSingleNode((LettuceClusterCommandCallback<Set<RedisClusterNode>>) client -> LettuceConverters
.toSetOfRedisClusterNodes(client.clusterSlaves(nodeToUse.getId())), master)
.getValue();
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisClusterCommands#getClusterSlotForKey(byte[])
*/
@Override
public Integer clusterGetSlotForKey(byte[] key) {
return SlotHash.getSlot(key);
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisClusterCommands#getClusterNodeForSlot(int)
*/
@Override
public RedisClusterNode clusterGetNodeForSlot(int slot) {
DirectFieldAccessor accessor = new DirectFieldAccessor(clusterClient);
return LettuceConverters
.toRedisClusterNode(((Partitions) accessor.getPropertyValue("partitions")).getPartitionBySlot(slot));
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisClusterCommands#getClusterNodeForKey(byte[])
*/
@Override
public RedisClusterNode clusterGetNodeForKey(byte[] key) {
return clusterGetNodeForSlot(clusterGetSlotForKey(key));
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisClusterCommands#getClusterInfo()
*/
@Override
public ClusterInfo clusterGetClusterInfo() {
return clusterCommandExecutor
.executeCommandOnArbitraryNode((LettuceClusterCommandCallback<ClusterInfo>) client -> new ClusterInfo(
LettuceConverters.toProperties(client.clusterInfo())))
.getValue();
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisClusterCommands#addSlots(org.springframework.data.redis.connection.RedisClusterNode, int[])
*/
@Override
public void clusterAddSlots(RedisClusterNode node, final int... slots) {
clusterCommandExecutor.executeCommandOnSingleNode(
(LettuceClusterCommandCallback<String>) client -> client.clusterAddSlots(slots), node);
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisClusterCommands#clusterAddSlots(org.springframework.data.redis.connection.RedisClusterNode, org.springframework.data.redis.connection.RedisClusterNode.SlotRange)
*/
@Override
public void clusterAddSlots(RedisClusterNode node, SlotRange range) {
Assert.notNull(range, "Range must not be null.");
clusterAddSlots(node, range.getSlotsArray());
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisClusterCommands#deleteSlots(org.springframework.data.redis.connection.RedisClusterNode, int[])
*/
@Override
public void clusterDeleteSlots(RedisClusterNode node, final int... slots) {
clusterCommandExecutor.executeCommandOnSingleNode(
(LettuceClusterCommandCallback<String>) client -> client.clusterDelSlots(slots), node);
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisClusterCommands#clusterDeleteSlotsInRange(org.springframework.data.redis.connection.RedisClusterNode, org.springframework.data.redis.connection.RedisClusterNode.SlotRange)
*/
@Override
public void clusterDeleteSlotsInRange(RedisClusterNode node, SlotRange range) {
Assert.notNull(range, "Range must not be null.");
clusterDeleteSlots(node, range.getSlotsArray());
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisClusterCommands#clusterForget(org.springframework.data.redis.connection.RedisClusterNode)
*/
@Override
public void clusterForget(final RedisClusterNode node) {
List<RedisClusterNode> nodes = new ArrayList<>(clusterGetNodes());
final RedisClusterNode nodeToRemove = topologyProvider.getTopology().lookup(node);
nodes.remove(nodeToRemove);
this.clusterCommandExecutor.executeCommandAsyncOnNodes(
(LettuceClusterCommandCallback<String>) client -> client.clusterForget(nodeToRemove.getId()), nodes);
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisClusterCommands#clusterMeet(org.springframework.data.redis.connection.RedisClusterNode)
*/
@Override
public void clusterMeet(final RedisClusterNode node) {
Assert.notNull(node, "Cluster node must not be null for CLUSTER MEET command!");
Assert.hasText(node.getHost(), "Node to meet cluster must have a host!");
Assert.isTrue(node.getPort() > 0, "Node to meet cluster must have a port greater 0!");
this.clusterCommandExecutor.executeCommandOnAllNodes(
(LettuceClusterCommandCallback<String>) client -> client.clusterMeet(node.getHost(), node.getPort()));
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisClusterCommands#clusterSetSlot(org.springframework.data.redis.connection.RedisClusterNode, int, org.springframework.data.redis.connection.RedisClusterCommands.AddSlots)
*/
@Override
public void clusterSetSlot(final RedisClusterNode node, final int slot, final AddSlots mode) {
Assert.notNull(node, "Node must not be null.");
Assert.notNull(mode, "AddSlots mode must not be null.");
final RedisClusterNode nodeToUse = topologyProvider.getTopology().lookup(node);
final String nodeId = nodeToUse.getId();
clusterCommandExecutor.executeCommandOnSingleNode((LettuceClusterCommandCallback<String>) client -> {
switch (mode) {
case MIGRATING:
return client.clusterSetSlotMigrating(slot, nodeId);
case IMPORTING:
return client.clusterSetSlotImporting(slot, nodeId);
case NODE:
return client.clusterSetSlotNode(slot, nodeId);
case STABLE:
return client.clusterSetSlotStable(slot);
default:
throw new InvalidDataAccessApiUsageException("Invalid import mode for cluster slot: " + slot);
}
}, node);
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisClusterCommands#getKeysInSlot(int, java.lang.Integer)
*/
@Override
public List<byte[]> clusterGetKeysInSlot(int slot, Integer count) {
try {
return getConnection().clusterGetKeysInSlot(slot, count);
} catch (Exception ex) {
throw exceptionConverter.translate(ex);
}
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisClusterCommands#countKeys(int)
*/
@Override
public Long clusterCountKeysInSlot(int slot) {
try {
return getConnection().clusterCountKeysInSlot(slot);
} catch (Exception ex) {
throw exceptionConverter.translate(ex);
}
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisClusterCommands#clusterReplicate(org.springframework.data.redis.connection.RedisClusterNode, org.springframework.data.redis.connection.RedisClusterNode)
*/
@Override
public void clusterReplicate(final RedisClusterNode master, RedisClusterNode slave) {
final RedisClusterNode masterNode = topologyProvider.getTopology().lookup(master);
clusterCommandExecutor.executeCommandOnSingleNode(
(LettuceClusterCommandCallback<String>) client -> client.clusterReplicate(masterNode.getId()), slave);
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.lettuce.LettuceConnection#ping()
*/
@Override
public String ping() {
Collection<String> ping = clusterCommandExecutor
.executeCommandOnAllNodes((LettuceClusterCommandCallback<String>) BaseRedisCommands::ping).resultsAsList();
for (String result : ping) {
if (!ObjectUtils.nullSafeEquals("PONG", result)) {
return "";
}
}
return "PONG";
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisClusterConnection#ping(org.springframework.data.redis.connection.RedisClusterNode)
*/
@Override
public String ping(RedisClusterNode node) {
return clusterCommandExecutor
.executeCommandOnSingleNode((LettuceClusterCommandCallback<String>) BaseRedisCommands::ping, node).getValue();
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisClusterConnection#keys(org.springframework.data.redis.connection.RedisClusterNode, byte[])
*/
@Override
public Set<byte[]> keys(RedisClusterNode node, final byte[] pattern) {
return doGetClusterKeyCommands().keys(node, pattern);
}
public byte[] randomKey(RedisClusterNode node) {
return doGetClusterKeyCommands().randomKey(node);
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisConnectionCommands#select(int)
*/
@Override
public void select(int dbIndex) {
if (dbIndex != 0) {
throw new InvalidDataAccessApiUsageException("Cannot SELECT non zero index in cluster mode.");
}
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.lettuce.LettuceConnection#getAsyncDedicatedConnection()
*/
@Override
protected StatefulConnection<byte[], byte[]> doGetAsyncDedicatedConnection() {
return clusterClient.connect(CODEC);
}
// --> cluster node stuff
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisClusterCommands#getClusterNodes()
*/
@Override
public List<RedisClusterNode> clusterGetNodes() {
return LettuceConverters.partitionsToClusterNodes(clusterClient.getPartitions());
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.lettuce.LettuceConnection#watch(byte[][])
*/
@Override
public void watch(byte[]... keys) {
throw new InvalidDataAccessApiUsageException("WATCH is currently not supported in cluster mode.");
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.lettuce.LettuceConnection#unwatch()
*/
@Override
public void unwatch() {
throw new InvalidDataAccessApiUsageException("UNWATCH is currently not supported in cluster mode.");
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.lettuce.LettuceConnection#multi()
*/
@Override
public void multi() {
throw new InvalidDataAccessApiUsageException("MULTI is currently not supported in cluster mode.");
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisClusterCommands#clusterGetMasterSlaveMap()
*/
@Override
public Map<RedisClusterNode, Collection<RedisClusterNode>> clusterGetMasterSlaveMap() {
List<NodeResult<Collection<RedisClusterNode>>> nodeResults = clusterCommandExecutor.executeCommandAsyncOnNodes(
(LettuceClusterCommandCallback<Collection<RedisClusterNode>>) client -> Converters
.toSetOfRedisClusterNodes(client.clusterSlaves(client.clusterMyId())),
topologyProvider.getTopology().getActiveMasterNodes()).getResults();
Map<RedisClusterNode, Collection<RedisClusterNode>> result = new LinkedHashMap<>();
for (NodeResult<Collection<RedisClusterNode>> nodeResult : nodeResults) {
result.put(nodeResult.getNode(), nodeResult.getValue());
}
return result;
}
public ClusterCommandExecutor getClusterCommandExecutor() {
return clusterCommandExecutor;
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisConnection#close()
*/
@Override
public void close() throws DataAccessException {
if (!isClosed() && disposeClusterCommandExecutorOnClose) {
try {
clusterCommandExecutor.destroy();
} catch (Exception ex) {
log.warn("Cannot properly close cluster command executor", ex);
}
}
super.close();
}
/**
* Lettuce specific implementation of {@link ClusterCommandCallback}.
*
* @author Christoph Strobl
* @param <T>
* @since 1.7
*/
protected interface LettuceClusterCommandCallback<T>
extends ClusterCommandCallback<RedisClusterCommands<byte[], byte[]>, T> {}
/**
* Lettuce specific implementation of {@link MultiKeyClusterCommandCallback}.
*
* @author Christoph Strobl
* @param <T>
* @since 1.7
*/
protected interface LettuceMultiKeyClusterCommandCallback<T>
extends MultiKeyClusterCommandCallback<RedisClusterCommands<byte[], byte[]>, T> {}
/**
* Lettuce specific implementation of {@link ClusterNodeResourceProvider}.
*
* @author Christoph Strobl
* @since 1.7
*/
static class LettuceClusterNodeResourceProvider implements ClusterNodeResourceProvider, DisposableBean {
private final RedisClusterClient client;
private volatile StatefulRedisClusterConnection<byte[], byte[]> connection;
public LettuceClusterNodeResourceProvider(RedisClusterClient client) {
this.client = client;
}
@Override
@SuppressWarnings("unchecked")
public RedisClusterCommands<byte[], byte[]> getResourceForSpecificNode(RedisClusterNode node) {
Assert.notNull(node, "Node must not be null!");
if (connection == null) {
synchronized (this) {
if (connection == null) {
this.connection = client.connect(CODEC);
}
}
}
try {
return connection.getConnection(node.getHost(), node.getPort()).sync();
} catch (RedisException e) {
// unwrap cause when cluster node not known in cluster
if (e.getCause() instanceof IllegalArgumentException) {
throw (IllegalArgumentException) e.getCause();
}
throw e;
}
}
@Override
@SuppressWarnings("unchecked")
public void returnResourceForSpecificNode(RedisClusterNode node, Object resource) {}
@Override
public void destroy() throws Exception {
if (connection != null) {
connection.close();
}
}
}
/**
* Lettuce specific implementation of {@link ClusterTopologyProvider}.
*
* @author Christoph Strobl
* @since 1.7
*/
static class LettuceClusterTopologyProvider implements ClusterTopologyProvider {
private final RedisClusterClient client;
/**
* @param client must not be {@literal null}.
*/
public LettuceClusterTopologyProvider(RedisClusterClient client) {
Assert.notNull(client, "RedisClusterClient must not be null.");
this.client = client;
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.ClusterTopologyProvider#getTopology()
*/
@Override
public ClusterTopology getTopology() {
return new ClusterTopology(
new LinkedHashSet<>(LettuceConverters.partitionsToClusterNodes(client.getPartitions())));
}
}
}