io.grpc.netty.Utils.java Source code

Java tutorial

Introduction

Here is the source code for io.grpc.netty.Utils.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 com.google.common.base.Preconditions.checkState;
import static io.grpc.internal.GrpcUtil.CONTENT_TYPE_KEY;
import static io.grpc.internal.TransportFrameUtil.toHttp2Headers;
import static io.grpc.internal.TransportFrameUtil.toRawSerializedHeaders;
import static io.netty.channel.ChannelOption.SO_LINGER;
import static io.netty.channel.ChannelOption.SO_TIMEOUT;
import static io.netty.util.CharsetUtil.UTF_8;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import io.grpc.InternalChannelz;
import io.grpc.InternalMetadata;
import io.grpc.Metadata;
import io.grpc.Status;
import io.grpc.internal.GrpcUtil;
import io.grpc.internal.SharedResourceHolder.Resource;
import io.grpc.netty.GrpcHttp2HeadersUtils.GrpcHttp2InboundHeaders;
import io.grpc.netty.NettySocketSupport.NativeSocketOptions;
import io.netty.channel.Channel;
import io.netty.channel.ChannelConfig;
import io.netty.channel.ChannelFactory;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.ReflectiveChannelFactory;
import io.netty.channel.ServerChannel;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.http2.Http2Exception;
import io.netty.handler.codec.http2.Http2Headers;
import io.netty.util.AsciiString;
import io.netty.util.concurrent.DefaultThreadFactory;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.nio.channels.ClosedChannelException;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.CheckReturnValue;
import javax.annotation.Nullable;

/**
 * Common utility methods.
 */
class Utils {
    private static final Logger logger = Logger.getLogger(Utils.class.getName());

    public static final AsciiString STATUS_OK = AsciiString.of("200");
    public static final AsciiString HTTP_METHOD = AsciiString.of(GrpcUtil.HTTP_METHOD);
    public static final AsciiString HTTP_GET_METHOD = AsciiString.of("GET");
    public static final AsciiString HTTPS = AsciiString.of("https");
    public static final AsciiString HTTP = AsciiString.of("http");
    public static final AsciiString CONTENT_TYPE_HEADER = AsciiString.of(CONTENT_TYPE_KEY.name());
    public static final AsciiString CONTENT_TYPE_GRPC = AsciiString.of(GrpcUtil.CONTENT_TYPE_GRPC);
    public static final AsciiString TE_HEADER = AsciiString.of(GrpcUtil.TE_HEADER.name());
    public static final AsciiString TE_TRAILERS = AsciiString.of(GrpcUtil.TE_TRAILERS);
    public static final AsciiString USER_AGENT = AsciiString.of(GrpcUtil.USER_AGENT_KEY.name());
    public static final Resource<EventLoopGroup> NIO_BOSS_EVENT_LOOP_GROUP = new DefaultEventLoopGroupResource(1,
            "grpc-nio-boss-ELG", EventLoopGroupType.NIO);
    public static final Resource<EventLoopGroup> NIO_WORKER_EVENT_LOOP_GROUP = new DefaultEventLoopGroupResource(0,
            "grpc-nio-worker-ELG", EventLoopGroupType.NIO);
    public static final Resource<EventLoopGroup> DEFAULT_BOSS_EVENT_LOOP_GROUP;
    public static final Resource<EventLoopGroup> DEFAULT_WORKER_EVENT_LOOP_GROUP;

    public static final ChannelFactory<? extends ServerChannel> DEFAULT_SERVER_CHANNEL_FACTORY;
    public static final Class<? extends Channel> DEFAULT_CLIENT_CHANNEL_TYPE;

    @Nullable
    private static final Constructor<? extends EventLoopGroup> EPOLL_EVENT_LOOP_GROUP_CONSTRUCTOR;

    static {
        // Decide default channel types and EventLoopGroup based on Epoll availability
        if (isEpollAvailable()) {
            DEFAULT_CLIENT_CHANNEL_TYPE = epollChannelType();
            DEFAULT_SERVER_CHANNEL_FACTORY = new ReflectiveChannelFactory<>(epollServerChannelType());
            EPOLL_EVENT_LOOP_GROUP_CONSTRUCTOR = epollEventLoopGroupConstructor();
            DEFAULT_BOSS_EVENT_LOOP_GROUP = new DefaultEventLoopGroupResource(1, "grpc-default-boss-ELG",
                    EventLoopGroupType.EPOLL);
            DEFAULT_WORKER_EVENT_LOOP_GROUP = new DefaultEventLoopGroupResource(0, "grpc-default-worker-ELG",
                    EventLoopGroupType.EPOLL);
        } else {
            logger.log(Level.FINE, "Epoll is not available, using Nio.", getEpollUnavailabilityCause());
            DEFAULT_SERVER_CHANNEL_FACTORY = nioServerChannelFactory();
            DEFAULT_CLIENT_CHANNEL_TYPE = NioSocketChannel.class;
            DEFAULT_BOSS_EVENT_LOOP_GROUP = NIO_BOSS_EVENT_LOOP_GROUP;
            DEFAULT_WORKER_EVENT_LOOP_GROUP = NIO_WORKER_EVENT_LOOP_GROUP;
            EPOLL_EVENT_LOOP_GROUP_CONSTRUCTOR = null;
        }
    }

    public static Metadata convertHeaders(Http2Headers http2Headers) {
        if (http2Headers instanceof GrpcHttp2InboundHeaders) {
            GrpcHttp2InboundHeaders h = (GrpcHttp2InboundHeaders) http2Headers;
            return InternalMetadata.newMetadata(h.numHeaders(), h.namesAndValues());
        }
        return InternalMetadata.newMetadata(convertHeadersToArray(http2Headers));
    }

    @CheckReturnValue
    private static byte[][] convertHeadersToArray(Http2Headers http2Headers) {
        // The Netty AsciiString class is really just a wrapper around a byte[] and supports
        // arbitrary binary data, not just ASCII.
        byte[][] headerValues = new byte[http2Headers.size() * 2][];
        int i = 0;
        for (Map.Entry<CharSequence, CharSequence> entry : http2Headers) {
            headerValues[i++] = bytes(entry.getKey());
            headerValues[i++] = bytes(entry.getValue());
        }
        return toRawSerializedHeaders(headerValues);
    }

    private static byte[] bytes(CharSequence seq) {
        if (seq instanceof AsciiString) {
            // Fast path - sometimes copy.
            AsciiString str = (AsciiString) seq;
            return str.isEntireArrayUsed() ? str.array() : str.toByteArray();
        }
        // Slow path - copy.
        return seq.toString().getBytes(UTF_8);
    }

    public static Http2Headers convertClientHeaders(Metadata headers, AsciiString scheme, AsciiString defaultPath,
            AsciiString authority, AsciiString method, AsciiString userAgent) {
        Preconditions.checkNotNull(defaultPath, "defaultPath");
        Preconditions.checkNotNull(authority, "authority");
        Preconditions.checkNotNull(method, "method");

        // Discard any application supplied duplicates of the reserved headers
        headers.discardAll(CONTENT_TYPE_KEY);
        headers.discardAll(GrpcUtil.TE_HEADER);
        headers.discardAll(GrpcUtil.USER_AGENT_KEY);

        return GrpcHttp2OutboundHeaders.clientRequestHeaders(toHttp2Headers(headers), authority, defaultPath,
                method, scheme, userAgent);
    }

    public static Http2Headers convertServerHeaders(Metadata headers) {
        // Discard any application supplied duplicates of the reserved headers
        headers.discardAll(CONTENT_TYPE_KEY);
        headers.discardAll(GrpcUtil.TE_HEADER);
        headers.discardAll(GrpcUtil.USER_AGENT_KEY);

        return GrpcHttp2OutboundHeaders.serverResponseHeaders(toHttp2Headers(headers));
    }

    public static Metadata convertTrailers(Http2Headers http2Headers) {
        if (http2Headers instanceof GrpcHttp2InboundHeaders) {
            GrpcHttp2InboundHeaders h = (GrpcHttp2InboundHeaders) http2Headers;
            return InternalMetadata.newMetadata(h.numHeaders(), h.namesAndValues());
        }
        return InternalMetadata.newMetadata(convertHeadersToArray(http2Headers));
    }

    public static Http2Headers convertTrailers(Metadata trailers, boolean headersSent) {
        if (!headersSent) {
            return convertServerHeaders(trailers);
        }
        return GrpcHttp2OutboundHeaders.serverResponseTrailers(toHttp2Headers(trailers));
    }

    public static Status statusFromThrowable(Throwable t) {
        Status s = Status.fromThrowable(t);
        if (s.getCode() != Status.Code.UNKNOWN) {
            return s;
        }
        if (t instanceof ClosedChannelException) {
            // ClosedChannelException is used any time the Netty channel is closed. Proper error
            // processing requires remembering the error that occurred before this one and using it
            // instead.
            //
            // Netty uses an exception that has no stack trace, while we would never hope to show this to
            // users, if it happens having the extra information may provide a small hint of where to
            // look.
            ClosedChannelException extraT = new ClosedChannelException();
            extraT.initCause(t);
            return Status.UNKNOWN.withDescription("channel closed").withCause(extraT);
        }
        if (t instanceof IOException) {
            return Status.UNAVAILABLE.withDescription("io exception").withCause(t);
        }
        if (t instanceof Http2Exception) {
            return Status.INTERNAL.withDescription("http2 exception").withCause(t);
        }
        return s;
    }

    @VisibleForTesting
    static boolean isEpollAvailable() {
        try {
            return (boolean) (Boolean) Class.forName("io.netty.channel.epoll.Epoll")
                    .getDeclaredMethod("isAvailable").invoke(null);
        } catch (ClassNotFoundException e) {
            // this is normal if netty-epoll runtime dependency doesn't exist.
            return false;
        } catch (Exception e) {
            throw new RuntimeException("Exception while checking Epoll availability", e);
        }
    }

    private static Throwable getEpollUnavailabilityCause() {
        try {
            return (Throwable) Class.forName("io.netty.channel.epoll.Epoll")
                    .getDeclaredMethod("unavailabilityCause").invoke(null);
        } catch (Exception e) {
            return e;
        }
    }

    // Must call when epoll is available
    private static Class<? extends Channel> epollChannelType() {
        try {
            Class<? extends Channel> channelType = Class.forName("io.netty.channel.epoll.EpollSocketChannel")
                    .asSubclass(Channel.class);
            return channelType;
        } catch (ClassNotFoundException e) {
            throw new RuntimeException("Cannot load EpollSocketChannel", e);
        }
    }

    // Must call when epoll is available
    private static Constructor<? extends EventLoopGroup> epollEventLoopGroupConstructor() {
        try {
            return Class.forName("io.netty.channel.epoll.EpollEventLoopGroup").asSubclass(EventLoopGroup.class)
                    .getConstructor(Integer.TYPE, ThreadFactory.class);
        } catch (ClassNotFoundException e) {
            throw new RuntimeException("Cannot load EpollEventLoopGroup", e);
        } catch (NoSuchMethodException e) {
            throw new RuntimeException("EpollEventLoopGroup constructor not found", e);
        }
    }

    // Must call when epoll is available
    private static Class<? extends ServerChannel> epollServerChannelType() {
        try {
            Class<? extends ServerChannel> serverSocketChannel = Class
                    .forName("io.netty.channel.epoll.EpollServerSocketChannel").asSubclass(ServerChannel.class);
            return serverSocketChannel;
        } catch (ClassNotFoundException e) {
            throw new RuntimeException("Cannot load EpollServerSocketChannel", e);
        }
    }

    private static EventLoopGroup createEpollEventLoopGroup(int parallelism, ThreadFactory threadFactory) {
        checkState(EPOLL_EVENT_LOOP_GROUP_CONSTRUCTOR != null, "Epoll is not available");

        try {
            return EPOLL_EVENT_LOOP_GROUP_CONSTRUCTOR.newInstance(parallelism, threadFactory);
        } catch (Exception e) {
            throw new RuntimeException("Cannot create Epoll EventLoopGroup", e);
        }
    }

    private static ChannelFactory<ServerChannel> nioServerChannelFactory() {
        return new ChannelFactory<ServerChannel>() {
            @Override
            public ServerChannel newChannel() {
                return new NioServerSocketChannel();
            }
        };
    }

    /**
     * Returns TCP_USER_TIMEOUT channel option for Epoll channel if Epoll is available, otherwise
     * null.
     */
    @Nullable
    static ChannelOption<Integer> maybeGetTcpUserTimeoutOption() {
        return getEpollChannelOption("TCP_USER_TIMEOUT");
    }

    @Nullable
    @SuppressWarnings("unchecked")
    private static <T> ChannelOption<T> getEpollChannelOption(String optionName) {
        if (isEpollAvailable()) {
            try {
                return (ChannelOption<T>) Class.forName("io.netty.channel.epoll.EpollChannelOption")
                        .getField(optionName).get(null);
            } catch (Exception e) {
                throw new RuntimeException("ChannelOption(" + optionName + ") is not available", e);
            }
        }
        return null;
    }

    private static final class DefaultEventLoopGroupResource implements Resource<EventLoopGroup> {
        private final String name;
        private final int numEventLoops;
        private final EventLoopGroupType eventLoopGroupType;

        DefaultEventLoopGroupResource(int numEventLoops, String name, EventLoopGroupType eventLoopGroupType) {
            this.name = name;
            this.numEventLoops = numEventLoops;
            this.eventLoopGroupType = eventLoopGroupType;
        }

        @Override
        public EventLoopGroup create() {
            // Use Netty's DefaultThreadFactory in order to get the benefit of FastThreadLocal.
            ThreadFactory threadFactory = new DefaultThreadFactory(name, /* daemon= */ true);
            switch (eventLoopGroupType) {
            case NIO:
                return new NioEventLoopGroup(numEventLoops, threadFactory);
            case EPOLL:
                return createEpollEventLoopGroup(numEventLoops, threadFactory);
            default:
                throw new AssertionError("Unknown/Unsupported EventLoopGroupType: " + eventLoopGroupType);
            }
        }

        @Override
        public void close(EventLoopGroup instance) {
            instance.shutdownGracefully(0, 0, TimeUnit.SECONDS);
        }

        @Override
        public String toString() {
            return name;
        }
    }

    static InternalChannelz.SocketOptions getSocketOptions(Channel channel) {
        ChannelConfig config = channel.config();
        InternalChannelz.SocketOptions.Builder b = new InternalChannelz.SocketOptions.Builder();

        // The API allows returning null but not sure if it can happen in practice.
        // Let's be paranoid and do null checking just in case.
        Integer lingerSeconds = config.getOption(SO_LINGER);
        if (lingerSeconds != null) {
            b.setSocketOptionLingerSeconds(lingerSeconds);
        }

        Integer timeoutMillis = config.getOption(SO_TIMEOUT);
        if (timeoutMillis != null) {
            // in java, SO_TIMEOUT only applies to receiving
            b.setSocketOptionTimeoutMillis(timeoutMillis);
        }

        for (Entry<ChannelOption<?>, Object> opt : config.getOptions().entrySet()) {
            ChannelOption<?> key = opt.getKey();
            // Constants are pooled, so there should only be one instance of each constant
            if (key.equals(SO_LINGER) || key.equals(SO_TIMEOUT)) {
                continue;
            }
            Object value = opt.getValue();
            // zpencer: Can a netty option be null?
            b.addOption(key.name(), String.valueOf(value));
        }

        NativeSocketOptions nativeOptions = NettySocketSupport.getNativeSocketOptions(channel);
        if (nativeOptions != null) {
            b.setTcpInfo(nativeOptions.tcpInfo); // may be null
            for (Entry<String, String> entry : nativeOptions.otherInfo.entrySet()) {
                b.addOption(entry.getKey(), entry.getValue());
            }
        }
        return b.build();
    }

    private enum EventLoopGroupType {
        NIO, EPOLL
    }

    private Utils() {
        // Prevents instantiation
    }
}