Skip to content

Commit

Permalink
Support Virtual Threads in Jetty & Tomcat (#701)
Browse files Browse the repository at this point in the history
Allows enabling virtual thread support for Jetty and Tomcat.

Undertow has issues with Virtual threads so not implemented. See spring-projects/spring-boot#39812

Co-authored-by: Sergio del Amo <sergio.delamo@softamo.com>

---------

Co-authored-by: Sergio del Amo <sergio.delamo@softamo.com>
  • Loading branch information
graemerocher and sdelamo committed May 22, 2024
1 parent 9135e6e commit ab6b5f9
Show file tree
Hide file tree
Showing 7 changed files with 209 additions and 48 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,20 @@
import io.micronaut.context.annotation.Primary;
import io.micronaut.context.env.Environment;
import io.micronaut.context.exceptions.ConfigurationException;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.core.io.ResourceResolver;
import io.micronaut.core.io.socket.SocketUtils;
import io.micronaut.http.ssl.ClientAuthentication;
import io.micronaut.http.ssl.SslConfiguration;
import io.micronaut.inject.qualifiers.Qualifiers;
import io.micronaut.scheduling.LoomSupport;
import io.micronaut.scheduling.TaskExecutors;
import io.micronaut.servlet.engine.DefaultMicronautServlet;
import io.micronaut.servlet.engine.MicronautServletConfiguration;
import io.micronaut.servlet.engine.server.ServletServerFactory;
import io.micronaut.servlet.engine.server.ServletStaticResourceConfiguration;
import jakarta.inject.Singleton;
import java.util.concurrent.ExecutorService;
import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.HttpConnectionFactory;
Expand All @@ -43,6 +48,7 @@
import org.eclipse.jetty.util.resource.Resource;
import org.eclipse.jetty.util.resource.ResourceCollection;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.eclipse.jetty.util.thread.QueuedThreadPool;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand Down Expand Up @@ -111,7 +117,7 @@ protected Server jettyServer(
final Integer port = getConfiguredPort();
String contextPath = getContextPath();

Server server = new Server();
Server server = newServer(applicationContext, configuration);

final ServletContextHandler contextHandler = new ServletContextHandler(server, contextPath, false, false);
final ServletHolder servletHolder = new ServletHolder(new DefaultMicronautServlet(applicationContext));
Expand Down Expand Up @@ -202,11 +208,31 @@ protected Server jettyServer(
return server;
}

/**
* Create a new server instance.
* @param applicationContext The application context
* @param configuration The configuration
* @return The server
*/
protected @NonNull Server newServer(@NonNull ApplicationContext applicationContext, @NonNull MicronautServletConfiguration configuration) {
Server server;
if (configuration.isEnableVirtualThreads() && LoomSupport.isSupported()) {
QueuedThreadPool threadPool = new QueuedThreadPool();
threadPool.setVirtualThreadsExecutor(
applicationContext.getBean(ExecutorService.class, Qualifiers.byName(TaskExecutors.BLOCKING))
);
server = new Server(threadPool);
} else {
server = new Server();
}
return server;
}

/**
* For each static resource configuration, create a {@link ContextHandler} that serves the static resources.
*
* @param config
* @return
* @param config The static resource configuration
* @return the context handler
*/
private ContextHandler toHandler(ServletStaticResourceConfiguration config) {
Resource[] resourceArray = config.getPaths().stream()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package io.micronaut.servlet.jetty

import io.micronaut.test.extensions.spock.annotation.MicronautTest
import jakarta.inject.Inject
import org.eclipse.jetty.server.Server
import org.eclipse.jetty.util.thread.QueuedThreadPool
import spock.lang.Requires
import spock.lang.Specification

@MicronautTest
@Requires({ jvm.java21 })
class JettyVirtualThreadSpec extends Specification {
@Inject Server server

void "test virtual thread enabled on JDK 21+"() {
expect:
server.threadPool instanceof QueuedThreadPool
server.threadPool.virtualThreadsExecutor != null

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@
*/
package io.micronaut.servlet.tomcat;

import io.micronaut.context.annotation.Requires;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.core.util.StringUtils;
import jakarta.inject.Named;
import java.io.File;
import java.util.List;

Expand Down Expand Up @@ -48,6 +52,7 @@
@Factory
public class TomcatFactory extends ServletServerFactory {

private static final String HTTPS = "HTTPS";
private static final Logger LOG = LoggerFactory.getLogger(TomcatFactory.class);

/**
Expand Down Expand Up @@ -77,12 +82,16 @@ public TomcatConfiguration getServerConfiguration() {
* The Tomcat server bean.
*
* @param connector The connector
* @param httpsConnector The HTTPS connector
* @param configuration The servlet configuration
* @return The Tomcat server
*/
@Singleton
@Primary
protected Tomcat tomcatServer(Connector connector, MicronautServletConfiguration configuration) {
protected Tomcat tomcatServer(
Connector connector,
@Named(HTTPS) @Nullable Connector httpsConnector,
MicronautServletConfiguration configuration) {
configuration.setAsyncFileServingEnabled(false);
Tomcat tomcat = new Tomcat();
tomcat.setHostname(getConfiguredHost());
Expand Down Expand Up @@ -118,56 +127,15 @@ protected Tomcat tomcatServer(Connector connector, MicronautServletConfiguration
configuration.getMultipartConfigElement()
.ifPresent(servlet::setMultipartConfigElement);

SslConfiguration sslConfiguration = getSslConfiguration();
if (sslConfiguration.isEnabled()) {
String protocol = sslConfiguration.getProtocol().orElse("TLS");
int sslPort = sslConfiguration.getPort();
if (sslPort == SslConfiguration.DEFAULT_PORT && getEnvironment().getActiveNames().contains(Environment.TEST)) {
sslPort = 0;
}
Connector httpsConnector = new Connector();
SSLHostConfig sslHostConfig = new SSLHostConfig();
SSLHostConfigCertificate certificate = new SSLHostConfigCertificate(sslHostConfig, SSLHostConfigCertificate.Type.UNDEFINED);
sslHostConfig.addCertificate(certificate);
httpsConnector.addSslHostConfig(sslHostConfig);
httpsConnector.setPort(sslPort);
httpsConnector.setSecure(true);
httpsConnector.setScheme("https");
httpsConnector.setProperty("clientAuth", "false");
httpsConnector.setProperty("sslProtocol", protocol);
httpsConnector.setProperty("SSLEnabled", "true");
sslConfiguration.getCiphers().ifPresent(cyphers ->
sslHostConfig.setCiphers(String.join(",", cyphers))
);
sslConfiguration.getClientAuthentication().ifPresent(ca ->
httpsConnector.setProperty("clientAuth", ca == ClientAuthentication.WANT ? "want" : "true")
);


SslConfiguration.KeyStoreConfiguration keyStoreConfig = sslConfiguration.getKeyStore();
keyStoreConfig.getPassword().ifPresent(certificate::setCertificateKeystorePassword);
keyStoreConfig.getPath().ifPresent(certificate::setCertificateKeystoreFile);
keyStoreConfig.getProvider().ifPresent(certificate::setCertificateKeystorePassword);
keyStoreConfig.getType().ifPresent(certificate::setCertificateKeystoreType);

SslConfiguration.TrustStoreConfiguration trustStore = sslConfiguration.getTrustStore();
trustStore.getPassword().ifPresent(sslHostConfig::setTruststorePassword);
trustStore.getPath().ifPresent(sslHostConfig::setTruststoreFile);
trustStore.getProvider().ifPresent(sslHostConfig::setTruststoreProvider);
trustStore.getType().ifPresent(sslHostConfig::setTruststoreType);

SslConfiguration.KeyConfiguration keyConfig = sslConfiguration.getKey();
keyConfig.getAlias().ifPresent(certificate::setCertificateKeyAlias);
keyConfig.getPassword().ifPresent(certificate::setCertificateKeyPassword);

if (httpsConnector != null) {
tomcat.getService().addConnector(httpsConnector);
}

return tomcat;
}

/**
* @return Create the protocol.
* @return Create the connector.
*/
@Singleton
@Primary
Expand All @@ -177,4 +145,54 @@ protected Connector tomcatConnector() {
return tomcatConnector;
}

}
/**
* The HTTPS connector.
* @param sslConfiguration The SSL configuration.
* @return The SSL connector
*/
@Singleton
@Named(HTTPS)
@Requires(property = SslConfiguration.PREFIX + ".enabled", value = StringUtils.TRUE)
protected Connector sslConnector(SslConfiguration sslConfiguration) {
String protocol = sslConfiguration.getProtocol().orElse("TLS");
int sslPort = sslConfiguration.getPort();
if (sslPort == SslConfiguration.DEFAULT_PORT && getEnvironment().getActiveNames().contains(Environment.TEST)) {
sslPort = 0;
}
Connector httpsConnector = new Connector();
SSLHostConfig sslHostConfig = new SSLHostConfig();
SSLHostConfigCertificate certificate = new SSLHostConfigCertificate(sslHostConfig, SSLHostConfigCertificate.Type.UNDEFINED);
sslHostConfig.addCertificate(certificate);
httpsConnector.addSslHostConfig(sslHostConfig);
httpsConnector.setPort(sslPort);
httpsConnector.setSecure(true);
httpsConnector.setScheme("https");
httpsConnector.setProperty("clientAuth", "false");
httpsConnector.setProperty("sslProtocol", protocol);
httpsConnector.setProperty("SSLEnabled", "true");
sslConfiguration.getCiphers().ifPresent(cyphers ->
sslHostConfig.setCiphers(String.join(",", cyphers))
);
sslConfiguration.getClientAuthentication().ifPresent(ca ->
httpsConnector.setProperty("clientAuth", ca == ClientAuthentication.WANT ? "want" : "true")
);


SslConfiguration.KeyStoreConfiguration keyStoreConfig = sslConfiguration.getKeyStore();
keyStoreConfig.getPassword().ifPresent(certificate::setCertificateKeystorePassword);
keyStoreConfig.getPath().ifPresent(certificate::setCertificateKeystoreFile);
keyStoreConfig.getProvider().ifPresent(certificate::setCertificateKeystorePassword);
keyStoreConfig.getType().ifPresent(certificate::setCertificateKeystoreType);

SslConfiguration.TrustStoreConfiguration trustStore = sslConfiguration.getTrustStore();
trustStore.getPassword().ifPresent(sslHostConfig::setTruststorePassword);
trustStore.getPath().ifPresent(sslHostConfig::setTruststoreFile);
trustStore.getProvider().ifPresent(sslHostConfig::setTruststoreProvider);
trustStore.getType().ifPresent(sslHostConfig::setTruststoreType);

SslConfiguration.KeyConfiguration keyConfig = sslConfiguration.getKey();
keyConfig.getAlias().ifPresent(certificate::setCertificateKeyAlias);
keyConfig.getPassword().ifPresent(certificate::setCertificateKeyPassword);
return httpsConnector;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* Copyright 2017-2024 original authors
*
* 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
*
* https://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 io.micronaut.servlet.tomcat;

import io.micronaut.context.annotation.Requires;
import io.micronaut.context.event.BeanCreatedEvent;
import io.micronaut.context.event.BeanCreatedEventListener;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.servlet.http.ServletConfiguration;
import jakarta.inject.Singleton;
import org.apache.catalina.connector.Connector;
import org.apache.coyote.ProtocolHandler;
import org.apache.tomcat.util.threads.VirtualThreadExecutor;

/**
* Enables virtual thread configuration if enabled.
*/
@Requires(sdk = Requires.Sdk.JAVA, version = "21")
@Singleton
class TomcatVirtualThreadEnabler implements BeanCreatedEventListener<Connector> {
private final ServletConfiguration servletConfiguration;

public TomcatVirtualThreadEnabler(ServletConfiguration servletConfiguration) {
this.servletConfiguration = servletConfiguration;
}

@Override
public Connector onCreated(@NonNull BeanCreatedEvent<Connector> event) {
Connector connector = event.getBean();
if (servletConfiguration.isEnableVirtualThreads()) {
ProtocolHandler protocolHandler = connector.getProtocolHandler();
protocolHandler.setExecutor(new VirtualThreadExecutor("tomcat-handler-"));
}
return connector;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package io.micronaut.servlet.tomcat


import io.micronaut.test.extensions.spock.annotation.MicronautTest
import jakarta.inject.Inject
import org.apache.catalina.startup.Tomcat
import org.apache.tomcat.util.threads.VirtualThreadExecutor
import spock.lang.Requires
import spock.lang.Specification

@MicronautTest
@Requires({ jvm.java21 })
class TomcatVirtualThreadSpec extends Specification {
@Inject Tomcat server

void "test virtual thread enabled on JDK 21+"() {
expect:
server.connector.protocolHandler.executor instanceof VirtualThreadExecutor
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,16 @@ public interface ServletConfiguration {
default boolean isAsyncSupported() {
return true;
}

/**
* Whether to enable virtual thread support if available.
*
* <p>If virtual threads are not available this option does nothing.</p>
*
* @return True if they should be enabled
* @since 4.8.0
*/
default boolean isEnableVirtualThreads() {
return true;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ public class MicronautServletConfiguration implements Named, ServletConfiguratio
private boolean asyncFileServingEnabled = true;

private boolean asyncSupported = true;
private boolean enableVirtualThreads = true;


/**
Expand Down Expand Up @@ -105,6 +106,20 @@ public void setTestAsyncSupported(@Nullable Boolean asyncSupported) {
}
}

@Override
public boolean isEnableVirtualThreads() {
return this.enableVirtualThreads;
}

/**
* Whether virtual threads are enabled.
* @param enableVirtualThreads True if they are enabled
* @since 4.8.0
*/
public void setEnableVirtualThreads(boolean enableVirtualThreads) {
this.enableVirtualThreads = enableVirtualThreads;
}

/**
* @return The servlet mapping.
*/
Expand Down

0 comments on commit ab6b5f9

Please sign in to comment.