Skip to content

Commit

Permalink
chore: use custom thread pool for vaadin-dev-server tasks
Browse files Browse the repository at this point in the history
Vaadin dev-server executes long-running tasks (e.g. npm install) using
the common ForkJoin pool. This can cause a slow startup or even timeouts
when the pool size is very small.
Using a dedicated executor for dev-server tasks should prevent the
above issues.

Fixes #20793
  • Loading branch information
mcollovati committed Dec 31, 2024
1 parent 041dead commit 6e02103
Show file tree
Hide file tree
Showing 3 changed files with 68 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ protected Stream<String> getExcludedPatterns() {
"com\\.vaadin\\.base\\.devserver\\.DebugWindowConnection",
"com\\.vaadin\\.base\\.devserver\\.DebugWindowConnection\\$DevToolsInterfaceImpl",
"com\\.vaadin\\.base\\.devserver\\.DevModeHandlerManagerImpl",
"com\\.vaadin\\.base\\.devserver\\.DevModeHandlerManagerImpl\\$InternalThreadFactory",
"com\\.vaadin\\.base\\.devserver\\.DevServerWatchDog",
"com\\.vaadin\\.base\\.devserver\\.DevServerWatchDog\\$WatchDogServer",
"com\\.vaadin\\.base\\.devserver\\.DevToolsInterface",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,20 @@
*/
package com.vaadin.base.devserver;

import com.vaadin.flow.server.Command;
import jakarta.servlet.annotation.HandlesTypes;

import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.io.Serializable;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand All @@ -35,6 +37,7 @@
import com.vaadin.base.devserver.startup.DevModeStartupListener;
import com.vaadin.flow.internal.DevModeHandler;
import com.vaadin.flow.internal.DevModeHandlerManager;
import com.vaadin.flow.server.Command;
import com.vaadin.flow.server.Mode;
import com.vaadin.flow.server.VaadinContext;
import com.vaadin.flow.server.frontend.FrontendUtils;
Expand Down Expand Up @@ -68,6 +71,7 @@ private static final class DevModeHandlerAlreadyStartedAttribute
private DevModeHandler devModeHandler;
private BrowserLauncher browserLauncher;
private final Set<Command> shutdownCommands = new HashSet<>();
private ExecutorService executorService;

private String applicationUrl;
private boolean fullyStarted = false;
Expand Down Expand Up @@ -96,8 +100,11 @@ public DevModeHandler getDevModeHandler() {
@Override
public void initDevModeHandler(Set<Class<?>> classes, VaadinContext context)
throws VaadinInitializerException {
setDevModeHandler(
DevModeInitializer.initDevModeHandler(classes, context));
shutdownExecutorService();
executorService = Executors.newFixedThreadPool(4,
new InternalThreadFactory());
setDevModeHandler(DevModeInitializer.initDevModeHandler(classes,
context, executorService));
CompletableFuture.runAsync(() -> {
DevModeHandler devModeHandler = getDevModeHandler();
if (devModeHandler instanceof AbstractDevServerRunner) {
Expand All @@ -111,11 +118,17 @@ public void initDevModeHandler(Set<Class<?>> classes, VaadinContext context)
startWatchingThemeFolder(context, config);
watchExternalDependencies(context, config);
setFullyStarted(true);
});
}, executorService);
setDevModeStarted(context);
this.browserLauncher = new BrowserLauncher(context);
}

private void shutdownExecutorService() {
if (executorService != null) {
executorService.shutdownNow();
}
}

private void watchExternalDependencies(VaadinContext context,
ApplicationConfiguration config) {
File frontendFolder = FrontendUtils.getProjectFrontendDir(config);
Expand Down Expand Up @@ -159,6 +172,7 @@ public void stopDevModeHandler() {
devModeHandler.stop();
devModeHandler = null;
}
shutdownExecutorService();
for (Command shutdownCommand : shutdownCommands) {
try {
shutdownCommand.execute();
Expand Down Expand Up @@ -231,4 +245,18 @@ public static boolean isDevModeAlreadyStarted(VaadinContext context) {
private static Logger getLogger() {
return LoggerFactory.getLogger(DevModeHandlerManagerImpl.class);
}

private static class InternalThreadFactory implements ThreadFactory {
private final AtomicInteger threadNumber = new AtomicInteger(1);

@Override
public Thread newThread(Runnable runnable) {
String threadName = "vaadin-dev-server-"
+ threadNumber.getAndIncrement();
Thread thread = new Thread(runnable, threadName);
thread.setDaemon(true);
thread.setPriority(Thread.NORM_PRIORITY);
return thread;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.Executor;
import java.util.concurrent.ForkJoinPool;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
Expand Down Expand Up @@ -74,9 +76,6 @@
import com.vaadin.flow.server.startup.VaadinInitializerException;
import com.vaadin.pro.licensechecker.LicenseChecker;

import elemental.json.Json;
import elemental.json.JsonObject;

import static com.vaadin.flow.server.Constants.PACKAGE_JSON;
import static com.vaadin.flow.server.Constants.PROJECT_FRONTEND_GENERATED_DIR_TOKEN;
import static com.vaadin.flow.server.Constants.VAADIN_SERVLET_RESOURCES;
Expand Down Expand Up @@ -169,6 +168,12 @@ private static Set<String> calculateApplicableClassNames() {
/**
* Initialize the devmode server if not in production mode or compatibility
* mode.
* <p>
* </p>
* Uses common ForkJoin pool to execute asynchronous tasks. It is
* recommended to use
* {@link #initDevModeHandler(Set, VaadinContext, Executor)} and provide a a
* custom executor if initialization starts long-running tasks.
*
* @param classes
* classes to check for npm- and js modules
Expand All @@ -179,9 +184,34 @@ private static Set<String> calculateApplicableClassNames() {
*
* @throws VaadinInitializerException
* if dev mode can't be initialized
* @deprecated use {@link #initDevModeHandler(Set, VaadinContext, Executor)}
* providing a custom executor.
*/
@Deprecated(forRemoval = true)
public static DevModeHandler initDevModeHandler(Set<Class<?>> classes,
VaadinContext context) throws VaadinInitializerException {
return initDevModeHandler(classes, context, ForkJoinPool.commonPool());
}

/**
* Initialize the devmode server if not in production mode or compatibility
* mode.
*
* @param classes
* classes to check for npm- and js modules
* @param context
* VaadinContext we are running in
* @param taskExecutor
* the executor to use for asynchronous execution
* @return the initialized dev mode handler or {@code null} if none was
* created
*
* @throws VaadinInitializerException
* if dev mode can't be initialized
*/
public static DevModeHandler initDevModeHandler(Set<Class<?>> classes,
VaadinContext context, Executor taskExecutor)
throws VaadinInitializerException {

ApplicationConfiguration config = ApplicationConfiguration.get(context);
if (config.isProductionMode()) {
Expand Down Expand Up @@ -317,7 +347,7 @@ public static DevModeHandler initDevModeHandler(Set<Class<?>> classes,
};

CompletableFuture<Void> nodeTasksFuture = CompletableFuture
.runAsync(runnable);
.runAsync(runnable, taskExecutor);

Lookup devServerLookup = Lookup.compose(lookup,
Lookup.of(config, ApplicationConfiguration.class));
Expand Down

0 comments on commit 6e02103

Please sign in to comment.