From d786792fcf32eb5d94c870a83020941b8693deb1 Mon Sep 17 00:00:00 2001 From: Kristóf Marussy Date: Sat, 10 Dec 2022 14:32:17 +0100 Subject: chore(web): upgrade to Jetty 12.0.0.alpha3 Also refactor virtual thread ExecutorService handling and integration tests. --- .../refinery/language/web/ServerLauncher.java | 17 +------ .../refinery/language/web/VirtualThreadUtils.java | 52 ++++++++++++++++++++++ .../VirtualThreadExecutorServiceProvider.java | 8 +--- .../ProblemWebSocketServletIntegrationTest.java | 30 +++++++++---- .../AwaitTerminationExecutorServiceProvider.java | 2 +- .../web/tests/RestartableCachedThreadPool.java | 14 +++--- 6 files changed, 84 insertions(+), 39 deletions(-) create mode 100644 subprojects/language-web/src/main/java/tools/refinery/language/web/VirtualThreadUtils.java (limited to 'subprojects/language-web') diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/ServerLauncher.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/ServerLauncher.java index 960eef71..f49f46ee 100644 --- a/subprojects/language-web/src/main/java/tools/refinery/language/web/ServerLauncher.java +++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/ServerLauncher.java @@ -11,7 +11,6 @@ import org.eclipse.jetty.ee10.servlet.ServletHolder; import org.eclipse.jetty.ee10.servlet.SessionHandler; import org.eclipse.jetty.ee10.websocket.server.config.JettyWebSocketServletContainerInitializer; import org.eclipse.jetty.server.Server; -import org.eclipse.jetty.util.VirtualThreads; import org.eclipse.jetty.util.resource.Resource; import org.eclipse.jetty.util.resource.ResourceFactory; import org.slf4j.Logger; @@ -44,13 +43,7 @@ public class ServerLauncher { private final Server server; public ServerLauncher(InetSocketAddress bindAddress, String[] allowedOrigins, String webSocketUrl) { - server = new Server(bindAddress); - enableVirtualThreads(server); - if (server.getThreadPool() instanceof VirtualThreads.Configurable virtualThreadsConfigurable) { - // Change this to setVirtualThreadsExecutor once - // https://github.com/eclipse/jetty.project/commit/83154b4ffe4767ef44981598d6c26e6a5d32e57c gets released. - virtualThreadsConfigurable.setUseVirtualThreads(VirtualThreads.areSupported()); - } + server = VirtualThreadUtils.newServerWithVirtualThreadsThreadPool("jetty", bindAddress); var handler = new ServletContextHandler(); addSessionHandler(handler); addProblemServlet(handler, allowedOrigins); @@ -150,14 +143,6 @@ public class ServerLauncher { } } - public static void enableVirtualThreads(Server server) { - if (server.getThreadPool() instanceof VirtualThreads.Configurable virtualThreadsConfigurable) { - // Change this to setVirtualThreadsExecutor once - // https://github.com/eclipse/jetty.project/commit/83154b4ffe4767ef44981598d6c26e6a5d32e57c gets released. - virtualThreadsConfigurable.setUseVirtualThreads(VirtualThreads.areSupported()); - } - } - private static String getListenAddress() { var listenAddress = System.getenv("LISTEN_ADDRESS"); if (listenAddress == null) { diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/VirtualThreadUtils.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/VirtualThreadUtils.java new file mode 100644 index 00000000..a055e755 --- /dev/null +++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/VirtualThreadUtils.java @@ -0,0 +1,52 @@ +package tools.refinery.language.web; + +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.util.thread.QueuedThreadPool; +import org.eclipse.jetty.util.thread.ThreadPool; + +import java.net.InetSocketAddress; +import java.time.Duration; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +public final class VirtualThreadUtils { + private VirtualThreadUtils() { + throw new IllegalStateException("This is a static utility class and should not be instantiated directly"); + } + + public static ExecutorService newNamedVirtualThreadsExecutor(String name) { + // Based on + // https://github.com/eclipse/jetty.project/blob/83154b4ffe4767ef44981598d6c26e6a5d32e57c/jetty-server/src/main/config/etc/jetty-threadpool-virtual-preview.xml + return Executors.newThreadPerTaskExecutor(Thread.ofVirtual() + .allowSetThreadLocals(true) + .inheritInheritableThreadLocals(false) + .name(name + "-virtual-", 0) + .factory()); + } + + public static ThreadPool newThreadPoolWithVirtualThreadsExecutor(String name) { + // Based on + // https://github.com/eclipse/jetty.project/blob/83154b4ffe4767ef44981598d6c26e6a5d32e57c/jetty-server/src/main/config/etc/jetty-threadpool-virtual-preview.xml + int timeout = (int) Duration.ofMinutes(1).toMillis(); + var threadPool = new QueuedThreadPool(200, 10, timeout, -1, null, null); + threadPool.setName(name); + threadPool.setDetailedDump(false); + threadPool.setVirtualThreadsExecutor(newNamedVirtualThreadsExecutor(name)); + return threadPool; + } + + public static Server newServerWithVirtualThreadsThreadPool(String name, InetSocketAddress listenAddress) { + var server = new Server(newThreadPoolWithVirtualThreadsExecutor(name)); + var connector = new ServerConnector(server); + try { + connector.setHost(listenAddress.getHostName()); + connector.setPort(listenAddress.getPort()); + server.addConnector(connector); + } catch (Exception e) { + connector.close(); + throw e; + } + return server; + } +} diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/VirtualThreadExecutorServiceProvider.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/VirtualThreadExecutorServiceProvider.java index ead98927..abbcbd53 100644 --- a/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/VirtualThreadExecutorServiceProvider.java +++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/VirtualThreadExecutorServiceProvider.java @@ -1,9 +1,9 @@ package tools.refinery.language.web.xtext; import org.eclipse.xtext.ide.ExecutorServiceProvider; +import tools.refinery.language.web.VirtualThreadUtils; import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; public class VirtualThreadExecutorServiceProvider extends ExecutorServiceProvider { private static final String THREAD_POOL_NAME = "xtextWeb"; @@ -11,10 +11,6 @@ public class VirtualThreadExecutorServiceProvider extends ExecutorServiceProvide @Override protected ExecutorService createInstance(String key) { var name = key == null ? THREAD_POOL_NAME : THREAD_POOL_NAME + "-" + key; - return Executors.newThreadPerTaskExecutor(Thread.ofVirtual() - .allowSetThreadLocals(true) - .inheritInheritableThreadLocals(false) - .name(name + "-", 0) - .factory()); + return VirtualThreadUtils.newNamedVirtualThreadsExecutor(name); } } diff --git a/subprojects/language-web/src/test/java/tools/refinery/language/web/ProblemWebSocketServletIntegrationTest.java b/subprojects/language-web/src/test/java/tools/refinery/language/web/ProblemWebSocketServletIntegrationTest.java index 24fab4e3..ecbefc4f 100644 --- a/subprojects/language-web/src/test/java/tools/refinery/language/web/ProblemWebSocketServletIntegrationTest.java +++ b/subprojects/language-web/src/test/java/tools/refinery/language/web/ProblemWebSocketServletIntegrationTest.java @@ -14,9 +14,7 @@ import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.server.Server; import org.eclipse.xtext.testing.GlobalRegistries; import org.eclipse.xtext.testing.GlobalRegistries.GlobalStateMemento; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.*; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; import tools.refinery.language.web.tests.WebSocketIntegrationTestClient; @@ -25,6 +23,7 @@ import tools.refinery.language.web.xtext.servlet.XtextWebSocketServlet; import java.io.IOException; import java.net.InetSocketAddress; +import java.net.ServerSocket; import java.net.URI; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; @@ -34,18 +33,29 @@ import static org.hamcrest.Matchers.*; import static org.junit.jupiter.api.Assertions.assertThrows; class ProblemWebSocketServletIntegrationTest { - private static final int SERVER_PORT = 28080; + private static final String HOSTNAME = "127.0.0.1"; private static final String SERVLET_URI = "/xtext-service"; private GlobalStateMemento stateBeforeInjectorCreation; + private TestInfo testInfo; + + private int serverPort; + private Server server; private WebSocketClient client; @BeforeEach - void beforeEach() throws Exception { + void beforeEach(TestInfo testInfo) throws Exception { + this.testInfo = testInfo; + // Find a free port for running the test. See e.g., https://stackoverflow.com/a/65937797 + try (var serverSocket = new ServerSocket()) { + serverSocket.setReuseAddress(true); + serverSocket.bind(new InetSocketAddress(HOSTNAME, 0)); + serverPort = serverSocket.getLocalPort(); + } stateBeforeInjectorCreation = GlobalRegistries.makeCopyOfGlobalState(); client = new WebSocketClient(); client.start(); @@ -147,7 +157,7 @@ class ProblemWebSocketServletIntegrationTest { } } - @ParameterizedTest(name = "Origin: {0}") + @ParameterizedTest(name = "validOriginTest(\"{0}\")") @ValueSource(strings = { "https://refinery.example", "https://refinery.example:443", "HTTPS://REFINERY.EXAMPLE" }) void validOriginTest(String origin) { startServer("https://refinery.example,https://refinery.example:443"); @@ -176,8 +186,9 @@ class ProblemWebSocketServletIntegrationTest { } private void startServer(String allowedOrigins) { - server = new Server(new InetSocketAddress(SERVER_PORT)); - ServerLauncher.enableVirtualThreads(server); + var testName = getClass().getSimpleName() + "-" + testInfo.getDisplayName(); + var listenAddress = new InetSocketAddress(HOSTNAME, serverPort); + server = VirtualThreadUtils.newServerWithVirtualThreadsThreadPool(testName, listenAddress); var handler = new ServletContextHandler(); var holder = new ServletHolder(ProblemWebSocketServlet.class); if (allowedOrigins != null) { @@ -201,7 +212,8 @@ class ProblemWebSocketServletIntegrationTest { upgradeRequest.setSubProtocols(subProtocols); CompletableFuture sessionFuture; try { - sessionFuture = client.connect(webSocketClient, URI.create("ws://localhost:" + SERVER_PORT + SERVLET_URI), + sessionFuture = client.connect(webSocketClient, + URI.create("ws://%s:%d%s".formatted(HOSTNAME, serverPort, SERVLET_URI)), upgradeRequest); } catch (IOException e) { throw new AssertionError("Unexpected exception while connection to websocket", e); diff --git a/subprojects/language-web/src/test/java/tools/refinery/language/web/tests/AwaitTerminationExecutorServiceProvider.java b/subprojects/language-web/src/test/java/tools/refinery/language/web/tests/AwaitTerminationExecutorServiceProvider.java index 25343109..c634e8fc 100644 --- a/subprojects/language-web/src/test/java/tools/refinery/language/web/tests/AwaitTerminationExecutorServiceProvider.java +++ b/subprojects/language-web/src/test/java/tools/refinery/language/web/tests/AwaitTerminationExecutorServiceProvider.java @@ -13,7 +13,7 @@ public class AwaitTerminationExecutorServiceProvider extends VirtualThreadExecut @Override protected ExecutorService createInstance(String key) { - var instance = new RestartableCachedThreadPool(); + var instance = new RestartableCachedThreadPool(() -> super.createInstance(key)); synchronized (servicesToShutDown) { servicesToShutDown.add(instance); } diff --git a/subprojects/language-web/src/test/java/tools/refinery/language/web/tests/RestartableCachedThreadPool.java b/subprojects/language-web/src/test/java/tools/refinery/language/web/tests/RestartableCachedThreadPool.java index a8655313..cf805eda 100644 --- a/subprojects/language-web/src/test/java/tools/refinery/language/web/tests/RestartableCachedThreadPool.java +++ b/subprojects/language-web/src/test/java/tools/refinery/language/web/tests/RestartableCachedThreadPool.java @@ -1,5 +1,6 @@ package tools.refinery.language.web.tests; +import com.google.inject.Provider; import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -13,14 +14,17 @@ public class RestartableCachedThreadPool implements ExecutorService { private ExecutorService delegate; - public RestartableCachedThreadPool() { - delegate = createExecutorService(); + private final Provider executorServiceProvider; + + public RestartableCachedThreadPool(Provider executorServiceProvider) { + this.executorServiceProvider = executorServiceProvider; + delegate = executorServiceProvider.get(); } public void waitForAllTasksToFinish() { delegate.shutdown(); waitForTermination(); - delegate = createExecutorService(); + delegate = executorServiceProvider.get(); } public void waitForTermination() { @@ -35,10 +39,6 @@ public class RestartableCachedThreadPool implements ExecutorService { } } - protected ExecutorService createExecutorService() { - return Executors.newCachedThreadPool(); - } - @Override public boolean awaitTermination(long arg0, @NotNull TimeUnit arg1) throws InterruptedException { return delegate.awaitTermination(arg0, arg1); -- cgit v1.2.3-70-g09d2