package com.lambdaworks.redis.cluster; import static com.lambdaworks.redis.protocol.CommandType.*; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.reflect.*; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; import java.util.function.Predicate; import com.lambdaworks.redis.*; import com.lambdaworks.redis.api.StatefulRedisConnection; import com.lambdaworks.redis.api.sync.RedisCommands; import com.lambdaworks.redis.cluster.api.NodeSelectionSupport; import com.lambdaworks.redis.cluster.api.StatefulRedisClusterConnection; import com.lambdaworks.redis.cluster.api.async.RedisAdvancedClusterAsyncCommands; import com.lambdaworks.redis.cluster.api.rx.RedisAdvancedClusterReactiveCommands; import com.lambdaworks.redis.cluster.api.sync.NodeSelection; import com.lambdaworks.redis.cluster.api.sync.NodeSelectionCommands; import com.lambdaworks.redis.cluster.api.sync.RedisAdvancedClusterCommands; import com.lambdaworks.redis.cluster.api.sync.RedisClusterCommands; 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.internal.AbstractInvocationHandler; import com.lambdaworks.redis.internal.LettuceAssert; import com.lambdaworks.redis.protocol.CompleteableCommand; import com.lambdaworks.redis.protocol.ConnectionWatchdog; import com.lambdaworks.redis.protocol.RedisCommand; import io.netty.channel.ChannelHandler; /** * A thread-safe connection to a Redis Cluster. Multiple threads may share one {@link StatefulRedisClusterConnectionImpl} * * A {@link ConnectionWatchdog} monitors each connection and reconnects automatically until {@link #close} is called. All * pending commands will be (re)sent after successful reconnection. * * @author Mark Paluch * @since 4.0 */ @ChannelHandler.Sharable public class StatefulRedisClusterConnectionImpl<K, V> extends RedisChannelHandler<K, V> implements StatefulRedisClusterConnection<K, V> { private Partitions partitions; private char[] password; private boolean readOnly; protected final RedisCodec<K, V> codec; protected final RedisAdvancedClusterCommands<K, V> sync; protected final RedisAdvancedClusterAsyncCommandsImpl<K, V> async; protected final RedisAdvancedClusterReactiveCommandsImpl<K, V> reactive; /** * Initialize a new connection. * * @param writer the channel writer * @param codec Codec used to encode/decode keys and values. * @param timeout Maximum time to wait for a response. * @param unit Unit of time for the timeout. */ public StatefulRedisClusterConnectionImpl(RedisChannelWriter<K, V> writer, RedisCodec<K, V> codec, long timeout, TimeUnit unit) { super(writer, timeout, unit); this.codec = codec; this.async = new RedisAdvancedClusterAsyncCommandsImpl<>(this, codec); this.sync = (RedisAdvancedClusterCommands) Proxy.newProxyInstance(AbstractRedisClient.class.getClassLoader(), new Class<?>[] { RedisAdvancedClusterConnection.class, RedisAdvancedClusterCommands.class }, syncInvocationHandler()); this.reactive = new RedisAdvancedClusterReactiveCommandsImpl<>(this, codec); } @Override public RedisAdvancedClusterCommands<K, V> sync() { return sync; } public InvocationHandler syncInvocationHandler() { return new ClusterFutureSyncInvocationHandler<>(this, async()); } @Override public RedisAdvancedClusterAsyncCommands<K, V> async() { return async; } @Override public RedisAdvancedClusterReactiveCommands<K, V> reactive() { return reactive; } @Deprecated protected RedisAdvancedClusterReactiveCommandsImpl<K, V> getReactiveCommands() { return reactive; } private RedisURI lookup(String nodeId) { for (RedisClusterNode partition : partitions) { if (partition.getNodeId().equals(nodeId)) { return partition.getUri(); } } return null; } @Override public StatefulRedisConnection<K, V> getConnection(String nodeId) { RedisURI redisURI = lookup(nodeId); if (redisURI == null) { throw new RedisException("NodeId " + nodeId + " does not belong to the cluster"); } return getClusterDistributionChannelWriter().getClusterConnectionProvider() .getConnection(ClusterConnectionProvider.Intent.WRITE, nodeId); } @Override public StatefulRedisConnection<K, V> getConnection(String host, int port) { return getClusterDistributionChannelWriter().getClusterConnectionProvider() .getConnection(ClusterConnectionProvider.Intent.WRITE, host, port); } public ClusterDistributionChannelWriter<K, V> getClusterDistributionChannelWriter() { return (ClusterDistributionChannelWriter<K, V>) super.getChannelWriter(); } @Override public void activated() { super.activated(); // do not block in here, since the channel flow will be interrupted. if (password != null) { async.authAsync(new String(password)); } if (readOnly) { async.readOnly(); } } @Override public <T, C extends RedisCommand<K, V, T>> C dispatch(C cmd) { RedisCommand<K, V, T> local = cmd; if (local.getType().name().equals(AUTH.name())) { local = attachOnComplete(local, status -> { if (status.equals("OK") && cmd.getArgs().getFirstString() != null) { this.password = cmd.getArgs().getFirstString().toCharArray(); } }); } if (local.getType().name().equals(READONLY.name())) { local = attachOnComplete(local, status -> { if (status.equals("OK")) { this.readOnly = true; } }); } if (local.getType().name().equals(READWRITE.name())) { local = attachOnComplete(local, status -> { if (status.equals("OK")) { this.readOnly = false; } }); } return super.dispatch((C) local); } private <T> RedisCommand<K, V, T> attachOnComplete(RedisCommand<K, V, T> command, Consumer<T> consumer) { if (command instanceof CompleteableCommand) { CompleteableCommand<T> completeable = (CompleteableCommand<T>) command; completeable.onComplete(consumer); } return command; } public void setPartitions(Partitions partitions) { this.partitions = partitions; getClusterDistributionChannelWriter().setPartitions(partitions); } public Partitions getPartitions() { return partitions; } @Override public void setReadFrom(ReadFrom readFrom) { LettuceAssert.notNull(readFrom, "ReadFrom must not be null"); getClusterDistributionChannelWriter().setReadFrom(readFrom); } @Override public ReadFrom getReadFrom() { return getClusterDistributionChannelWriter().getReadFrom(); } /** * Invocation-handler to synchronize API calls which use Futures as backend. This class leverages the need to implement a * full sync class which just delegates every request. * * @param <K> Key type. * @param <V> Value type. * @author Mark Paluch * @since 3.0 */ @SuppressWarnings("unchecked") private static class ClusterFutureSyncInvocationHandler<K, V> extends AbstractInvocationHandler { private final StatefulRedisClusterConnection<K, V> connection; private final Object asyncApi; private final Map<Method, Method> apiMethodCache = new ConcurrentHashMap<>(RedisClusterCommands.class.getMethods().length, 1); private final Map<Method, Method> connectionMethodCache = new ConcurrentHashMap<>(5, 1); private static final Constructor<MethodHandles.Lookup> LOOKUP_CONSTRUCTOR; static { try { LOOKUP_CONSTRUCTOR = MethodHandles.Lookup.class.getDeclaredConstructor(Class.class, int.class); if (!LOOKUP_CONSTRUCTOR.isAccessible()) { LOOKUP_CONSTRUCTOR.setAccessible(true); } } catch (NoSuchMethodException exp) { // should be impossible, but... throw new IllegalStateException(exp); } } ClusterFutureSyncInvocationHandler(StatefulRedisClusterConnection<K, V> connection, Object asyncApi) { this.connection = connection; this.asyncApi = asyncApi; } static MethodHandles.Lookup privateMethodHandleLookup(Class<?> declaringClass) { try { return LOOKUP_CONSTRUCTOR.newInstance(declaringClass, MethodHandles.Lookup.PRIVATE); } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { throw new IllegalStateException(e); } } static MethodHandle getDefaultMethodHandle(Method method) { Class<?> declaringClass = method.getDeclaringClass(); try { return privateMethodHandleLookup(declaringClass).unreflectSpecial(method, declaringClass); } catch (IllegalAccessException e) { throw new IllegalArgumentException("Did not pass in an interface method: " + method); } } /** * * @see AbstractInvocationHandler#handleInvocation(Object, Method, Object[]) */ @Override protected Object handleInvocation(Object proxy, Method method, Object[] args) throws Throwable { try { if (method.isDefault()) { return getDefaultMethodHandle(method).bindTo(proxy).invokeWithArguments(args); } if (method.getName().equals("getConnection") && args.length > 0) { Method targetMethod = connectionMethodCache.computeIfAbsent(method, key -> { try { return connection.getClass().getMethod(key.getName(), key.getParameterTypes()); } catch (NoSuchMethodException e) { throw new IllegalStateException(e); } }); Object result = targetMethod.invoke(connection, args); if (result instanceof StatefulRedisClusterConnection) { StatefulRedisClusterConnection<K, V> connection = (StatefulRedisClusterConnection<K, V>) result; return connection.sync(); } if (result instanceof StatefulRedisConnection) { StatefulRedisConnection<K, V> connection = (StatefulRedisConnection<K, V>) result; return connection.sync(); } } if (method.getName().equals("readonly") && args.length == 1) { return nodes((Predicate<RedisClusterNode>) args[0], ClusterConnectionProvider.Intent.READ, false); } if (method.getName().equals("nodes") && args.length == 1) { return nodes((Predicate<RedisClusterNode>) args[0], ClusterConnectionProvider.Intent.WRITE, false); } if (method.getName().equals("nodes") && args.length == 2) { return nodes((Predicate<RedisClusterNode>) args[0], ClusterConnectionProvider.Intent.WRITE, (Boolean) args[1]); } Method targetMethod = apiMethodCache.computeIfAbsent(method, key -> { try { return asyncApi.getClass().getMethod(key.getName(), key.getParameterTypes()); } catch (NoSuchMethodException e) { throw new IllegalStateException(e); } }); Object result = targetMethod.invoke(asyncApi, args); if (result instanceof RedisFuture) { RedisFuture<?> command = (RedisFuture<?>) result; if (!method.getName().equals("exec") && !method.getName().equals("multi")) { if (connection instanceof StatefulRedisConnection && ((StatefulRedisConnection) connection).isMulti()) { return null; } } return LettuceFutures.awaitOrCancel(command, connection.getTimeout(), connection.getTimeoutUnit()); } return result; } catch (InvocationTargetException e) { throw e.getTargetException(); } } protected NodeSelection<K, V> nodes(Predicate<RedisClusterNode> predicate, ClusterConnectionProvider.Intent intent, boolean dynamic) { NodeSelectionSupport<RedisCommands<K, V>, ?> selection; if (dynamic) { selection = new DynamicSyncNodeSelection<>(connection, predicate, intent); } else { selection = new StaticSyncNodeSelection<>(connection, predicate, intent); } NodeSelectionInvocationHandler h = new NodeSelectionInvocationHandler((AbstractNodeSelection<?, ?, ?, ?>) selection, true, connection.getTimeout(), connection.getTimeoutUnit()); return (NodeSelection<K, V>) Proxy.newProxyInstance(NodeSelectionSupport.class.getClassLoader(), new Class<?>[] { NodeSelectionCommands.class, NodeSelection.class }, h); } } }