package grails.plugin.lightweightdeploy; import com.codahale.metrics.JmxReporter; import com.codahale.metrics.MetricRegistry; import com.codahale.metrics.health.HealthCheckRegistry; import com.codahale.metrics.health.jvm.ThreadDeadlockHealthCheck; import com.codahale.metrics.jetty8.InstrumentedHandler; import com.codahale.metrics.jetty8.InstrumentedQueuedThreadPool; import com.codahale.metrics.jvm.BufferPoolMetricSet; import com.codahale.metrics.jvm.GarbageCollectorMetricSet; import com.codahale.metrics.jvm.MemoryUsageGaugeSet; import com.codahale.metrics.jvm.ThreadStatesGaugeSet; import com.codahale.metrics.servlets.AdminServlet; import com.google.common.base.Strings; import com.google.common.collect.Collections2; import com.google.common.collect.Iterables; import grails.plugin.lightweightdeploy.connector.*; import grails.plugin.lightweightdeploy.jetty.BiDiGzipFilter; import grails.plugin.lightweightdeploy.jmx.JmxServer; import grails.plugin.lightweightdeploy.logging.RequestLoggingFactory; import grails.plugin.lightweightdeploy.logging.ServerLoggingFactory; import grails.plugin.lightweightdeploy.logging.StartupShutdownLogger; import org.eclipse.jetty.server.Connector; import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.handler.HandlerCollection; import org.eclipse.jetty.server.session.HashSessionIdManager; import org.eclipse.jetty.servlet.FilterHolder; import org.eclipse.jetty.servlet.ServletContextHandler; import org.eclipse.jetty.servlet.ServletHolder; import org.eclipse.jetty.util.thread.ThreadPool; import org.eclipse.jetty.webapp.WebAppContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.servlet.DispatcherType; import javax.servlet.Filter; import java.io.IOException; import java.lang.management.ManagementFactory; import java.util.EnumSet; import java.util.Set; /** * Based heavily on code from Burt Beckwith's standalone plugin and Codehale's Dropwizard. */ public class Launcher { private static final Logger logger = LoggerFactory.getLogger(Launcher.class); /** * The directory under the exploded dir which stores the war */ private static final String WAR_EXPLODED_SUBDIR = "war"; private Configuration configuration; private MetricRegistry metricsRegistry; private HealthCheckRegistry healthCheckRegistry; private StartupShutdownLogger startupShutdownLogger; /** * Start the server. */ public static void main(String[] args) { try { verifyArgs(args); new Launcher(args[0]).start(); } catch (Throwable e) { System.err.println("Failure launching application"); e.printStackTrace(); System.exit(1); } } public Launcher(String configYmlPath) throws IOException { this(new Configuration(configYmlPath)); } public Launcher(Configuration configuration) { this.configuration = configuration; logger.info("Using configuration: " + this.configuration); this.metricsRegistry = configureMetricRegistry(); this.healthCheckRegistry = new HealthCheckRegistry(); this.startupShutdownLogger = new StartupShutdownLogger(logger, configuration.getAppName()); configureLogging(); } public Configuration getConfiguration() { return configuration; } public HealthCheckRegistry getHealthCheckRegistry() { return healthCheckRegistry; } public MetricRegistry getMetricsRegistry() { return metricsRegistry; } protected MetricRegistry configureMetricRegistry() { final MetricRegistry metricRegistry = new MetricRegistry(); metricRegistry.register("jvm.buffers", new BufferPoolMetricSet(ManagementFactory.getPlatformMBeanServer())); metricRegistry.register("jvm.gc", new GarbageCollectorMetricSet()); metricRegistry.register("jvm.memory", new MemoryUsageGaugeSet()); metricRegistry.register("jvm.threads", new ThreadStatesGaugeSet()); if (getConfiguration().isJmxEnabled()) { JmxReporter.forRegistry(metricRegistry).build().start(); } return metricRegistry; } protected void configureLogging() { if (this.configuration.isServerLoggingEnabled()) { ServerLoggingFactory loggingFactory = new ServerLoggingFactory(this.configuration); loggingFactory.configure(); } } protected void start() throws Exception { War war = new War(this.configuration.getWorkDir()); Server server = configureJetty(war); startJetty(server); } protected Server configureJetty(War war) throws IOException { System.setProperty("org.eclipse.jetty.xml.XmlParser.NotValidating", "true"); final Server server = createServer(); HandlerCollection handlerCollection = new HandlerCollection(); handlerCollection.addHandler(configureExternal(server, war)); if (this.configuration.getHttpConfiguration().hasAdminPort()) { handlerCollection.addHandler(configureInternal(server)); } if (this.configuration.isRequestLoggingEnabled()) { RequestLoggingFactory requestLoggingFactory = new RequestLoggingFactory(this.configuration); handlerCollection.addHandler(requestLoggingFactory.configure()); } server.setHandler(handlerCollection); if (this.configuration.isJmxEnabled()) { JmxServer jmxServer = new JmxServer(this.configuration.getJmxConfiguration()); jmxServer.start(); } return server; } protected Server createServer() { final Server server = new Server(); // Add our the instrumented thread pool server.setThreadPool(createThreadPool()); // Don't send Date and Server headers server.setSendDateHeader(false); server.setSendServerVersion(false); // Allow a grace period during shutdown server.setStopAtShutdown(true); server.setGracefulShutdown(2000); // Add the shutdown message server.addLifeCycleListener(startupShutdownLogger); return server; } protected Handler configureExternal(Server server, War war) throws IOException { logger.info("Configuring external connector(s)"); final HttpConfiguration httpConfiguration = configuration.getHttpConfiguration(); final ExternalConnectorFactory connectorFactory = new ExternalConnectorFactory(httpConfiguration, metricsRegistry); final Set<? extends Connector> connectors = connectorFactory.build(); for (Connector connector : connectors) { server.addConnector(connector); } return createExternalContext(server, connectors, war.getDirectory().getPath() + "/" + WAR_EXPLODED_SUBDIR, httpConfiguration.getContextPath()); } protected Handler configureInternal(Server server) { logger.info("Configuring admin connector"); final InternalConnectorFactory connectorFactory = new InternalConnectorFactory(getConfiguration().getHttpConfiguration()); final Set<? extends Connector> connectors = connectorFactory.build(); for (Connector externalConnector : connectors) { server.addConnector(externalConnector); } return createInternalContext(server, connectors); } protected void startJetty(Server server) throws Exception { try { server.start(); logger.info("Startup complete. Server running on " + this.configuration.getHttpConfiguration().getPort()); } catch (Exception e) { logger.error("Error starting jetty. Exiting JVM.", e); server.stop(); } } protected Handler createInternalContext(Server server, Set<? extends Connector> connectors) { final ServletContextHandler handler = new InternalContext(getHealthCheckRegistry(), getMetricsRegistry()); restrictToConnectors(handler, connectors); configureInternalServlets(handler); return handler; } protected void configureInternalServlets(ServletContextHandler handler) { handler.addServlet(new ServletHolder(new AdminServlet()), "/*"); } protected Handler createExternalContext(Server server, Set<? extends Connector> connectors, String webAppRoot, String contextPath) throws IOException { final WebAppContext handler = new ExternalContext(webAppRoot, getMetricsRegistry(), getHealthCheckRegistry(), contextPath); // Enable sessions support if required final SessionsConfiguration sessionsConfiguration = configuration.getHttpConfiguration().getSessionsConfiguration(); if (sessionsConfiguration.isEnabled()) { final HashSessionIdManager idManager = new HashSessionIdManager(); if (!Strings.isNullOrEmpty(sessionsConfiguration.getWorkerName())) { idManager.setWorkerName(sessionsConfiguration.getWorkerName()); } // Assumes ExternalContext extends WebAppContext which configures sessions by default handler.getSessionHandler().getSessionManager().setSessionIdManager(idManager); } else { handler.setSessionHandler(null); } restrictToConnectors(handler, connectors); configureExternalServlets(handler); // Optionally support GZip requests/responses configureGzip(handler); // Instrument our handler final Handler instrumented = new InstrumentedHandler(metricsRegistry, handler); return instrumented; } private void configureGzip(ServletContextHandler handler) { GzipConfiguration gzipConfiguration = configuration.getHttpConfiguration().getGzipConfiguration(); if (gzipConfiguration.isEnabled()) { Filter filter = buildGzipFilter(gzipConfiguration); FilterHolder holder = new FilterHolder(filter); handler.addFilter(holder, "/*", EnumSet.allOf(DispatcherType.class)); } } private Filter buildGzipFilter(GzipConfiguration gzipConfiguration) { BiDiGzipFilter filter = new BiDiGzipFilter(); filter.setMinGzipSize(gzipConfiguration.getMinimumEntitySize()); filter.setBufferSize(gzipConfiguration.getBufferSize()); filter.setDeflateCompressionLevel(gzipConfiguration.getDeflateCompressionLevel()); if (gzipConfiguration.getExcludedUserAgents() != null) { filter.setExcludedAgents(gzipConfiguration.getExcludedUserAgents()); } if (gzipConfiguration.getCompressedMimeTypes() != null) { filter.setMimeTypes(gzipConfiguration.getCompressedMimeTypes()); } if (gzipConfiguration.getIncludedMethods() != null) { filter.setMethods(gzipConfiguration.getIncludedMethods()); } if (gzipConfiguration.getExcludedUserAgentPatterns() != null) { filter.setExcludedAgentPatterns(gzipConfiguration.getExcludedUserAgentPatterns()); } filter.setVary(gzipConfiguration.getVary()); filter.setDeflateNoWrap(gzipConfiguration.isGzipCompatibleDeflation()); return filter; } protected void restrictToConnectors(ServletContextHandler handler, Set<? extends Connector> connectors) { handler.setConnectorNames(Iterables.toArray(Collections2.transform(connectors, ConnectorNameFunction.INSTANCE), String.class)); } protected void configureHealthChecks() { HealthCheckRegistry healthCheckRegistry = getHealthCheckRegistry(); healthCheckRegistry.register("threadDeadlock", new ThreadDeadlockHealthCheck()); } /** * Override point for subclasses */ protected void configureExternalServlets(WebAppContext context) { configureHealthChecks(); } protected static void verifyArgs(String[] args) { if (args.length < 1) { throw new IllegalArgumentException("Requires 1 argument, which is the path to the config.yml file"); } } protected ThreadPool createThreadPool() { final InstrumentedQueuedThreadPool pool = new InstrumentedQueuedThreadPool(metricsRegistry); pool.setMinThreads(this.configuration.getHttpConfiguration().getMinThreads()); pool.setMaxThreads(this.configuration.getHttpConfiguration().getMaxThreads()); return pool; } }