FELIX-6859 allow a bounded VirtualThreadPool as the virtual threads executor - #560
Merged
Merged
Conversation
paulrutter
force-pushed
the
FELIX-6859-bounded-virtual-thread-executor
branch
3 times, most recently
from
September 4, 2026 00:07
8903adb to
1495ee9
Compare
…xecutor
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 <noreply@anthropic.com>
paulrutter
force-pushed
the
FELIX-6859-bounded-virtual-thread-executor
branch
from
September 4, 2026 06:55
1495ee9 to
e41af82
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes FELIX-6859.
Jetty 12 documents a preferred virtual threads setup: a
QueuedThreadPoolwhose virtual threads executor is a boundedVirtualThreadPool. Felix HTTP could not produce it. The two virtual thread options were the extremes — an unbounded number of concurrent tasks, or a standaloneVirtualThreadPoolthat 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 say so.Change
Adds
org.apache.felix.http.jetty.virtualthreads.maxto the jetty12 bundle. When virtual threads are enabled and this is set to a positive value, the thread pool becomes aQueuedThreadPool, sized bythreadpool.maxas usual, whose virtual threads executor is aVirtualThreadPoolwithsetMaxConcurrentTasks(<value>).Note that unlike
threadpool.max, this property bounds the number of concurrent tasks, not the number of threads. That distinction is the reason the issue exists, and it is called out in both the README row and the metatype description.virtualthreads.enablethreadpool.maxvirtualthreads.maxfalseQueuedThreadPool, 200 platform threadsfalseQueuedThreadPoolwith<max>platform threadstrueQueuedThreadPool+ unboundednewVirtualThreadPerTaskExecutor()trueVirtualThreadPool, bounded by<max>trueQueuedThreadPool+VirtualThreadPoolbounded by<value>(new)Backwards compatibility
Fully backwards compatible.
-1, and only a positive value selects the new branch. Every existing combination reaches exactly the pool it reached before — including the fourth row, wherethreadpool.maxkeeps acting as a bound on concurrent tasks. That reuse is the very conflation this issue is about, but changing it would silently unbound existing deployments, so it stays, with a comment in the code saying so.maxConcurrentTasks <= 0as unbounded (doStart()only creates the semaphore for a positive value). Selecting a "bounded" pool that isn't bounded would be worse than falling through.org.apache.felix.http.jetty.internalis not an exported package, so renaming the constant and the getter is not an API break.virtualthreads.enableat all.Life cycle of the VirtualThreadPool
QueuedThreadPool.setVirtualThreadsExecutor(Executor)only stores the reference — it does not add the argument as a managed bean.VirtualThreadPoolis aContainerLifeCyclewhosedoStart()creates both the virtual executor and the bounding semaphore, and whoseexecute()throwsRejectedExecutionException("not running")while unstarted. Hence thethreadPool.addBean(virtualThreadPool)call.Measured on Jetty 12.1.9,
addBeancovers teardown as well as startup:start()stop()addBeanaddBeanSo no explicit teardown is needed:
stopJetty()callsserver.stop(), theQueuedThreadPoolis a managed bean of theServer, and theVirtualThreadPoolis a managed bean of theQueuedThreadPool. The bottom row also shows theaddBeanis not optional — without it the pool never starts and rejects every request.Testing
The selection was extracted from
createServer()into a package privatecreateThreadPool(JettyConfig)so that the resulting pool can be asserted without starting a server. This was prompted by review: the integration test only asserted that a request succeeds, which holds for every combination in the table, so it could not tell the new setup apart from the pre-existing ones.JettyServiceThreadPoolTest— 8 cases covering every row of the table, asserting the pool type, its bound, and that theVirtualThreadPoolis a bean of theQueuedThreadPool. Virtual thread cases are skipped by assumption on a JVM without virtual threads, probed the same wayJettyServiceprobes it, so it stays correct beyond Java 21.JettyConfigTest— default of-1, plusIntegerandStringproperty values.JettyVirtualThreadsBoundedExecutorIT— end-to-end, and the only test that exercises the life cycle above, since an unstarted pool would reject the request.All 19 unit tests and all 3
JettyVirtualThreads*ITpass on Corretto 21.0.6.The new assertions were mutation-checked rather than merely observed green: raising the threshold of the new branch so it is never taken fails
testVirtualThreadsBoundedExecutorandtestVirtualThreadsBoundedExecutorWithoutThreadPoolMax, and removing theaddBeancall alone failstestVirtualThreadsBoundedExecutor.Notes
virtualthreads.enable, and that section's combination matrix gains a third column and the fifth row. The bullet in FELIX-6859 Document missing Felix HTTP configuration properties #559 saying the preferred combination has no Felix HTTP configuration "yet" now points at the property that provides it. FELIX-6859 Document missing Felix HTTP configuration properties #559's own metatype fixes (renegotiateAllowed,requestlog.osgi.format) are untouched.2.0.7-SNAPSHOTreleases as 2.0.8.🤖 Generated with Claude Code
Virtual threads ITs now run on every JDK 21 or later
The pre-existing guard in
JettyVirtualThreadsITmatched the version string withstartsWith("21"), so the body was skipped on every later JDK — on the Java 23 job of the CI matrix all three virtual threads ITs asserted nothing while still reporting as passing. It now comparesRuntime.version().feature() >= 21, so they run on Java 21 and on every later JDK.Note that below Java 21 the test still reports as green rather than as skipped: the Pax Exam runner swallows a failed JUnit assumption and reports the test as passing. Confirmed by forcing the assumption to fail locally, which still gave
Tests run: 1, Skipped: 0. That matches the behaviour before this change, so nothing regresses, but it is worth knowing that the Java 17 job cannot demonstrate a skip here.Verified on Corretto 25 by mutation: changing the expected status to 201 makes all three ITs fail with
expected:<201> but was:<200>, which shows the request really is made and asserted there. Green on Corretto 21.0.6 and 25.0.2.Metatype fix for virtualthreads.enable
Spotted while adding the new attribute next door. The
virtualthreads.enableattribute definition passed-1as its default, which selects theintconstructor ofAttributeDefinitionImpl. The flag was therefore declared as an INTEGER attribute defaulting to-1, even thoughJettyConfigreads it withgetBooleanPropertyand the description already said it defaults tofalse— a configuration UI rendered a number field for a boolean flag. Passingfalseselects the boolean constructor, giving BOOLEAN with a default offalse.Reading a previously stored integer value is unaffected, since
getBooleanPropertyparses it the same way as before.I audited every boolean-typed property in the jetty12 metatype for the same mistake: this was the only one. The jetty11 bundle has no
virtualthreads.enableand no comparable mismatch, so it is untouched.