From 72a3f0f50560d70f2f2ff3e1ab6dbb92f80a5f0f Mon Sep 17 00:00:00 2001 From: Kristóf Marussy Date: Tue, 5 Sep 2023 02:20:57 +0200 Subject: feat(web): control server settings with env vars --- .../refinery/language/web/ProblemWebModule.java | 6 ++ .../refinery/language/web/ServerLauncher.java | 4 +- .../language/web/semantics/SemanticsService.java | 31 +++++- .../server/ThreadPoolExecutorServiceProvider.java | 110 +++++++++++++++++++++ .../web/tests/ProblemWebInjectorProvider.java | 1 + 5 files changed, 148 insertions(+), 4 deletions(-) create mode 100644 subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/ThreadPoolExecutorServiceProvider.java (limited to 'subprojects/language-web/src') 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 b0197c01..6a6e0107 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 @@ -9,11 +9,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.server.ThreadPoolExecutorServiceProvider; 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; @@ -37,4 +39,8 @@ public class ProblemWebModule extends AbstractProblemWebModule { public Class bindOccurrencesService() { return ProblemOccurrencesService.class; } + + public Class bindExecutorServiceProvider() { + return ThreadPoolExecutorServiceProvider.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 d633b3fc..155efc6f 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 @@ -165,7 +165,7 @@ public class ServerLauncher { private static int getListenPort() { var portStr = System.getenv("REFINERY_LISTEN_PORT"); if (portStr != null) { - return Integer.parseInt(portStr); + return Integer.parseUnsignedInt(portStr); } return DEFAULT_LISTEN_PORT; } @@ -187,7 +187,7 @@ public class ServerLauncher { private static int getPublicPort() { var portStr = System.getenv("REFINERY_PUBLIC_PORT"); if (portStr != null) { - return Integer.parseInt(portStr); + return Integer.parseUnsignedInt(portStr); } return DEFAULT_PUBLIC_PORT; } diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsService.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsService.java index ba55dc77..26924f0a 100644 --- a/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsService.java +++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsService.java @@ -22,10 +22,14 @@ import tools.refinery.language.model.problem.Problem; import tools.refinery.language.web.xtext.server.push.PushWebDocument; import java.util.List; +import java.util.Optional; import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicBoolean; @Singleton public class SemanticsService extends AbstractCachedService { + public static final String SEMANTICS_EXECUTOR = "semantics"; + private static final Logger LOG = LoggerFactory.getLogger(SemanticsService.class); @Inject @@ -39,9 +43,24 @@ public class SemanticsService extends AbstractCachedService { private ExecutorService executorService; + private final long timeoutMs; + + private final long warmupTimeoutMs; + + private final AtomicBoolean warmedUp = new AtomicBoolean(false); + + public SemanticsService() { + timeoutMs = getTimeout("REFINERY_SEMANTICS_TIMEOUT_MS").orElse(1000L); + warmupTimeoutMs = getTimeout("REFINERY_SEMANTICS_WARMUP_TIMEOUT_MS").orElse(timeoutMs * 2); + } + + private static Optional getTimeout(String name) { + return Optional.ofNullable(System.getenv(name)).map(Long::parseUnsignedLong); + } + @Inject public void setExecutorServiceProvider(ExecutorServiceProvider provider) { - executorService = provider.get(this.getClass().getName()); + executorService = provider.get(SEMANTICS_EXECUTOR); } @Override @@ -60,9 +79,14 @@ public class SemanticsService extends AbstractCachedService { var worker = workerProvider.get(); worker.setProblem(problem, cancelIndicator); var future = executorService.submit(worker); + boolean warmedUpCurrently = warmedUp.get(); + long timeout = warmedUpCurrently ? timeoutMs : warmupTimeoutMs; SemanticsResult result = null; try { - result = future.get(2, TimeUnit.SECONDS); + result = future.get(timeout, TimeUnit.MILLISECONDS); + if (!warmedUpCurrently) { + warmedUp.set(true); + } } catch (InterruptedException e) { future.cancel(true); LOG.error("Semantics service interrupted", e); @@ -80,6 +104,9 @@ public class SemanticsService extends AbstractCachedService { return new SemanticsInternalErrorResult(message); } catch (TimeoutException e) { future.cancel(true); + if (!warmedUpCurrently) { + warmedUp.set(true); + } LOG.trace("Semantics service timeout", e); return new SemanticsInternalErrorResult("Partial interpretation timed out"); } diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/ThreadPoolExecutorServiceProvider.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/ThreadPoolExecutorServiceProvider.java new file mode 100644 index 00000000..ba26ff58 --- /dev/null +++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/ThreadPoolExecutorServiceProvider.java @@ -0,0 +1,110 @@ +/* + * SPDX-FileCopyrightText: 2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.language.web.xtext.server; + +import com.google.inject.Singleton; +import org.eclipse.xtext.ide.ExecutorServiceProvider; +import org.eclipse.xtext.web.server.model.XtextWebDocumentAccess; +import org.jetbrains.annotations.NotNull; +import tools.refinery.language.web.semantics.SemanticsService; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.util.Optional; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.atomic.AtomicInteger; + +@Singleton +public class ThreadPoolExecutorServiceProvider extends ExecutorServiceProvider { + private static final String DOCUMENT_LOCK_EXECUTOR; + private static final AtomicInteger POOL_ID = new AtomicInteger(1); + + private final int executorThreadCount; + private final int lockExecutorThreadCount; + private final int semanticsExecutorThreadCount; + + static { + var lookup = MethodHandles.lookup(); + MethodHandle getter; + try { + var privateLookup = MethodHandles.privateLookupIn(XtextWebDocumentAccess.class, lookup); + getter = privateLookup.findStaticGetter(XtextWebDocumentAccess.class, "DOCUMENT_LOCK_EXECUTOR", + String.class); + } catch (IllegalAccessException | NoSuchFieldException e) { + throw new IllegalStateException("Failed to find getter", e); + } + try { + DOCUMENT_LOCK_EXECUTOR = (String) getter.invokeExact(); + } catch (Error e) { + // Rethrow JVM errors. + throw e; + } catch (Throwable e) { + throw new IllegalStateException("Failed to get DOCUMENT_LOCK_EXECUTOR", e); + } + } + + public ThreadPoolExecutorServiceProvider() { + executorThreadCount = getCount("REFINERY_XTEXT_THREAD_COUNT").orElse(0); + lockExecutorThreadCount = getCount("REFINERY_XTEXT_LOCKING_THREAD_COUNT").orElse(executorThreadCount); + semanticsExecutorThreadCount = getCount("REFINERY_XTEXT_SEMANTICS_THREAD_COUNT").orElse(executorThreadCount); + } + + private static Optional getCount(String name) { + return Optional.ofNullable(System.getenv(name)).map(Integer::parseUnsignedInt); + } + + @Override + protected ExecutorService createInstance(String key) { + String name = "xtext-" + POOL_ID.getAndIncrement(); + if (key != null) { + name = name + key + "-"; + } + var threadFactory = new Factory(name, 5); + int size = getSize(key); + if (size == 0) { + return Executors.newCachedThreadPool(threadFactory); + } + return Executors.newFixedThreadPool(size, threadFactory); + } + + private int getSize(String key) { + if (SemanticsService.SEMANTICS_EXECUTOR.equals(key)) { + return semanticsExecutorThreadCount; + } else if (DOCUMENT_LOCK_EXECUTOR.equals(key)) { + return lockExecutorThreadCount; + } else { + return executorThreadCount; + } + } + + private static class Factory implements ThreadFactory { + // We have to explicitly store the {@link ThreadGroup} to create a {@link ThreadFactory}. + @SuppressWarnings("squid:S3014") + private final ThreadGroup threadGroup = Thread.currentThread().getThreadGroup(); + private final AtomicInteger threadId = new AtomicInteger(1); + private final String namePrefix; + private final int priority; + + public Factory(String name, int priority) { + namePrefix = name + "-thread-"; + this.priority = priority; + } + + @Override + public Thread newThread(@NotNull Runnable runnable) { + var thread = new Thread(threadGroup, runnable, namePrefix + threadId.getAndIncrement()); + if (thread.isDaemon()) { + thread.setDaemon(false); + } + if (thread.getPriority() != priority) { + thread.setPriority(priority); + } + return thread; + } + } +} diff --git a/subprojects/language-web/src/test/java/tools/refinery/language/web/tests/ProblemWebInjectorProvider.java b/subprojects/language-web/src/test/java/tools/refinery/language/web/tests/ProblemWebInjectorProvider.java index 4a5eed95..e9d889c4 100644 --- a/subprojects/language-web/src/test/java/tools/refinery/language/web/tests/ProblemWebInjectorProvider.java +++ b/subprojects/language-web/src/test/java/tools/refinery/language/web/tests/ProblemWebInjectorProvider.java @@ -34,6 +34,7 @@ public class ProblemWebInjectorProvider extends ProblemInjectorProvider { // the tasks in the service and the {@link // org.eclipse.xtext.testing.extensions.InjectionExtension}. return new ProblemWebModule() { + @Override @SuppressWarnings("unused") public Class bindExecutorServiceProvider() { return AwaitTerminationExecutorServiceProvider.class; -- cgit v1.2.3-54-g00ecf