package com.lambdaworks.redis.cluster; import java.util.ArrayList; import java.util.Collection; import java.util.Queue; import java.util.Set; import com.lambdaworks.redis.ClientOptions; import com.lambdaworks.redis.RedisChannelWriter; import com.lambdaworks.redis.RedisException; import com.lambdaworks.redis.internal.LettuceSets; import com.lambdaworks.redis.protocol.CommandHandler; import com.lambdaworks.redis.protocol.ConnectionWatchdog; import com.lambdaworks.redis.protocol.RedisCommand; import com.lambdaworks.redis.resource.ClientResources; import io.netty.channel.ChannelHandler; import io.netty.util.internal.logging.InternalLogger; import io.netty.util.internal.logging.InternalLoggerFactory; /** * Command handler for node connections within the Redis Cluster context. This handler can requeue commands if it is * disconnected and closed but has commands in the queue. If the handler was connected it would retry commands using the * {@literal MOVED} or {@literal ASK} redirection. * * @author Mark Paluch */ @ChannelHandler.Sharable class ClusterNodeCommandHandler<K, V> extends CommandHandler<K, V> { private static final InternalLogger logger = InternalLoggerFactory.getInstance(ClusterNodeCommandHandler.class); private static final Set<LifecycleState> CHANNEL_OPEN_STATES = LettuceSets.unmodifiableSet(LifecycleState.ACTIVATING, LifecycleState.ACTIVE, LifecycleState.CONNECTED); private final RedisChannelWriter<K, V> clusterChannelWriter; /** * Initialize a new instance that handles commands from the supplied queue. * * @param clientOptions client options for this connection * @param clientResources client resources for this connection * @param queue The command queue * @param clusterChannelWriter top-most channel writer. */ public ClusterNodeCommandHandler(ClientOptions clientOptions, ClientResources clientResources, Queue<RedisCommand<K, V, ?>> queue, RedisChannelWriter<K, V> clusterChannelWriter) { super(clientOptions, clientResources, queue); this.clusterChannelWriter = clusterChannelWriter; } /** * Prepare the closing of the channel. */ public void prepareClose() { if (channel != null) { ConnectionWatchdog connectionWatchdog = channel.pipeline().get(ConnectionWatchdog.class); if (connectionWatchdog != null) { connectionWatchdog.setReconnectSuspended(true); } } } /** * Move queued and buffered commands from the inactive connection to the master command writer. This is done only if the * current connection is disconnected and auto-reconnect is enabled (command-retries). If the connection would be open, we * could get into a race that the commands we're moving are right now in processing. Alive connections can handle redirects * and retries on their own. */ @Override public void close() { logger.debug("{} close()", logPrefix()); if (clusterChannelWriter != null) { if (isAutoReconnect() && !CHANNEL_OPEN_STATES.contains(getState())) { Collection<RedisCommand<K, V, ?>> commands = shiftCommands(queue); retriggerCommands(commands); } Collection<RedisCommand<K, V, ?>> commands = shiftCommands(commandBuffer); retriggerCommands(commands); } super.close(); } protected void retriggerCommands(Collection<RedisCommand<K, V, ?>> commands) { for (RedisCommand<K, V, ?> queuedCommand : commands) { if (queuedCommand == null || queuedCommand.isCancelled()) { continue; } try { clusterChannelWriter.write(queuedCommand); } catch (RedisException e) { queuedCommand.completeExceptionally(e); } } } /** * Retrieve commands within a lock to prevent concurrent modification */ private Collection<RedisCommand<K, V, ?>> shiftCommands(Collection<RedisCommand<K, V, ?>> source) { synchronized (stateLock) { try { lockWritersExclusive(); try { return new ArrayList<>(source); } finally { source.clear(); } } finally { unlockWritersExclusive(); } } } public boolean isAutoReconnect() { return clientOptions.isAutoReconnect(); } }