From ab6b5f919922555ce8387728af958274aa2552e9 Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Wed, 22 May 2024 16:41:10 +0200 Subject: [PATCH] Support Virtual Threads in Jetty & Tomcat (#701) 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 --------- Co-authored-by: Sergio del Amo --- .../micronaut/servlet/jetty/JettyFactory.java | 32 +++++- .../jetty/JettyVirtualThreadSpec.groovy | 21 ++++ .../servlet/tomcat/TomcatFactory.java | 108 ++++++++++-------- .../tomcat/TomcatVirtualThreadEnabler.java | 49 ++++++++ .../tomcat/TomcatVirtualThreadSpec.groovy | 20 ++++ .../servlet/http/ServletConfiguration.java | 12 ++ .../engine/MicronautServletConfiguration.java | 15 +++ 7 files changed, 209 insertions(+), 48 deletions(-) create mode 100644 http-server-jetty/src/test/groovy/io/micronaut/servlet/jetty/JettyVirtualThreadSpec.groovy create mode 100644 http-server-tomcat/src/main/java/io/micronaut/servlet/tomcat/TomcatVirtualThreadEnabler.java create mode 100644 http-server-tomcat/src/test/groovy/io/micronaut/servlet/tomcat/TomcatVirtualThreadSpec.groovy diff --git a/http-server-jetty/src/main/java/io/micronaut/servlet/jetty/JettyFactory.java b/http-server-jetty/src/main/java/io/micronaut/servlet/jetty/JettyFactory.java index 559bf2f06..920b4c84e 100644 --- a/http-server-jetty/src/main/java/io/micronaut/servlet/jetty/JettyFactory.java +++ b/http-server-jetty/src/main/java/io/micronaut/servlet/jetty/JettyFactory.java @@ -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; @@ -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; @@ -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)); @@ -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() diff --git a/http-server-jetty/src/test/groovy/io/micronaut/servlet/jetty/JettyVirtualThreadSpec.groovy b/http-server-jetty/src/test/groovy/io/micronaut/servlet/jetty/JettyVirtualThreadSpec.groovy new file mode 100644 index 000000000..e95f9bdea --- /dev/null +++ b/http-server-jetty/src/test/groovy/io/micronaut/servlet/jetty/JettyVirtualThreadSpec.groovy @@ -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 + + } +} diff --git a/http-server-tomcat/src/main/java/io/micronaut/servlet/tomcat/TomcatFactory.java b/http-server-tomcat/src/main/java/io/micronaut/servlet/tomcat/TomcatFactory.java index 57c1aa99a..1297a3e9d 100644 --- a/http-server-tomcat/src/main/java/io/micronaut/servlet/tomcat/TomcatFactory.java +++ b/http-server-tomcat/src/main/java/io/micronaut/servlet/tomcat/TomcatFactory.java @@ -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; @@ -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); /** @@ -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()); @@ -118,48 +127,7 @@ 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); } @@ -167,7 +135,7 @@ protected Tomcat tomcatServer(Connector connector, MicronautServletConfiguration } /** - * @return Create the protocol. + * @return Create the connector. */ @Singleton @Primary @@ -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; + } + } diff --git a/http-server-tomcat/src/main/java/io/micronaut/servlet/tomcat/TomcatVirtualThreadEnabler.java b/http-server-tomcat/src/main/java/io/micronaut/servlet/tomcat/TomcatVirtualThreadEnabler.java new file mode 100644 index 000000000..28dc0984d --- /dev/null +++ b/http-server-tomcat/src/main/java/io/micronaut/servlet/tomcat/TomcatVirtualThreadEnabler.java @@ -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 { + private final ServletConfiguration servletConfiguration; + + public TomcatVirtualThreadEnabler(ServletConfiguration servletConfiguration) { + this.servletConfiguration = servletConfiguration; + } + + @Override + public Connector onCreated(@NonNull BeanCreatedEvent event) { + Connector connector = event.getBean(); + if (servletConfiguration.isEnableVirtualThreads()) { + ProtocolHandler protocolHandler = connector.getProtocolHandler(); + protocolHandler.setExecutor(new VirtualThreadExecutor("tomcat-handler-")); + } + return connector; + } +} diff --git a/http-server-tomcat/src/test/groovy/io/micronaut/servlet/tomcat/TomcatVirtualThreadSpec.groovy b/http-server-tomcat/src/test/groovy/io/micronaut/servlet/tomcat/TomcatVirtualThreadSpec.groovy new file mode 100644 index 000000000..839257399 --- /dev/null +++ b/http-server-tomcat/src/test/groovy/io/micronaut/servlet/tomcat/TomcatVirtualThreadSpec.groovy @@ -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 + } +} diff --git a/servlet-core/src/main/java/io/micronaut/servlet/http/ServletConfiguration.java b/servlet-core/src/main/java/io/micronaut/servlet/http/ServletConfiguration.java index cc6ab9128..bd6bb1a74 100644 --- a/servlet-core/src/main/java/io/micronaut/servlet/http/ServletConfiguration.java +++ b/servlet-core/src/main/java/io/micronaut/servlet/http/ServletConfiguration.java @@ -38,4 +38,16 @@ public interface ServletConfiguration { default boolean isAsyncSupported() { return true; } + + /** + * Whether to enable virtual thread support if available. + * + *

If virtual threads are not available this option does nothing.

+ * + * @return True if they should be enabled + * @since 4.8.0 + */ + default boolean isEnableVirtualThreads() { + return true; + } } diff --git a/servlet-engine/src/main/java/io/micronaut/servlet/engine/MicronautServletConfiguration.java b/servlet-engine/src/main/java/io/micronaut/servlet/engine/MicronautServletConfiguration.java index 5e0a16360..7369f0d78 100644 --- a/servlet-engine/src/main/java/io/micronaut/servlet/engine/MicronautServletConfiguration.java +++ b/servlet-engine/src/main/java/io/micronaut/servlet/engine/MicronautServletConfiguration.java @@ -50,6 +50,7 @@ public class MicronautServletConfiguration implements Named, ServletConfiguratio private boolean asyncFileServingEnabled = true; private boolean asyncSupported = true; + private boolean enableVirtualThreads = true; /** @@ -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. */