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");