io.grpc.netty.NettyClientTransport.java Source code

Java tutorial

Introduction

Here is the source code for io.grpc.netty.NettyClientTransport.java

Source

/*
 * Copyright 2014 The gRPC 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 io.grpc.netty;

import static io.grpc.internal.GrpcUtil.KEEPALIVE_TIME_NANOS_DISABLED;
import static io.netty.channel.ChannelOption.SO_KEEPALIVE;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.MoreObjects;
import com.google.common.base.Preconditions;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.SettableFuture;
import io.grpc.Attributes;
import io.grpc.CallOptions;
import io.grpc.ChannelLogger;
import io.grpc.InternalChannelz.SocketStats;
import io.grpc.InternalLogId;
import io.grpc.Metadata;
import io.grpc.MethodDescriptor;
import io.grpc.Status;
import io.grpc.internal.ClientStream;
import io.grpc.internal.ConnectionClientTransport;
import io.grpc.internal.FailingClientStream;
import io.grpc.internal.GrpcUtil;
import io.grpc.internal.Http2Ping;
import io.grpc.internal.KeepAliveManager;
import io.grpc.internal.KeepAliveManager.ClientKeepAlivePinger;
import io.grpc.internal.StatsTraceContext;
import io.grpc.internal.TransportTracer;
import io.grpc.netty.NettyChannelBuilder.LocalSocketPicker;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFactory;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoop;
import io.netty.channel.EventLoopGroup;
import io.netty.handler.codec.http2.StreamBufferingEncoder.Http2ChannelClosedException;
import io.netty.util.AsciiString;
import io.netty.util.AttributeKey;
import io.netty.util.concurrent.Future;
import io.netty.util.concurrent.GenericFutureListener;
import java.net.SocketAddress;
import java.nio.channels.ClosedChannelException;
import java.util.Map;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import javax.annotation.Nullable;

/**
 * A Netty-based {@link ConnectionClientTransport} implementation.
 */
class NettyClientTransport implements ConnectionClientTransport {
    static final AttributeKey<ChannelLogger> LOGGER_KEY = AttributeKey.newInstance("channelLogger");

    private final InternalLogId logId;
    private final Map<ChannelOption<?>, ?> channelOptions;
    private final SocketAddress remoteAddress;
    private final ChannelFactory<? extends Channel> channelFactory;
    private final EventLoopGroup group;
    private final ProtocolNegotiator negotiator;
    private final String authorityString;
    private final AsciiString authority;
    private final AsciiString userAgent;
    private final int flowControlWindow;
    private final int maxMessageSize;
    private final int maxHeaderListSize;
    private KeepAliveManager keepAliveManager;
    private final long keepAliveTimeNanos;
    private final long keepAliveTimeoutNanos;
    private final boolean keepAliveWithoutCalls;
    private final AsciiString negotiationScheme;
    private final Runnable tooManyPingsRunnable;
    private NettyClientHandler handler;
    // We should not send on the channel until negotiation completes. This is a hard requirement
    // by SslHandler but is appropriate for HTTP/1.1 Upgrade as well.
    private Channel channel;
    /** If {@link #start} has been called, non-{@code null} if channel is {@code null}. */
    private Status statusExplainingWhyTheChannelIsNull;
    /** Since not thread-safe, may only be used from event loop. */
    private ClientTransportLifecycleManager lifecycleManager;
    /** Since not thread-safe, may only be used from event loop. */
    private final TransportTracer transportTracer;
    private final Attributes eagAttributes;
    private final LocalSocketPicker localSocketPicker;
    private final ChannelLogger channelLogger;

    NettyClientTransport(SocketAddress address, ChannelFactory<? extends Channel> channelFactory,
            Map<ChannelOption<?>, ?> channelOptions, EventLoopGroup group, ProtocolNegotiator negotiator,
            int flowControlWindow, int maxMessageSize, int maxHeaderListSize, long keepAliveTimeNanos,
            long keepAliveTimeoutNanos, boolean keepAliveWithoutCalls, String authority, @Nullable String userAgent,
            Runnable tooManyPingsRunnable, TransportTracer transportTracer, Attributes eagAttributes,
            LocalSocketPicker localSocketPicker, ChannelLogger channelLogger) {
        this.negotiator = Preconditions.checkNotNull(negotiator, "negotiator");
        this.negotiationScheme = this.negotiator.scheme();
        this.remoteAddress = Preconditions.checkNotNull(address, "address");
        this.group = Preconditions.checkNotNull(group, "group");
        this.channelFactory = channelFactory;
        this.channelOptions = Preconditions.checkNotNull(channelOptions, "channelOptions");
        this.flowControlWindow = flowControlWindow;
        this.maxMessageSize = maxMessageSize;
        this.maxHeaderListSize = maxHeaderListSize;
        this.keepAliveTimeNanos = keepAliveTimeNanos;
        this.keepAliveTimeoutNanos = keepAliveTimeoutNanos;
        this.keepAliveWithoutCalls = keepAliveWithoutCalls;
        this.authorityString = authority;
        this.authority = new AsciiString(authority);
        this.userAgent = new AsciiString(GrpcUtil.getGrpcUserAgent("netty", userAgent));
        this.tooManyPingsRunnable = Preconditions.checkNotNull(tooManyPingsRunnable, "tooManyPingsRunnable");
        this.transportTracer = Preconditions.checkNotNull(transportTracer, "transportTracer");
        this.eagAttributes = Preconditions.checkNotNull(eagAttributes, "eagAttributes");
        this.localSocketPicker = Preconditions.checkNotNull(localSocketPicker, "localSocketPicker");
        this.logId = InternalLogId.allocate(getClass(), remoteAddress.toString());
        this.channelLogger = Preconditions.checkNotNull(channelLogger, "channelLogger");
    }

    @Override
    public void ping(final PingCallback callback, final Executor executor) {
        if (channel == null) {
            executor.execute(new Runnable() {
                @Override
                public void run() {
                    callback.onFailure(statusExplainingWhyTheChannelIsNull.asException());
                }
            });
            return;
        }
        // The promise and listener always succeed in NettyClientHandler. So this listener handles the
        // error case, when the channel is closed and the NettyClientHandler no longer in the pipeline.
        ChannelFutureListener failureListener = new ChannelFutureListener() {
            @Override
            public void operationComplete(ChannelFuture future) throws Exception {
                if (!future.isSuccess()) {
                    Status s = statusFromFailedFuture(future);
                    Http2Ping.notifyFailed(callback, executor, s.asException());
                }
            }
        };
        // Write the command requesting the ping
        handler.getWriteQueue().enqueue(new SendPingCommand(callback, executor), true).addListener(failureListener);
    }

    @Override
    public ClientStream newStream(MethodDescriptor<?, ?> method, Metadata headers, CallOptions callOptions) {
        Preconditions.checkNotNull(method, "method");
        Preconditions.checkNotNull(headers, "headers");
        if (channel == null) {
            return new FailingClientStream(statusExplainingWhyTheChannelIsNull);
        }
        StatsTraceContext statsTraceCtx = StatsTraceContext.newClientContext(callOptions, getAttributes(), headers);
        return new NettyClientStream(new NettyClientStream.TransportState(handler, channel.eventLoop(),
                maxMessageSize, statsTraceCtx, transportTracer, method.getFullMethodName()) {
            @Override
            protected Status statusFromFailedFuture(ChannelFuture f) {
                return NettyClientTransport.this.statusFromFailedFuture(f);
            }
        }, method, headers, channel, authority, negotiationScheme, userAgent, statsTraceCtx, transportTracer,
                callOptions);
    }

    @SuppressWarnings("unchecked")
    @Override
    public Runnable start(Listener transportListener) {
        lifecycleManager = new ClientTransportLifecycleManager(
                Preconditions.checkNotNull(transportListener, "listener"));
        EventLoop eventLoop = group.next();
        if (keepAliveTimeNanos != KEEPALIVE_TIME_NANOS_DISABLED) {
            keepAliveManager = new KeepAliveManager(new ClientKeepAlivePinger(this), eventLoop, keepAliveTimeNanos,
                    keepAliveTimeoutNanos, keepAliveWithoutCalls);
        }

        handler = NettyClientHandler.newHandler(lifecycleManager, keepAliveManager, flowControlWindow,
                maxHeaderListSize, GrpcUtil.STOPWATCH_SUPPLIER, tooManyPingsRunnable, transportTracer,
                eagAttributes, authorityString);
        NettyHandlerSettings.setAutoWindow(handler);

        ChannelHandler negotiationHandler = negotiator.newHandler(handler);

        Bootstrap b = new Bootstrap();
        b.attr(LOGGER_KEY, channelLogger);
        b.group(eventLoop);
        b.channelFactory(channelFactory);
        // For non-socket based channel, the option will be ignored.
        b.option(SO_KEEPALIVE, true);
        // For non-epoll based channel, the option will be ignored.
        if (keepAliveTimeNanos != KEEPALIVE_TIME_NANOS_DISABLED) {
            ChannelOption<Integer> tcpUserTimeout = Utils.maybeGetTcpUserTimeoutOption();
            if (tcpUserTimeout != null) {
                b.option(tcpUserTimeout, (int) TimeUnit.NANOSECONDS.toMillis(keepAliveTimeoutNanos));
            }
        }
        for (Map.Entry<ChannelOption<?>, ?> entry : channelOptions.entrySet()) {
            // Every entry in the map is obtained from
            // NettyChannelBuilder#withOption(ChannelOption<T> option, T value)
            // so it is safe to pass the key-value pair to b.option().
            b.option((ChannelOption<Object>) entry.getKey(), entry.getValue());
        }

        ChannelHandler bufferingHandler = new WriteBufferingAndExceptionHandler(negotiationHandler);

        /**
         * We don't use a ChannelInitializer in the client bootstrap because its "initChannel" method
         * is executed in the event loop and we need this handler to be in the pipeline immediately so
         * that it may begin buffering writes.
         */
        b.handler(bufferingHandler);
        ChannelFuture regFuture = b.register();
        if (regFuture.isDone() && !regFuture.isSuccess()) {
            channel = null;
            // Initialization has failed badly. All new streams should be made to fail.
            Throwable t = regFuture.cause();
            if (t == null) {
                t = new IllegalStateException("Channel is null, but future doesn't have a cause");
            }
            statusExplainingWhyTheChannelIsNull = Utils.statusFromThrowable(t);
            // Use a Runnable since lifecycleManager calls transportListener
            return new Runnable() {
                @Override
                public void run() {
                    // NOTICE: we not are calling lifecycleManager from the event loop. But there isn't really
                    // an event loop in this case, so nothing should be accessing the lifecycleManager. We
                    // could use GlobalEventExecutor (which is what regFuture would use for notifying
                    // listeners in this case), but avoiding on-demand thread creation in an error case seems
                    // a good idea and is probably clearer threading.
                    lifecycleManager.notifyTerminated(statusExplainingWhyTheChannelIsNull);
                }
            };
        }
        channel = regFuture.channel();
        // Start the write queue as soon as the channel is constructed
        handler.startWriteQueue(channel);
        // This write will have no effect, yet it will only complete once the negotiationHandler
        // flushes any pending writes. We need it to be staged *before* the `connect` so that
        // the channel can't have been closed yet, removing all handlers. This write will sit in the
        // AbstractBufferingHandler's buffer, and will either be flushed on a successful connection,
        // or failed if the connection fails.
        channel.writeAndFlush(NettyClientHandler.NOOP_MESSAGE).addListener(new ChannelFutureListener() {
            @Override
            public void operationComplete(ChannelFuture future) throws Exception {
                if (!future.isSuccess()) {
                    // Need to notify of this failure, because NettyClientHandler may not have been added to
                    // the pipeline before the error occurred.
                    lifecycleManager.notifyTerminated(Utils.statusFromThrowable(future.cause()));
                }
            }
        });
        // Start the connection operation to the server.
        SocketAddress localAddress = localSocketPicker.createSocketAddress(remoteAddress, eagAttributes);
        if (localAddress != null) {
            channel.connect(remoteAddress, localAddress);
        } else {
            channel.connect(remoteAddress);
        }

        if (keepAliveManager != null) {
            keepAliveManager.onTransportStarted();
        }

        return null;
    }

    @Override
    public void shutdown(Status reason) {
        // start() could have failed
        if (channel == null) {
            return;
        }
        // Notifying of termination is automatically done when the channel closes.
        if (channel.isOpen()) {
            handler.getWriteQueue().enqueue(new GracefulCloseCommand(reason), true);
        }
    }

    @Override
    public void shutdownNow(final Status reason) {
        // Notifying of termination is automatically done when the channel closes.
        if (channel != null && channel.isOpen()) {
            handler.getWriteQueue().enqueue(new Runnable() {
                @Override
                public void run() {
                    lifecycleManager.notifyShutdown(reason);
                    // Call close() directly since negotiation may not have completed, such that a write would
                    // be queued.
                    channel.close();
                    channel.write(new ForcefulCloseCommand(reason));
                }
            }, true);
        }
    }

    @Override
    public String toString() {
        return MoreObjects.toStringHelper(this).add("logId", logId.getId()).add("remoteAddress", remoteAddress)
                .add("channel", channel).toString();
    }

    @Override
    public InternalLogId getLogId() {
        return logId;
    }

    @Override
    public Attributes getAttributes() {
        return handler.getAttributes();
    }

    @Override
    public ListenableFuture<SocketStats> getStats() {
        final SettableFuture<SocketStats> result = SettableFuture.create();
        if (channel.eventLoop().inEventLoop()) {
            // This is necessary, otherwise we will block forever if we get the future from inside
            // the event loop.
            result.set(getStatsHelper(channel));
            return result;
        }
        channel.eventLoop().submit(new Runnable() {
            @Override
            public void run() {
                result.set(getStatsHelper(channel));
            }
        }).addListener(new GenericFutureListener<Future<Object>>() {
            @Override
            public void operationComplete(Future<Object> future) throws Exception {
                if (!future.isSuccess()) {
                    result.setException(future.cause());
                }
            }
        });
        return result;
    }

    private SocketStats getStatsHelper(Channel ch) {
        assert ch.eventLoop().inEventLoop();
        return new SocketStats(transportTracer.getStats(), channel.localAddress(), channel.remoteAddress(),
                Utils.getSocketOptions(ch), handler == null ? null : handler.getSecurityInfo());
    }

    @VisibleForTesting
    Channel channel() {
        return channel;
    }

    @VisibleForTesting
    KeepAliveManager keepAliveManager() {
        return keepAliveManager;
    }

    /**
     * Convert ChannelFuture.cause() to a Status, taking into account that all handlers are removed
     * from the pipeline when the channel is closed. Since handlers are removed, you may get an
     * unhelpful exception like ClosedChannelException.
     *
     * <p>This method must only be called on the event loop.
     */
    private Status statusFromFailedFuture(ChannelFuture f) {
        Throwable t = f.cause();
        if (t instanceof ClosedChannelException
                // Exception thrown by the StreamBufferingEncoder if the channel is closed while there
                // are still streams buffered. This exception is not helpful. Replace it by the real
                // cause of the shutdown (if available).
                || t instanceof Http2ChannelClosedException) {
            Status shutdownStatus = lifecycleManager.getShutdownStatus();
            if (shutdownStatus == null) {
                return Status.UNKNOWN.withDescription("Channel closed but for unknown reason")
                        .withCause(new ClosedChannelException().initCause(t));
            }
            return shutdownStatus;
        }
        return Utils.statusFromThrowable(t);
    }
}