From a8ffe38e6e5401011352cda5bc92a0a7a88ef40e Mon Sep 17 00:00:00 2001 From: Kristóf Marussy Date: Tue, 22 Nov 2022 19:16:47 +0100 Subject: chore: upgrade to Java 19 Use Java 19 and Jetty 12 to take advantage of Project Loom preview features to reduce CPU usage due to XtextWebDocumentAccess thread pools. --- .../refinery/language/web/ProblemWebModule.java | 13 ++-- .../refinery/language/web/ServerLauncher.java | 77 ++++++++++++---------- .../VirtualThreadExecutorServiceProvider.java | 20 ++++++ .../language/web/xtext/servlet/XtextWebSocket.java | 16 ++--- .../web/xtext/servlet/XtextWebSocketServlet.java | 2 +- 5 files changed, 79 insertions(+), 49 deletions(-) create mode 100644 subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/VirtualThreadExecutorServiceProvider.java (limited to 'subprojects/language-web/src/main/java/tools') diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/ProblemWebModule.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/ProblemWebModule.java index ec55036f..706413a9 100644 --- a/subprojects/language-web/src/main/java/tools/refinery/language/web/ProblemWebModule.java +++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/ProblemWebModule.java @@ -3,12 +3,13 @@ */ package tools.refinery.language.web; +import org.eclipse.xtext.ide.ExecutorServiceProvider; import org.eclipse.xtext.web.server.XtextServiceDispatcher; import org.eclipse.xtext.web.server.model.IWebDocumentProvider; import org.eclipse.xtext.web.server.model.XtextWebDocumentAccess; import org.eclipse.xtext.web.server.occurrences.OccurrencesService; - import tools.refinery.language.web.occurrences.ProblemOccurrencesService; +import tools.refinery.language.web.xtext.VirtualThreadExecutorServiceProvider; import tools.refinery.language.web.xtext.server.push.PushServiceDispatcher; import tools.refinery.language.web.xtext.server.push.PushWebDocumentAccess; import tools.refinery.language.web.xtext.server.push.PushWebDocumentProvider; @@ -20,16 +21,20 @@ public class ProblemWebModule extends AbstractProblemWebModule { public Class bindIWebDocumentProvider() { return PushWebDocumentProvider.class; } - + public Class bindXtextWebDocumentAccess() { return PushWebDocumentAccess.class; } - + public Class bindXtextServiceDispatcher() { return PushServiceDispatcher.class; } - + public Class bindOccurrencesService() { return ProblemOccurrencesService.class; } + + public Class bindExecutorServiceProvider() { + return VirtualThreadExecutorServiceProvider.class; + } } 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 58c8ea4e..5da16850 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 @@ -5,20 +5,21 @@ package tools.refinery.language.web; import jakarta.servlet.DispatcherType; import jakarta.servlet.SessionTrackingMode; +import org.eclipse.jetty.ee10.servlet.DefaultServlet; +import org.eclipse.jetty.ee10.servlet.ServletContextHandler; +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.server.session.SessionHandler; -import org.eclipse.jetty.servlet.DefaultServlet; -import org.eclipse.jetty.servlet.ServletContextHandler; -import org.eclipse.jetty.servlet.ServletHolder; +import org.eclipse.jetty.util.VirtualThreads; import org.eclipse.jetty.util.resource.Resource; -import org.eclipse.jetty.websocket.server.config.JettyWebSocketServletContainerInitializer; +import org.eclipse.jetty.util.resource.ResourceFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import tools.refinery.language.web.config.BackendConfigServlet; import tools.refinery.language.web.xtext.servlet.XtextWebSocketServlet; import java.io.File; -import java.io.IOException; import java.net.InetSocketAddress; import java.net.URI; import java.net.URISyntaxException; @@ -42,13 +43,18 @@ public class ServerLauncher { private final Server server; - public ServerLauncher(InetSocketAddress bindAddress, Resource baseResource, String[] allowedOrigins, - String webSocketUrl) { + public ServerLauncher(InetSocketAddress bindAddress, String[] allowedOrigins, String webSocketUrl) { server = new Server(bindAddress); + 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()); + } var handler = new ServletContextHandler(); addSessionHandler(handler); addProblemServlet(handler, allowedOrigins); addBackendConfigServlet(handler, webSocketUrl); + var baseResource = getBaseResource(); if (baseResource != null) { handler.setBaseResource(baseResource); handler.setWelcomeFiles(new String[]{"index.html"}); @@ -95,6 +101,35 @@ public class ServerLauncher { handler.addServlet(defaultServletHolder, "/"); } + private Resource getBaseResource() { + var factory = ResourceFactory.of(server); + var baseResourceOverride = System.getenv("BASE_RESOURCE"); + if (baseResourceOverride != null) { + // If a user override is provided, use it. + return factory.newResource(baseResourceOverride); + } + var indexUrlInJar = ServerLauncher.class.getResource("/webapp/index.html"); + if (indexUrlInJar != null) { + // If the app is packaged in the jar, serve it. + URI webRootUri = null; + try { + webRootUri = URI.create(indexUrlInJar.toURI().toASCIIString().replaceFirst("/index.html$", "/")); + } catch (URISyntaxException e) { + throw new IllegalStateException("Jar has invalid base resource URI", e); + } + return factory.newResource(webRootUri); + } + // Look for unpacked production artifacts (convenience for running from IDE). + var unpackedResourcePathComponents = new String[]{System.getProperty("user.dir"), "build", "webpack", + "production"}; + var unpackedResourceDir = new File(String.join(File.separator, unpackedResourcePathComponents)); + if (unpackedResourceDir.isDirectory()) { + return factory.newResource(unpackedResourceDir.toPath()); + } + // Fall back to just serving a 404. + return null; + } + public void start() throws Exception { server.start(); LOG.info("Server started on {}", server.getURI()); @@ -104,10 +139,9 @@ public class ServerLauncher { public static void main(String[] args) { try { var bindAddress = getBindAddress(); - var baseResource = getBaseResource(); var allowedOrigins = getAllowedOrigins(); var webSocketUrl = getWebSocketUrl(); - var serverLauncher = new ServerLauncher(bindAddress, baseResource, allowedOrigins, webSocketUrl); + var serverLauncher = new ServerLauncher(bindAddress, allowedOrigins, webSocketUrl); serverLauncher.start(); } catch (Exception exception) { LOG.error("Fatal server error", exception); @@ -137,29 +171,6 @@ public class ServerLauncher { return new InetSocketAddress(listenAddress, listenPort); } - private static Resource getBaseResource() throws IOException, URISyntaxException { - var baseResourceOverride = System.getenv("BASE_RESOURCE"); - if (baseResourceOverride != null) { - // If a user override is provided, use it. - return Resource.newResource(baseResourceOverride); - } - var indexUrlInJar = ServerLauncher.class.getResource("/webapp/index.html"); - if (indexUrlInJar != null) { - // If the app is packaged in the jar, serve it. - var webRootUri = URI.create(indexUrlInJar.toURI().toASCIIString().replaceFirst("/index.html$", "/")); - return Resource.newResource(webRootUri); - } - // Look for unpacked production artifacts (convenience for running from IDE). - var unpackedResourcePathComponents = new String[]{System.getProperty("user.dir"), "build", "webpack", - "production"}; - var unpackedResourceDir = new File(String.join(File.separator, unpackedResourcePathComponents)); - if (unpackedResourceDir.isDirectory()) { - return Resource.newResource(unpackedResourceDir); - } - // Fall back to just serving a 404. - return null; - } - private static String getPublicHost() { var publicHost = System.getenv("PUBLIC_HOST"); if (publicHost != null) { 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 new file mode 100644 index 00000000..ead98927 --- /dev/null +++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/VirtualThreadExecutorServiceProvider.java @@ -0,0 +1,20 @@ +package tools.refinery.language.web.xtext; + +import org.eclipse.xtext.ide.ExecutorServiceProvider; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +public class VirtualThreadExecutorServiceProvider extends ExecutorServiceProvider { + private static final String THREAD_POOL_NAME = "xtextWeb"; + + @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()); + } +} diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/XtextWebSocket.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/XtextWebSocket.java index 82391d8b..1d9e0463 100644 --- a/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/XtextWebSocket.java +++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/XtextWebSocket.java @@ -3,10 +3,10 @@ package tools.refinery.language.web.xtext.servlet; import com.google.gson.Gson; import com.google.gson.JsonIOException; import com.google.gson.JsonParseException; -import org.eclipse.jetty.websocket.api.Session; -import org.eclipse.jetty.websocket.api.StatusCode; -import org.eclipse.jetty.websocket.api.WriteCallback; -import org.eclipse.jetty.websocket.api.annotations.*; +import org.eclipse.jetty.ee10.websocket.api.Session; +import org.eclipse.jetty.ee10.websocket.api.StatusCode; +import org.eclipse.jetty.ee10.websocket.api.WriteCallback; +import org.eclipse.jetty.ee10.websocket.api.annotations.*; import org.eclipse.xtext.resource.IResourceServiceProvider; import org.eclipse.xtext.web.server.ISession; import org.slf4j.Logger; @@ -17,7 +17,6 @@ import tools.refinery.language.web.xtext.server.TransactionExecutor; import tools.refinery.language.web.xtext.server.message.XtextWebRequest; import tools.refinery.language.web.xtext.server.message.XtextWebResponse; -import java.io.IOException; import java.io.Reader; @WebSocket @@ -108,12 +107,7 @@ public class XtextWebSocket implements WriteCallback, ResponseHandler { throw new ResponseHandlerException("Trying to send message when websocket is disconnected"); } var responseString = gson.toJson(response); - try { - webSocketSession.getRemote().sendPartialString(responseString, true, this); - } catch (IOException e) { - throw new ResponseHandlerException( - "Cannot initiate async write to websocket " + webSocketSession.getRemoteAddress(), e); - } + webSocketSession.getRemote().sendPartialString(responseString, true, this); } @Override diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/XtextWebSocketServlet.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/XtextWebSocketServlet.java index a2ad2943..9a32b937 100644 --- a/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/XtextWebSocketServlet.java +++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/XtextWebSocketServlet.java @@ -2,7 +2,7 @@ package tools.refinery.language.web.xtext.servlet; import jakarta.servlet.ServletConfig; import jakarta.servlet.ServletException; -import org.eclipse.jetty.websocket.server.*; +import org.eclipse.jetty.ee10.websocket.server.*; import org.eclipse.xtext.resource.IResourceServiceProvider; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -- cgit v1.2.3-54-g00ecf