diff --git a/http/README.md b/http/README.md index 1cd8cf55e5..38585ff5d8 100644 --- a/http/README.md +++ b/http/README.md @@ -440,7 +440,8 @@ properties can be used (some legacy property names still exist but are not docum | `org.apache.felix.jetty.alpn.defaultProtocol` | The default protocol when negotiation fails. Default is http/1.1. | | `org.apache.felix.jakarta.websocket.enable` | Enables Jakarta websocket support. Default is false. | | `org.apache.felix.jetty.websocket.enable` | Enables Jetty websocket support. Default is false. | -| `org.apache.felix.http.jetty.virtualthreads.enable` | Enables using virtual threads in Jetty 12 (JDK 21 required). Default is false. | +| `org.apache.felix.http.jetty.threadpool.max` | The maximum number of threads in the Jetty thread pool. Default is unlimited. Works for both platform threads and virtual threads (Jetty 12 only). | +| `org.apache.felix.http.jetty.virtualthreads.enable` | Enables using virtual threads in Jetty 12 (JDK 21 required). Default is false. When enabled, `org.apache.felix.http.jetty.threadpool.max` is used for a bounded virtual thread pool. | ### Multiple Servers It is possible to configure several Http Services, each running on a different port. The first service can be configured as outlined above using the service PID for `"org.apache.felix.http"`. Additional servers can be configured through OSGi factory configurations using `"org.apache.felix.http"` as the factory PID. The properties for the configuration are outlined as above. diff --git a/http/jetty12/src/main/java/org/apache/felix/http/jetty/internal/JettyService.java b/http/jetty12/src/main/java/org/apache/felix/http/jetty/internal/JettyService.java index cc5a118891..efe4f8fb83 100644 --- a/http/jetty12/src/main/java/org/apache/felix/http/jetty/internal/JettyService.java +++ b/http/jetty12/src/main/java/org/apache/felix/http/jetty/internal/JettyService.java @@ -62,6 +62,7 @@ import org.eclipse.jetty.util.ssl.SslContextFactory; import org.eclipse.jetty.util.thread.QueuedThreadPool; import org.eclipse.jetty.util.thread.ThreadPool; +import org.eclipse.jetty.util.thread.VirtualThreadPool; import org.osgi.framework.Bundle; import org.osgi.framework.BundleContext; import org.osgi.framework.Constants; @@ -276,18 +277,28 @@ private void initializeJetty() throws Exception { final int threadPoolMax = this.config.getThreadPoolMax(); - if (threadPoolMax >= 0) { + if (!this.config.isUseVirtualThreads() && threadPoolMax >= 0) { this.server = new Server(new QueuedThreadPool(threadPoolMax)); } else if (this.config.isUseVirtualThreads()){ - QueuedThreadPool threadPool = new QueuedThreadPool(); + // See https://jetty.org/docs/jetty/12/programming-guide/arch/threads.html#thread-pool-virtual-threads Method newVirtualThreadPerTaskExecutorMethod = null; try { newVirtualThreadPerTaskExecutorMethod = Executors.class.getMethod("newVirtualThreadPerTaskExecutor"); } catch (NoSuchMethodException e){ - throw new IllegalArgumentException("Virtual threads are only available in Java 21 or later, or via preview flags in Java 17-20"); + throw new IllegalArgumentException("Virtual threads are only available in Java 21 or later, or via preview flags in Java 19-20"); + } + if (threadPoolMax >= 0) { + // Configurable, bounded, virtual thread executor + VirtualThreadPool threadPool = new VirtualThreadPool(); + threadPool.setMaxThreads(threadPoolMax); + this.server = new Server(threadPool); + } else { + // Simple, unlimited, virtual thread Executor + QueuedThreadPool threadPool = new QueuedThreadPool(); + final Executor virtualExecutor = (Executor) newVirtualThreadPerTaskExecutorMethod.invoke(null); + threadPool.setVirtualThreadsExecutor(virtualExecutor); + this.server = new Server(threadPool); } - threadPool.setVirtualThreadsExecutor((Executor) newVirtualThreadPerTaskExecutorMethod.invoke(null)); - this.server = new Server(threadPool); } else { this.server = new Server(); } @@ -402,6 +413,9 @@ private void initializeJetty() throws Exception ThreadPool.SizedThreadPool sizedThreadPool = (ThreadPool.SizedThreadPool) threadPool; message.append("minThreads=").append(sizedThreadPool.getMinThreads()).append(","); message.append("maxThreads=").append(sizedThreadPool.getMaxThreads()).append(","); + } else if (threadPool instanceof VirtualThreadPool) { + VirtualThreadPool sizedThreadPool = (VirtualThreadPool) threadPool; + message.append("maxVirtualThreads=").append(sizedThreadPool.getMaxThreads()).append(","); } Connector connector = this.server.getConnectors()[0]; if (connector instanceof ServerConnector) { diff --git a/http/jetty12/src/test/java/org/apache/felix/http/jetty/it/JettyVirtualThreadsThreadPoolIT.java b/http/jetty12/src/test/java/org/apache/felix/http/jetty/it/JettyVirtualThreadsThreadPoolIT.java new file mode 100644 index 0000000000..e22a2b775a --- /dev/null +++ b/http/jetty12/src/test/java/org/apache/felix/http/jetty/it/JettyVirtualThreadsThreadPoolIT.java @@ -0,0 +1,60 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 org.apache.felix.http.jetty.it; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.ops4j.pax.exam.CoreOptions.mavenBundle; +import static org.ops4j.pax.exam.cm.ConfigurationAdminOptions.newConfiguration; + +import java.io.IOException; +import java.net.URI; +import java.util.Hashtable; +import java.util.Map; + +import javax.inject.Inject; +import jakarta.servlet.Servlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.eclipse.jetty.client.ContentResponse; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.transport.HttpClientTransportOverHTTP; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.ops4j.pax.exam.Option; +import org.ops4j.pax.exam.junit.PaxExam; +import org.ops4j.pax.exam.spi.reactors.ExamReactorStrategy; +import org.ops4j.pax.exam.spi.reactors.PerClass; +import org.osgi.framework.BundleContext; +import org.osgi.service.http.HttpService; +import org.osgi.service.servlet.whiteboard.HttpWhiteboardConstants; + +@RunWith(PaxExam.class) +@ExamReactorStrategy(PerClass.class) +public class JettyVirtualThreadsThreadPoolIT extends JettyVirtualThreadsIT { + @Override + protected Option felixHttpConfig(int httpPort) { + return newConfiguration("org.apache.felix.http") + .put("org.osgi.service.http.port", httpPort) + .put("org.apache.felix.http.jetty.threadpool.max", 100) + .put("org.apache.felix.http.jetty.virtualthreads.enable", Boolean.TRUE.toString()) + .asOption(); + } +}