From e41af825359b02fa668759cc609856c51ca6c691 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paul=20R=C3=BCtter?= Date: Fri, 4 Sep 2026 00:35:43 +0200 Subject: [PATCH] FELIX-6859 allow a bounded VirtualThreadPool as the virtual threads executor Jetty 12 documents a QueuedThreadPool whose virtual threads executor is a bounded VirtualThreadPool as the preferred setup, but Felix HTTP could not be configured to build it. The two virtual thread options were the extremes: an unbounded number of concurrent tasks, or a standalone VirtualThreadPool that creates only virtual threads. A deployment that wants platform threads for the acceptors and the selectors, and a bound on concurrent request tasks, had no way to express that. Adds org.apache.felix.http.jetty.virtualthreads.max to the jetty12 bundle. When virtual threads are enabled and this property is set to a positive value, the thread pool becomes a QueuedThreadPool, sized by threadpool.max as usual, whose virtual threads executor is a VirtualThreadPool bounded by setMaxConcurrentTasks. Note that unlike threadpool.max, this property bounds the number of concurrent tasks rather than the number of threads. The VirtualThreadPool is added as a bean of the QueuedThreadPool, because setVirtualThreadsExecutor() only stores the executor and does not manage its life cycle; an unstarted VirtualThreadPool rejects every task with a RejectedExecutionException. Being a managed bean also covers the teardown, so the pool is stopped when the server stops. Non-positive values fall through to the existing behaviour, since Jetty itself treats maxConcurrentTasks <= 0 as unbounded. The pre-existing branch that builds a standalone VirtualThreadPool keeps using threadpool.max as its bound, so no released behaviour changes. The thread pool selection is extracted from createServer() into a package private createThreadPool(JettyConfig), returning null when no thread pool is configured so that Jetty's own default applies, so that the resulting pool can be asserted without starting a server. JettyServiceThreadPoolTest covers every combination: the type of pool, its bound, and that the VirtualThreadPool is registered as a bean of the QueuedThreadPool. Two related fixes in the same area: * The attribute definition for virtualthreads.enable passed -1 as its default value, which selects the int constructor of AttributeDefinitionImpl, so the flag was declared as an INTEGER attribute defaulting to -1 even though JettyConfig reads it with getBooleanProperty. It is now declared as a BOOLEAN defaulting to false. Reading a previously stored integer value is unaffected. * The guard in JettyVirtualThreadsIT matched the version string with startsWith("21"), so the test body was skipped on every later JDK while the test was still reported as passing. It now compares the feature version, so the ITs run on Java 21 and on every later JDK. Note that the Pax Exam runner swallows a failed assumption and reports the test as passing rather than as skipped, so below Java 21 the test still shows up as green, as it did before. Co-Authored-By: Claude Opus 5 --- http/README.md | 46 +++-- .../internal/ConfigMetaTypeProvider.java | 8 +- .../http/jetty/internal/JettyConfig.java | 8 + .../http/jetty/internal/JettyService.java | 53 +++++- .../http/jetty/internal/JettyConfigTest.java | 17 ++ .../internal/JettyServiceThreadPoolTest.java | 177 ++++++++++++++++++ .../JettyVirtualThreadsBoundedExecutorIT.java | 46 +++++ .../http/jetty/it/JettyVirtualThreadsIT.java | 12 +- 8 files changed, 335 insertions(+), 32 deletions(-) create mode 100644 http/jetty12/src/test/java/org/apache/felix/http/jetty/internal/JettyServiceThreadPoolTest.java create mode 100644 http/jetty12/src/test/java/org/apache/felix/http/jetty/it/JettyVirtualThreadsBoundedExecutorIT.java diff --git a/http/README.md b/http/README.md index 2483e7c420..8adc663938 100644 --- a/http/README.md +++ b/http/README.md @@ -479,23 +479,34 @@ properties can be used (some legacy property names still exist but are not docum | `org.apache.felix.http.jetty.selectors` | The number of Jetty selector threads for the connector. Selectors notice and schedule established connections that can make I/O progress. Default is `-1`, which lets Jetty choose: for a sized thread pool Jetty uses `max(1, min(cpus / 2, maxThreads / 16))`, and for a thread pool that is not sized, such as the `VirtualThreadPool`, it uses `max(1, cpus / 2)`. Keep the selector count below the number of carrier threads when virtual threads are active. | | `org.apache.felix.http.jetty.threadpool.max` | The maximum number of threads in the Jetty thread pool. Default is `-1`, meaning the property is unset and Jetty's own default applies, which is a `QueuedThreadPool` with 200 platform threads. When `org.apache.felix.http.jetty.virtualthreads.enable` is `true`, this property also selects the type of thread pool, see [Thread pool and virtual threads](#thread-pool-and-virtual-threads). | | `org.apache.felix.http.jetty.virtualthreads.enable` | Enables virtual threads in Jetty 12 (JDK 21 or later). Default is `false`. The value of `org.apache.felix.http.jetty.threadpool.max` then selects which thread pool is built, see [Thread pool and virtual threads](#thread-pool-and-virtual-threads). | +| `org.apache.felix.http.jetty.virtualthreads.max` | The maximum number of virtual thread tasks that run at the same time, or `-1` to leave the number unbounded. Note that unlike `org.apache.felix.http.jetty.threadpool.max` this bounds concurrent tasks, not the number of threads. Only relevant when `org.apache.felix.http.jetty.virtualthreads.enable` is `true`. When set to a positive value, Jetty's preferred setup is used: a `QueuedThreadPool`, sized by `org.apache.felix.http.jetty.threadpool.max`, whose virtual threads executor is a bounded `VirtualThreadPool`. Platform threads then still run the acceptors and the selectors. Default is `-1`. Jetty 12 bundle only, available from version 2.0.8. | ### Thread pool and virtual threads -The combination of `org.apache.felix.http.jetty.threadpool.max` and -`org.apache.felix.http.jetty.virtualthreads.enable` decides which thread pool the Jetty 12 bundle -builds. Both properties are unset by default, which gives a `QueuedThreadPool` with 200 platform -threads, the Jetty default. - -| `virtualthreads.enable` | `threadpool.max` | Thread pool | -|--|--|--| -| `false` | unset (`-1`) | Jetty's default `QueuedThreadPool` with 200 platform threads. | -| `false` | set | A `QueuedThreadPool` with `` platform threads. | -| `true` | unset (`-1`) | A `QueuedThreadPool` whose virtual threads executor is `Executors.newVirtualThreadPerTaskExecutor()`. The number of concurrent virtual thread tasks is **unbounded**. | -| `true` | set | A standalone `VirtualThreadPool` with `setMaxConcurrentTasks()`. A semaphore limits the number of tasks that run at the same time. | - -Note that enabling virtual threads without setting `org.apache.felix.http.jetty.threadpool.max` -yields the unbounded variant, which Jetty warns can exhaust memory during a load spike. +The combination of `org.apache.felix.http.jetty.threadpool.max`, +`org.apache.felix.http.jetty.virtualthreads.enable` and +`org.apache.felix.http.jetty.virtualthreads.max` decides which thread pool the Jetty 12 bundle +builds. All three properties are unset by default, which gives a `QueuedThreadPool` with 200 +platform threads, the Jetty default. + +| `virtualthreads.enable` | `threadpool.max` | `virtualthreads.max` | Thread pool | +|--|--|--|--| +| `false` | unset (`-1`) | - | Jetty's default `QueuedThreadPool` with 200 platform threads. | +| `false` | set | - | A `QueuedThreadPool` with `` platform threads. | +| `true` | unset (`-1`) | unset (`-1`) | A `QueuedThreadPool` whose virtual threads executor is `Executors.newVirtualThreadPerTaskExecutor()`. The number of concurrent virtual thread tasks is **unbounded**. | +| `true` | set | unset (`-1`) | A standalone `VirtualThreadPool` with `setMaxConcurrentTasks()`. A semaphore limits the number of tasks that run at the same time. The pool creates only virtual threads. | +| `true` | unset or set | set | Jetty's preferred setup: a `QueuedThreadPool`, sized by `threadpool.max`, whose virtual threads executor is a `VirtualThreadPool` with `setMaxConcurrentTasks()`. Platform threads still run the acceptors and the selectors. Available from Jetty 12 bundle version 2.0.8. | + +Note that enabling virtual threads without setting either `org.apache.felix.http.jetty.threadpool.max` +or `org.apache.felix.http.jetty.virtualthreads.max` yields the unbounded variant, which Jetty warns +can exhaust memory during a load spike. + +Note also that `org.apache.felix.http.jetty.threadpool.max` and +`org.apache.felix.http.jetty.virtualthreads.max` bound different things: +`threadpool.max` bounds threads, `virtualthreads.max` bounds the number of tasks that run at the +same time. The fourth row above is the exception, where `threadpool.max` is reused as a bound on +concurrent tasks. That is kept for backwards compatibility; `virtualthreads.max` is the properly +named equivalent. A few more things to be aware of when virtual threads are enabled: @@ -503,9 +514,10 @@ A few more things to be aware of when virtual threads are enabled: and Jetty sets the reserved thread count to zero, so it always uses the Produce-Execute-Consume mode. The standalone `VirtualThreadPool` only creates virtual threads. * Keep `org.apache.felix.http.jetty.selectors` below the number of carrier threads. -* Jetty documents a third combination as the preferred one: a `QueuedThreadPool` whose virtual - threads executor is a bounded `VirtualThreadPool`. There is no Felix HTTP configuration for that - combination yet, this is tracked in [FELIX-6859](https://issues.apache.org/jira/browse/FELIX-6859). +* The combination Jetty documents as the preferred one, a `QueuedThreadPool` whose virtual threads + executor is a bounded `VirtualThreadPool`, is the last row of the table. It is configured with + `org.apache.felix.http.jetty.virtualthreads.max` and is available from Jetty 12 bundle version + 2.0.8, see [FELIX-6859](https://issues.apache.org/jira/browse/FELIX-6859). Virtual threads require JDK 21 or later and are only supported by the Jetty 12 bundle. The Jetty 11 bundle only honours `org.apache.felix.http.jetty.threadpool.max`. diff --git a/http/jetty12/src/main/java/org/apache/felix/http/jetty/internal/ConfigMetaTypeProvider.java b/http/jetty12/src/main/java/org/apache/felix/http/jetty/internal/ConfigMetaTypeProvider.java index 2372d8c59f..ec29ea5f85 100644 --- a/http/jetty12/src/main/java/org/apache/felix/http/jetty/internal/ConfigMetaTypeProvider.java +++ b/http/jetty12/src/main/java/org/apache/felix/http/jetty/internal/ConfigMetaTypeProvider.java @@ -168,9 +168,15 @@ public ObjectClassDefinition getObjectClassDefinition( String id, String locale adList.add(new AttributeDefinitionImpl(JettyConfig.FELIX_JETTY_USE_VIRTUAL_THREADS, "Use Virtual Threads", "Use virtual threads in Jetty (JDK 21 or higher). Defaults to false.", - -1, + false, bundle.getBundleContext().getProperty(JettyConfig.FELIX_JETTY_USE_VIRTUAL_THREADS))); + adList.add(new AttributeDefinitionImpl(JettyConfig.FELIX_JETTY_VIRTUAL_THREADS_MAX, + "Virtual Threads Max Concurrent Tasks", + "The maximum number of virtual thread tasks that run at the same time, or -1 to leave the number unbounded. Note that unlike 'Threadpool Max' this bounds concurrent tasks, not the number of threads. Only relevant if 'Use Virtual Threads' is enabled. When set to a positive value, Jetty's preferred setup is used: a QueuedThreadPool, sized by 'Threadpool Max', with a bounded VirtualThreadPool as its virtual threads executor. Defaults to -1.", + -1, + bundle.getBundleContext().getProperty(JettyConfig.FELIX_JETTY_VIRTUAL_THREADS_MAX))); + adList.add(new AttributeDefinitionImpl(JettyConfig.FELIX_JETTY_ACCEPTORS, "Acceptors", "Number of acceptor threads to use, or -1 for a default value. Acceptors accept new TCP/IP connections. If 0, then the selector threads are used to accept connections.", diff --git a/http/jetty12/src/main/java/org/apache/felix/http/jetty/internal/JettyConfig.java b/http/jetty12/src/main/java/org/apache/felix/http/jetty/internal/JettyConfig.java index 0e2c605008..28e9eb6936 100644 --- a/http/jetty12/src/main/java/org/apache/felix/http/jetty/internal/JettyConfig.java +++ b/http/jetty12/src/main/java/org/apache/felix/http/jetty/internal/JettyConfig.java @@ -90,6 +90,9 @@ public final class JettyConfig /** Felix specific property to enable the use of virtual threads in Jetty */ public static final String FELIX_JETTY_USE_VIRTUAL_THREADS = "org.apache.felix.http.jetty.virtualthreads.enable"; + /** Felix specific property to bound the number of concurrent virtual thread tasks in Jetty */ + public static final String FELIX_JETTY_VIRTUAL_THREADS_MAX = "org.apache.felix.http.jetty.virtualthreads.max"; + /** Felix specific property to control the number of jetty acceptor threads */ public static final String FELIX_JETTY_ACCEPTORS = "org.apache.felix.http.jetty.acceptors"; @@ -498,6 +501,11 @@ public boolean isUseVirtualThreads() { return this.getBooleanProperty(FELIX_JETTY_USE_VIRTUAL_THREADS, false); } + public int getVirtualThreadsMax() + { + return getIntProperty(FELIX_JETTY_VIRTUAL_THREADS_MAX, -1); + } + public int getRequestBufferSize() { return getIntProperty(FELIX_JETTY_REQUEST_BUFFER_SIZE, 8 * 1024); 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 9706368ed1..982be440b7 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 @@ -438,10 +438,28 @@ private void initializeJetty() throws Exception private Server createServer() throws Exception { - final int threadPoolMax = this.config.getThreadPoolMax(); - if (!this.config.isUseVirtualThreads() && threadPoolMax >= 0) { - return new Server(new QueuedThreadPool(threadPoolMax)); - } else if (this.config.isUseVirtualThreads()) { + final ThreadPool threadPool = createThreadPool(this.config); + return threadPool == null ? new Server() : new Server(threadPool); + } + + /** + * Selects the Jetty thread pool for the configured combination of + * {@link JettyConfig#FELIX_JETTY_THREADPOOL_MAX}, + * {@link JettyConfig#FELIX_JETTY_USE_VIRTUAL_THREADS} and + * {@link JettyConfig#FELIX_JETTY_VIRTUAL_THREADS_MAX}. + * + * Package private so that the selection can be asserted without starting a server. + * + * @param config The configuration + * @return The thread pool, or {@code null} when none is configured, in which case + * Jetty's own default applies. + */ + static ThreadPool createThreadPool(final JettyConfig config) throws Exception + { + final int threadPoolMax = config.getThreadPoolMax(); + if (!config.isUseVirtualThreads()) { + return threadPoolMax >= 0 ? new QueuedThreadPool(threadPoolMax) : null; + } else { // See https://jetty.org/docs/jetty/12/programming-guide/arch/threads.html#thread-pool-virtual-threads Method newVirtualThreadPerTaskExecutorMethod = null; try { @@ -449,22 +467,37 @@ private Server createServer() throws Exception } catch (NoSuchMethodException e) { throw new IllegalArgumentException("Virtual threads are only available in Java 21 or later, or via preview flags in Java 19-20"); } - if (threadPoolMax >= 0) { + final int maxConcurrentTasks = config.getVirtualThreadsMax(); + if (maxConcurrentTasks > 0) { + // Jetty's preferred setup: a QueuedThreadPool, which keeps platform threads for + // the acceptors and the selectors, with a bounded VirtualThreadPool as its virtual + // threads executor. The VirtualThreadPool is added as a bean because + // setVirtualThreadsExecutor() only stores the executor, it does not manage its + // life cycle, and an unstarted VirtualThreadPool rejects every task. + QueuedThreadPool threadPool = threadPoolMax >= 0 ? new QueuedThreadPool(threadPoolMax) : new QueuedThreadPool(); + VirtualThreadPool virtualThreadPool = new VirtualThreadPool(); + virtualThreadPool.setMaxConcurrentTasks(maxConcurrentTasks); + threadPool.setVirtualThreadsExecutor(virtualThreadPool); + threadPool.addBean(virtualThreadPool); + return threadPool; + } else if (threadPoolMax >= 0) { // Standalone VirtualThreadPool as the server's thread pool: only virtual - // threads, with a semaphore limiting concurrent tasks to threadPoolMax. + // threads, with a semaphore limiting concurrent tasks to threadPoolMax. Note + // that this branch predates virtualthreads.max and reuses threadpool.max as a + // bound on concurrent tasks rather than on threads. It is kept as is for + // backwards compatibility; virtualthreads.max above is the properly named + // equivalent. VirtualThreadPool threadPool = new VirtualThreadPool(); threadPool.setMaxConcurrentTasks(threadPoolMax); - return new Server(threadPool); + return threadPool; } else { // QueuedThreadPool with an unbounded virtual-threads executor: platform // threads still run the acceptors and selectors, tasks run on virtual threads. QueuedThreadPool threadPool = new QueuedThreadPool(); final Executor virtualExecutor = (Executor) newVirtualThreadPerTaskExecutorMethod.invoke(null); threadPool.setVirtualThreadsExecutor(virtualExecutor); - return new Server(threadPool); + return threadPool; } - } else { - return new Server(); } } diff --git a/http/jetty12/src/test/java/org/apache/felix/http/jetty/internal/JettyConfigTest.java b/http/jetty12/src/test/java/org/apache/felix/http/jetty/internal/JettyConfigTest.java index e0e7bcc269..42c790f689 100644 --- a/http/jetty12/src/test/java/org/apache/felix/http/jetty/internal/JettyConfigTest.java +++ b/http/jetty12/src/test/java/org/apache/felix/http/jetty/internal/JettyConfigTest.java @@ -149,6 +149,23 @@ public class JettyConfigTest assertEquals("string2", ((List)toCheck.get("list")).get(1)); } + @Test public void testGetDefaultVirtualThreadsMax() + { + assertEquals(-1, this.config.getVirtualThreadsMax()); + } + + @Test public void testGetVirtualThreadsMax() + { + Hashtable props = new Hashtable<>(); + props.put(JettyConfig.FELIX_JETTY_VIRTUAL_THREADS_MAX, 200); + this.config.update(props); + assertEquals(200, this.config.getVirtualThreadsMax()); + + props.put(JettyConfig.FELIX_JETTY_VIRTUAL_THREADS_MAX, "200"); + this.config.update(props); + assertEquals(200, this.config.getVirtualThreadsMax()); + } + @Before public void setUp() { diff --git a/http/jetty12/src/test/java/org/apache/felix/http/jetty/internal/JettyServiceThreadPoolTest.java b/http/jetty12/src/test/java/org/apache/felix/http/jetty/internal/JettyServiceThreadPoolTest.java new file mode 100644 index 0000000000..8ddd9e063a --- /dev/null +++ b/http/jetty12/src/test/java/org/apache/felix/http/jetty/internal/JettyServiceThreadPoolTest.java @@ -0,0 +1,177 @@ +/* + * 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.internal; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assume.assumeTrue; + +import java.util.Hashtable; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; + +import org.eclipse.jetty.util.thread.QueuedThreadPool; +import org.eclipse.jetty.util.thread.ThreadPool; +import org.eclipse.jetty.util.thread.VirtualThreadPool; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; +import org.osgi.framework.BundleContext; + +/** + * Unit test for the thread pool selection in JettyService, which depends on the + * combination of threadpool.max, virtualthreads.enable and virtualthreads.max. + */ +public class JettyServiceThreadPoolTest +{ + JettyConfig config; + BundleContext context; + + /** + * The virtual thread cases only apply to Java 21 or later. Probed the same way + * JettyService probes it, so this stays correct beyond Java 21. + */ + private static void assumeVirtualThreads() + { + try + { + Executors.class.getMethod("newVirtualThreadPerTaskExecutor"); + } + catch (NoSuchMethodException e) + { + assumeTrue("virtual threads are not available on this JVM", false); + } + } + + private ThreadPool createThreadPool(final Object... keysAndValues) throws Exception + { + final Hashtable props = new Hashtable<>(); + for (int i = 0; i < keysAndValues.length; i += 2) + { + props.put((String) keysAndValues[i], keysAndValues[i + 1]); + } + this.config.update(props); + return JettyService.createThreadPool(this.config); + } + + @Test public void testNoThreadPoolConfigured() throws Exception + { + // null means Jetty's own default applies, which is a QueuedThreadPool with 200 threads + assertNull(createThreadPool()); + } + + @Test public void testPlatformThreadPoolWithMax() throws Exception + { + final ThreadPool threadPool = createThreadPool(JettyConfig.FELIX_JETTY_THREADPOOL_MAX, 42); + assertTrue(threadPool instanceof QueuedThreadPool); + assertEquals(42, ((QueuedThreadPool) threadPool).getMaxThreads()); + assertNull(((QueuedThreadPool) threadPool).getVirtualThreadsExecutor()); + } + + @Test public void testVirtualThreadsUnbounded() throws Exception + { + assumeVirtualThreads(); + final ThreadPool threadPool = createThreadPool( + JettyConfig.FELIX_JETTY_USE_VIRTUAL_THREADS, Boolean.TRUE.toString()); + + assertTrue(threadPool instanceof QueuedThreadPool); + final Executor executor = ((QueuedThreadPool) threadPool).getVirtualThreadsExecutor(); + // an unbounded per task executor, not a VirtualThreadPool + assertFalse(executor instanceof VirtualThreadPool); + } + + @Test public void testVirtualThreadsStandalonePoolBoundedByThreadPoolMax() throws Exception + { + assumeVirtualThreads(); + final ThreadPool threadPool = createThreadPool( + JettyConfig.FELIX_JETTY_USE_VIRTUAL_THREADS, Boolean.TRUE.toString(), + JettyConfig.FELIX_JETTY_THREADPOOL_MAX, 100); + + // the pre-existing behaviour: threadpool.max bounds the concurrent tasks + assertTrue(threadPool instanceof VirtualThreadPool); + assertEquals(100, ((VirtualThreadPool) threadPool).getMaxConcurrentTasks()); + } + + @Test public void testVirtualThreadsBoundedExecutor() throws Exception + { + assumeVirtualThreads(); + final ThreadPool threadPool = createThreadPool( + JettyConfig.FELIX_JETTY_USE_VIRTUAL_THREADS, Boolean.TRUE.toString(), + JettyConfig.FELIX_JETTY_THREADPOOL_MAX, 100, + JettyConfig.FELIX_JETTY_VIRTUAL_THREADS_MAX, 50); + + // Jetty's preferred setup: platform threads for the acceptors and the selectors, + // with a bounded VirtualThreadPool as the virtual threads executor + assertTrue(threadPool instanceof QueuedThreadPool); + final QueuedThreadPool queuedThreadPool = (QueuedThreadPool) threadPool; + assertEquals(100, queuedThreadPool.getMaxThreads()); + + final Executor executor = queuedThreadPool.getVirtualThreadsExecutor(); + assertTrue(executor instanceof VirtualThreadPool); + assertEquals(50, ((VirtualThreadPool) executor).getMaxConcurrentTasks()); + + // added as a bean so that the QueuedThreadPool starts and stops it; an unstarted + // VirtualThreadPool rejects every task + assertTrue(queuedThreadPool.getBeans(VirtualThreadPool.class).contains(executor)); + } + + @Test public void testVirtualThreadsBoundedExecutorWithoutThreadPoolMax() throws Exception + { + assumeVirtualThreads(); + final ThreadPool threadPool = createThreadPool( + JettyConfig.FELIX_JETTY_USE_VIRTUAL_THREADS, Boolean.TRUE.toString(), + JettyConfig.FELIX_JETTY_VIRTUAL_THREADS_MAX, 50); + + assertTrue(threadPool instanceof QueuedThreadPool); + final Executor executor = ((QueuedThreadPool) threadPool).getVirtualThreadsExecutor(); + assertTrue(executor instanceof VirtualThreadPool); + assertEquals(50, ((VirtualThreadPool) executor).getMaxConcurrentTasks()); + } + + @Test public void testVirtualThreadsMaxIgnoredWhenNotPositive() throws Exception + { + assumeVirtualThreads(); + // Jetty treats maxConcurrentTasks <= 0 as unbounded, so such a value must not select + // the bounded setup, and the pre-existing behaviour has to be kept + final ThreadPool threadPool = createThreadPool( + JettyConfig.FELIX_JETTY_USE_VIRTUAL_THREADS, Boolean.TRUE.toString(), + JettyConfig.FELIX_JETTY_THREADPOOL_MAX, 100, + JettyConfig.FELIX_JETTY_VIRTUAL_THREADS_MAX, 0); + + assertTrue(threadPool instanceof VirtualThreadPool); + assertEquals(100, ((VirtualThreadPool) threadPool).getMaxConcurrentTasks()); + } + + @Test public void testVirtualThreadsMaxIgnoredWhenDisabled() throws Exception + { + final ThreadPool threadPool = createThreadPool( + JettyConfig.FELIX_JETTY_VIRTUAL_THREADS_MAX, 50, + JettyConfig.FELIX_JETTY_THREADPOOL_MAX, 42); + + assertTrue(threadPool instanceof QueuedThreadPool); + assertNull(((QueuedThreadPool) threadPool).getVirtualThreadsExecutor()); + } + + @Before + public void setUp() + { + this.context = Mockito.mock(BundleContext.class); + this.config = new JettyConfig(this.context); + } +} diff --git a/http/jetty12/src/test/java/org/apache/felix/http/jetty/it/JettyVirtualThreadsBoundedExecutorIT.java b/http/jetty12/src/test/java/org/apache/felix/http/jetty/it/JettyVirtualThreadsBoundedExecutorIT.java new file mode 100644 index 0000000000..dbe184631b --- /dev/null +++ b/http/jetty12/src/test/java/org/apache/felix/http/jetty/it/JettyVirtualThreadsBoundedExecutorIT.java @@ -0,0 +1,46 @@ +/* + * 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.ops4j.pax.exam.cm.ConfigurationAdminOptions.newConfiguration; + +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; + +/** + * Runs the virtual threads test against Jetty's preferred setup: a QueuedThreadPool + * with a bounded VirtualThreadPool as its virtual threads executor. + * + * Serving a request at all exercises the life cycle of that VirtualThreadPool, since an + * unstarted one rejects every task with a RejectedExecutionException. + */ +@RunWith(PaxExam.class) +@ExamReactorStrategy(PerClass.class) +public class JettyVirtualThreadsBoundedExecutorIT 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()) + .put("org.apache.felix.http.jetty.virtualthreads.max", 50) + .asOption(); + } +} diff --git a/http/jetty12/src/test/java/org/apache/felix/http/jetty/it/JettyVirtualThreadsIT.java b/http/jetty12/src/test/java/org/apache/felix/http/jetty/it/JettyVirtualThreadsIT.java index d18958c642..719754662b 100644 --- a/http/jetty12/src/test/java/org/apache/felix/http/jetty/it/JettyVirtualThreadsIT.java +++ b/http/jetty12/src/test/java/org/apache/felix/http/jetty/it/JettyVirtualThreadsIT.java @@ -18,6 +18,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; +import static org.junit.Assume.assumeTrue; import static org.ops4j.pax.exam.CoreOptions.mavenBundle; import static org.ops4j.pax.exam.cm.ConfigurationAdminOptions.newConfiguration; @@ -89,10 +90,13 @@ public void setup(){ @Test public void testJettyRunningWithVirtualThreads() throws Exception { - if (!System.getProperty("java.version").startsWith("21")) { - // This test only works on Java 21 or newer - return; - } + // Virtual threads require Java 21 or later. Comparing the feature version rather than + // matching on the version string keeps this running on every later JDK. Note that the + // Pax Exam runner swallows a failed assumption and reports the test as passing rather + // than as skipped, so on an older JDK this still shows up as a green test. + assumeTrue("virtual threads require Java 21 or later, running on " + Runtime.version(), + Runtime.version().feature() >= 21); + try (HttpClient httpClient = new HttpClient()) { httpClient.start(); Object value = bundleContext.getServiceReference(HttpService.class).getProperty("org.osgi.service.http.port");