Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Provide a consistent configuration to Armeria Dropwizard #2373

Merged
merged 12 commits into from
Jan 13, 2020
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,7 @@ public abstract class ArmeriaBundle<C extends Configuration>
implements ConfiguredBundle<C>, ArmeriaServerConfigurator {

@Override
public void initialize(Bootstrap<?> bootstrap) {
}
public void initialize(Bootstrap<?> bootstrap) {}

@Override
public void run(C configuration, Environment environment) throws Exception {
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -15,206 +15,61 @@
*/
package com.linecorp.armeria.dropwizard;

import java.security.cert.CertificateException;
import java.time.Duration;
import java.util.Objects;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

import javax.annotation.Nullable;
import javax.net.ssl.SSLException;
import javax.validation.Valid;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;

import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.util.thread.ThreadPool;
import org.hibernate.validator.constraints.NotEmpty;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.codahale.metrics.MetricRegistry;
import com.fasterxml.jackson.annotation.JsonGetter;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonTypeName;
import com.google.common.annotations.VisibleForTesting;
import com.fasterxml.jackson.annotation.JsonUnwrapped;
import com.google.common.collect.ImmutableMap;

import com.linecorp.armeria.common.Flags;
import com.linecorp.armeria.common.metric.DropwizardMeterRegistries;
import com.linecorp.armeria.common.util.ThreadFactories;
import com.linecorp.armeria.dropwizard.connector.ArmeriaHttpConnectorFactory;
import com.linecorp.armeria.dropwizard.connector.ArmeriaServerDecorator;
import com.linecorp.armeria.dropwizard.logging.AccessLogWriterFactory;
import com.linecorp.armeria.dropwizard.logging.CommonAccessLogWriterFactory;
import com.linecorp.armeria.server.ServerBuilder;
import com.linecorp.armeria.server.jetty.JettyService;
import com.linecorp.armeria.server.logging.AccessLogWriter;

import io.dropwizard.jersey.setup.JerseyEnvironment;
import io.dropwizard.jetty.ConnectorFactory;
import io.dropwizard.jetty.ContextRoutingHandler;
import io.dropwizard.server.AbstractServerFactory;
import io.dropwizard.server.ServerFactory;
import io.dropwizard.server.SimpleServerFactory;
import io.dropwizard.setup.Environment;
import io.dropwizard.util.Size;
import io.dropwizard.validation.MinSize;
import io.micrometer.core.instrument.MeterRegistry;

/**
* A Dropwizard {@link ServerFactory} implementation for Armeria that replaces
* Dropwizard's default Jetty handler with one provided by Armeria.
*/
@JsonTypeName(ArmeriaServerFactory.TYPE)
class ArmeriaServerFactory extends SimpleServerFactory {
// TODO: This class could be stripped down to the essential fields. Implement ServerFactory instead.
class ArmeriaServerFactory extends AbstractServerFactory {
ikhoon marked this conversation as resolved.
Show resolved Hide resolved

public static final String TYPE = "armeria";
private static final Logger logger = LoggerFactory.getLogger(ArmeriaServerFactory.class);

@JsonProperty
private @Valid ConnectorFactory connector = ArmeriaHttpConnectorFactory.build();
@JsonProperty
private @Valid @NotNull AccessLogWriterFactory accessLogWriter = new CommonAccessLogWriterFactory();
@JsonProperty
private boolean jerseyEnabled = true;
@JsonProperty
private @MinSize(0) Size maxRequestLength = Size.bytes(Flags.defaultMaxRequestLength());
@JsonProperty
private @Min(0) int maxNumConnections = Flags.maxNumConnections();
@JsonProperty
private boolean dateHeaderEnabled = true;
@JsonProperty
private boolean serverHeaderEnabled;
@JsonProperty
private boolean verboseResponses;
@JsonProperty
@Nullable
private String defaultHostname;
@JsonUnwrapped
private @Valid ArmeriaSettings armeriaSettings;

@JsonIgnore
@Nullable
private transient ServerBuilder serverBuilder;

/**
* Sets up the Armeria ServerBuilder with values from the Dropwizard Configuration.
* Ref <a href="https://line.github.io/armeria/advanced-production-checklist.html">Production Checklist</a>
*
* @param serverBuilder A non-production ready {@link ServerBuilder}
* @return A production-ready {@link ServerBuilder}
*/
@VisibleForTesting
ServerBuilder decorateServerBuilderFromConfig(ServerBuilder serverBuilder) {
Objects.requireNonNull(serverBuilder);
final ScheduledThreadPoolExecutor blockingTaskExecutor = new ScheduledThreadPoolExecutor(
getMaxThreads(),
ThreadFactories.newThreadFactory("armeria-dropwizard-blocking-tasks", true));
blockingTaskExecutor.setKeepAliveTime(60, TimeUnit.SECONDS);
blockingTaskExecutor.allowCoreThreadTimeOut(true);

serverBuilder.maxNumConnections(getMaxNumConnections())
.blockingTaskExecutor(blockingTaskExecutor, true)
.maxRequestLength(maxRequestLength.toBytes())
.idleTimeoutMillis(getIdleThreadTimeout().toMilliseconds())
.gracefulShutdownTimeout(
Duration.ofMillis(getShutdownGracePeriod().toMilliseconds()),
Duration.ofMillis(getShutdownGracePeriod().toMilliseconds()))
.verboseResponses(hasVerboseResponses());
if (!isDateHeaderEnabled()) {
serverBuilder.disableDateHeader();
}
if (!isServerHeaderEnabled()) {
serverBuilder.disableServerHeader();
}
if (getDefaultHostname() != null) {
serverBuilder.defaultHostname(getDefaultHostname());
}
// TODO: Add more items to server builder via Configuration
return serverBuilder;
}

@Nullable
private String getDefaultHostname() {
return defaultHostname;
}

public void setDefaultHostname(@Nullable String defaultHostname) {
this.defaultHostname = defaultHostname;
}

@JsonGetter("verboseResponses")
private boolean hasVerboseResponses() {
return verboseResponses;
}

public void setVerboseResponses(boolean verboseResponses) {
this.verboseResponses = verboseResponses;
}

public boolean isDateHeaderEnabled() {
return dateHeaderEnabled;
}

public void setDateHeaderEnabled(boolean dateHeaderEnabled) {
this.dateHeaderEnabled = dateHeaderEnabled;
}

public boolean isServerHeaderEnabled() {
return serverHeaderEnabled;
}

public void setServerHeaderEnabled(boolean serverHeaderEnabled) {
this.serverHeaderEnabled = serverHeaderEnabled;
}

@Override
public ConnectorFactory getConnector() {
return connector;
}

@Override
public void setConnector(ConnectorFactory factory) {
connector = Objects.requireNonNull(factory, "server.connector");
}

public boolean isJerseyEnabled() {
return jerseyEnabled;
}

public void setJerseyEnabled(boolean jerseyEnabled) {
this.jerseyEnabled = jerseyEnabled;
}

public AccessLogWriterFactory getAccessLogWriter() {
return accessLogWriter;
}

/**
* Sets an {@link AccessLogWriter} onto this ServerFactory.
*
* @param accessLogWriter An instance of an {#link AccessLogWriter}
*/
public void setAccessLogWriter(@Valid AccessLogWriterFactory accessLogWriter) {
this.accessLogWriter = Objects.requireNonNull(
accessLogWriter, "server[type=\"" + TYPE + "\"].accessLogWriter");
}

public Size getMaxRequestLength() {
return maxRequestLength;
}

public void setMaxRequestLength(Size maxRequestLength) {
this.maxRequestLength = maxRequestLength;
}

public int getMaxNumConnections() {
return maxNumConnections;
}

public void setMaxNumConnections(int maxNumConnections) {
this.maxNumConnections = maxNumConnections;
}
@NotEmpty
private String applicationContextPath = "/application";
@NotEmpty
private String adminContextPath = "/admin";
@JsonProperty
private boolean jerseyEnabled = true;

@JsonIgnore
public ServerBuilder getServerBuilder() {
Expand All @@ -235,12 +90,20 @@ public Server build(Environment environment) {
}

addDefaultHandlers(server, environment, metrics);
serverBuilder = getArmeriaServerBuilder(server, connector, metrics);
serverBuilder = buildServerBuilder(server, metrics);
return server;
}

private void addDefaultHandlers(Server server, Environment environment,
MetricRegistry metrics) {
@Override
public void configure(Environment environment) {
logger.info("Registering jersey handler with root path prefix: {}", applicationContextPath);
environment.getApplicationContext().setContextPath(applicationContextPath);

logger.info("Registering admin handler with root path prefix: {}", adminContextPath);
environment.getAdminContext().setContextPath(adminContextPath);
}

private void addDefaultHandlers(Server server, Environment environment, MetricRegistry metrics) {
final JerseyEnvironment jersey = environment.jersey();
final Handler applicationHandler = createAppServlet(
server,
Expand All @@ -250,85 +113,66 @@ private void addDefaultHandlers(Server server, Environment environment,
environment.getApplicationContext(),
environment.getJerseyServletContainer(),
metrics);
final Handler adminHandler = createAdminServlet(
server,
environment.getAdminContext(),
metrics,
environment.healthChecks());
final ContextRoutingHandler routingHandler = new ContextRoutingHandler(ImmutableMap.of(
getApplicationContextPath(), applicationHandler,
getAdminContextPath(), adminHandler));
final Handler adminHandler = createAdminServlet(server, environment.getAdminContext(),
metrics, environment.healthChecks());
final ContextRoutingHandler routingHandler = new ContextRoutingHandler(
ImmutableMap.of(applicationContextPath, applicationHandler, adminContextPath, adminHandler));
final Handler gzipHandler = buildGzipHandler(routingHandler);
server.setHandler(addStatsHandler(addRequestLog(server, gzipHandler, environment.getName())));
}

private ServerBuilder getArmeriaServerBuilder(Server server,
ConnectorFactory connector,
MetricRegistry metricRegistry) {
logger.debug("Building Armeria Server");
private ServerBuilder buildServerBuilder(Server server, MetricRegistry metricRegistry) {
final ServerBuilder serverBuilder = com.linecorp.armeria.server.Server.builder();
try {
decorateServerBuilder(
serverBuilder, connector, accessLogWriter,
DropwizardMeterRegistries.newRegistry(metricRegistry));
} catch (SSLException | CertificateException e) {
logger.error("Unable to define TLS Server", e);
// TODO: Throw an exception?
serverBuilder.meterRegistry(DropwizardMeterRegistries.newRegistry(metricRegistry));

if (armeriaSettings != null) {
ArmeriaConfigurationUtil.configureServer(serverBuilder, armeriaSettings);
} else {
logger.warn("Armeria configuration was null. ServerBuilder is not customized from it.");
}

final JettyService jettyService = getJettyService(server);
return decorateServerBuilderFromConfig(serverBuilder)
.serviceUnder("/", jettyService);
return serverBuilder.blockingTaskExecutor(newBlockingTaskExecutor(), true)
.serviceUnder("/", JettyService.of(server));
}

private ScheduledThreadPoolExecutor newBlockingTaskExecutor() {
final ScheduledThreadPoolExecutor blockingTaskExecutor = new ScheduledThreadPoolExecutor(
getMaxThreads(),
ThreadFactories.newThreadFactory("armeria-dropwizard-blocking-tasks", true));
blockingTaskExecutor.setKeepAliveTime(60, TimeUnit.SECONDS);
blockingTaskExecutor.allowCoreThreadTimeOut(true);
return blockingTaskExecutor;
}

/**
* Wrap a {@link Server} in a {@link JettyService}.
*
* @param jettyServer An instance of a Jetty {@link Server}
* @return Armeria {@link JettyService} for the provided jettyServer
*/
private static JettyService getJettyService(Server jettyServer) {
Objects.requireNonNull(jettyServer, "Armeria cannot build a service from a null server");
return JettyService.of(jettyServer);
ArmeriaSettings getArmeriaSettings() {
return armeriaSettings;
}

/**
* Builds on a {@link ServerBuilder}.
*
* @param sb An instance of a {@link ServerBuilder}
* @param connectorFactory {@code null} or {@link ConnectorFactory}. If non-null must be an instance of
* an {@link ArmeriaServerDecorator}
* @param writerFactory {@code null} or {@link AccessLogWriterFactory}
* @param meterRegistry {@code null} or {@link MeterRegistry}
* @throws SSLException Thrown when configuring TLS
* @throws CertificateException Thrown when validating certificates
*/
@VisibleForTesting
ServerBuilder decorateServerBuilder(ServerBuilder sb,
@Nullable ConnectorFactory connectorFactory,
@Nullable AccessLogWriterFactory writerFactory,
MeterRegistry meterRegistry)
throws SSLException, CertificateException {
Objects.requireNonNull(sb, "builder to decorate must not be null");
Objects.requireNonNull(meterRegistry, "meterRegistry");
if (connectorFactory != null) {
if (!(connectorFactory instanceof ArmeriaServerDecorator)) {
throw new ClassCastException("server.connector.type must be an instance of " +
ArmeriaServerDecorator.class.getName());
}
((ArmeriaServerDecorator) connectorFactory).decorate(sb);
} else {
logger.warn("connectorFactory was null. ServerBuilder not decorated from it.");
}
sb.meterRegistry(meterRegistry);
if (writerFactory != null && !writerFactory.getWriter()
.equals(AccessLogWriter.disabled())) {
logger.trace("Setting up Armeria AccessLogWriter");
sb.accessLogWriter(writerFactory.getWriter(), true);
} else {
logger.info("Armeria access logs will not be written");
sb.accessLogWriter(AccessLogWriter.disabled(), true);
}
return sb;
void setArmeriaSettings(ArmeriaSettings armeriaSettings) {
this.armeriaSettings = armeriaSettings;
}

String getApplicationContextPath() {
return applicationContextPath;
}

void setApplicationContextPath(String applicationContextPath) {
this.applicationContextPath = applicationContextPath;
}

String getAdminContextPath() {
return adminContextPath;
}

void setAdminContextPath(String adminContextPath) {
this.adminContextPath = adminContextPath;
}

public boolean isJerseyEnabled() {
return jerseyEnabled;
}

public void setJerseyEnabled(boolean jerseyEnabled) {
this.jerseyEnabled = jerseyEnabled;
}
}
Loading