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