
co.cask.http.NettyHttpService Maven / Gradle / Ivy
The newest version!
/*
* Copyright © 2014-2017 Cask Data, Inc.
*
* 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 co.cask.http;
import co.cask.http.internal.BasicHandlerContext;
import co.cask.http.internal.HttpDispatcher;
import co.cask.http.internal.HttpResourceHandler;
import co.cask.http.internal.NonStickyEventExecutorGroup;
import co.cask.http.internal.RequestRouter;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.group.ChannelGroup;
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.http.HttpContentCompressor;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.HttpServerKeepAliveHandler;
import io.netty.handler.ssl.SslHandler;
import io.netty.handler.stream.ChunkedWriteHandler;
import io.netty.util.concurrent.EventExecutorGroup;
import io.netty.util.concurrent.Future;
import io.netty.util.concurrent.ImmediateEventExecutor;
import io.netty.util.concurrent.UnorderedThreadPoolEventExecutor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import javax.annotation.Nullable;
/**
* Webservice implemented using the netty framework. Implements Guava's Service interface to manage the states
* of the webservice.
*/
public final class NettyHttpService {
private static final Logger LOG = LoggerFactory.getLogger(NettyHttpService.class);
private enum State {
NOT_STARTED,
RUNNING,
STOPPED,
FAILED
}
private final String serviceName;
private final int bossThreadPoolSize;
private final int workerThreadPoolSize;
private final int execThreadPoolSize;
private final long execThreadKeepAliveSecs;
private final Map channelConfigs;
private final Map childChannelConfigs;
private final RejectedExecutionHandler rejectedExecutionHandler;
private final HandlerContext handlerContext;
private final HttpResourceHandler resourceHandler;
private final ChannelPipelineModifier pipelineModifier;
private final int httpChunkLimit;
private final SSLHandlerFactory sslHandlerFactory;
private State state;
private ServerBootstrap bootstrap;
private ChannelGroup channelGroup;
private EventExecutorGroup eventExecutorGroup;
private InetSocketAddress bindAddress;
/**
* Initialize NettyHttpService. Also includes SSL implementation.
*
* @param serviceName name of this service. Threads created for this service will be prefixed with the given name.
* @param bindAddress Address for the service to bind to.
* @param bossThreadPoolSize Size of the boss thread pool.
* @param workerThreadPoolSize Size of the worker thread pool.
* @param execThreadPoolSize Size of the thread pool for the executor.
* @param execThreadKeepAliveSecs maximum time that excess idle threads will wait for new tasks before terminating.
* @param channelConfigs Configurations for the server socket channel.
* @param rejectedExecutionHandler rejection policy for executor.
* @param urlRewriter URLRewriter to rewrite incoming URLs.
* @param httpHandlers HttpHandlers to handle the calls.
* @param handlerHooks Hooks to be called before/after request processing by httpHandlers.
* @param pipelineModifier Function used to modify the pipeline.
* @param sslHandlerFactory Object used to share SSL certificate details
* @param exceptionHandler Handles exceptions from calling handler methods
*/
private NettyHttpService(String serviceName,
InetSocketAddress bindAddress, int bossThreadPoolSize, int workerThreadPoolSize,
int execThreadPoolSize, long execThreadKeepAliveSecs,
Map channelConfigs,
Map childChannelConfigs,
RejectedExecutionHandler rejectedExecutionHandler, URLRewriter urlRewriter,
Iterable extends HttpHandler> httpHandlers,
Iterable extends HandlerHook> handlerHooks, int httpChunkLimit,
ChannelPipelineModifier pipelineModifier,
SSLHandlerFactory sslHandlerFactory, ExceptionHandler exceptionHandler) {
this.serviceName = serviceName;
this.bindAddress = bindAddress;
this.bossThreadPoolSize = bossThreadPoolSize;
this.workerThreadPoolSize = workerThreadPoolSize;
this.execThreadPoolSize = execThreadPoolSize;
this.execThreadKeepAliveSecs = execThreadKeepAliveSecs;
this.channelConfigs = new HashMap<>(channelConfigs);
this.childChannelConfigs = new HashMap<>(childChannelConfigs);
this.rejectedExecutionHandler = rejectedExecutionHandler;
this.resourceHandler = new HttpResourceHandler(httpHandlers, handlerHooks, urlRewriter, exceptionHandler);
this.handlerContext = new BasicHandlerContext(this.resourceHandler);
this.httpChunkLimit = httpChunkLimit;
this.pipelineModifier = pipelineModifier;
this.sslHandlerFactory = sslHandlerFactory;
this.state = State.NOT_STARTED;
}
/**
* Creates a {@link Builder} for creating new instance of {@link NettyHttpService}.
*
* @param serviceName name of the http service. The name will be used to name threads created for the service.
* @return builder for creating a NettyHttpService
*/
public static Builder builder(String serviceName) {
return new Builder(serviceName);
}
/**
* Starts the HTTP service.
*
* @throws Exception if the service failed to started
*/
public synchronized void start() throws Exception {
if (state == State.RUNNING) {
LOG.debug("Ignore start() call on HTTP service {} since it has already been started.", serviceName);
return;
}
if (state != State.NOT_STARTED) {
if (state == State.STOPPED) {
throw new IllegalStateException("Cannot start the HTTP service "
+ serviceName + " again since it has been stopped");
}
throw new IllegalStateException("Cannot start the HTTP service "
+ serviceName + " because it was failed earlier");
}
try {
LOG.info("Starting HTTP Service {} at address {}", serviceName, bindAddress);
channelGroup = new DefaultChannelGroup(ImmediateEventExecutor.INSTANCE);
resourceHandler.init(handlerContext);
eventExecutorGroup = createEventExecutorGroup(execThreadPoolSize);
bootstrap = createBootstrap(channelGroup);
Channel serverChannel = bootstrap.bind(bindAddress).sync().channel();
channelGroup.add(serverChannel);
bindAddress = (InetSocketAddress) serverChannel.localAddress();
LOG.debug("Started HTTP Service {} at address {}", serviceName, bindAddress);
state = State.RUNNING;
} catch (Throwable t) {
// Release resources if there is any failure
channelGroup.close().awaitUninterruptibly();
try {
if (bootstrap != null) {
shutdownExecutorGroups(0, 5, TimeUnit.SECONDS,
bootstrap.config().group(), bootstrap.config().childGroup(), eventExecutorGroup);
} else {
shutdownExecutorGroups(0, 5, TimeUnit.SECONDS, eventExecutorGroup);
}
} catch (Throwable t2) {
t.addSuppressed(t2);
}
state = State.FAILED;
throw t;
}
}
/**
* @return port where the service is running.
*/
public InetSocketAddress getBindAddress() {
return bindAddress;
}
/**
* @return the name of the HTTP service.
*/
public String getServiceName() {
return serviceName;
}
/**
* Stops the HTTP service gracefully and release all resources. Same as calling {@link #stop(long, long, TimeUnit)}
* with {@code 0} second quiet period and {@code 5} seconds timeout.
*
* @throws Exception if there is exception raised during shutdown.
*/
public void stop() throws Exception {
stop(0, 5, TimeUnit.SECONDS);
}
/**
* Stops the HTTP service gracefully and release all resources.
*
* @param quietPeriod the quiet period as described in the documentation of {@link EventExecutorGroup}
* @param timeout the maximum amount of time to wait until the executor is
* {@linkplain EventExecutorGroup#shutdown()}
* regardless if a task was submitted during the quiet period
* @param unit the unit of {@code quietPeriod} and {@code timeout}
* @throws Exception if there is exception raised during shutdown.
*/
public synchronized void stop(long quietPeriod, long timeout, TimeUnit unit) throws Exception {
if (state == State.STOPPED) {
LOG.debug("Ignore stop() call on HTTP service {} since it has already been stopped.", serviceName);
return;
}
LOG.info("Stopping HTTP Service {}", serviceName);
try {
try {
channelGroup.close().awaitUninterruptibly();
} finally {
try {
shutdownExecutorGroups(quietPeriod, timeout, unit,
bootstrap.config().group(), bootstrap.config().childGroup(), eventExecutorGroup);
} finally {
resourceHandler.destroy(handlerContext);
}
}
} catch (Throwable t) {
state = State.FAILED;
throw t;
}
state = State.STOPPED;
LOG.debug("Stopped HTTP Service {} on address {}", serviceName, bindAddress);
}
/**
* Create {@link EventExecutorGroup} for executing handle methods.
*
* @param size size of threadPool
* @return instance of {@link EventExecutorGroup} or {@code null} if {@code size} is {@code <= 0}.
*/
@Nullable
private EventExecutorGroup createEventExecutorGroup(int size) {
if (size <= 0) {
return null;
}
ThreadFactory threadFactory = new ThreadFactory() {
private final ThreadGroup threadGroup = new ThreadGroup(serviceName + "-executor-thread");
private final AtomicLong count = new AtomicLong(0);
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(threadGroup, r, String.format("%s-executor-%d", serviceName, count.getAndIncrement()));
t.setDaemon(true);
return t;
}
};
UnorderedThreadPoolEventExecutor executor = new UnorderedThreadPoolEventExecutor(size, threadFactory,
rejectedExecutionHandler);
if (execThreadKeepAliveSecs > 0) {
executor.setKeepAliveTime(execThreadKeepAliveSecs, TimeUnit.SECONDS);
executor.allowCoreThreadTimeOut(true);
}
return new NonStickyEventExecutorGroup(executor);
}
/**
* Returns a {@link ThreadFactory} that creates daemon threads.
*
* @param nameFormat a format string to be used with {@link String#format(String, Object...)}
* with one integer argument representing the number of threads created by the factory so far.
* @return a {@link ThreadFactory}
*/
private ThreadFactory createDaemonThreadFactory(final String nameFormat) {
return new ThreadFactory() {
private final AtomicInteger count = new AtomicInteger();
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
t.setName(String.format(nameFormat, count.getAndIncrement()));
t.setDaemon(true);
return t;
}
};
}
/**
* Creates the server bootstrap.
*/
private ServerBootstrap createBootstrap(final ChannelGroup channelGroup) throws Exception {
EventLoopGroup bossGroup = new NioEventLoopGroup(bossThreadPoolSize,
createDaemonThreadFactory(serviceName + "-boss-thread-%d"));
EventLoopGroup workerGroup = new NioEventLoopGroup(workerThreadPoolSize,
createDaemonThreadFactory(serviceName + "-worker-thread-%d"));
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap
.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
channelGroup.add(ch);
ChannelPipeline pipeline = ch.pipeline();
if (sslHandlerFactory != null) {
// Add SSLHandler if SSL is enabled
pipeline.addLast("ssl", sslHandlerFactory.create(ch.alloc()));
}
pipeline.addLast("codec", new HttpServerCodec());
pipeline.addLast("compressor", new HttpContentCompressor());
pipeline.addLast("chunkedWriter", new ChunkedWriteHandler());
pipeline.addLast("keepAlive", new HttpServerKeepAliveHandler());
pipeline.addLast("router", new RequestRouter(resourceHandler, httpChunkLimit, sslHandlerFactory != null));
if (eventExecutorGroup == null) {
pipeline.addLast("dispatcher", new HttpDispatcher());
} else {
pipeline.addLast(eventExecutorGroup, "dispatcher", new HttpDispatcher());
}
if (pipelineModifier != null) {
pipelineModifier.modify(pipeline);
}
}
});
for (Map.Entry entry : channelConfigs.entrySet()) {
bootstrap.option(entry.getKey(), entry.getValue());
}
for (Map.Entry entry : childChannelConfigs.entrySet()) {
bootstrap.childOption(entry.getKey(), entry.getValue());
}
return bootstrap;
}
/**
* Shutdown the given list of {@link EventExecutorGroup}s gracefully.
*/
private void shutdownExecutorGroups(long quietPeriod, long timeout, TimeUnit unit, EventExecutorGroup...groups) {
Exception ex = null;
List> futures = new ArrayList<>();
for (EventExecutorGroup group : groups) {
if (group == null) {
continue;
}
futures.add(group.shutdownGracefully(quietPeriod, timeout, unit));
}
for (Future> future : futures) {
try {
future.syncUninterruptibly();
} catch (Exception e) {
if (ex == null) {
ex = e;
} else {
ex.addSuppressed(e);
}
}
}
if (ex != null) {
// Just log, don't rethrow since it shouldn't happen normally and
// there is nothing much can be done from the caller side
LOG.warn("Exception raised when shutting down executor", ex);
}
}
/**
* Builder to help create the NettyHttpService.
*/
public static class Builder {
private static final int DEFAULT_BOSS_THREAD_POOL_SIZE = 1;
private static final int DEFAULT_WORKER_THREAD_POOL_SIZE = 10;
private static final int DEFAULT_CONNECTION_BACKLOG = 1000;
private static final int DEFAULT_EXEC_HANDLER_THREAD_POOL_SIZE = 60;
private static final long DEFAULT_EXEC_HANDLER_THREAD_KEEP_ALIVE_TIME_SECS = 60L;
// Caller runs by default
private static final RejectedExecutionHandler DEFAULT_REJECTED_EXECUTION_HANDLER =
new ThreadPoolExecutor.CallerRunsPolicy();
private static final int DEFAULT_HTTP_CHUNK_LIMIT = 150 * 1024 * 1024;
private final String serviceName;
private final Map channelConfigs;
private final Map childChannelConfigs;
private Iterable extends HttpHandler> handlers;
private Iterable extends HandlerHook> handlerHooks = Collections.emptyList();
private URLRewriter urlRewriter = null;
private int bossThreadPoolSize;
private int workerThreadPoolSize;
private int execThreadPoolSize;
private long execThreadKeepAliveSecs;
private String host;
private int port;
private RejectedExecutionHandler rejectedExecutionHandler;
private int httpChunkLimit;
private SSLHandlerFactory sslHandlerFactory;
private ChannelPipelineModifier pipelineModifier;
private ExceptionHandler exceptionHandler;
// Protected constructor to prevent instantiating Builder instance directly.
protected Builder(String serviceName) {
this.serviceName = serviceName;
bossThreadPoolSize = DEFAULT_BOSS_THREAD_POOL_SIZE;
workerThreadPoolSize = DEFAULT_WORKER_THREAD_POOL_SIZE;
execThreadPoolSize = DEFAULT_EXEC_HANDLER_THREAD_POOL_SIZE;
execThreadKeepAliveSecs = DEFAULT_EXEC_HANDLER_THREAD_KEEP_ALIVE_TIME_SECS;
rejectedExecutionHandler = DEFAULT_REJECTED_EXECUTION_HANDLER;
httpChunkLimit = DEFAULT_HTTP_CHUNK_LIMIT;
port = 0;
channelConfigs = new HashMap<>();
childChannelConfigs = new HashMap<>();
channelConfigs.put(ChannelOption.SO_BACKLOG, DEFAULT_CONNECTION_BACKLOG);
sslHandlerFactory = null;
exceptionHandler = new ExceptionHandler();
}
/**
* Sets the {@link ChannelPipelineModifier} to use for modifying {@link ChannelPipeline} on new {@link Channel}
* registration.
*
* @param pipelineModifier the modifier to use
* @return this builder instance.
*/
public Builder setChannelPipelineModifier(ChannelPipelineModifier pipelineModifier) {
this.pipelineModifier = pipelineModifier;
return this;
}
/**
* Add HttpHandlers that service the request.
*
* @param handlers Iterable of HttpHandlers.
* @return instance of {@code Builder}.
*/
public Builder setHttpHandlers(Iterable extends HttpHandler> handlers) {
this.handlers = handlers;
return this;
}
/**
* Add HttpHandlers that service the request.
*
* @param handlers a list of {@link HttpHandler}s to add
* @return instance of {@code Builder}.
*/
public Builder setHttpHandlers(HttpHandler... handlers) {
return setHttpHandlers(Arrays.asList(handlers));
}
/**
* Set HandlerHooks to be executed pre and post handler calls. They are executed in the same order as specified
* by the iterable.
*
* @param handlerHooks Iterable of HandlerHooks.
* @return an instance of {@code Builder}.
*/
public Builder setHandlerHooks(Iterable extends HandlerHook> handlerHooks) {
this.handlerHooks = handlerHooks;
return this;
}
/**
* Set URLRewriter to re-write URL of an incoming request before any handlers or their hooks are called.
*
* @param urlRewriter instance of URLRewriter.
* @return an instance of {@code Builder}.
*/
public Builder setUrlRewriter(URLRewriter urlRewriter) {
this.urlRewriter = urlRewriter;
return this;
}
/**
* Set size of bossThreadPool in netty default value is 1 if it is not set.
*
* @param bossThreadPoolSize size of bossThreadPool.
* @return an instance of {@code Builder}.
*/
public Builder setBossThreadPoolSize(int bossThreadPoolSize) {
this.bossThreadPoolSize = bossThreadPoolSize;
return this;
}
/**
* Set size of workerThreadPool in netty default value is 10 if it is not set.
*
* @param workerThreadPoolSize size of workerThreadPool.
* @return an instance of {@code Builder}.
*/
public Builder setWorkerThreadPoolSize(int workerThreadPoolSize) {
this.workerThreadPoolSize = workerThreadPoolSize;
return this;
}
/**
* Set size of backlog in netty service - size of accept queue of the TCP stack.
*
* @param connectionBacklog backlog in netty server. Default value is 1000.
* @return an instance of {@code Builder}.
*/
public Builder setConnectionBacklog(int connectionBacklog) {
channelConfigs.put(ChannelOption.SO_BACKLOG, connectionBacklog);
return this;
}
/**
* Sets channel configuration for the netty service.
*
* @param channelOption the {@link ChannelOption} to set
* @param value Value of the configuration.
* @return an instance of {@code Builder}.
* @see io.netty.channel.ChannelConfig
* @see io.netty.channel.socket.ServerSocketChannelConfig
*/
public Builder setChannelConfig(ChannelOption> channelOption, Object value) {
channelConfigs.put(channelOption, value);
return this;
}
/**
* Sets channel configuration for the child socket channel for the netty service.
*
* @param channelOption the {@link ChannelOption} to set
* @param value Value of the configuration.
* @return an instance of {@code Builder}.
* @see io.netty.channel.ChannelConfig
* @see io.netty.channel.socket.ServerSocketChannelConfig
*/
public Builder setChildChannelConfig(ChannelOption> channelOption, Object value) {
childChannelConfigs.put(channelOption, value);
return this;
}
/**
* Set size of executorThreadPool in netty default value is 60 if it is not set.
* If the size is {@code <= 0}, then no executor will be used, hence calls to {@link HttpHandler} would be made from
* worker threads directly.
*
* @param execThreadPoolSize size of workerThreadPool.
* @return an instance of {@code Builder}.
*/
public Builder setExecThreadPoolSize(int execThreadPoolSize) {
this.execThreadPoolSize = execThreadPoolSize;
return this;
}
/**
* Set the maximum time that excess idle threads will wait for new tasks before terminating.
* Default value is 60 seconds. If the value is {@code <= 0}, then idle threads will not be terminated.
*
* @param threadKeepAliveSecs thread keep alive seconds.
* @return an instance of {@code Builder}.
*/
public Builder setExecThreadKeepAliveSeconds(long threadKeepAliveSecs) {
this.execThreadKeepAliveSecs = threadKeepAliveSecs;
return this;
}
/**
* Set RejectedExecutionHandler - rejection policy for executor.
*
* @param rejectedExecutionHandler rejectionExecutionHandler.
* @return an instance of {@code Builder}.
*/
public Builder setRejectedExecutionHandler(RejectedExecutionHandler rejectedExecutionHandler) {
this.rejectedExecutionHandler = rejectedExecutionHandler;
return this;
}
/**
* Set the port on which the service should listen to.
* By default the service will run on a random port.
*
* @param port port on which the service should listen to.
* @return instance of {@code Builder}.
*/
public Builder setPort(int port) {
this.port = port;
return this;
}
/**
* Set the bindAddress for the service. Default value is localhost.
*
* @param host bindAddress for the service.
* @return instance of {@code Builder}.
*/
public Builder setHost(String host) {
this.host = host;
return this;
}
/**
* Set the HTTP chunk limit.
*
* @param value the chunk limit
* @return instance of {@code Builder}.
*/
public Builder setHttpChunkLimit(int value) {
this.httpChunkLimit = value;
return this;
}
/**
* Enable SSL by using the provided SSL information.
*
* @param sslConfig the SSL configuration
* @return instance of {@code Builder}.
*/
public Builder enableSSL(SSLConfig sslConfig) {
return enableSSL(new SSLHandlerFactory(sslConfig));
}
/**
* Enable SSL by using the given {@link SSLHandlerFactory} to create {@link SslHandler}.
*
* @param sslHandlerFactory the factory for creating SslHandlers
* @return instance of {@code Builder}.
*/
public Builder enableSSL(SSLHandlerFactory sslHandlerFactory) {
this.sslHandlerFactory = sslHandlerFactory;
return this;
}
/**
* Set the {@link ExceptionHandler} for the service.
*
* @param exceptionHandler the exception handler to use
* @return instance of {@code Builder}.
*/
public Builder setExceptionHandler(ExceptionHandler exceptionHandler) {
if (exceptionHandler == null) {
throw new IllegalArgumentException("exceptionHandler cannot be null");
}
this.exceptionHandler = exceptionHandler;
return this;
}
/**
* @return instance of {@code NettyHttpService}
*/
public NettyHttpService build() {
InetSocketAddress bindAddress;
if (host == null) {
bindAddress = new InetSocketAddress("localhost", port);
} else {
bindAddress = new InetSocketAddress(host, port);
}
return new NettyHttpService(serviceName, bindAddress, bossThreadPoolSize, workerThreadPoolSize,
execThreadPoolSize, execThreadKeepAliveSecs, channelConfigs, childChannelConfigs,
rejectedExecutionHandler, urlRewriter, handlers, handlerHooks, httpChunkLimit,
pipelineModifier, sslHandlerFactory, exceptionHandler);
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy