All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.lettuce.core.protocol.CommandHandler Maven / Gradle / Ivy

Go to download

Advanced and thread-safe Java Redis client for synchronous, asynchronous, and reactive usage. Supports Cluster, Sentinel, Pipelining, Auto-Reconnect, Codecs and much more.

The newest version!
/*
 * Copyright 2011-Present, Redis Ltd. and Contributors
 * All rights reserved.
 *
 * Licensed under the MIT License.
 *
 * This file contains contributions from third-party contributors
 * 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
 *
 *      https://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 io.lettuce.core.protocol;

import static io.lettuce.core.ConnectionEvents.*;

import java.io.IOException;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Optional;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.atomic.AtomicLong;

import io.lettuce.core.ClientOptions;
import io.lettuce.core.ConnectionBuilder;
import io.lettuce.core.RedisConnectionException;
import io.lettuce.core.RedisCredentials;
import io.lettuce.core.RedisException;
import io.lettuce.core.RedisURI;
import io.lettuce.core.api.push.PushListener;
import io.lettuce.core.api.push.PushMessage;
import io.lettuce.core.datastructure.queue.HashIndexedQueue;
import io.lettuce.core.internal.LettuceAssert;
import io.lettuce.core.internal.LettuceSets;
import io.lettuce.core.metrics.CommandLatencyRecorder;
import io.lettuce.core.output.CommandOutput;
import io.lettuce.core.output.PushOutput;
import io.lettuce.core.resource.ClientResources;
import io.lettuce.core.tracing.TraceContext;
import io.lettuce.core.tracing.TraceContextProvider;
import io.lettuce.core.tracing.Tracer;
import io.lettuce.core.tracing.Tracing;
import io.netty.buffer.ByteBuf;
import io.netty.channel.Channel;
import io.netty.channel.ChannelDuplexHandler;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPromise;
import io.netty.channel.local.LocalAddress;
import io.netty.util.Recycler;
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.
 *
 * @author Will Glozer
 * @author Mark Paluch
 * @author Jongyeol Choi
 * @author Grzegorz Szpak
 * @author Daniel Albuquerque
 * @author Gavin Cook
 * @author Anuraag Agrawal
 */
public class CommandHandler extends ChannelDuplexHandler implements HasQueuedCommands {

    /**
     * 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.
     */
    static final Set SUPPRESS_IO_EXCEPTION_MESSAGES = LettuceSets.unmodifiableSet("Connection reset by peer",
            "Broken pipe", "Connection timed out");

    private static final InternalLogger logger = InternalLoggerFactory.getInstance(CommandHandler.class);

    private static final AtomicLong COMMAND_HANDLER_COUNTER = new AtomicLong();

    private final ClientOptions clientOptions;

    private final ClientResources clientResources;

    private final Endpoint endpoint;

    private final Queue> stack;

    private final long commandHandlerId = COMMAND_HANDLER_COUNTER.incrementAndGet();

    private final boolean traceEnabled = logger.isTraceEnabled();

    private final boolean debugEnabled = logger.isDebugEnabled();

    private final CommandLatencyRecorder commandLatencyRecorder;

    private final boolean latencyMetricsEnabled;

    private final boolean tracingEnabled;

    private final DecodeBufferPolicy decodeBufferPolicy;

    private final boolean boundedQueues;

    private final BackpressureSource backpressureSource = new BackpressureSource();

    private RedisStateMachine rsm;

    private Channel channel;

    private ByteBuf buffer;

    private boolean hasDecodeProgress;

    private PushOutput pushOutput;

    private LifecycleState lifecycleState = LifecycleState.NOT_CONNECTED;

    private String logPrefix;

    private PristineFallbackCommand fallbackCommand;

    private boolean pristine;

    private Tracing.Endpoint tracedEndpoint;

    /**
     * Initialize a new instance that handles commands from the supplied queue.
     *
     * @param clientOptions client options for this connection, must not be {@code null}
     * @param clientResources client resources for this connection, must not be {@code null}
     * @param endpoint must not be {@code null}.
     */
    public CommandHandler(ClientOptions clientOptions, ClientResources clientResources, Endpoint endpoint) {

        LettuceAssert.notNull(clientOptions, "ClientOptions must not be null");
        LettuceAssert.notNull(clientResources, "ClientResources must not be null");
        LettuceAssert.notNull(endpoint, "RedisEndpoint must not be null");

        this.clientOptions = clientOptions;
        this.clientResources = clientResources;
        this.endpoint = endpoint;
        this.commandLatencyRecorder = clientResources.commandLatencyRecorder();
        this.latencyMetricsEnabled = commandLatencyRecorder.isEnabled();
        this.boundedQueues = clientOptions.getRequestQueueSize() != Integer.MAX_VALUE;
        this.stack = clientOptions.isUseHashIndexedQueue() ? new HashIndexedQueue<>() : new ArrayDeque<>();

        Tracing tracing = clientResources.tracing();

        this.tracingEnabled = tracing.isEnabled();

        this.decodeBufferPolicy = clientOptions.getDecodeBufferPolicy();
    }

    public Endpoint getEndpoint() {
        return endpoint;
    }

    public Queue> getStack() {
        return stack;
    }

    protected void setState(LifecycleState lifecycleState) {

        if (this.lifecycleState != LifecycleState.CLOSED) {
            this.lifecycleState = lifecycleState;
        }
    }

    void setBuffer(ByteBuf buffer) {
        this.buffer = buffer;
    }

    @Override
    public Collection> drainQueue() {
        return drainCommands(stack);
    }

    protected LifecycleState getState() {
        return lifecycleState;
    }

    public boolean isClosed() {
        return lifecycleState == LifecycleState.CLOSED;
    }

    /**
     * @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());
        }

        channel = ctx.channel();

        if (debugEnabled) {
            logPrefix = null;
            logger.debug("{} channelRegistered()", logPrefix());
        }

        logPrefix = null;
        pristine = true;
        fallbackCommand = null;

        setState(LifecycleState.REGISTERED);

        buffer = ctx.alloc().buffer(8192 * 8);
        rsm = new RedisStateMachine();
        ctx.fireChannelRegistered();
    }

    /**
     * @see io.netty.channel.ChannelInboundHandlerAdapter#channelUnregistered(io.netty.channel.ChannelHandlerContext)
     */
    @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;
        }

        channel = null;
        buffer.release();
        rsm.close();
        rsm = null;

        reset();

        setState(LifecycleState.CLOSED);

        ctx.fireChannelUnregistered();
    }

    /**
     * @see io.netty.channel.ChannelInboundHandlerAdapter#userEventTriggered(io.netty.channel.ChannelHandlerContext, Object)
     */
    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {

        if (evt == EnableAutoRead.INSTANCE) {
            channel.config().setAutoRead(true);
        } else if (evt instanceof Reset) {
            reset();
        }

        super.userEventTriggered(ctx, evt);
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {

        InternalLogLevel logLevel = InternalLogLevel.WARN;

        if (!stack.isEmpty()) {
            RedisCommand command = stack.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;
            endpoint.notifyException(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);
    }

    /**
     * @see io.netty.channel.ChannelInboundHandlerAdapter#channelActive(io.netty.channel.ChannelHandlerContext)
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {

        if (debugEnabled) {
            logger.debug("{} channelActive()", logPrefix());
        }

        setState(LifecycleState.CONNECTED);

        tracedEndpoint = clientResources.tracing().createEndpoint(ctx.channel().remoteAddress());

        endpoint.notifyChannelActive(ctx.channel());
        super.channelActive(ctx);

        if (debugEnabled) {
            logger.debug("{} channelActive() done", logPrefix());
        }
    }

    private static  List drainCommands(Queue source) {

        List target = new ArrayList<>(source.size());

        T cmd;
        while ((cmd = source.poll()) != null) {
            target.add(cmd);
        }

        return target;
    }

    /**
     * @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;
        }

        tracedEndpoint = null;
        setState(LifecycleState.DISCONNECTED);
        setState(LifecycleState.DEACTIVATING);

        endpoint.notifyChannelInactive(ctx.channel());
        endpoint.notifyDrainQueuedCommands(this);

        setState(LifecycleState.DEACTIVATED);

        PristineFallbackCommand command = this.fallbackCommand;
        if (isProtectedMode(command)) {
            onProtectedMode(command.getOutput().getError());
        }

        if (debugEnabled) {
            logger.debug("{} channelInactive() done", logPrefix());
        }

        super.channelInactive(ctx);
    }

    /**
     * @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) msg, promise);
            return;
        }

        if (msg instanceof List) {

            List> batch = (List>) msg;

            if (batch.size() == 1) {

                writeSingleCommand(ctx, batch.get(0), promise);
                return;
            }

            writeBatch(ctx, batch, promise);
            return;
        }

        if (msg instanceof Collection) {
            writeBatch(ctx, (Collection>) msg, promise);
        }
    }

    private void writeSingleCommand(ChannelHandlerContext ctx, RedisCommand command, ChannelPromise promise) {

        if (!isWriteable(command)) {
            promise.trySuccess();
            return;
        }

        addToStack(command, promise);

        attachTracing(ctx, command);

        ctx.write(command, promise);
    }

    private void writeBatch(ChannelHandlerContext ctx, Collection> batch, ChannelPromise promise) {

        Collection> deduplicated = new LinkedHashSet<>(batch.size(), 1);

        for (RedisCommand command : batch) {

            if (isWriteable(command) && !deduplicated.add(command)) {
                deduplicated.remove(command);
                command.completeExceptionally(
                        new RedisException("Attempting to write duplicate command that is already enqueued: " + command));
            }
        }

        try {
            validateWrite(deduplicated.size());
        } catch (Exception e) {

            for (RedisCommand redisCommand : deduplicated) {
                redisCommand.completeExceptionally(e);
            }

            throw e;
        }

        for (RedisCommand command : deduplicated) {
            attachTracing(ctx, command);
            addToStack(command, promise);
        }

        if (!deduplicated.isEmpty()) {
            ctx.write(deduplicated, promise);
        } else {
            promise.trySuccess();
        }
    }

    private void attachTracing(ChannelHandlerContext ctx, RedisCommand command) {

        if (!tracingEnabled || !(command instanceof CompleteableCommand)) {
            return;
        }

        TracedCommand traced = CommandWrapper.unwrap(command, TracedCommand.class);
        TraceContextProvider provider = (traced == null ? clientResources.tracing().initialTraceContextProvider() : traced);
        Tracer tracer = clientResources.tracing().getTracerProvider().getTracer();

        if (provider != null) {

            TraceContext context = provider.getTraceContext();

            Tracer.Span span = tracer.nextSpan(context);
            span.name(command.getType().toString());

            if (channel.hasAttr(ConnectionBuilder.REDIS_URI)) {
                String redisUriStr = channel.attr(ConnectionBuilder.REDIS_URI).get();
                RedisURI redisURI = RedisURI.create(redisUriStr);
                span.tag("server.address", redisURI.toString());
                span.tag("db.namespace", String.valueOf(redisURI.getDatabase()));
                span.tag("user.name", Optional.ofNullable(redisURI.getCredentialsProvider().resolveCredentials().block())
                        .map(RedisCredentials::getUsername).orElse(""));
            }

            if (tracedEndpoint != null) {
                span.remoteEndpoint(tracedEndpoint);
            } else {
                span.remoteEndpoint(clientResources.tracing().createEndpoint(ctx.channel().remoteAddress()));
            }

            span.start(command);

            if (traced != null) {
                traced.setSpan(span);
            }
        }
    }

    private void addToStack(RedisCommand command, ChannelPromise promise) {

        try {

            if (!ActivationCommand.isActivationCommand(command)) {
                validateWrite(1);
            }

            if (command.getOutput() == null) {
                // fire&forget commands are excluded from metrics and replies
                complete(command);
            }

            RedisCommand redisCommand = potentiallyWrapLatencyCommand(command);

            stack.add(redisCommand);
            if (!promise.isVoid()) {
                promise.addListener(AddToStack.newInstance(stack, redisCommand));
            }
        } catch (Exception e) {
            command.completeExceptionally(e);
            throw e;
        }
    }

    private void validateWrite(int commands) {

        if (usesBoundedQueues()) {

            // number of maintenance commands (AUTH, CLIENT SETNAME, SELECT, READONLY) should be allowed on top
            // of number of user commands to ensure the driver recovers properly from a disconnect

            if (stack.size() + commands > clientOptions.getRequestQueueSize())

                throw new RedisException("Internal stack size exceeded: " + clientOptions.getRequestQueueSize()
                        + ". Commands are not accepted until the stack size drops.");
        }
    }

    private boolean usesBoundedQueues() {
        return boundedQueues;
    }

    private static boolean isWriteable(RedisCommand command) {
        return !command.isDone();
    }

    private RedisCommand potentiallyWrapLatencyCommand(RedisCommand command) {

        if (!latencyMetricsEnabled) {
            return command;
        }

        if (command instanceof WithLatency) {

            WithLatency withLatency = (WithLatency) command;

            withLatency.firstResponse(-1);
            withLatency.sent(nanoTime());

            return command;
        }

        LatencyMeteredCommand latencyMeteredCommand = new LatencyMeteredCommand<>(command);
        latencyMeteredCommand.firstResponse(-1);
        latencyMeteredCommand.sent(nanoTime());

        return latencyMeteredCommand;
    }

    /**
     * @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;
        input.touch("CommandHandler.read(…)");

        if (!input.isReadable() || input.refCnt() == 0) {
            logger.warn("{} Input not readable {}, {}", logPrefix(), input.isReadable(), input.refCnt());
            return;
        }

        if (debugEnabled) {
            logger.debug("{} Received: {} bytes, {} commands in the stack", logPrefix(), input.readableBytes(), stack.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.touch("CommandHandler.read(…)");
            buffer.writeBytes(input);

            decode(ctx, buffer);
        } finally {
            input.release();
        }
    }

    protected void decode(ChannelHandlerContext ctx, ByteBuf buffer) throws InterruptedException {

        if (pristine) {

            if (stack.isEmpty() && buffer.isReadable() && !isPushDecode(buffer)) {

                if (debugEnabled) {
                    logger.debug("{} Received response without a command context (empty stack)", logPrefix());
                }

                if (consumeResponse(buffer)) {
                    pristine = false;
                }

                return;
            }
        }

        while (canDecode(buffer)) {

            if (isPushDecode(buffer)) {

                if (pushOutput == null) {
                    pushOutput = new PushOutput<>(ByteBufferCopyCodec.INSTANCE);
                }

                try {
                    if (!decode(ctx, buffer, pushOutput)) {
                        hasDecodeProgress = true;
                        decodeBufferPolicy.afterPartialDecode(buffer);
                        return;
                    }

                } catch (Exception e) {

                    ctx.close();
                    throw e;
                }

                hasDecodeProgress = false;
                PushOutput output = pushOutput;
                pushOutput = null;
                notifyPushListeners(output);
            } else {

                RedisCommand command = stack.peek();
                if (debugEnabled) {
                    logger.debug("{} Stack contains: {} commands", logPrefix(), stack.size());
                }

                pristine = false;

                try {

                    if (!decode(ctx, buffer, command)) {
                        hasDecodeProgress = true;
                        decodeBufferPolicy.afterPartialDecode(buffer);
                        return;
                    }
                } catch (Exception e) {

                    ctx.close();
                    throw e;
                }

                hasDecodeProgress = false;
                if (isProtectedMode(command)) {
                    onProtectedMode(command.getOutput().getError());
                } else {

                    if (canComplete(command)) {
                        stack.poll();

                        try {
                            if (debugEnabled) {
                                logger.debug("{} Completing command {}", logPrefix(), command);
                            }
                            complete(command);
                        } catch (Exception e) {
                            logger.warn("{} Unexpected exception during request: {}", logPrefix, e.toString(), e);
                        }
                    }
                }
                afterDecode(ctx, command);
            }
        }

        decodeBufferPolicy.afterDecoding(buffer);
    }

    protected void notifyPushListeners(PushMessage notification) {

        Collection pushListeners = endpoint.getPushListeners();

        try {
            pushListeners.forEach(pushListener -> {
                pushListener.onPushMessage(notification);
            });
        } catch (Exception e) {
            logger.warn("PushListener.onPushMessage failed with " + e.toString(), e);
        }
    }

    /**
     * Decoding hook: Can the buffer be decoded to a command.
     *
     * @param buffer
     * @return
     */
    protected boolean canDecode(ByteBuf buffer) {
        return buffer.isReadable() && (isMessageDecode() || isPushDecode(buffer));
    }

    private boolean isPushMessage(ByteBuf buffer) {
        return buffer.getByte(buffer.readerIndex()) == RedisStateMachine.State.Type.PUSH.marker;
    }

    protected boolean isPushDecode(ByteBuf buffer) {
        return (!hasDecodeProgress && isPushMessage(buffer)) || pushOutput != null;
    }

    private boolean isMessageDecode() {
        return !stack.isEmpty();
    }

    /**
     * Decoding hook: Can the command be completed.
     *
     * @param command
     * @return
     */
    protected boolean canComplete(RedisCommand command) {
        return true;
    }

    /**
     * Decoding hook: Complete a command.
     *
     * @param command
     * @see RedisCommand#complete()
     */
    protected void complete(RedisCommand command) {
        command.complete();
    }

    /**
     * Decode a command.
     *
     * @param ctx
     * @param buffer
     * @param command
     * @return
     */
    private boolean decode(ChannelHandlerContext ctx, ByteBuf buffer, RedisCommand command) {

        if (latencyMetricsEnabled && command instanceof WithLatency) {

            WithLatency withLatency = (WithLatency) command;
            if (withLatency.getFirstResponse() == -1) {
                withLatency.firstResponse(nanoTime());
            }

            if (!decode0(ctx, buffer, command)) {
                return false;
            }

            recordLatency(withLatency, command);

            return true;
        }

        return decode0(ctx, buffer, command);
    }

    /**
     * Decode to a {@link CommandOutput}.
     *
     * @param ctx
     * @param buffer
     * @param output
     * @return
     */
    private boolean decode(ChannelHandlerContext ctx, ByteBuf buffer, CommandOutput output) {
        return decode0(ctx, buffer, output);
    }

    private boolean decode0(ChannelHandlerContext ctx, ByteBuf buffer, RedisCommand command) {

        if (!decode(buffer, command, getCommandOutput(command))) {

            if (command instanceof DemandAware.Sink) {

                DemandAware.Sink sink = (DemandAware.Sink) command;
                sink.setSource(backpressureSource);

                ctx.channel().config().setAutoRead(sink.hasDemand());
            }

            return false;
        }

        if (!ctx.channel().config().isAutoRead()) {
            ctx.channel().config().setAutoRead(true);
        }

        return true;
    }

    private boolean decode0(ChannelHandlerContext ctx, ByteBuf buffer, CommandOutput pushOutput) {

        if (!rsm.decode(buffer, pushOutput, ctx::fireExceptionCaught)) {
            return false;
        }

        if (!ctx.channel().config().isAutoRead()) {
            ctx.channel().config().setAutoRead(true);
        }

        return true;
    }

    /**
     * Decoding hook: Retrieve {@link CommandOutput} for {@link RedisCommand} decoding.
     *
     * @param command
     * @return
     * @see RedisCommand#getOutput()
     */
    protected CommandOutput getCommandOutput(RedisCommand command) {
        return command.getOutput();
    }

    protected boolean decode(ByteBuf buffer, CommandOutput output) {
        return rsm.decode(buffer, output);
    }

    protected boolean decode(ByteBuf buffer, RedisCommand command, CommandOutput output) {
        return rsm.decode(buffer, output, command::completeExceptionally);
    }

    /**
     * Consume a response without having a command on the stack.
     *
     * @param buffer
     * @return {@code true} if the buffer decode was successful. {@code false} if the buffer was not decoded.
     */
    private boolean consumeResponse(ByteBuf buffer) {

        PristineFallbackCommand command = this.fallbackCommand;

        if (command == null || !command.isDone()) {

            if (debugEnabled) {
                logger.debug("{} Consuming response using FallbackCommand", logPrefix());
            }

            if (command == null) {
                command = new PristineFallbackCommand();
                this.fallbackCommand = command;
            }

            if (!decode(buffer, command.getOutput())) {
                return false;
            }

            if (isProtectedMode(command)) {
                onProtectedMode(command.getOutput().getError());
            }
        }

        return true;
    }

    private boolean isProtectedMode(RedisCommand command) {
        return command != null && command.getOutput() != null && command.getOutput().hasError()
                && RedisConnectionException.isProtectedMode(command.getOutput().getError());
    }

    private void onProtectedMode(String message) {

        RedisConnectionException exception = new RedisConnectionException(message);

        endpoint.notifyException(exception);

        stack.forEach(cmd -> cmd.completeExceptionally(exception));
        stack.clear();

        if (channel != null) {
            channel.disconnect();
        }
    }

    /**
     * Hook method called after command completion.
     *
     * @param ctx
     * @param command
     */
    protected void afterDecode(ChannelHandlerContext ctx, RedisCommand command) {
        decodeBufferPolicy.afterCommandDecoded(buffer);
    }

    private void recordLatency(WithLatency withLatency, RedisCommand command) {

        if (withLatency != null && latencyMetricsEnabled && channel != null && remote() != null) {

            long firstResponseLatency = withLatency.getFirstResponse() - withLatency.getSent();
            long completionLatency = nanoTime() - withLatency.getSent();

            commandLatencyRecorder.recordCommandLatency(local(), remote(), command, firstResponseLatency, completionLatency);
        }
    }

    private SocketAddress remote() {
        return channel.remoteAddress();
    }

    private SocketAddress local() {
        if (channel.localAddress() != null) {
            return channel.localAddress();
        }
        return LocalAddress.ANY;
    }

    boolean isConnected() {
        return lifecycleState.ordinal() >= LifecycleState.CONNECTED.ordinal()
                && lifecycleState.ordinal() < LifecycleState.DISCONNECTED.ordinal();
    }

    private void reset() {

        resetInternals();
        cancelCommands("Reset", drainCommands(stack));
    }

    private void resetInternals() {

        if (rsm != null) {
            rsm.reset();
        }

        if (buffer.refCnt() > 0) {
            buffer.clear();
        }
    }

    private static void cancelCommands(String message, List> toCancel) {

        for (RedisCommand cmd : toCancel) {
            if (cmd.getOutput() != null) {
                cmd.getOutput().setError(message);
            }
            cmd.cancel();
        }
    }

    /**
     * @return the channel Id.
     * @since 6.1
     */
    public String getChannelId() {
        return channel == null ? "unknown" : ChannelLogDescriptor.getId(channel);
    }

    private String logPrefix() {

        if (logPrefix != null) {
            return logPrefix;
        }

        String buffer = "[" + ChannelLogDescriptor.logDescriptor(channel) + ", epid=" + endpoint.getId() + ", chid=0x"
                + getCommandHandlerId() + ']';
        return logPrefix = buffer;
    }

    private String getCommandHandlerId() {
        return Long.toHexString(commandHandlerId);
    }

    private static long nanoTime() {
        return System.nanoTime();
    }

    public enum LifecycleState {
        NOT_CONNECTED, REGISTERED, CONNECTED, ACTIVATING, ACTIVE, DISCONNECTED, DEACTIVATING, DEACTIVATED, CLOSED,
    }

    /**
     * Source for backpressure.
     */
    class BackpressureSource implements DemandAware.Source {

        @Override
        public void requestMore() {

            if (isConnected() && !isClosed()) {
                if (!channel.config().isAutoRead()) {
                    channel.pipeline().fireUserEventTriggered(EnableAutoRead.INSTANCE);
                }
            }
        }

    }

    enum EnableAutoRead {
        INSTANCE
    }

    /**
     * Add to stack listener. This listener is pooled and must be {@link #recycle() recycled after usage}.
     */
    static class AddToStack implements GenericFutureListener> {

        private static final Recycler RECYCLER = new Recycler() {

            @Override
            protected AddToStack newObject(Handle handle) {
                return new AddToStack(handle);
            }

        };

        private final Recycler.Handle handle;

        private Queue> stack;

        private RedisCommand command;

        AddToStack(Recycler.Handle handle) {
            this.handle = handle;
        }

        /**
         * Allocate a new instance.
         *
         * @param stack
         * @param command
         * @return
         */
        @SuppressWarnings("unchecked")
        static AddToStack newInstance(Queue> stack, RedisCommand command) {

            AddToStack entry = RECYCLER.get();

            entry.stack = stack;
            entry.command = command;

            return entry;
        }

        @SuppressWarnings("unchecked")
        @Override
        public void operationComplete(Future future) {

            try {
                if (!future.isSuccess()) {
                    stack.remove(command);
                }
            } finally {
                recycle();
            }
        }

        private void recycle() {

            this.stack = null;
            this.command = null;

            handle.recycle(this);
        }

    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy