/** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you 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 org.jooby.internal.netty; import static javaslang.API.Case; import static javaslang.API.Match; import static javaslang.Predicates.is; import java.lang.reflect.Field; import java.lang.reflect.ParameterizedType; import java.util.Iterator; import java.util.Map; import java.util.Map.Entry; import java.util.Optional; import java.util.concurrent.Executor; import java.util.concurrent.ThreadFactory; import java.util.function.BiConsumer; import javax.inject.Inject; import org.jooby.spi.HttpHandler; import org.jooby.spi.Server; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.collect.ImmutableList; import com.google.common.collect.Maps; import com.typesafe.config.Config; import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.Channel; import io.netty.channel.ChannelOption; import io.netty.channel.EventLoopGroup; import io.netty.channel.epoll.Epoll; import io.netty.channel.epoll.EpollChannelOption; import io.netty.channel.epoll.EpollEventLoopGroup; import io.netty.channel.epoll.EpollServerSocketChannel; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.nio.NioServerSocketChannel; import io.netty.handler.logging.LogLevel; import io.netty.handler.logging.LoggingHandler; import io.netty.handler.ssl.SslContext; import io.netty.util.concurrent.DefaultEventExecutorGroup; import io.netty.util.concurrent.DefaultThreadFactory; import io.netty.util.concurrent.EventExecutorGroup; public class NettyServer implements Server { /** The logging system. */ private final Logger log = LoggerFactory.getLogger(Server.class); private EventLoopGroup bossLoop; private EventLoopGroup workerLoop; private Channel ch; private Config conf; private HttpHandler dispatcher; private DefaultEventExecutorGroup executor; @Inject public NettyServer(final HttpHandler dispatcher, final Config config) { this.dispatcher = dispatcher; this.conf = config; } @Override public void start() throws Exception { int bossThreads = conf.getInt("netty.threads.Boss"); bossLoop = eventLoop(bossThreads, "boss"); int workerThreads = conf.getInt("netty.threads.Worker"); if (workerThreads > 0) { workerLoop = eventLoop(workerThreads, "worker"); } else { workerLoop = bossLoop; } ThreadFactory threadFactory = new DefaultThreadFactory(conf.getString("netty.threads.Name")); this.executor = new DefaultEventExecutorGroup(conf.getInt("netty.threads.Max"), threadFactory); this.ch = bootstrap(executor, null, conf.getInt("application.port")); boolean securePort = conf.hasPath("application.securePort"); if (securePort) { bootstrap(executor, NettySslContext.build(conf), conf.getInt("application.securePort")); } } private Channel bootstrap(final EventExecutorGroup executor, final SslContext sslCtx, final int port) throws InterruptedException { ServerBootstrap bootstrap = new ServerBootstrap(); boolean epoll = bossLoop instanceof EpollEventLoopGroup; bootstrap.group(bossLoop, workerLoop) .channel(epoll ? EpollServerSocketChannel.class : NioServerSocketChannel.class) .handler(new LoggingHandler(Server.class, LogLevel.DEBUG)) .childHandler(new NettyPipeline(executor, dispatcher, conf, sslCtx)); configure(conf.getConfig("netty.options"), "netty.options", (option, value) -> bootstrap.option(option, value)); configure(conf.getConfig("netty.worker.options"), "netty.worker.options", (option, value) -> bootstrap.childOption(option, value)); return bootstrap .bind(host(conf.getString("application.host")), port) .sync() .channel(); } private String host(final String host) { return "localhost".equals(host) ? "0.0.0.0" : host; } @Override public void stop() throws Exception { shutdownGracefully(ImmutableList.of(bossLoop, workerLoop, executor).iterator()); } @Override public void join() throws InterruptedException { ch.closeFuture().sync(); } @Override public Optional<Executor> executor() { return Optional.ofNullable(executor); } @SuppressWarnings({"rawtypes", "unchecked" }) private void configure(final Config config, final String path, final BiConsumer<ChannelOption<Object>, Object> setter) { config.entrySet().forEach(entry -> { Entry<ChannelOption, Class<?>> result = findOption(entry.getKey()); if (result != null) { ChannelOption option = result.getKey(); String optionName = entry.getKey(); Class<?> optionType = result.getValue(); Object value = Match(optionType).of( Case(is(Boolean.class), () -> config.getBoolean(optionName)), Case(is(Integer.class), () -> config.getInt(optionName)), Case(is(Long.class), () -> config.getLong(optionName))); log.debug("{}.{}({})", path, option, value); setter.accept(option, value); } else { log.error("Unknown option: {}.{}", path, entry.getKey()); } }); } @SuppressWarnings("rawtypes") private Map.Entry<ChannelOption, Class<?>> findOption(final String optionName) { try { Field field = EpollChannelOption.class.getField(optionName); ChannelOption option = (ChannelOption) field.get(null); Class optionType = (Class) ((ParameterizedType) field.getGenericType()) .getActualTypeArguments()[0]; return Maps.immutableEntry(option, optionType); } catch (NoSuchFieldException | SecurityException | IllegalAccessException ex) { return null; } } private EventLoopGroup eventLoop(final int threads, final String name) { log.debug("netty.threads.{}({})", name, threads); if (Epoll.isAvailable()) { return new EpollEventLoopGroup(threads, new DefaultThreadFactory("epoll-" + name, false)); } return new NioEventLoopGroup(threads, new DefaultThreadFactory("nio-" + name, false)); } /** * Shutdown executor in order. * * @param iterator Executors to shutdown. */ private void shutdownGracefully(final Iterator<EventExecutorGroup> iterator) { if (iterator.hasNext()) { EventExecutorGroup group = iterator.next(); if (!group.isShuttingDown()) { group.shutdownGracefully().addListener(future -> { if (!future.isSuccess()) { log.debug("shutdown of {} resulted in exception", group, future.cause()); } shutdownGracefully(iterator); }); } } } }