Skip to content

Commit 3ff16ba

Browse files
committed
FELIX-6720 Enable virtual thread support in Jetty12
- Add `org.apache.felix.http.jetty.virtualthreads.enable` as configuration option, defaulting to false - Add logging if virtual threads are used - Use reflection to detect if Virtual Threads can be used and throw if this is enabled but cannot be used - Add integration test
1 parent 8b2d2fa commit 3ff16ba

4 files changed

Lines changed: 145 additions & 1 deletion

File tree

http/jetty12/src/main/java/org/apache/felix/http/jetty/internal/ConfigMetaTypeProvider.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,12 @@ public ObjectClassDefinition getObjectClassDefinition( String id, String locale
166166
-1,
167167
bundle.getBundleContext().getProperty(JettyConfig.FELIX_JETTY_THREADPOOL_MAX)));
168168

169+
adList.add(new AttributeDefinitionImpl(JettyConfig.FELIX_JETTY_USE_VIRTUAL_THREADS,
170+
"Use Virtual Threads",
171+
"Use virtual threads in Jetty (JDK 21 or higher). Defaults to false.",
172+
-1,
173+
bundle.getBundleContext().getProperty(JettyConfig.FELIX_JETTY_USE_VIRTUAL_THREADS)));
174+
169175
adList.add(new AttributeDefinitionImpl(JettyConfig.FELIX_JETTY_ACCEPTORS,
170176
"Acceptors",
171177
"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.",

http/jetty12/src/main/java/org/apache/felix/http/jetty/internal/JettyConfig.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,9 @@ public final class JettyConfig
8888
/** Felix specific property to control the maximum size of the jetty thread pool */
8989
public static final String FELIX_JETTY_THREADPOOL_MAX = "org.apache.felix.http.jetty.threadpool.max";
9090

91+
/** Felix specific property to enable the use of virtual threads in Jetty */
92+
public static final String FELIX_JETTY_USE_VIRTUAL_THREADS = "org.apache.felix.http.jetty.virtualthreads.enable";
93+
9194
/** Felix specific property to control the number of jetty acceptor threads */
9295
public static final String FELIX_JETTY_ACCEPTORS = "org.apache.felix.http.jetty.acceptors";
9396

@@ -471,6 +474,10 @@ public int getSelectors()
471474
return getIntProperty(FELIX_JETTY_SELECTORS, -1);
472475
}
473476

477+
public boolean isUseVirtualThreads() {
478+
return this.getBooleanProperty(FELIX_JETTY_USE_VIRTUAL_THREADS, false);
479+
}
480+
474481
public int getRequestBufferSize()
475482
{
476483
return getIntProperty(FELIX_JETTY_REQUEST_BUFFER_SIZE, 8 * 1024);

http/jetty12/src/main/java/org/apache/felix/http/jetty/internal/JettyService.java

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import static org.eclipse.jetty.http.UriCompliance.UNAMBIGUOUS;
2121
import static org.eclipse.jetty.http.UriCompliance.UNSAFE;
2222

23+
import java.lang.reflect.Method;
2324
import java.net.Inet4Address;
2425
import java.net.Inet6Address;
2526
import java.net.InetAddress;
@@ -31,6 +32,8 @@
3132
import java.util.Enumeration;
3233
import java.util.Hashtable;
3334
import java.util.List;
35+
import java.util.concurrent.Executor;
36+
import java.util.concurrent.Executors;
3437

3538
import org.apache.felix.http.base.internal.HttpServiceController;
3639
import org.apache.felix.http.base.internal.logger.SystemLogger;
@@ -274,7 +277,17 @@ private void initializeJetty() throws Exception
274277

275278
final int threadPoolMax = this.config.getThreadPoolMax();
276279
if (threadPoolMax >= 0) {
277-
this.server = new Server( new QueuedThreadPool(threadPoolMax) );
280+
this.server = new Server(new QueuedThreadPool(threadPoolMax));
281+
} else if (this.config.isUseVirtualThreads()){
282+
QueuedThreadPool threadPool = new QueuedThreadPool();
283+
Method newVirtualThreadPerTaskExecutorMethod = null;
284+
try {
285+
newVirtualThreadPerTaskExecutorMethod = Executors.class.getMethod("newVirtualThreadPerTaskExecutor");
286+
} catch (NoSuchMethodException e){
287+
throw new IllegalArgumentException("Virtual threads are only available in Java 21 or later, or via preview flags in Java 17-20");
288+
}
289+
threadPool.setVirtualThreadsExecutor((Executor) newVirtualThreadPerTaskExecutorMethod.invoke(null));
290+
this.server = new Server(threadPool);
278291
} else {
279292
this.server = new Server();
280293
}
@@ -397,6 +410,7 @@ private void initializeJetty() throws Exception
397410
message.append("acceptors=").append(serverConnector.getAcceptors()).append(",");
398411
message.append("selectors=").append(serverConnector.getSelectorManager().getSelectorCount());
399412
}
413+
message.append(",").append("virtualThreadsEnabled=").append(this.config.isUseVirtualThreads());
400414
message.append("]");
401415

402416
SystemLogger.LOGGER.info(message.toString());
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package org.apache.felix.http.jetty.it;
18+
19+
import static org.junit.Assert.assertEquals;
20+
import static org.junit.Assert.assertNotNull;
21+
import static org.ops4j.pax.exam.CoreOptions.mavenBundle;
22+
import static org.ops4j.pax.exam.cm.ConfigurationAdminOptions.newConfiguration;
23+
24+
import java.io.IOException;
25+
import java.net.URI;
26+
import java.util.Hashtable;
27+
import java.util.Map;
28+
29+
import javax.inject.Inject;
30+
import jakarta.servlet.Servlet;
31+
import jakarta.servlet.http.HttpServlet;
32+
import jakarta.servlet.http.HttpServletRequest;
33+
import jakarta.servlet.http.HttpServletResponse;
34+
35+
import org.eclipse.jetty.client.ContentResponse;
36+
import org.eclipse.jetty.client.HttpClient;
37+
import org.eclipse.jetty.client.transport.HttpClientTransportOverHTTP;
38+
import org.junit.Before;
39+
import org.junit.Test;
40+
import org.junit.runner.RunWith;
41+
import org.ops4j.pax.exam.Option;
42+
import org.ops4j.pax.exam.junit.PaxExam;
43+
import org.ops4j.pax.exam.spi.reactors.ExamReactorStrategy;
44+
import org.ops4j.pax.exam.spi.reactors.PerClass;
45+
import org.osgi.framework.BundleContext;
46+
import org.osgi.service.http.HttpService;
47+
import org.osgi.service.servlet.whiteboard.HttpWhiteboardConstants;
48+
49+
@RunWith(PaxExam.class)
50+
@ExamReactorStrategy(PerClass.class)
51+
public class JettyVirtualThreadsIT extends AbstractJettyTestSupport {
52+
53+
@Inject
54+
protected BundleContext bundleContext;
55+
56+
@Override
57+
protected Option[] additionalOptions() throws IOException {
58+
String jettyVersion = System.getProperty("jetty.version", JETTY_VERSION);
59+
return new Option[] {
60+
spifly(),
61+
62+
// bundles for the server side
63+
mavenBundle().groupId("org.eclipse.jetty.ee10").artifactId("jetty-ee10-webapp").version(jettyVersion),
64+
mavenBundle().groupId("org.eclipse.jetty").artifactId("jetty-ee").version(jettyVersion),
65+
mavenBundle().groupId("org.eclipse.jetty.ee10").artifactId("jetty-ee10-servlet").version(jettyVersion),
66+
mavenBundle().groupId("org.eclipse.jetty").artifactId("jetty-xml").version(jettyVersion),
67+
68+
// additional bundles for the client side
69+
mavenBundle().groupId("org.eclipse.jetty").artifactId("jetty-alpn-client").version(jettyVersion),
70+
mavenBundle().groupId("org.eclipse.jetty").artifactId("jetty-client").version(jettyVersion)
71+
};
72+
}
73+
74+
@Override
75+
protected Option felixHttpConfig(int httpPort) {
76+
return newConfiguration("org.apache.felix.http")
77+
.put("org.osgi.service.http.port", httpPort)
78+
.put("org.apache.felix.http.jetty.virtualthreads.enable", Boolean.TRUE.toString())
79+
.asOption();
80+
}
81+
82+
@Before
83+
public void setup(){
84+
assertNotNull(bundleContext);
85+
bundleContext.registerService(Servlet.class, new ExampleEndpoint(), new Hashtable<>(Map.of(
86+
HttpWhiteboardConstants.HTTP_WHITEBOARD_SERVLET_PATTERN, "/*"
87+
)));
88+
}
89+
90+
@Test
91+
public void testJettyRunningWithVirtualThreads() throws Exception {
92+
if (!System.getProperty("java.version").startsWith("21")) {
93+
// This test only works on Java 21 or newer
94+
return;
95+
}
96+
HttpClientTransportOverHTTP transport = new HttpClientTransportOverHTTP();
97+
HttpClient httpClient = new HttpClient(transport);
98+
httpClient.start();
99+
100+
Object value = bundleContext.getServiceReference(HttpService.class).getProperty("org.osgi.service.http.port");
101+
int httpPort = Integer.parseInt((String) value);
102+
103+
URI destUri = new URI(String.format("http://localhost:%d/endpoint/working", httpPort));
104+
105+
ContentResponse response = httpClient.GET(destUri);
106+
assertEquals(200, response.getStatus());
107+
assertEquals("OK", response.getContentAsString());
108+
}
109+
110+
static final class ExampleEndpoint extends HttpServlet {
111+
@Override
112+
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
113+
resp.setStatus(200);
114+
resp.getWriter().write("OK");
115+
}
116+
}
117+
}

0 commit comments

Comments
 (0)