Skip to content

Commit e99b2cb

Browse files
feat: Make an Executor available through VaadinService (#21424)
* feat: Make an Executor available through VaadinService Provides access to an Executor instance from VaadinService to submit asynchronous tasks. The default single-threaded executor can be replaced by a custom instance by registering it with a VaadinServiceInitListener. The spring add-on tries to detect an existing TaskExecutor bean, otherwise it falls back to the default executor. To provide a specific TaskExecutor bean, different from the application default, the bean definition can be named VaadinTaskExecutor or be annotated with `@VaadinTaskExecutor`. Closes #21366 * fix test * better default executor * fix test * format * apply review suggestions --------- Co-authored-by: Mikhail Shabarov <61410877+mshabarov@users.noreply.github.com>
1 parent ef98ac9 commit e99b2cb

File tree

9 files changed

+756
-16
lines changed

9 files changed

+756
-16
lines changed

flow-server/src/main/java/com/vaadin/flow/server/ServiceInitEvent.java

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,16 @@
1515
*/
1616
package com.vaadin.flow.server;
1717

18-
import com.vaadin.flow.server.communication.IndexHtmlRequestListener;
19-
2018
import java.util.ArrayList;
2119
import java.util.EventObject;
2220
import java.util.List;
2321
import java.util.Objects;
22+
import java.util.Optional;
23+
import java.util.concurrent.Executor;
2424
import java.util.stream.Stream;
2525

26+
import com.vaadin.flow.server.communication.IndexHtmlRequestListener;
27+
2628
/**
2729
* Event fired to {@link VaadinServiceInitListener} when a {@link VaadinService}
2830
* is being initialized.
@@ -39,6 +41,7 @@ public class ServiceInitEvent extends EventObject {
3941
private List<IndexHtmlRequestListener> addedIndexHtmlRequestListeners = new ArrayList<>();
4042
private List<DependencyFilter> addedDependencyFilters = new ArrayList<>();
4143
private List<VaadinRequestInterceptor> addedVaadinRequestInterceptors = new ArrayList<>();
44+
private Executor executor;
4245

4346
/**
4447
* Creates a new service init event for a given {@link VaadinService} and
@@ -107,6 +110,27 @@ public void addVaadinRequestInterceptor(
107110
addedVaadinRequestInterceptors.add(vaadinRequestInterceptor);
108111
}
109112

113+
/**
114+
* Sets the {@link Executor} to be used by Vaadin for running asynchronous
115+
* tasks.
116+
* <p>
117+
* The application can also benefit from this executor to submit its own
118+
* asynchronous tasks.
119+
* <p>
120+
* The developer is responsible for managing the executor's lifecycle, for
121+
* example, by registering a {@link VaadinService} destroy listener to shut
122+
* it down.
123+
* <p>
124+
* A {@literal null} value can be given to switch back to the Vaadin default
125+
* executor.
126+
*
127+
* @param executor
128+
* the executor to set.
129+
*/
130+
public void setExecutor(Executor executor) {
131+
this.executor = executor;
132+
}
133+
110134
/**
111135
* Gets a stream of all custom request handlers that have been added for the
112136
* service.
@@ -147,6 +171,17 @@ public Stream<VaadinRequestInterceptor> getAddedVaadinRequestInterceptor() {
147171
return addedVaadinRequestInterceptors.stream();
148172
}
149173

174+
/**
175+
* Gets the optional {@link Executor} that is currently set to be used by
176+
* Vaadin for running asynchronous tasks.
177+
*
178+
* @return an {@link Optional} containing the {@link Executor}, or an empty
179+
* {@link Optional} if no executor is set.
180+
*/
181+
public Optional<Executor> getExecutor() {
182+
return Optional.ofNullable(executor);
183+
}
184+
150185
@Override
151186
public VaadinService getSource() {
152187
return (VaadinService) super.getSource();

flow-server/src/main/java/com/vaadin/flow/server/VaadinService.java

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,14 @@
4141
import java.util.concurrent.CancellationException;
4242
import java.util.concurrent.ConcurrentHashMap;
4343
import java.util.concurrent.CopyOnWriteArrayList;
44+
import java.util.concurrent.Executor;
45+
import java.util.concurrent.ExecutorService;
4446
import java.util.concurrent.Future;
47+
import java.util.concurrent.LinkedBlockingQueue;
48+
import java.util.concurrent.ThreadFactory;
49+
import java.util.concurrent.ThreadPoolExecutor;
4550
import java.util.concurrent.TimeUnit;
51+
import java.util.concurrent.atomic.AtomicInteger;
4652
import java.util.concurrent.locks.Lock;
4753
import java.util.concurrent.locks.ReentrantLock;
4854
import java.util.stream.Collectors;
@@ -184,6 +190,10 @@ public abstract class VaadinService implements Serializable {
184190

185191
private Instantiator instantiator;
186192

193+
private Executor executor;
194+
195+
private boolean defaultExecutorInUse;
196+
187197
private VaadinContext vaadinContext;
188198

189199
private Iterable<VaadinRequestInterceptor> vaadinRequestInterceptors;
@@ -267,6 +277,9 @@ public void init() throws ServiceException {
267277
instantiator.getServiceInitListeners()
268278
.forEach(listener -> listener.serviceInit(event));
269279

280+
this.executor = event.getExecutor()
281+
.orElseGet(this::createDefaultExecutor);
282+
270283
event.getAddedRequestHandlers().forEach(handlers::add);
271284

272285
Collections.reverse(handlers);
@@ -293,6 +306,18 @@ public void init() throws ServiceException {
293306
.collect(Collectors.toList());
294307
});
295308

309+
if (this.executor == null) {
310+
throw new ServiceException(
311+
"Unable to create the default Executor for "
312+
+ getClass().getName()
313+
+ ". This is most likely a bug in a custom VaadinService implementation "
314+
+ "that overrides the createDefaultExecutor() method "
315+
+ "but returns a null Executor instance. "
316+
+ "As a workaround, you can register a "
317+
+ VaadinServiceInitListener.class.getSimpleName()
318+
+ " providing a custom Executor instance.");
319+
}
320+
296321
DeploymentConfiguration configuration = getDeploymentConfiguration();
297322
if (!configuration.isProductionMode()) {
298323
Logger logger = getLogger();
@@ -503,6 +528,98 @@ public Instantiator getInstantiator() {
503528
return instantiator;
504529
}
505530

531+
/**
532+
* Creates a default executor instance to use with this service.
533+
* <p>
534+
* This default implementation creates a thread pool executor with a custom
535+
* thread factory to generate daemon threads. It uses a core pool size of 8,
536+
* an unbounded maximum pool size, and a keep-alive time of 60 seconds for
537+
* idle threads. The thread pool grows dynamically as required, and idle
538+
* core threads are allowed to time out.
539+
* <p>
540+
* A custom {@link VaadinService} implementation can override this method to
541+
* provide its own ad-hoc executor tailored to specific environments like
542+
* CDI or Spring.
543+
* <p>
544+
* Implementors should never return {@literal null}; if an executor instance
545+
* cannot be provided, the method should call
546+
* {@code super.createDefaultExecutor()}.
547+
* <p>
548+
* The application can provide a more appropriate executor implementation
549+
* through a {@link VaadinServiceInitListener} and calling
550+
* {@link ServiceInitEvent#setExecutor(Executor)}.
551+
*
552+
* @return a default executor instance to use, never {@literal null}.
553+
* @see VaadinServiceInitListener
554+
* @see ServiceInitEvent#setExecutor(Executor)
555+
*/
556+
protected Executor createDefaultExecutor() {
557+
this.defaultExecutorInUse = true;
558+
int corePoolSize = 8;
559+
int keepAliveTimeSec = 60;
560+
561+
class VaadinThreadFactory implements ThreadFactory {
562+
private final AtomicInteger threadNumber = new AtomicInteger(0);
563+
564+
@Override
565+
public Thread newThread(Runnable runnable) {
566+
int threadNumber = this.threadNumber.incrementAndGet();
567+
if (threadNumber == 1) {
568+
getLogger().info(
569+
"The application is using Vaadin's default ThreadPoolExecutor "
570+
+ "(pool size = {}, keep alive time = {} seconds). "
571+
+ "A custom executor with an appropriate thread pool "
572+
+ "can be provided registering a {}.",
573+
corePoolSize, keepAliveTimeSec,
574+
VaadinServiceInitListener.class.getSimpleName());
575+
}
576+
Thread thread = new Thread(runnable,
577+
"VaadinTaskExecutor-thread-" + threadNumber);
578+
// Thread marked as daemon to prevent task execution to block
579+
// JVM shutdown
580+
thread.setDaemon(true);
581+
thread.setPriority(Thread.NORM_PRIORITY);
582+
return thread;
583+
}
584+
}
585+
// Defaults taken from Spring Boot configuration
586+
// org.springframework.boot.autoconfigure.task.TaskExecutionProperties.Pool
587+
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
588+
corePoolSize, Integer.MAX_VALUE, keepAliveTimeSec,
589+
TimeUnit.SECONDS, new LinkedBlockingQueue<>(),
590+
new VaadinThreadFactory());
591+
// Enables dynamic growing and shrinking of the pool.
592+
threadPoolExecutor.allowCoreThreadTimeOut(true);
593+
return threadPoolExecutor;
594+
}
595+
596+
/**
597+
* Gets the executor instance used by Vaadin for managing concurrent tasks.
598+
* <p>
599+
* By default, a thread pool executor with a custom with core pool size of
600+
* 8, an unbounded maximum pool size, and a keep-alive time of 60 seconds
601+
* for idle threads is provided. The thread pool grows dynamically as
602+
* required, and idle core threads are allowed to time out.
603+
* <p>
604+
* {@link VaadinService} implementations for specific environments like CDI
605+
* or Spring might provide their own ad-hoc Executors tailored to those
606+
* environments.
607+
* <p>
608+
* A custom executor can be configured by registering a
609+
* {@link VaadinServiceInitListener} and providing the executor instance to
610+
* the {@link ServiceInitEvent}.
611+
* <p>
612+
* A Vaadin application can also benefit from this executor to submit
613+
* asynchronous tasks.
614+
*
615+
* @return the Executor instance, never {@literal null}.
616+
* @see VaadinServiceInitListener
617+
* @see ServiceInitEvent#setExecutor(Executor)
618+
*/
619+
public Executor getExecutor() {
620+
return executor;
621+
}
622+
506623
/**
507624
* Gets the class loader to use for loading classes loaded by name, e.g.
508625
* custom UI classes. This is by default the class loader that was used to
@@ -2216,6 +2333,10 @@ public Registration addServiceDestroyListener(
22162333
*/
22172334
public void destroy() {
22182335
ServiceDestroyEvent event = new ServiceDestroyEvent(this);
2336+
if (defaultExecutorInUse && executor instanceof ExecutorService cast) {
2337+
cast.shutdownNow();
2338+
this.executor = null;
2339+
}
22192340
RuntimeException exception = null;
22202341
for (ServiceDestroyListener listener : serviceDestroyListeners) {
22212342
try {

flow-server/src/test/java/com/vaadin/flow/server/MockVaadinServletService.java

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,11 @@
1515
*/
1616
package com.vaadin.flow.server;
1717

18-
import jakarta.servlet.ServletException;
19-
2018
import java.util.Collections;
2119
import java.util.List;
2220

21+
import jakarta.servlet.ServletException;
22+
2323
import com.vaadin.flow.di.Instantiator;
2424
import com.vaadin.flow.function.DeploymentConfiguration;
2525
import com.vaadin.flow.router.Router;
@@ -67,11 +67,22 @@ public MockVaadinServletService() {
6767
this(new MockDeploymentConfiguration());
6868
}
6969

70+
public MockVaadinServletService(boolean init) {
71+
this(new MockDeploymentConfiguration(), init);
72+
}
73+
7074
public MockVaadinServletService(
7175
DeploymentConfiguration deploymentConfiguration) {
76+
this(deploymentConfiguration, true);
77+
}
78+
79+
public MockVaadinServletService(
80+
DeploymentConfiguration deploymentConfiguration, boolean init) {
7281
super(new MockVaadinServlet(deploymentConfiguration),
7382
deploymentConfiguration);
74-
init();
83+
if (init) {
84+
init();
85+
}
7586
}
7687

7788
public void setRouter(Router router) {

0 commit comments

Comments
 (0)