/* * 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()))); } } }