package com.twitter.common.application.modules;
import java.util.Set;
import java.util.logging.Logger;
import javax.servlet.http.HttpServlet;
import com.google.common.collect.ImmutableSet;
import com.google.inject.AbstractModule;
import com.google.inject.Inject;
import com.google.inject.Injector;
import com.google.inject.Key;
import com.google.inject.Singleton;
import com.google.inject.TypeLiteral;
import com.google.inject.name.Names;
import com.google.inject.servlet.GuiceFilter;
import com.google.inject.servlet.GuiceServletContextListener;
import org.mortbay.jetty.RequestLog;
import com.twitter.common.application.ShutdownRegistry;
import com.twitter.common.application.http.DefaultQuitHandler;
import com.twitter.common.application.http.GraphViewer;
import com.twitter.common.application.http.HttpAssetConfig;
import com.twitter.common.application.http.HttpFilterConfig;
import com.twitter.common.application.http.HttpServletConfig;
import com.twitter.common.application.http.Registration;
import com.twitter.common.application.http.Registration.IndexLink;
import com.twitter.common.application.modules.LifecycleModule.ServiceRunner;
import com.twitter.common.application.modules.LocalServiceRegistry.LocalService;
import com.twitter.common.args.Arg;
import com.twitter.common.args.CmdLine;
import com.twitter.common.args.constraints.NotEmpty;
import com.twitter.common.args.constraints.Range;
import com.twitter.common.base.Command;
import com.twitter.common.base.ExceptionalSupplier;
import com.twitter.common.base.Supplier;
import com.twitter.common.net.http.HttpServerDispatch;
import com.twitter.common.net.http.JettyHttpServerDispatch;
import com.twitter.common.net.http.RequestLogger;
import com.twitter.common.net.http.handlers.AbortHandler;
import com.twitter.common.net.http.handlers.ContentionPrinter;
import com.twitter.common.net.http.handlers.HealthHandler;
import com.twitter.common.net.http.handlers.LogConfig;
import com.twitter.common.net.http.handlers.LogPrinter;
import com.twitter.common.net.http.handlers.QuitHandler;
import com.twitter.common.net.http.handlers.StringTemplateServlet.CacheTemplates;
import com.twitter.common.net.http.handlers.ThreadStackPrinter;
import com.twitter.common.net.http.handlers.TimeSeriesDataSource;
import com.twitter.common.net.http.handlers.VarsHandler;
import com.twitter.common.net.http.handlers.VarsJsonHandler;
import com.twitter.common.net.http.handlers.pprof.ContentionProfileHandler;
import com.twitter.common.net.http.handlers.pprof.CpuProfileHandler;
import com.twitter.common.net.http.handlers.pprof.HeapProfileHandler;
import static com.google.common.base.Preconditions.checkNotNull;
/**
* Binding module for injections related to the HTTP server and the default set of servlets.
*
* This module uses a single command line argument 'http_port'. If unset, the HTTP server will
* be started on an ephemeral port.
*
* The default HTTP server includes several generic servlets that are useful for debugging.
*
* This class also offers several convenience methods for other modules to register HTTP servlets
* which will be included in the HTTP server configuration.
*
* Bindings provided by this module:
* <ul>
* <li>{@code @CacheTemplates boolean} - True if parsed stringtemplates for servlets are cached.
* </ul>
*/
public class HttpModule extends AbstractModule {
@Range(lower = 0, upper = 65535)
@CmdLine(name = "http_port",
help = "The port to start an HTTP server on. Default value will choose a random port.")
protected static final Arg<Integer> HTTP_PORT = Arg.create(0);
@CmdLine(name = "http_primary_service", help = "True if HTTP is the primary service.")
protected static final Arg<Boolean> HTTP_PRIMARY_SERVICE = Arg.create(false);
@NotEmpty
@CmdLine(name = "http_announce_port_names",
help = "Names to identify the HTTP port with when advertising the service.")
protected static final Arg<Set<String>> ANNOUNCE_NAMES =
Arg.<Set<String>>create(ImmutableSet.of("http"));
private static final Logger LOG = Logger.getLogger(HttpModule.class.getName());
// TODO(William Farner): Consider making this configurable if needed.
private static final boolean CACHE_TEMPLATES = true;
private static class DefaultAbortHandler implements Runnable {
@Override public void run() {
LOG.info("ABORTING PROCESS IMMEDIATELY!");
System.exit(0);
}
}
private static class DefaultHealthChecker implements Supplier<Boolean> {
@Override public Boolean get() {
return Boolean.TRUE;
}
}
private final Key<? extends Runnable> abortHandler;
private final Key<? extends Runnable> quitHandlerKey;
private final Key<? extends ExceptionalSupplier<Boolean, ?>> healthCheckerKey;
public HttpModule() {
this(builder());
}
private HttpModule(Builder builder) {
this.abortHandler = checkNotNull(builder.abortHandlerKey);
this.quitHandlerKey = checkNotNull(builder.quitHandlerKey);
this.healthCheckerKey = checkNotNull(builder.healthCheckerKey);
}
/**
* Creates a builder to override default bindings.
*
* @return A new builder.
*/
public static Builder builder() {
return new Builder();
}
/**
* Builder to customize bindings.
*/
public static class Builder {
private Key<? extends Runnable> abortHandlerKey = Key.get(DefaultAbortHandler.class);
private Key<? extends Runnable> quitHandlerKey = Key.get(DefaultQuitHandler.class);
private Key<? extends ExceptionalSupplier<Boolean, ?>> healthCheckerKey =
Key.get(DefaultHealthChecker.class);
/**
* Specifies a custom abort handler to be invoked when an HTTP abort signal is received.
*
* @param key Abort callback handler binding key.
* @return A reference to this builder.
*/
public Builder abortHandler(Key<? extends Runnable> key) {
this.abortHandlerKey = key;
return this;
}
/**
* Specifies a custom quit handler to be invoked when an HTTP quit signal is received.
*
* @param key Quit callback handler binding key.
* @return A reference to this builder.
*/
public Builder quitHandler(Key<? extends Runnable> key) {
this.quitHandlerKey = key;
return this;
}
/**
* Specifies a custom health checker to control responses to HTTP health checks.
*
* @param key Health check callback binding key.
* @return A reference to this builder.
*/
public Builder healthChecker(Key<? extends ExceptionalSupplier<Boolean, ?>> key) {
this.healthCheckerKey = key;
return this;
}
/**
* Constructs an http module.
*
* @return An http module constructed from this builder.
*/
public HttpModule build() {
return new HttpModule(this);
}
}
@Override
protected void configure() {
requireBinding(Injector.class);
requireBinding(ShutdownRegistry.class);
bind(Runnable.class)
.annotatedWith(Names.named(AbortHandler.ABORT_HANDLER_KEY))
.to(abortHandler);
bind(abortHandler).in(Singleton.class);
bind(Runnable.class).annotatedWith(Names.named(QuitHandler.QUIT_HANDLER_KEY))
.to(quitHandlerKey);
bind(quitHandlerKey).in(Singleton.class);
bind(new TypeLiteral<ExceptionalSupplier<Boolean, ?>>() { })
.annotatedWith(Names.named(HealthHandler.HEALTH_CHECKER_KEY))
.to(healthCheckerKey);
bind(healthCheckerKey).in(Singleton.class);
// Allow template reloading in interactive mode for easy debugging of string templates.
bindConstant().annotatedWith(CacheTemplates.class).to(CACHE_TEMPLATES);
bind(HttpServerDispatch.class).to(JettyHttpServerDispatch.class)
.in(Singleton.class);
bind(RequestLog.class).to(RequestLogger.class);
Registration.registerServlet(binder(), "/abortabortabort", AbortHandler.class, true);
Registration.registerServlet(binder(), "/contention", ContentionPrinter.class, false);
Registration.registerServlet(binder(), "/graphdata", TimeSeriesDataSource.class, true);
Registration.registerServlet(binder(), "/health", HealthHandler.class, true);
Registration.registerServlet(binder(), "/healthz", HealthHandler.class, true);
Registration.registerServlet(binder(), "/logconfig", LogConfig.class, false);
Registration.registerServlet(binder(), "/logs", LogPrinter.class, false);
Registration.registerServlet(binder(), "/pprof/heap", HeapProfileHandler.class, false);
Registration.registerServlet(binder(), "/pprof/profile", CpuProfileHandler.class, false);
Registration.registerServlet(
binder(), "/pprof/contention", ContentionProfileHandler.class, false);
Registration.registerServlet(binder(), "/quitquitquit", QuitHandler.class, true);
Registration.registerServlet(binder(), "/threads", ThreadStackPrinter.class, false);
Registration.registerServlet(binder(), "/vars", VarsHandler.class, false);
Registration.registerServlet(binder(), "/vars.json", VarsJsonHandler.class, false);
GraphViewer.registerResources(binder());
LifecycleModule.bindServiceRunner(binder(), HttpServerLauncher.class);
// Ensure at least an empty filter set is bound.
Registration.getFilterBinder(binder());
// Ensure at least an empty set of additional links is bound.
Registration.getEndpointBinder(binder());
}
public static final class HttpServerLauncher implements ServiceRunner {
private final HttpServerDispatch httpServer;
private final Set<HttpServletConfig> httpServlets;
private final Set<HttpAssetConfig> httpAssets;
private final Set<HttpFilterConfig> httpFilters;
private final Set<String> additionalIndexLinks;
private final Injector injector;
@Inject HttpServerLauncher(
HttpServerDispatch httpServer,
Set<HttpServletConfig> httpServlets,
Set<HttpAssetConfig> httpAssets,
Set<HttpFilterConfig> httpFilters,
@IndexLink Set<String> additionalIndexLinks,
Injector injector) {
this.httpServer = checkNotNull(httpServer);
this.httpServlets = checkNotNull(httpServlets);
this.httpAssets = checkNotNull(httpAssets);
this.httpFilters = checkNotNull(httpFilters);
this.additionalIndexLinks = checkNotNull(additionalIndexLinks);
this.injector = checkNotNull(injector);
}
@Override public LocalService launch() {
if (!httpServer.listen(HTTP_PORT.get())) {
throw new IllegalStateException("Failed to start HTTP server, all servlets disabled.");
}
httpServer.registerListener(new GuiceServletContextListener() {
@Override protected Injector getInjector() {
return injector;
}
});
httpServer.registerFilter(GuiceFilter.class, "/*");
for (HttpServletConfig config : httpServlets) {
HttpServlet handler = injector.getInstance(config.handlerClass);
httpServer.registerHandler(config.path, handler, config.params, config.silent);
}
for (HttpAssetConfig config : httpAssets) {
httpServer.registerHandler(config.path, config.handler, null, config.silent);
}
for (HttpFilterConfig filter : httpFilters) {
httpServer.registerFilter(filter.filterClass, filter.pathSpec);
}
for (String indexLink : additionalIndexLinks) {
httpServer.registerIndexLink(indexLink);
}
Command shutdown = new Command() {
@Override public void execute() {
LOG.info("Shutting down embedded http server");
httpServer.stop();
}
};
return HTTP_PRIMARY_SERVICE.get()
? LocalService.primaryService(httpServer.getPort(), shutdown)
: LocalService.auxiliaryService(ANNOUNCE_NAMES.get(), httpServer.getPort(), shutdown);
}
}
}