/*
* 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;
import java.util.*;
import java.util.Map.Entry;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.core.task.AsyncTaskExecutor;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.ClusterRedirectException;
import org.springframework.data.redis.ClusterStateFailureException;
import org.springframework.data.redis.ExceptionTranslationStrategy;
import org.springframework.data.redis.TooManyClusterRedirectionsException;
import org.springframework.data.redis.connection.util.ByteArraySet;
import org.springframework.data.redis.connection.util.ByteArrayWrapper;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
/**
* {@link ClusterCommandExecutor} takes care of running commands across the known cluster nodes. By providing an
* {@link AsyncTaskExecutor} the execution behavior can be influenced.
*
* @author Christoph Strobl
* @author Mark Paluch
* @since 1.7
*/
public class ClusterCommandExecutor implements DisposableBean {
private AsyncTaskExecutor executor;
private final ClusterTopologyProvider topologyProvider;
private final ClusterNodeResourceProvider resourceProvider;
private final ExceptionTranslationStrategy exceptionTranslationStrategy;
private int maxRedirects = 5;
/**
* Create a new instance of {@link ClusterCommandExecutor}.
*
* @param topologyProvider must not be {@literal null}.
* @param resourceProvider must not be {@literal null}.
* @param exceptionTranslation must not be {@literal null}.
*/
public ClusterCommandExecutor(ClusterTopologyProvider topologyProvider, ClusterNodeResourceProvider resourceProvider,
ExceptionTranslationStrategy exceptionTranslation) {
Assert.notNull(topologyProvider, "ClusterTopologyProvider must not be null!");
Assert.notNull(resourceProvider, "ClusterNodeResourceProvider must not be null!");
Assert.notNull(exceptionTranslation, "ExceptionTranslationStrategy must not be null!");
this.topologyProvider = topologyProvider;
this.resourceProvider = resourceProvider;
this.exceptionTranslationStrategy = exceptionTranslation;
}
/**
* @param topologyProvider must not be {@literal null}.
* @param resourceProvider must not be {@literal null}.
* @param exceptionTranslation must not be {@literal null}.
* @param executor can be {@literal null}.
*/
public ClusterCommandExecutor(ClusterTopologyProvider topologyProvider, ClusterNodeResourceProvider resourceProvider,
ExceptionTranslationStrategy exceptionTranslation, AsyncTaskExecutor executor) {
this(topologyProvider, resourceProvider, exceptionTranslation);
this.executor = executor;
}
{
if (executor == null) {
ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
threadPoolTaskExecutor.initialize();
this.executor = threadPoolTaskExecutor;
}
}
/**
* Run {@link ClusterCommandCallback} on a random node.
*
* @param cmd must not be {@literal null}.
* @return
*/
public <T> NodeResult<T> executeCommandOnArbitraryNode(ClusterCommandCallback<?, T> cmd) {
Assert.notNull(cmd, "ClusterCommandCallback must not be null!");
List<RedisClusterNode> nodes = new ArrayList<RedisClusterNode>(getClusterTopology().getActiveNodes());
return executeCommandOnSingleNode(cmd, nodes.get(new Random().nextInt(nodes.size())));
}
/**
* Run {@link ClusterCommandCallback} on given {@link RedisClusterNode}.
*
* @param cmd must not be {@literal null}.
* @param node must not be {@literal null}.
* @throws IllegalArgumentException in case no resource can be acquired for given node.
* @return
*/
public <S, T> NodeResult<T> executeCommandOnSingleNode(ClusterCommandCallback<S, T> cmd, RedisClusterNode node) {
return executeCommandOnSingleNode(cmd, node, 0);
}
private <S, T> NodeResult<T> executeCommandOnSingleNode(ClusterCommandCallback<S, T> cmd, RedisClusterNode node,
int redirectCount) {
Assert.notNull(cmd, "ClusterCommandCallback must not be null!");
Assert.notNull(node, "RedisClusterNode must not be null!");
if (redirectCount > maxRedirects) {
throw new TooManyClusterRedirectionsException(String.format(
"Cannot follow Cluster Redirects over more than %s legs. Please consider increasing the number of redirects to follow. Current value is: %s.",
redirectCount, maxRedirects));
}
RedisClusterNode nodeToUse = lookupNode(node);
S client = this.resourceProvider.getResourceForSpecificNode(nodeToUse);
Assert.notNull(client, "Could not acquire resource for node. Is your cluster info up to date?");
try {
return new NodeResult<T>(node, cmd.doInCluster(client));
} catch (RuntimeException ex) {
RuntimeException translatedException = convertToDataAccessExeption(ex);
if (translatedException instanceof ClusterRedirectException) {
ClusterRedirectException cre = (ClusterRedirectException) translatedException;
return executeCommandOnSingleNode(cmd,
topologyProvider.getTopology().lookup(cre.getTargetHost(), cre.getTargetPort()), redirectCount + 1);
} else {
throw translatedException != null ? translatedException : ex;
}
} finally {
this.resourceProvider.returnResourceForSpecificNode(nodeToUse, client);
}
}
/**
* Lookup node from the topology.
*
* @param node
* @return
* @throws IllegalArgumentException in case the node could not be resolved to a topology-known node
*/
private RedisClusterNode lookupNode(RedisClusterNode node) {
try {
return topologyProvider.getTopology().lookup(node);
} catch (ClusterStateFailureException e) {
throw new IllegalArgumentException(String.format("Node %s is unknown to cluster", node), e);
}
}
/**
* Run {@link ClusterCommandCallback} on all reachable master nodes.
*
* @param cmd
* @return
* @throws ClusterCommandExecutionFailureException
*/
public <S, T> MulitNodeResult<T> executeCommandOnAllNodes(final ClusterCommandCallback<S, T> cmd) {
return executeCommandAsyncOnNodes(cmd, getClusterTopology().getActiveMasterNodes());
}
/**
* @param callback
* @param nodes
* @return
* @throws ClusterCommandExecutionFailureException
* @throws IllegalArgumentException in case the node could not be resolved to a topology-known node
*/
public <S, T> MulitNodeResult<T> executeCommandAsyncOnNodes(final ClusterCommandCallback<S, T> callback,
Iterable<RedisClusterNode> nodes) {
Assert.notNull(callback, "Callback must not be null!");
Assert.notNull(nodes, "Nodes must not be null!");
List<RedisClusterNode> resolvedRedisClusterNodes = new ArrayList<RedisClusterNode>();
ClusterTopology topology = topologyProvider.getTopology();
for (final RedisClusterNode node : nodes) {
try {
resolvedRedisClusterNodes.add(topology.lookup(node));
} catch (ClusterStateFailureException e) {
throw new IllegalArgumentException(String.format("Node %s is unknown to cluster", node), e);
}
}
Map<NodeExecution, Future<NodeResult<T>>> futures = new LinkedHashMap<NodeExecution, Future<NodeResult<T>>>();
for (final RedisClusterNode node : resolvedRedisClusterNodes) {
futures.put(new NodeExecution(node), executor.submit(() -> executeCommandOnSingleNode(callback, node)));
}
return collectResults(futures);
}
private <T> MulitNodeResult<T> collectResults(Map<NodeExecution, Future<NodeResult<T>>> futures) {
boolean done = false;
MulitNodeResult<T> result = new MulitNodeResult<T>();
Map<RedisClusterNode, Throwable> exceptions = new HashMap<RedisClusterNode, Throwable>();
Set<String> saveGuard = new HashSet<String>();
while (!done) {
done = true;
for (Map.Entry<NodeExecution, Future<NodeResult<T>>> entry : futures.entrySet()) {
if (!entry.getValue().isDone() && !entry.getValue().isCancelled()) {
done = false;
} else {
try {
String futureId = ObjectUtils.getIdentityHexString(entry.getValue());
if (!saveGuard.contains(futureId)) {
result.add(entry.getValue().get());
saveGuard.add(futureId);
}
} catch (ExecutionException e) {
RuntimeException ex = convertToDataAccessExeption((Exception) e.getCause());
exceptions.put(entry.getKey().getNode(), ex != null ? ex : e.getCause());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
RuntimeException ex = convertToDataAccessExeption((Exception) e.getCause());
exceptions.put(entry.getKey().getNode(), ex != null ? ex : e.getCause());
break;
}
}
}
try {
Thread.sleep(10);
} catch (InterruptedException e) {
done = true;
Thread.currentThread().interrupt();
}
}
if (!exceptions.isEmpty()) {
throw new ClusterCommandExecutionFailureException(new ArrayList<Throwable>(exceptions.values()));
}
return result;
}
/**
* Run {@link MultiKeyClusterCommandCallback} with on a curated set of nodes serving one or more keys.
*
* @param cmd
* @return
* @throws ClusterCommandExecutionFailureException
*/
public <S, T> MulitNodeResult<T> executeMuliKeyCommand(final MultiKeyClusterCommandCallback<S, T> cmd,
Iterable<byte[]> keys) {
Map<RedisClusterNode, Set<byte[]>> nodeKeyMap = new HashMap<RedisClusterNode, Set<byte[]>>();
for (byte[] key : keys) {
for (RedisClusterNode node : getClusterTopology().getKeyServingNodes(key)) {
if (nodeKeyMap.containsKey(node)) {
nodeKeyMap.get(node).add(key);
} else {
Set<byte[]> keySet = new LinkedHashSet<byte[]>();
keySet.add(key);
nodeKeyMap.put(node, keySet);
}
}
}
Map<NodeExecution, Future<NodeResult<T>>> futures = new LinkedHashMap<NodeExecution, Future<NodeResult<T>>>();
for (final Entry<RedisClusterNode, Set<byte[]>> entry : nodeKeyMap.entrySet()) {
if (entry.getKey().isMaster()) {
for (final byte[] key : entry.getValue()) {
futures.put(new NodeExecution(entry.getKey(), key),
executor.submit(() -> executeMultiKeyCommandOnSingleNode(cmd, entry.getKey(), key)));
}
}
}
return collectResults(futures);
}
private <S, T> NodeResult<T> executeMultiKeyCommandOnSingleNode(MultiKeyClusterCommandCallback<S, T> cmd,
RedisClusterNode node, byte[] key) {
Assert.notNull(cmd, "MultiKeyCommandCallback must not be null!");
Assert.notNull(node, "RedisClusterNode must not be null!");
Assert.notNull(key, "Keys for execution must not be null!");
S client = this.resourceProvider.getResourceForSpecificNode(node);
Assert.notNull(client, "Could not acquire resource for node. Is your cluster info up to date?");
try {
return new NodeResult<T>(node, cmd.doInCluster(client, key), key);
} catch (RuntimeException ex) {
RuntimeException translatedException = convertToDataAccessExeption(ex);
throw translatedException != null ? translatedException : ex;
} finally {
this.resourceProvider.returnResourceForSpecificNode(node, client);
}
}
private ClusterTopology getClusterTopology() {
return this.topologyProvider.getTopology();
}
private DataAccessException convertToDataAccessExeption(Exception e) {
return exceptionTranslationStrategy.translate(e);
}
/**
* Set the maximum number of redirects to follow on {@code MOVED} or {@code ASK}.
*
* @param maxRedirects set to zero to suspend redirects.
*/
public void setMaxRedirects(int maxRedirects) {
this.maxRedirects = maxRedirects;
}
/*
* (non-Javadoc)
* @see org.springframework.beans.factory.DisposableBean#destroy()
*/
@Override
public void destroy() throws Exception {
if (executor instanceof DisposableBean) {
((DisposableBean) executor).destroy();
}
if (resourceProvider instanceof DisposableBean) {
((DisposableBean) resourceProvider).destroy();
}
}
/**
* Callback interface for Redis 'low level' code using the cluster client directly. To be used with
* {@link ClusterCommandExecutor} execution methods.
*
* @author Christoph Strobl
* @param <T> native driver connection
* @param <S>
* @since 1.7
*/
public static interface ClusterCommandCallback<T, S> {
S doInCluster(T client);
}
/**
* Callback interface for Redis 'low level' code using the cluster client to execute multi key commands.
*
* @author Christoph Strobl
* @param <T> native driver connection
* @param <S>
*/
public static interface MultiKeyClusterCommandCallback<T, S> {
S doInCluster(T client, byte[] key);
}
/**
* {@link NodeExecution} encapsulates the execution of a command on a specific node along with arguments, such as
* keys, involved.
*
* @author Christoph Strobl
* @since 1.7
*/
private static class NodeExecution {
private RedisClusterNode node;
private Object[] args;
public NodeExecution(RedisClusterNode node, Object... args) {
this.node = node;
this.args = args;
}
/**
* Get the {@link RedisClusterNode} the execution happens on.
*/
public RedisClusterNode getNode() {
return node;
}
/*
* (non-Javadoc)
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode() {
int result = ObjectUtils.nullSafeHashCode(node);
return result + ObjectUtils.nullSafeHashCode(args);
}
/*
* (non-Javadoc)
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (!(obj instanceof NodeExecution)) {
return false;
}
NodeExecution that = (NodeExecution) obj;
if (!ObjectUtils.nullSafeEquals(this.node, that.node)) {
return false;
}
return ObjectUtils.nullSafeEquals(this.args, that.args);
}
}
/**
* {@link NodeResult} encapsules the actual value returned by a {@link ClusterCommandCallback} on a given
* {@link RedisClusterNode}.
*
* @author Christoph Strobl
* @param <T>
* @since 1.7
*/
public static class NodeResult<T> {
private RedisClusterNode node;
private T value;
private ByteArrayWrapper key;
/**
* Create new {@link NodeResult}.
*
* @param node
* @param value
*/
public NodeResult(RedisClusterNode node, T value) {
this(node, value, new byte[] {});
}
/**
* Create new {@link NodeResult}.
*
* @param node
* @param value
* @parm key
*/
public NodeResult(RedisClusterNode node, T value, byte[] key) {
this.node = node;
this.value = value;
this.key = new ByteArrayWrapper(key);
}
/**
* Get the actual value of the command execution.
*
* @return can be {@literal null}.
*/
public T getValue() {
return value;
}
/**
* Get the {@link RedisClusterNode} the command was executed on.
*
* @return never {@literal null}.
*/
public RedisClusterNode getNode() {
return node;
}
/**
* @return
*/
public byte[] getKey() {
return key.getArray();
}
}
/**
* {@link MulitNodeResult} holds all {@link NodeResult} of a command executed on multiple {@link RedisClusterNode}.
*
* @author Christoph Strobl
* @param <T>
* @since 1.7
*/
public static class MulitNodeResult<T> {
List<NodeResult<T>> nodeResults = new ArrayList<NodeResult<T>>();
private void add(NodeResult<T> result) {
nodeResults.add(result);
}
/**
* @return never {@literal null}.
*/
public List<NodeResult<T>> getResults() {
return Collections.unmodifiableList(nodeResults);
}
/**
* Get {@link List} of all individual {@link NodeResult#value}. <br />
* The resulting {@link List} may contain {@literal null} values.
*
* @return never {@literal null}.
*/
public List<T> resultsAsList() {
return toList(nodeResults);
}
/**
* Get {@link List} of all individual {@link NodeResult#value}. <br />
* The resulting {@link List} may contain {@literal null} values.
*
* @return never {@literal null}.
*/
public List<T> resultsAsListSortBy(byte[]... keys) {
ArrayList<NodeResult<T>> clone = new ArrayList<NodeResult<T>>(nodeResults);
Collections.sort(clone, new ResultByReferenceKeyPositionComperator(keys));
return toList(clone);
}
/**
* @param returnValue
* @return
*/
public T getFirstNonNullNotEmptyOrDefault(T returnValue) {
for (NodeResult<T> nodeResult : nodeResults) {
if (nodeResult.getValue() != null) {
if (nodeResult.getValue() instanceof Map) {
if (CollectionUtils.isEmpty((Map<?, ?>) nodeResult.getValue())) {
return nodeResult.getValue();
}
} else if (CollectionUtils.isEmpty((Collection<?>) nodeResult.getValue())) {
return nodeResult.getValue();
} else {
return nodeResult.getValue();
}
}
}
return returnValue;
}
private List<T> toList(Collection<NodeResult<T>> source) {
ArrayList<T> result = new ArrayList<T>();
for (NodeResult<T> nodeResult : source) {
result.add(nodeResult.getValue());
}
return result;
}
/**
* {@link Comparator} for sorting {@link NodeResult} by reference keys.
*
* @author Christoph Strobl
*/
private static class ResultByReferenceKeyPositionComperator implements Comparator<NodeResult<?>> {
List<ByteArrayWrapper> reference;
public ResultByReferenceKeyPositionComperator(byte[]... keys) {
reference = new ArrayList<ByteArrayWrapper>(new ByteArraySet(Arrays.asList(keys)));
}
@Override
public int compare(NodeResult<?> o1, NodeResult<?> o2) {
return Integer.compare(reference.indexOf(o1.key), reference.indexOf(o2.key));
}
}
}
}