/*
* Copyright 2011-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 com.lambdaworks.redis.protocol;
import java.io.IOException;
import java.net.SocketAddress;
import java.nio.channels.ClosedChannelException;
import java.nio.charset.Charset;
import java.util.*;
import java.util.concurrent.atomic.AtomicLong;
import com.lambdaworks.redis.*;
import com.lambdaworks.redis.internal.LettuceAssert;
import com.lambdaworks.redis.internal.LettuceFactories;
import com.lambdaworks.redis.internal.LettuceLists;
import com.lambdaworks.redis.internal.LettuceSets;
import com.lambdaworks.redis.resource.ClientResources;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
import io.netty.channel.*;
import io.netty.channel.local.LocalAddress;
import io.netty.util.concurrent.Future;
import io.netty.util.concurrent.GenericFutureListener;
import io.netty.util.internal.logging.InternalLogLevel;
import io.netty.util.internal.logging.InternalLogger;
import io.netty.util.internal.logging.InternalLoggerFactory;
/**
* A netty {@link ChannelHandler} responsible for writing redis commands and reading responses from the server.
*
* @param <K> Key type.
* @param <V> Value type.
* @author Will Glozer
* @author Mark Paluch
* @author Jongyeol Choi
*/
@ChannelHandler.Sharable
public class CommandHandler<K, V> extends ChannelDuplexHandler implements RedisChannelWriter<K, V> {
private static final InternalLogger logger = InternalLoggerFactory.getInstance(CommandHandler.class);
private static final WriteLogListener WRITE_LOG_LISTENER = new WriteLogListener();
private static final AtomicLong CHANNEL_COUNTER = new AtomicLong();
/**
* When we encounter an unexpected IOException we look for these {@link Throwable#getMessage() messages} (because we have no
* better way to distinguish) and log them at DEBUG rather than WARN, since they are generally caused by unclean client
* disconnects rather than an actual problem.
*/
private static final Set<String> SUPPRESS_IO_EXCEPTION_MESSAGES = LettuceSets.unmodifiableSet("Connection reset by peer",
"Broken pipe", "Connection timed out");
protected final long commandHandlerId = CHANNEL_COUNTER.incrementAndGet();
protected final ClientOptions clientOptions;
protected final ClientResources clientResources;
protected final Queue<RedisCommand<K, V, ?>> queue;
protected final AtomicLong writers = new AtomicLong();
protected final Object stateLock = new Object();
private final boolean latencyMetricsEnabled;
// all access to the commandBuffer is synchronized
protected final Deque<RedisCommand<K, V, ?>> commandBuffer = LettuceFactories.newConcurrentQueue();
protected final Collection<RedisCommand<K, V, ?>> transportBuffer = LettuceFactories.newConcurrentCollection();
protected final ByteBuf buffer = ByteBufAllocator.DEFAULT.directBuffer(8192 * 8);
protected final RedisStateMachine<K, V> rsm = new RedisStateMachine<>();
protected volatile Channel channel;
private volatile ConnectionWatchdog connectionWatchdog;
// If TRACE level logging has been enabled at startup.
private final boolean traceEnabled;
// If DEBUG level logging has been enabled at startup.
private final boolean debugEnabled;
// If WARN level logging has been enabled at startup.
private final boolean warnEnabled;
private final Reliability reliability;
private volatile LifecycleState lifecycleState = LifecycleState.NOT_CONNECTED;
private Thread exclusiveLockOwner;
private RedisChannelHandler<K, V> redisChannelHandler;
private Throwable connectionError;
private String logPrefix;
private boolean autoFlushCommands = true;
/**
* Initialize a new instance that handles commands from the supplied queue.
*
* @param clientOptions client options for this connection, must not be {@literal null}
* @param clientResources client resources for this connection, must not be {@literal null}
* @param queue The command queue, must not be {@literal null}
*/
public CommandHandler(ClientOptions clientOptions, ClientResources clientResources, Queue<RedisCommand<K, V, ?>> queue) {
LettuceAssert.notNull(clientOptions, "ClientOptions must not be null");
LettuceAssert.notNull(clientResources, "ClientResources must not be null");
LettuceAssert.notNull(queue, "Queue must not be null");
this.clientOptions = clientOptions;
this.clientResources = clientResources;
this.queue = queue;
this.traceEnabled = logger.isTraceEnabled();
this.debugEnabled = logger.isDebugEnabled();
this.warnEnabled = logger.isWarnEnabled();
this.reliability = clientOptions.isAutoReconnect() ? Reliability.AT_LEAST_ONCE : Reliability.AT_MOST_ONCE;
this.latencyMetricsEnabled = clientResources.commandLatencyCollector().isEnabled();
}
/**
* @see io.netty.channel.ChannelInboundHandlerAdapter#channelRegistered(io.netty.channel.ChannelHandlerContext)
*/
@Override
public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
if (isClosed()) {
logger.debug("{} Dropping register for a closed channel", logPrefix());
}
synchronized (stateLock) {
channel = ctx.channel();
}
if (debugEnabled) {
logPrefix = null;
logger.debug("{} channelRegistered()", logPrefix());
}
setState(LifecycleState.REGISTERED);
buffer.clear();
ctx.fireChannelRegistered();
}
@Override
public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {
if (debugEnabled) {
logger.debug("{} channelUnregistered()", logPrefix());
}
if (channel != null && ctx.channel() != channel) {
logger.debug("{} My channel and ctx.channel mismatch. Propagating event to other listeners", logPrefix());
ctx.fireChannelUnregistered();
return;
}
if (isClosed()) {
cancelCommands("Connection closed");
}
synchronized (stateLock) {
channel = null;
}
ctx.fireChannelUnregistered();
}
/**
* @see io.netty.channel.ChannelInboundHandlerAdapter#channelRead(io.netty.channel.ChannelHandlerContext, java.lang.Object)
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf input = (ByteBuf) msg;
if (!input.isReadable() || input.refCnt() == 0) {
logger.warn("{} Input not readable {}, {}", logPrefix(), input.isReadable(), input.refCnt());
return;
}
if (debugEnabled) {
logger.debug("{} Received: {} bytes, {} queued commands", logPrefix(), input.readableBytes(), queue.size());
}
try {
if (buffer.refCnt() < 1) {
logger.warn("{} Ignoring received data for closed or abandoned connection", logPrefix());
return;
}
if (debugEnabled && ctx.channel() != channel) {
logger.debug("{} Ignoring data for a non-registered channel {}", logPrefix(), ctx.channel());
return;
}
if (traceEnabled) {
logger.trace("{} Buffer: {}", logPrefix(), input.toString(Charset.defaultCharset()).trim());
}
buffer.writeBytes(input);
decode(ctx, buffer);
} finally {
input.release();
}
}
protected void decode(ChannelHandlerContext ctx, ByteBuf buffer) throws InterruptedException {
while (!queue.isEmpty()) {
RedisCommand<K, V, ?> command = queue.peek();
if (debugEnabled) {
logger.debug("{} Queue contains: {} commands", logPrefix(), queue.size());
}
try {
if (!decode(buffer, command)) {
return;
}
} catch (Exception e) {
ctx.close();
throw e;
}
queue.poll();
try {
command.complete();
} catch (Exception e) {
logger.warn("{} Unexpected exception during command completion: {}", logPrefix, e.toString(), e);
}
if (buffer.refCnt() != 0) {
buffer.discardReadBytes();
}
}
}
private boolean decode(ByteBuf buffer, RedisCommand<K, V, ?> command) {
if (latencyMetricsEnabled && command instanceof WithLatency) {
WithLatency withLatency = (WithLatency) command;
if (withLatency.getFirstResponse() == -1) {
withLatency.firstResponse(nanoTime());
}
if (!rsm.decode(buffer, command, command.getOutput())) {
return false;
}
recordLatency(withLatency, command.getType());
return true;
}
return rsm.decode(buffer, command, command.getOutput());
}
private void recordLatency(WithLatency withLatency, ProtocolKeyword commandType) {
if (withLatency != null && latencyMetricsEnabled && channel != null && remote() != null) {
long firstResponseLatency = withLatency.getSent() - withLatency.getFirstResponse();
long completionLatency = nanoTime() - withLatency.getSent();
clientResources.commandLatencyCollector().recordCommandLatency(local(), remote(), commandType,
firstResponseLatency, completionLatency);
}
}
private SocketAddress remote() {
return channel.remoteAddress();
}
private SocketAddress local() {
if (channel.localAddress() != null) {
return channel.localAddress();
}
return LocalAddress.ANY;
}
@Override
public <T, C extends RedisCommand<K, V, T>> C write(C command) {
LettuceAssert.notNull(command, "Command must not be null");
try {
incrementWriters();
if (lifecycleState == LifecycleState.CLOSED) {
throw new RedisException("Connection is closed");
}
if (clientOptions.getRequestQueueSize() != Integer.MAX_VALUE
&& commandBuffer.size() + queue.size() >= clientOptions.getRequestQueueSize()) {
throw new RedisException("Request queue size exceeded: " + clientOptions.getRequestQueueSize()
+ ". Commands are not accepted until the queue size drops.");
}
if ((channel == null || !isConnected()) && isRejectCommand()) {
throw new RedisException("Currently not connected. Commands are rejected.");
}
/*
* This lock causes safety for connection activation and somehow netty gets more stable and predictable performance
* than without a lock and all threads are hammering towards writeAndFlush.
*/
Channel channel = this.channel;
if (autoFlushCommands) {
if (channel != null && isConnected() && channel.isActive()) {
writeToChannel(command, channel);
} else {
writeToBuffer(command);
}
} else {
bufferCommand(command);
}
} finally {
decrementWriters();
if (debugEnabled) {
logger.debug("{} write() done", logPrefix());
}
}
return command;
}
protected <C extends RedisCommand<K, V, T>, T> void writeToBuffer(C command) {
if (commandBuffer.contains(command) || queue.contains(command)) {
return;
}
if (connectionError != null) {
if (debugEnabled) {
logger.debug("{} writeToBuffer() Completing command {} due to connection error", logPrefix(), command);
}
command.completeExceptionally(connectionError);
return;
}
bufferCommand(command);
}
protected <C extends RedisCommand<K, V, T>, T> void writeToChannel(C command, Channel channel) {
if (reliability == Reliability.AT_MOST_ONCE) {
// cancel on exceptions and remove from queue, because there is no housekeeping
writeAndFlush(command, channel.newPromise()).addListener(new AtMostOnceWriteListener(command, queue));
}
if (reliability == Reliability.AT_LEAST_ONCE) {
// commands are ok to stay within the queue, reconnect will retrigger them
if (warnEnabled) {
writeAndFlush(command, channel.newPromise()).addListener(WRITE_LOG_LISTENER);
} else {
writeAndFlush(command, channel.voidPromise());
}
}
}
protected void bufferCommand(RedisCommand<K, V, ?> command) {
if (debugEnabled) {
logger.debug("{} write() buffering command {}", logPrefix(), command);
}
commandBuffer.add(command);
}
/**
* Wait for stateLock and increment writers. Will wait if stateLock is locked and if writer counter is negative.
*/
protected void incrementWriters() {
if (exclusiveLockOwner == Thread.currentThread()) {
return;
}
synchronized (stateLock) {
for (;;) {
if (writers.get() >= 0) {
writers.incrementAndGet();
return;
}
}
}
}
/**
* Decrement writers without any wait.
*/
protected void decrementWriters() {
if (exclusiveLockOwner == Thread.currentThread()) {
return;
}
writers.decrementAndGet();
}
/**
* Wait for stateLock and no writers. Must be used in an outer {@code synchronized} block to prevent interleaving with other
* methods using writers. Sets writers to a negative value to create a lock for {@link #incrementWriters()}.
*/
protected void lockWritersExclusive() {
if (exclusiveLockOwner == Thread.currentThread()) {
writers.decrementAndGet();
return;
}
synchronized (stateLock) {
for (;;) {
if (writers.compareAndSet(0, -1)) {
exclusiveLockOwner = Thread.currentThread();
return;
}
}
}
}
/**
* Unlock writers.
*/
protected void unlockWritersExclusive() {
if (exclusiveLockOwner == Thread.currentThread()) {
if (writers.incrementAndGet() == 0) {
exclusiveLockOwner = null;
}
}
}
private boolean isRejectCommand() {
if (clientOptions == null) {
return false;
}
switch (clientOptions.getDisconnectedBehavior()) {
case REJECT_COMMANDS:
return true;
case ACCEPT_COMMANDS:
return false;
default:
case DEFAULT:
if (!clientOptions.isAutoReconnect()) {
return true;
}
return false;
}
}
boolean isConnected() {
return lifecycleState.ordinal() >= LifecycleState.CONNECTED.ordinal()
&& lifecycleState.ordinal() < LifecycleState.DISCONNECTED.ordinal();
}
@Override
@SuppressWarnings({ "rawtypes", "unchecked" })
public void flushCommands() {
if (debugEnabled) {
logger.debug("{} flushCommands()", logPrefix());
}
if (channel != null && isConnected()) {
List<RedisCommand<K, V, ?>> queuedCommands;
synchronized (stateLock) {
try {
lockWritersExclusive();
if (commandBuffer.isEmpty()) {
return;
}
queuedCommands = new ArrayList<>(commandBuffer.size());
RedisCommand<K, V, ?> cmd;
while ((cmd = commandBuffer.poll()) != null) {
queuedCommands.add(cmd);
}
} finally {
unlockWritersExclusive();
}
}
if (debugEnabled) {
logger.debug("{} flushCommands() Flushing {} commands", logPrefix(), queuedCommands.size());
}
if (reliability == Reliability.AT_MOST_ONCE) {
// cancel on exceptions and remove from queue, because there is no housekeeping
writeAndFlush(queuedCommands, channel.newPromise()).addListener(
new AtMostOnceWriteListener(queuedCommands, this.queue));
}
if (reliability == Reliability.AT_LEAST_ONCE) {
// commands are ok to stay within the queue, reconnect will retrigger them
if (warnEnabled) {
writeAndFlush(queuedCommands, channel.newPromise()).addListener(WRITE_LOG_LISTENER);
} else {
writeAndFlush(queuedCommands, channel.voidPromise());
}
}
}
}
private <C extends RedisCommand<K, V, ?>> ChannelFuture writeAndFlush(List<C> commands, ChannelPromise channelPromise) {
if (debugEnabled) {
logger.debug("{} write() writeAndFlush commands {}", logPrefix(), commands);
}
transportBuffer.addAll(commands);
channel.writeAndFlush(commands, channelPromise);
return channelPromise;
}
private <C extends RedisCommand<K, V, ?>> ChannelFuture writeAndFlush(C command, ChannelPromise channelPromise) {
if (debugEnabled) {
logger.debug("{} write() writeAndFlush command {}", logPrefix(), command);
}
transportBuffer.add(command);
channel.writeAndFlush(command, channelPromise);
return channelPromise;
}
/**
* @see io.netty.channel.ChannelDuplexHandler#write(io.netty.channel.ChannelHandlerContext, java.lang.Object,
* io.netty.channel.ChannelPromise)
*/
@Override
@SuppressWarnings("unchecked")
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
if (debugEnabled) {
logger.debug("{} write(ctx, {}, promise)", logPrefix(), msg);
}
if (msg instanceof RedisCommand) {
writeSingleCommand(ctx, (RedisCommand<K, V, ?>) msg, promise);
return;
}
if (msg instanceof Collection) {
writeBatch(ctx, (Collection<RedisCommand<K, V, ?>>) msg, promise);
}
}
private void writeSingleCommand(ChannelHandlerContext ctx, RedisCommand<K, V, ?> command, ChannelPromise promise)
throws Exception {
if (command.isCancelled()) {
transportBuffer.remove(command);
return;
}
queueCommand(command, promise);
ctx.write(command, promise);
}
private void writeBatch(ChannelHandlerContext ctx, Collection<RedisCommand<K, V, ?>> msg, ChannelPromise promise)
throws Exception {
Collection<RedisCommand<K, V, ?>> commands = msg;
Collection<RedisCommand<K, V, ?>> toWrite = commands;
boolean cancelledCommands = false;
for (RedisCommand<K, V, ?> command : commands) {
if (command.isCancelled()) {
cancelledCommands = true;
break;
}
}
if (cancelledCommands) {
toWrite = new ArrayList<>(commands.size());
for (RedisCommand<K, V, ?> command : commands) {
if (command.isCancelled()) {
transportBuffer.remove(command);
continue;
}
toWrite.add(command);
queueCommand(command, promise);
}
} else {
for (RedisCommand<K, V, ?> command : toWrite) {
queueCommand(command, promise);
}
}
if (!toWrite.isEmpty()) {
ctx.write(toWrite, promise);
}
}
private void queueCommand(RedisCommand<K, V, ?> command, ChannelPromise promise) throws Exception {
try {
if (command.getOutput() == null) {
// fire&forget commands are excluded from metrics
command.complete();
} else {
if (latencyMetricsEnabled) {
if (command instanceof WithLatency) {
WithLatency withLatency = (WithLatency) command;
withLatency.firstResponse(-1);
withLatency.sent(nanoTime());
queue.add(command);
} else {
LatencyMeteredCommand<K, V, ?> latencyMeteredCommand = new LatencyMeteredCommand<>(command);
latencyMeteredCommand.firstResponse(-1);
latencyMeteredCommand.sent(nanoTime());
queue.add(latencyMeteredCommand);
}
} else {
queue.add(command);
}
}
transportBuffer.remove(command);
} catch (Exception e) {
command.completeExceptionally(e);
promise.setFailure(e);
throw e;
}
}
private long nanoTime() {
return System.nanoTime();
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
logPrefix = null;
connectionWatchdog = null;
if (debugEnabled) {
logger.debug("{} channelActive()", logPrefix());
}
if (ctx != null && ctx.pipeline() != null) {
Map<String, ChannelHandler> map = ctx.pipeline().toMap();
for (ChannelHandler handler : map.values()) {
if (handler instanceof ConnectionWatchdog) {
connectionWatchdog = (ConnectionWatchdog) handler;
}
}
}
synchronized (stateLock) {
try {
lockWritersExclusive();
setState(LifecycleState.CONNECTED);
try {
// Move queued commands to buffer before issuing any commands because of connection activation.
// That's necessary to prepend queued commands first as some commands might get into the queue
// after the connection was disconnected. They need to be prepended to the command buffer
moveQueuedCommandsToCommandBuffer();
activateCommandHandlerAndExecuteBufferedCommands(ctx);
} catch (Exception e) {
if (debugEnabled) {
logger.debug("{} channelActive() ran into an exception", logPrefix());
}
if (clientOptions.isCancelCommandsOnReconnectFailure()) {
reset();
}
throw e;
}
} finally {
unlockWritersExclusive();
}
}
super.channelActive(ctx);
if (channel != null) {
channel.eventLoop().submit(new Runnable() {
@Override
public void run() {
channel.pipeline().fireUserEventTriggered(new ConnectionEvents.Activated());
}
});
}
if (debugEnabled) {
logger.debug("{} channelActive() done", logPrefix());
}
}
private void moveQueuedCommandsToCommandBuffer() {
List<RedisCommand<K, V, ?>> queuedCommands = drainCommands(queue);
Collections.reverse(queuedCommands);
List<RedisCommand<K, V, ?>> transportBufferCommands = drainCommands(transportBuffer);
Collections.reverse(transportBufferCommands);
// Queued commands first because they reached the queue before commands that are still in the transport buffer.
queuedCommands.addAll(transportBufferCommands);
logger.debug("{} moveQueuedCommandsToCommandBuffer {} command(s) added to buffer", logPrefix(), queuedCommands.size());
for (RedisCommand<K, V, ?> command : queuedCommands) {
commandBuffer.addFirst(command);
}
}
private List<RedisCommand<K, V, ?>> drainCommands(Collection<RedisCommand<K, V, ?>> source) {
List<RedisCommand<K, V, ?>> target = new ArrayList<>(source.size());
target.addAll(source);
source.removeAll(target);
return target;
}
protected void activateCommandHandlerAndExecuteBufferedCommands(ChannelHandlerContext ctx) {
connectionError = null;
if (debugEnabled) {
logger.debug("{} activateCommandHandlerAndExecuteBufferedCommands {} command(s) buffered", logPrefix(),
commandBuffer.size());
}
channel = ctx.channel();
if (redisChannelHandler != null) {
if (debugEnabled) {
logger.debug("{} activating channel handler", logPrefix());
}
setState(LifecycleState.ACTIVATING);
redisChannelHandler.activated();
}
setState(LifecycleState.ACTIVE);
flushCommands();
}
/**
* @see io.netty.channel.ChannelInboundHandlerAdapter#channelInactive(io.netty.channel.ChannelHandlerContext)
*/
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
if (debugEnabled) {
logger.debug("{} channelInactive()", logPrefix());
}
if (channel != null && ctx.channel() != channel) {
logger.debug("{} My channel and ctx.channel mismatch. Propagating event to other listeners.", logPrefix());
super.channelInactive(ctx);
return;
}
synchronized (stateLock) {
try {
lockWritersExclusive();
setState(LifecycleState.DISCONNECTED);
if (redisChannelHandler != null) {
if (debugEnabled) {
logger.debug("{} deactivating channel handler", logPrefix());
}
setState(LifecycleState.DEACTIVATING);
redisChannelHandler.deactivated();
}
setState(LifecycleState.DEACTIVATED);
// Shift all commands to the commandBuffer so the queue is empty.
// Allows to run onConnect commands before executing buffered commands
commandBuffer.addAll(queue);
queue.removeAll(commandBuffer);
} finally {
unlockWritersExclusive();
}
}
rsm.reset();
if (debugEnabled) {
logger.debug("{} channelInactive() done", logPrefix());
}
super.channelInactive(ctx);
}
protected void setState(LifecycleState lifecycleState) {
if (this.lifecycleState != LifecycleState.CLOSED) {
synchronized (stateLock) {
this.lifecycleState = lifecycleState;
}
}
}
protected LifecycleState getState() {
return lifecycleState;
}
public boolean isClosed() {
return lifecycleState == LifecycleState.CLOSED;
}
private void cancelCommands(String message) {
List<RedisCommand<K, V, ?>> toCancel;
synchronized (stateLock) {
try {
lockWritersExclusive();
toCancel = prepareReset();
} finally {
unlockWritersExclusive();
}
}
for (RedisCommand<K, V, ?> cmd : toCancel) {
if (cmd.getOutput() != null) {
cmd.getOutput().setError(message);
}
cmd.cancel();
}
}
protected List<RedisCommand<K, V, ?>> prepareReset() {
int size = queue.size() + commandBuffer.size();
List<RedisCommand<K, V, ?>> toCancel = new ArrayList<>(size);
RedisCommand<K, V, ?> c;
while ((c = queue.poll()) != null) {
toCancel.add(c);
}
while ((c = commandBuffer.poll()) != null) {
toCancel.add(c);
}
return toCancel;
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
InternalLogLevel logLevel = InternalLogLevel.WARN;
if (!queue.isEmpty()) {
RedisCommand<K, V, ?> command = queue.poll();
if (debugEnabled) {
logger.debug("{} Storing exception in {}", logPrefix(), command);
}
logLevel = InternalLogLevel.DEBUG;
try {
command.completeExceptionally(cause);
} catch (Exception ex) {
logger.warn("{} Unexpected exception during command completion exceptionally: {}", logPrefix, ex.toString(), ex);
}
}
if (channel == null || !channel.isActive() || !isConnected()) {
if (debugEnabled) {
logger.debug("{} Storing exception in connectionError", logPrefix());
}
logLevel = InternalLogLevel.DEBUG;
connectionError = cause;
}
if (cause instanceof IOException && logLevel.ordinal() > InternalLogLevel.INFO.ordinal()) {
logLevel = InternalLogLevel.INFO;
if (SUPPRESS_IO_EXCEPTION_MESSAGES.contains(cause.getMessage())) {
logLevel = InternalLogLevel.DEBUG;
}
}
logger.log(logLevel, "{} Unexpected exception during request: {}", logPrefix, cause.toString(), cause);
}
/**
* Close the connection.
*/
@Override
public void close() {
if (debugEnabled) {
logger.debug("{} close()", logPrefix());
}
if (isClosed()) {
return;
}
setState(LifecycleState.CLOSED);
Channel currentChannel = this.channel;
if (currentChannel != null) {
currentChannel.pipeline().fireUserEventTriggered(new ConnectionEvents.PrepareClose());
currentChannel.pipeline().fireUserEventTriggered(new ConnectionEvents.Close());
ChannelFuture close = currentChannel.pipeline().close();
if (currentChannel.isOpen()) {
close.syncUninterruptibly();
}
} else if (connectionWatchdog != null) {
connectionWatchdog.prepareClose(new ConnectionEvents.PrepareClose());
}
rsm.close();
if (buffer.refCnt() > 0) {
buffer.release();
}
}
/**
* Reset the writer state. Queued commands will be canceled and the internal state will be reset. This is useful when the
* internal state machine gets out of sync with the connection.
*/
@Override
public void reset() {
if (debugEnabled) {
logger.debug("{} reset()", logPrefix());
}
cancelCommands("Reset");
rsm.reset();
if (buffer.refCnt() > 0) {
buffer.clear();
}
}
/**
* Reset the command-handler to the initial not-connected state.
*/
public void initialState() {
setState(LifecycleState.NOT_CONNECTED);
queue.clear();
commandBuffer.clear();
Channel currentChannel = this.channel;
if (currentChannel != null) {
currentChannel.pipeline().fireUserEventTriggered(new ConnectionEvents.PrepareClose());
currentChannel.pipeline().fireUserEventTriggered(new ConnectionEvents.Close());
currentChannel.pipeline().close();
}
}
@Override
public void setRedisChannelHandler(RedisChannelHandler<K, V> redisChannelHandler) {
this.redisChannelHandler = redisChannelHandler;
}
@Override
public void setAutoFlushCommands(boolean autoFlush) {
synchronized (stateLock) {
this.autoFlushCommands = autoFlush;
}
}
protected String logPrefix() {
if (logPrefix != null) {
return logPrefix;
}
StringBuffer buffer = new StringBuffer(64);
buffer.append('[').append(ChannelLogDescriptor.logDescriptor(channel)).append(", ").append("chid=0x")
.append(Long.toHexString(commandHandlerId)).append(']');
return logPrefix = buffer.toString();
}
public enum LifecycleState {
NOT_CONNECTED, REGISTERED, CONNECTED, ACTIVATING, ACTIVE, DISCONNECTED, DEACTIVATING, DEACTIVATED, CLOSED,
}
private enum Reliability {
AT_MOST_ONCE, AT_LEAST_ONCE;
}
private static class AtMostOnceWriteListener<K, V, T> implements ChannelFutureListener {
private final Collection<RedisCommand<K, V, T>> sentCommands;
private final Queue<?> queue;
@SuppressWarnings({ "unchecked", "rawtypes" })
public AtMostOnceWriteListener(RedisCommand<K, V, T> sentCommand, Queue<?> queue) {
this((Collection) LettuceLists.newList(sentCommand), queue);
}
public AtMostOnceWriteListener(Collection<RedisCommand<K, V, T>> sentCommand, Queue<?> queue) {
this.sentCommands = sentCommand;
this.queue = queue;
}
@Override
public void operationComplete(ChannelFuture future) throws Exception {
future.await();
if (future.cause() != null) {
for (RedisCommand<?, ?, ?> sentCommand : sentCommands) {
sentCommand.completeExceptionally(future.cause());
}
queue.removeAll(sentCommands);
}
}
}
/**
* A generic future listener which logs unsuccessful writes.
*/
static class WriteLogListener implements GenericFutureListener<Future<Void>> {
@Override
public void operationComplete(Future<Void> future) throws Exception {
Throwable cause = future.cause();
if (!future.isSuccess() && !(cause instanceof ClosedChannelException)) {
String message = "Unexpected exception during request: {}";
InternalLogLevel logLevel = InternalLogLevel.WARN;
if (cause instanceof IOException && SUPPRESS_IO_EXCEPTION_MESSAGES.contains(cause.getMessage())) {
logLevel = InternalLogLevel.DEBUG;
}
logger.log(logLevel, message, cause.toString(), cause);
}
}
}
}