package com.lambdaworks.redis.cluster; import static com.lambdaworks.redis.cluster.ClusterScanSupport.asyncClusterKeyScanCursorMapper; import static com.lambdaworks.redis.cluster.ClusterScanSupport.asyncClusterStreamScanCursorMapper; import static com.lambdaworks.redis.cluster.models.partitions.RedisClusterNode.NodeFlag.MASTER; import java.lang.reflect.Proxy; import java.util.*; import java.util.function.BiFunction; import java.util.function.Function; import java.util.function.Predicate; import com.lambdaworks.redis.*; import com.lambdaworks.redis.api.async.RedisAsyncCommands; import com.lambdaworks.redis.api.async.RedisKeyAsyncCommands; import com.lambdaworks.redis.api.async.RedisScriptingAsyncCommands; import com.lambdaworks.redis.api.async.RedisServerAsyncCommands; import com.lambdaworks.redis.cluster.ClusterScanSupport.ScanCursorMapper; import com.lambdaworks.redis.cluster.api.NodeSelectionSupport; import com.lambdaworks.redis.cluster.api.StatefulRedisClusterConnection; import com.lambdaworks.redis.cluster.api.async.AsyncNodeSelection; import com.lambdaworks.redis.cluster.api.async.NodeSelectionAsyncCommands; import com.lambdaworks.redis.cluster.api.async.RedisAdvancedClusterAsyncCommands; import com.lambdaworks.redis.cluster.api.async.RedisClusterAsyncCommands; import com.lambdaworks.redis.cluster.models.partitions.Partitions; import com.lambdaworks.redis.cluster.models.partitions.RedisClusterNode; import com.lambdaworks.redis.codec.RedisCodec; import com.lambdaworks.redis.output.IntegerOutput; import com.lambdaworks.redis.output.KeyStreamingChannel; import com.lambdaworks.redis.output.ValueStreamingChannel; import com.lambdaworks.redis.protocol.AsyncCommand; import com.lambdaworks.redis.protocol.Command; import com.lambdaworks.redis.protocol.CommandType; /** * An advanced asynchronous and thread-safe API for a Redis Cluster connection. * * @author Mark Paluch * @since 3.3 */ @SuppressWarnings("unchecked") public class RedisAdvancedClusterAsyncCommandsImpl<K, V> extends AbstractRedisAsyncCommands<K, V> implements RedisAdvancedClusterAsyncConnection<K, V>, RedisAdvancedClusterAsyncCommands<K, V> { private final Random random = new Random(); /** * Initialize a new connection. * * @param connection the stateful connection * @param codec Codec used to encode/decode keys and values. */ public RedisAdvancedClusterAsyncCommandsImpl(StatefulRedisClusterConnectionImpl<K, V> connection, RedisCodec<K, V> codec) { super(connection, codec); } @Override public RedisFuture<Long> del(K... keys) { return del(Arrays.asList(keys)); } @Override public RedisFuture<Long> del(Iterable<K> keys) { Map<Integer, List<K>> partitioned = SlotHash.partition(codec, keys); if (partitioned.size() < 2) { return super.del(keys); } Map<Integer, RedisFuture<Long>> executions = new HashMap<>(); for (Map.Entry<Integer, List<K>> entry : partitioned.entrySet()) { RedisFuture<Long> del = super.del(entry.getValue()); executions.put(entry.getKey(), del); } return MultiNodeExecution.aggregateAsync(executions); } @Override public RedisFuture<Long> unlink(K... keys) { return unlink(Arrays.asList(keys)); } @Override public RedisFuture<Long> unlink(Iterable<K> keys) { Map<Integer, List<K>> partitioned = SlotHash.partition(codec, keys); if (partitioned.size() < 2) { return super.unlink(keys); } Map<Integer, RedisFuture<Long>> executions = new HashMap<>(); for (Map.Entry<Integer, List<K>> entry : partitioned.entrySet()) { RedisFuture<Long> unlink = super.unlink(entry.getValue()); executions.put(entry.getKey(), unlink); } return MultiNodeExecution.aggregateAsync(executions); } @Override public RedisFuture<Long> exists(K... keys) { return exists(Arrays.asList(keys)); } public RedisFuture<Long> exists(Iterable<K> keys) { Map<Integer, List<K>> partitioned = SlotHash.partition(codec, keys); if (partitioned.size() < 2) { return super.exists(keys); } Map<Integer, RedisFuture<Long>> executions = new HashMap<>(); for (Map.Entry<Integer, List<K>> entry : partitioned.entrySet()) { RedisFuture<Long> exists = super.exists(entry.getValue()); executions.put(entry.getKey(), exists); } return MultiNodeExecution.aggregateAsync(executions); } @Override public RedisFuture<List<V>> mget(K... keys) { return mget(Arrays.asList(keys)); } @Override public RedisFuture<List<V>> mget(Iterable<K> keys) { Map<Integer, List<K>> partitioned = SlotHash.partition(codec, keys); if (partitioned.size() < 2) { return super.mget(keys); } Map<K, Integer> slots = SlotHash.getSlots(partitioned); Map<Integer, RedisFuture<List<V>>> executions = new HashMap<>(); for (Map.Entry<Integer, List<K>> entry : partitioned.entrySet()) { RedisFuture<List<V>> mget = super.mget(entry.getValue()); executions.put(entry.getKey(), mget); } // restore order of key return new PipelinedRedisFuture<>(executions, objectPipelinedRedisFuture -> { List<V> result = new ArrayList<>(); for (K opKey : keys) { int slot = slots.get(opKey); int position = partitioned.get(slot).indexOf(opKey); RedisFuture<List<V>> listRedisFuture = executions.get(slot); result.add(MultiNodeExecution.execute(() -> listRedisFuture.get().get(position))); } return result; }); } @Override public RedisFuture<Long> mget(ValueStreamingChannel<V> channel, K... keys) { return mget(channel, Arrays.asList(keys)); } @Override public RedisFuture<Long> mget(ValueStreamingChannel<V> channel, Iterable<K> keys) { Map<Integer, List<K>> partitioned = SlotHash.partition(codec, keys); if (partitioned.size() < 2) { return super.mget(channel, keys); } Map<Integer, RedisFuture<Long>> executions = new HashMap<>(); for (Map.Entry<Integer, List<K>> entry : partitioned.entrySet()) { RedisFuture<Long> del = super.mget(channel, entry.getValue()); executions.put(entry.getKey(), del); } return MultiNodeExecution.aggregateAsync(executions); } @Override public RedisFuture<String> mset(Map<K, V> map) { Map<Integer, List<K>> partitioned = SlotHash.partition(codec, map.keySet()); if (partitioned.size() < 2) { return super.mset(map); } Map<Integer, RedisFuture<String>> executions = new HashMap<>(); for (Map.Entry<Integer, List<K>> entry : partitioned.entrySet()) { Map<K, V> op = new HashMap<>(); entry.getValue().forEach(k -> op.put(k, map.get(k))); RedisFuture<String> mset = super.mset(op); executions.put(entry.getKey(), mset); } return MultiNodeExecution.firstOfAsync(executions); } @Override public RedisFuture<Boolean> msetnx(Map<K, V> map) { Map<Integer, List<K>> partitioned = SlotHash.partition(codec, map.keySet()); if (partitioned.size() < 2) { return super.msetnx(map); } Map<Integer, RedisFuture<Boolean>> executions = new HashMap<>(); for (Map.Entry<Integer, List<K>> entry : partitioned.entrySet()) { Map<K, V> op = new HashMap<>(); entry.getValue().forEach(k -> op.put(k, map.get(k))); RedisFuture<Boolean> msetnx = super.msetnx(op); executions.put(entry.getKey(), msetnx); } return new PipelinedRedisFuture<>(executions, objectPipelinedRedisFuture -> { for (RedisFuture<Boolean> listRedisFuture : executions.values()) { Boolean b = MultiNodeExecution.execute(() -> listRedisFuture.get()); if (b == null || !b) { return false; } } return !executions.isEmpty(); }); } @Override public RedisFuture<String> clientSetname(K name) { Map<String, RedisFuture<String>> executions = new HashMap<>(); for (RedisClusterNode redisClusterNode : getStatefulConnection().getPartitions()) { RedisClusterAsyncCommands<K, V> byNodeId = getConnection(redisClusterNode.getNodeId()); if (byNodeId.isOpen()) { executions.put("NodeId: " + redisClusterNode.getNodeId(), byNodeId.clientSetname(name)); } RedisURI uri = redisClusterNode.getUri(); RedisClusterAsyncCommands<K, V> byHost = getConnection(uri.getHost(), uri.getPort()); if (byHost.isOpen()) { executions.put("HostAndPort: " + redisClusterNode.getNodeId(), byHost.clientSetname(name)); } } return MultiNodeExecution.firstOfAsync(executions); } @Override public RedisFuture<List<K>> clusterGetKeysInSlot(int slot, int count) { RedisClusterAsyncCommands<K, V> connectionBySlot = findConnectionBySlot(slot); if (connectionBySlot != null) { return connectionBySlot.clusterGetKeysInSlot(slot, count); } return super.clusterGetKeysInSlot(slot, count); } @Override public RedisFuture<Long> clusterCountKeysInSlot(int slot) { RedisClusterAsyncCommands<K, V> connectionBySlot = findConnectionBySlot(slot); if (connectionBySlot != null) { return connectionBySlot.clusterCountKeysInSlot(slot); } return super.clusterCountKeysInSlot(slot); } @Override public RedisFuture<Long> dbsize() { Map<String, RedisFuture<Long>> executions = executeOnMasters(RedisServerAsyncCommands::dbsize); return MultiNodeExecution.aggregateAsync(executions); } @Override public RedisFuture<String> flushall() { Map<String, RedisFuture<String>> executions = executeOnMasters(RedisServerAsyncCommands::flushall); return MultiNodeExecution.firstOfAsync(executions); } @Override public RedisFuture<String> flushdb() { Map<String, RedisFuture<String>> executions = executeOnMasters(RedisServerAsyncCommands::flushdb); return MultiNodeExecution.firstOfAsync(executions); } @Override public RedisFuture<String> scriptFlush() { Map<String, RedisFuture<String>> executions = executeOnNodes(RedisScriptingAsyncCommands::scriptFlush, redisClusterNode -> true); return MultiNodeExecution.firstOfAsync(executions); } @Override public RedisFuture<String> scriptKill() { Map<String, RedisFuture<String>> executions = executeOnNodes(RedisScriptingAsyncCommands::scriptFlush, redisClusterNode -> true); return MultiNodeExecution.alwaysOkOfAsync(executions); } @Override public RedisFuture<V> randomkey() { Partitions partitions = getStatefulConnection().getPartitions(); int index = random.nextInt(partitions.size()); RedisClusterAsyncCommands<K, V> connection = getConnection(partitions.getPartition(index).getNodeId()); return connection.randomkey(); } @Override public RedisFuture<List<K>> keys(K pattern) { Map<String, RedisFuture<List<K>>> executions = executeOnMasters(commands -> commands.keys(pattern)); return new PipelinedRedisFuture<>(executions, objectPipelinedRedisFuture -> { List<K> result = new ArrayList<>(); for (RedisFuture<List<K>> future : executions.values()) { result.addAll(MultiNodeExecution.execute(() -> future.get())); } return result; }); } @Override public RedisFuture<Long> keys(KeyStreamingChannel<K> channel, K pattern) { Map<String, RedisFuture<Long>> executions = executeOnMasters(commands -> commands.keys(channel, pattern)); return MultiNodeExecution.aggregateAsync(executions); } @Override public void shutdown(boolean save) { executeOnNodes(commands -> { commands.shutdown(save); Command<K, V, Long> command = new Command<>(CommandType.SHUTDOWN, new IntegerOutput<>(codec), null); AsyncCommand<K, V, Long> async = new AsyncCommand<K, V, Long>(command); async.complete(); return async; }, redisClusterNode -> true); } @Override public RedisFuture<Long> touch(K... keys) { return touch(Arrays.asList(keys)); } public RedisFuture<Long> touch(Iterable<K> keys) { Map<Integer, List<K>> partitioned = SlotHash.partition(codec, keys); if (partitioned.size() < 2) { return super.touch(keys); } Map<Integer, RedisFuture<Long>> executions = new HashMap<>(); for (Map.Entry<Integer, List<K>> entry : partitioned.entrySet()) { RedisFuture<Long> touch = super.touch(entry.getValue()); executions.put(entry.getKey(), touch); } return MultiNodeExecution.aggregateAsync(executions); } /** * Run a command on all available masters, * * @param function function producing the command * @param <T> result type * @return map of a key (counter) and commands. */ protected <T> Map<String, RedisFuture<T>> executeOnMasters( Function<RedisClusterAsyncCommands<K, V>, RedisFuture<T>> function) { return executeOnNodes(function, redisClusterNode -> redisClusterNode.is(MASTER)); } /** * Run a command on all available nodes that match {@code filter}. * * @param function function producing the command * @param filter filter function for the node selection * @param <T> result type * @return map of a key (counter) and commands. */ protected <T> Map<String, RedisFuture<T>> executeOnNodes( Function<RedisClusterAsyncCommands<K, V>, RedisFuture<T>> function, Function<RedisClusterNode, Boolean> filter) { Map<String, RedisFuture<T>> executions = new HashMap<>(); for (RedisClusterNode redisClusterNode : getStatefulConnection().getPartitions()) { if (!filter.apply(redisClusterNode)) { continue; } RedisURI uri = redisClusterNode.getUri(); RedisClusterAsyncCommands<K, V> connection = getConnection(uri.getHost(), uri.getPort()); if (connection.isOpen()) { executions.put(redisClusterNode.getNodeId(), function.apply(connection)); } } return executions; } private RedisClusterAsyncCommands<K, V> findConnectionBySlot(int slot) { RedisClusterNode node = getStatefulConnection().getPartitions().getPartitionBySlot(slot); if (node != null) { return getConnection(node.getUri().getHost(), node.getUri().getPort()); } return null; } @Override public RedisClusterAsyncCommands<K, V> getConnection(String nodeId) { return getStatefulConnection().getConnection(nodeId).async(); } @Override public RedisClusterAsyncCommands<K, V> getConnection(String host, int port) { return getStatefulConnection().getConnection(host, port).async(); } @Override public StatefulRedisClusterConnection<K, V> getStatefulConnection() { return (StatefulRedisClusterConnection<K, V>) connection; } @Override public AsyncNodeSelection<K, V> nodes(Predicate<RedisClusterNode> predicate) { return nodes(predicate, false); } @Override public AsyncNodeSelection<K, V> readonly(Predicate<RedisClusterNode> predicate) { return nodes(predicate, ClusterConnectionProvider.Intent.READ, false); } @Override public AsyncNodeSelection<K, V> nodes(Predicate<RedisClusterNode> predicate, boolean dynamic) { return nodes(predicate, ClusterConnectionProvider.Intent.WRITE, dynamic); } @SuppressWarnings("unchecked") protected AsyncNodeSelection<K, V> nodes(Predicate<RedisClusterNode> predicate, ClusterConnectionProvider.Intent intent, boolean dynamic) { NodeSelectionSupport<RedisAsyncCommands<K, V>, ?> selection; if (dynamic) { selection = new DynamicAsyncNodeSelection<>(getStatefulConnection(), predicate, intent); } else { selection = new StaticAsyncNodeSelection<>(getStatefulConnection(), predicate, intent); } NodeSelectionInvocationHandler h = new NodeSelectionInvocationHandler((AbstractNodeSelection<?, ?, ?, ?>) selection); return (AsyncNodeSelection<K, V>) Proxy.newProxyInstance(NodeSelectionSupport.class.getClassLoader(), new Class<?>[] { NodeSelectionAsyncCommands.class, AsyncNodeSelection.class }, h); } @Override public RedisFuture<KeyScanCursor<K>> scan() { return clusterScan(ScanCursor.INITIAL, (connection, cursor) -> connection.scan(), asyncClusterKeyScanCursorMapper()); } @Override public RedisFuture<KeyScanCursor<K>> scan(ScanArgs scanArgs) { return clusterScan(ScanCursor.INITIAL, (connection, cursor) -> connection.scan(scanArgs), asyncClusterKeyScanCursorMapper()); } @Override public RedisFuture<KeyScanCursor<K>> scan(ScanCursor scanCursor, ScanArgs scanArgs) { return clusterScan(scanCursor, (connection, cursor) -> connection.scan(cursor, scanArgs), asyncClusterKeyScanCursorMapper()); } @Override public RedisFuture<KeyScanCursor<K>> scan(ScanCursor scanCursor) { return clusterScan(scanCursor, (connection, cursor) -> connection.scan(cursor), asyncClusterKeyScanCursorMapper()); } @Override public RedisFuture<StreamScanCursor> scan(KeyStreamingChannel<K> channel) { return clusterScan(ScanCursor.INITIAL, (connection, cursor) -> connection.scan(channel), asyncClusterStreamScanCursorMapper()); } @Override public RedisFuture<StreamScanCursor> scan(KeyStreamingChannel<K> channel, ScanArgs scanArgs) { return clusterScan(ScanCursor.INITIAL, (connection, cursor) -> connection.scan(channel, scanArgs), asyncClusterStreamScanCursorMapper()); } @Override public RedisFuture<StreamScanCursor> scan(KeyStreamingChannel<K> channel, ScanCursor scanCursor, ScanArgs scanArgs) { return clusterScan(scanCursor, (connection, cursor) -> connection.scan(channel, cursor, scanArgs), asyncClusterStreamScanCursorMapper()); } @Override public RedisFuture<StreamScanCursor> scan(KeyStreamingChannel<K> channel, ScanCursor scanCursor) { return clusterScan(scanCursor, (connection, cursor) -> connection.scan(channel, cursor), asyncClusterStreamScanCursorMapper()); } private <T extends ScanCursor> RedisFuture<T> clusterScan(ScanCursor cursor, BiFunction<RedisKeyAsyncCommands<K, V>, ScanCursor, RedisFuture<T>> scanFunction, ScanCursorMapper<RedisFuture<T>> resultMapper) { return clusterScan(getStatefulConnection(), cursor, scanFunction, resultMapper); } /** * Perform a SCAN in the cluster. * */ static <T extends ScanCursor, K, V> RedisFuture<T> clusterScan(StatefulRedisClusterConnection<K, V> connection, ScanCursor cursor, BiFunction<RedisKeyAsyncCommands<K, V>, ScanCursor, RedisFuture<T>> scanFunction, ScanCursorMapper<RedisFuture<T>> mapper) { List<String> nodeIds = ClusterScanSupport.getNodeIds(connection, cursor); String currentNodeId = ClusterScanSupport.getCurrentNodeId(cursor, nodeIds); ScanCursor continuationCursor = ClusterScanSupport.getContinuationCursor(cursor); RedisFuture<T> scanCursor = scanFunction.apply(connection.getConnection(currentNodeId).async(), continuationCursor); return mapper.map(nodeIds, currentNodeId, scanCursor); } }