From f9b9bef2115b3090d786f5eacaab2729998326d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petter=20Holmstr=C3=B6m?= Date: Tue, 1 Oct 2024 17:41:06 +0300 Subject: [PATCH 01/30] WIP --- .../background-jobs/index.adoc | 122 +++++++++++++ .../background-jobs/jobs.adoc | 56 ++++++ .../background-jobs/triggers.adoc | 170 ++++++++++++++++++ .../background-jobs/ui-interaction.adoc | 9 + 4 files changed, 357 insertions(+) create mode 100644 articles/building-apps/application-layer/background-jobs/index.adoc create mode 100644 articles/building-apps/application-layer/background-jobs/jobs.adoc create mode 100644 articles/building-apps/application-layer/background-jobs/triggers.adoc create mode 100644 articles/building-apps/application-layer/background-jobs/ui-interaction.adoc diff --git a/articles/building-apps/application-layer/background-jobs/index.adoc b/articles/building-apps/application-layer/background-jobs/index.adoc new file mode 100644 index 0000000000..9a82bcc36d --- /dev/null +++ b/articles/building-apps/application-layer/background-jobs/index.adoc @@ -0,0 +1,122 @@ +--- +title: Background Jobs +description: How to handle background jobs in Vaadin applications. +order: 11 +--- + += Background Jobs + +Many business applications need to perform in background threads. These tasks could be long-running tasks triggered by the user, or scheduled jobs that run automatically at a specific time of day, or at specific intervals. + +Working with more than one thread increases the risk of bugs. Furthermore, there are many different ways of implementing background jobs. To reduce the risk, you should learn one way, and then apply it consistently in all your Vaadin applications. + +== Threads + +Whenever you work with background threads in a Vaadin application, you should never create new `Thread` objects directly. First, new threads are expensive to start. Second, the number of concurrent threads in a Java application is limited. An exact number is impossible to give, but typically it is measured in thousands. + +Instead, you should use thread pools, or virtual threads. + +A thread pool consists of a queue, and a pool of running threads. The threads pick tasks from the queue and execute them. When the thread pool receives a new job, it adds it to the queue. +The queue has an upper size limit. If the queue is full, the thread pool rejects the job, and throws an exception. + +Virtual threads were added in Java 21. Whereas ordinary threads are managed by the operating system,virtual threads are managed by the Java virtual machine. They are cheaper to start and run, which means you can have a much higher number of concurrent virtual threads than ordinary threads. + +See the https://docs.oracle.com/en/java/javase/21/core/virtual-threads.html[Java Documentation] for more information about virtual threads. + +== Task Execution + +The background jobs themselves should not need to manage their own thread pools, or virtual threads. Instead, they should use _executors_. An executor is an object that takes a `Runnable`, and executes it at some point in the future. Spring provides a `TaskExecutor`, that you should use in your background jobs. + +By default, Spring Boot sets up a `ThreadPoolTaskExecutor` in your application context. You can tweak the parameters of this executor through the `spring.task.executor.*` configuration properties. + +If you are using a newer version of Java and Spring Boot, and want to use virtual threads, you can enable them by setting the `spring.threads.virtual.enabled` configuration property to `true`. In this case, Spring Boot sets up a `SimpleAsyncTaskExecutor`, and creates a new virtual thread for every task. + +In practice, you inject an instance of `TaskExecutor` into your code, and submit work to it. Here is an example of a class that uses the `TaskExecutor`: + +[source,java] +---- +import org.springframework.core.task.TaskExecutor; + +@Component +public class MyWorker { + + private final TaskExecutor taskExecutor; + + MyWorker(TaskExecutor taskExecutor) { + this.taskExecutor = taskExecutor; + } + + public void performTask() { + taskExecutor.execute(() -> { + System.out.println("Hello, I'm running inside thread " + Thread.currentThread()); + }); + } +} +---- + +You can also use the `@Async` annotation to tell Spring to execute your code using the `TaskExecutor`: + +[source,java] +---- +import org.springframework.scheduling.annotation.Async; + +@Component +public class MyWorker { + + @Async + public void performTask() { + System.out.println("Hello, I'm running inside thread " + Thread.currentThread()); + } +} +---- + +The annotated version is more concise, but has some pitfalls you need to be aware of. + +First, support for the `@Async` annotation must be explicitly enabled in the Spring application context. If you forget to do this, and you call `performTask()`, the method executes in the calling thread, and not in a background thread. + +Second, you cannot call the `performTask()` method from within the `MyWorker` class itself. This is because Spring by default uses proxies to process `@Async` annotations, and local method calls bypass the proxy. + +== Task Scheduling + +Spring also has built in support for scheduling tasks. You can schedule jobs programmatically by injecting a `TaskScheduler` into your code, like this: + +[source,java] +---- +public class MyScheduler { + + +} +---- + +For this, it sets up a separate thread pool. You can interact with it through a `TaskScheduler`. + +== Spring Configuration + +Before you can schedule tasks, you have to enable scheduling and asynchronous execution. To do this, add the `@EnableAsync` and the `@EnableScheduling` annotations to your main application class: + +[source,java] +---- +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.scheduling.annotation.EnableScheduling; + +@SpringBootApplication +@EnableAsync +@EnableScheduling +public class Application implements AppShellConfigurator { + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } +} +---- + +In addition to using the `TaskExecutor` and `TaskScheduler` interfaces, you can also use the `@Async` and `@Scheduled` annotations to execute and schedule tasks. You'll see examples of both later on this page. + +[IMPORTANT] +Because both the task executor and the task scheduler implement the `TaskExecutor` interface, you have to use the names `taskExecutor` and `taskScheduler` in the constructor parameters. Otherwise, Spring does not know which instance to inject. + +See the https://docs.spring.io/spring-framework/reference/integration/scheduling.html[Spring Documentation] for more information about task execution. + +== Building + +section_outline::[] diff --git a/articles/building-apps/application-layer/background-jobs/jobs.adoc b/articles/building-apps/application-layer/background-jobs/jobs.adoc new file mode 100644 index 0000000000..0335c18428 --- /dev/null +++ b/articles/building-apps/application-layer/background-jobs/jobs.adoc @@ -0,0 +1,56 @@ +--- +title: Implementing Jobs +description: How to implement backgorund jobs. +order: 10 +--- + += Implementing Jobs + +When you implement a background job, you should decouple its implementation from how it is triggered, and where it is executed. This makes it possible to trigger the job in multiple ways. + +For instance, you may want to run the job every time the application starts up. In this case, you may want to run it in the main thread, blocking the initialization of the rest of the application until the job is finished. You may also want to run the job in a background thread every day at midnight, or whenever a certain application event is published. + +In code, a job is a Spring bean, annotated with the `@Component` annotation. It contains one or more methods, that when called, execute the job in the calling thread, like this: + +[source,java] +---- +import org.springframework.stereotype.Component; + +@Component +public class MyBackgroundJob { + + public void performBackgroundJob() { + ... + } +} +---- + +== Transactions + +If the job works on the database, it should manage its own transactions. Because a job is a Spring bean, you can use either declarative, or programmatic transaction management. Here is the earlier example, with declarative transactions: + +[source,java] +---- +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +@Component +public class MyBackgroundJob { + + @Transactional(propagation = Propagation.REQUIRES_NEW) + public void performBackgroundJob() { + ... + } +} +---- + +This guarantees that the job runs inside a new transaction, regardless of how it is triggered. + +== Security + +Unlike <>, background jobs should _not_ use method security. The reason is that Spring Security uses the `SecurityContext` to access information about the current user. This context is typically thread local, which means it is not available in a background thread. Therefore, whenever the job is executed by a background thread, Spring would deny access. + +// TODO === Batch Jobs (copy from my blog post) + +// TODO === Simultaneous Executions (copy from my blog post) \ No newline at end of file diff --git a/articles/building-apps/application-layer/background-jobs/triggers.adoc b/articles/building-apps/application-layer/background-jobs/triggers.adoc new file mode 100644 index 0000000000..0c744801ac --- /dev/null +++ b/articles/building-apps/application-layer/background-jobs/triggers.adoc @@ -0,0 +1,170 @@ +--- +title: Triggering Jobs +description: How to trigger backgorund jobs +order: 20 +--- + += Triggering Jobs + +A trigger is an object that starts a job. It decides which thread the job should execute in, executes it, and handles any errors that occurred. + +Business applications have many different triggers. You may want to trigger some jobs on application startup. Other jobs may run once a week, every day at midnight, or every five minutes. Some jobs may run in response to application events, and other in response to user input. You might even have some jobs that can be triggered through a REST API, or through Java Management Extensions (JMX). + +As was mentioned earlier, the same job can have more than one trigger. + +// === Startup Jobs + +== Scheduled Jobs + +For scheduled jobs, you should create a _scheduler_ that uses Spring's scheduling mechanism to trigger the job. + +Spring uses a separate thread pool for scheduled tasks. You should not use this thread pool to execute the jobs. Instead, your schedulers should hand over the jobs to the `TaskExecutor`. + +Before you can create a scheduler, you have to enable scheduling and asynchronous execution. To do this, add the `@EnableAsync` and the `@EnableScheduling` annotations to your main application class: + +[source,java] +---- +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.scheduling.annotation.EnableScheduling; + +@SpringBootApplication +@EnableAsync +@EnableScheduling +public class Application implements AppShellConfigurator { + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } +} +---- + +Since the scheduler is not intended to be called by any other objects, you should make it package private. + +A declarative scheduler looks like this: + +[source,java] +---- +@Component +class MyBackgroundJobScheduler { + + private static final Logger log = LoggerFactory.getLogger(MyBackgroundJobScheduler.class); + private final MyBackgroundJob job; + + MyBackgroundJobScheduler(MyBackgroundJob job) { + this.job = job; + } + + @Scheduled(fixedRate = 5, timeUnit = TimeUnit.MINUTES) // <1> + @Async // <2> + public void performBackgroundJob() { + try { + job.performBackgroundJob(); // <3> + } catch (Exception ex) { + log.error("Error performing background job", ex); // <4> + } + } +} +---- +<1> The job is executed every 5 minutes. +<2> The job is executed by the `TaskExecutor`, not by the scheduling thread pool. +<3> The scheduler object delegates to the job object. +<4> Log any errors, or handle them in some other way. + +If you prefer a more explicit, programmatic approach, you could do the following: + +[source,java] +---- +@Component +class MyBackgroundJobScheduler { + + private static final Logger log = LoggerFactory.getLogger(MyBackgroundJobScheduler.class); + private final MyBackgroundJob job; + private final TaskExecutor taskExecutor; + private final TaskScheduler taskScheduler; + + MyBackgroundJobScheduler(MyBackgroundJob job, + TaskExecutor taskExecutor, + TaskScheduler taskScheduler) { + this.job = job; + this.taskExecutor = taskExecutor; + this.taskScheduler = taskScheduler; + } + + @EventListener + public void onApplicationReadyEvent(ApplicationReadyEvent event) { // <1> + taskScheduler.scheduleAtFixedRate( + () -> taskExecutor.execute(this::performBackgroundJob), + Duration.ofMinutes(5) + ); + } + + private void performBackgroundJob() { + try { + job.performBackgroundJob(); + } catch (Exception ex) { + log.error("Error performing background job", ex); + } + } +} +---- +<1> This event is fired once, when the application has started up and is ready to serve requests. + +Programmatic schedulers are more verbose, but they are easier to debug. You should start with declarative schedulers, and switch to programmatic ones if you need more control over the scheduling, or run into problems that are difficult to debug. + +See the https://docs.spring.io/spring-framework/reference/integration/scheduling.html[Spring Documentation] for more information about task scheduling. + +== Event Triggered Jobs + +For event triggered jobs, you should create an _event listener_ that receives events from Spring's event publisher. By default, the event publisher calls each listener in the same thread that published the event. This can be overridden, but it is easier to handle it in the listener implementation itself. + +Since the listener is not intended to be called by any other objects, you should make it package private. + +A listener looks like this: + +[source,java] +---- +@Component +class PerformBackgroundJobOnMyEventTrigger { + private static final Logger log = LoggerFactory.getLogger(PerformBackgroundJobOnMyEventTrigger.class); + private final MyBackgroundJob job; + private final TaskExecutor taskExecutor; + + PerformBackgroundJobOnMyEventTrigger(MyBackgroundJob job, + TaskExecutor taskExecutor) { + this.job = job; + this.taskExecutor = taskExecutor; + } + + @EventListener + public void onMyEvent(MyEvent event) { + taskExecutor.execute(this::performBackgroundJob); + } + + private void performBackgroundJob() { + try { + job.performBackgroundJob(); + } catch (Exception ex) { + log.error("Error performing background job", ex); + } + } +} +---- + +== User Triggered Jobs + +For user triggered jobs, an <> acts as the trigger. + +[source,java] +---- +@Service +public class MyApplicationService { + + @Test + public void startBackgroundJob() { + + } + +} +---- + +If the job needs to interact with the user interface in some way, either while running, or after it has finished, it becomes a bit more involved. This is explained in the next section. diff --git a/articles/building-apps/application-layer/background-jobs/ui-interaction.adoc b/articles/building-apps/application-layer/background-jobs/ui-interaction.adoc new file mode 100644 index 0000000000..37e1a068c9 --- /dev/null +++ b/articles/building-apps/application-layer/background-jobs/ui-interaction.adoc @@ -0,0 +1,9 @@ +--- +title: UI Interaction +description: How to interact with the user interface from background jobs. +order: 30 +--- + += UI Interaction + +// TODO Write me \ No newline at end of file From 1bda84bf821f4351cc3ba723f78ba5b722f16fed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petter=20Holmstr=C3=B6m?= Date: Tue, 1 Oct 2024 17:46:10 +0300 Subject: [PATCH 02/30] WIP again --- .../background-jobs/index.adoc | 4 ++- .../background-jobs/jobs.adoc | 8 +++-- .../background-jobs/triggers.adoc | 32 ++++++------------- 3 files changed, 18 insertions(+), 26 deletions(-) diff --git a/articles/building-apps/application-layer/background-jobs/index.adoc b/articles/building-apps/application-layer/background-jobs/index.adoc index 9a82bcc36d..178d627179 100644 --- a/articles/building-apps/application-layer/background-jobs/index.adoc +++ b/articles/building-apps/application-layer/background-jobs/index.adoc @@ -78,13 +78,15 @@ Second, you cannot call the `performTask()` method from within the `MyWorker` cl == Task Scheduling +// TODO Continue here + Spring also has built in support for scheduling tasks. You can schedule jobs programmatically by injecting a `TaskScheduler` into your code, like this: [source,java] ---- public class MyScheduler { - + } ---- diff --git a/articles/building-apps/application-layer/background-jobs/jobs.adoc b/articles/building-apps/application-layer/background-jobs/jobs.adoc index 0335c18428..efc35981a3 100644 --- a/articles/building-apps/application-layer/background-jobs/jobs.adoc +++ b/articles/building-apps/application-layer/background-jobs/jobs.adoc @@ -51,6 +51,10 @@ This guarantees that the job runs inside a new transaction, regardless of how it Unlike <>, background jobs should _not_ use method security. The reason is that Spring Security uses the `SecurityContext` to access information about the current user. This context is typically thread local, which means it is not available in a background thread. Therefore, whenever the job is executed by a background thread, Spring would deny access. -// TODO === Batch Jobs (copy from my blog post) +== Batch Jobs -// TODO === Simultaneous Executions (copy from my blog post) \ No newline at end of file +// TODO Write me (copy from my blog post) + +== Simultaneous Executions + +// TODO Write me (copy from my blog post) diff --git a/articles/building-apps/application-layer/background-jobs/triggers.adoc b/articles/building-apps/application-layer/background-jobs/triggers.adoc index 0c744801ac..29b02227d5 100644 --- a/articles/building-apps/application-layer/background-jobs/triggers.adoc +++ b/articles/building-apps/application-layer/background-jobs/triggers.adoc @@ -10,9 +10,11 @@ A trigger is an object that starts a job. It decides which thread the job should Business applications have many different triggers. You may want to trigger some jobs on application startup. Other jobs may run once a week, every day at midnight, or every five minutes. Some jobs may run in response to application events, and other in response to user input. You might even have some jobs that can be triggered through a REST API, or through Java Management Extensions (JMX). -As was mentioned earlier, the same job can have more than one trigger. +The same job can have more than one trigger. -// === Startup Jobs +== Startup Jobs + +// TODO Write me == Scheduled Jobs @@ -20,24 +22,6 @@ For scheduled jobs, you should create a _scheduler_ that uses Spring's schedulin Spring uses a separate thread pool for scheduled tasks. You should not use this thread pool to execute the jobs. Instead, your schedulers should hand over the jobs to the `TaskExecutor`. -Before you can create a scheduler, you have to enable scheduling and asynchronous execution. To do this, add the `@EnableAsync` and the `@EnableScheduling` annotations to your main application class: - -[source,java] ----- -import org.springframework.scheduling.annotation.EnableAsync; -import org.springframework.scheduling.annotation.EnableScheduling; - -@SpringBootApplication -@EnableAsync -@EnableScheduling -public class Application implements AppShellConfigurator { - - public static void main(String[] args) { - SpringApplication.run(Application.class, args); - } -} ----- - Since the scheduler is not intended to be called by any other objects, you should make it package private. A declarative scheduler looks like this: @@ -111,8 +95,6 @@ class MyBackgroundJobScheduler { Programmatic schedulers are more verbose, but they are easier to debug. You should start with declarative schedulers, and switch to programmatic ones if you need more control over the scheduling, or run into problems that are difficult to debug. -See the https://docs.spring.io/spring-framework/reference/integration/scheduling.html[Spring Documentation] for more information about task scheduling. - == Event Triggered Jobs For event triggered jobs, you should create an _event listener_ that receives events from Spring's event publisher. By default, the event publisher calls each listener in the same thread that published the event. This can be overridden, but it is easier to handle it in the listener implementation itself. @@ -154,6 +136,8 @@ class PerformBackgroundJobOnMyEventTrigger { For user triggered jobs, an <> acts as the trigger. +// TODO Continue here + [source,java] ---- @Service @@ -167,4 +151,6 @@ public class MyApplicationService { } ---- -If the job needs to interact with the user interface in some way, either while running, or after it has finished, it becomes a bit more involved. This is explained in the next section. +// TODO If the job needs to interact with the user interface in some way, either while running, or after it has finished, it becomes a bit more involved. This is explained in the next section. + +// TODO How to trigger jobs using Control Center? From 95a719d32dcf09b35005b2803c4c5858c3967072 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petter=20Holmstr=C3=B6m?= Date: Thu, 3 Oct 2024 11:46:11 +0300 Subject: [PATCH 03/30] First draft of index.adoc --- .../background-jobs/index.adoc | 182 ++++++++++++++++-- 1 file changed, 161 insertions(+), 21 deletions(-) diff --git a/articles/building-apps/application-layer/background-jobs/index.adoc b/articles/building-apps/application-layer/background-jobs/index.adoc index 178d627179..06145f29dd 100644 --- a/articles/building-apps/application-layer/background-jobs/index.adoc +++ b/articles/building-apps/application-layer/background-jobs/index.adoc @@ -19,7 +19,7 @@ Instead, you should use thread pools, or virtual threads. A thread pool consists of a queue, and a pool of running threads. The threads pick tasks from the queue and execute them. When the thread pool receives a new job, it adds it to the queue. The queue has an upper size limit. If the queue is full, the thread pool rejects the job, and throws an exception. -Virtual threads were added in Java 21. Whereas ordinary threads are managed by the operating system,virtual threads are managed by the Java virtual machine. They are cheaper to start and run, which means you can have a much higher number of concurrent virtual threads than ordinary threads. +Virtual threads were added in Java 21. Whereas ordinary threads are managed by the operating system, virtual threads are managed by the Java virtual machine. They are cheaper to start and run, which means you can have a much higher number of concurrent virtual threads than ordinary threads. See the https://docs.oracle.com/en/java/javase/21/core/virtual-threads.html[Java Documentation] for more information about virtual threads. @@ -29,9 +29,11 @@ The background jobs themselves should not need to manage their own thread pools, By default, Spring Boot sets up a `ThreadPoolTaskExecutor` in your application context. You can tweak the parameters of this executor through the `spring.task.executor.*` configuration properties. -If you are using a newer version of Java and Spring Boot, and want to use virtual threads, you can enable them by setting the `spring.threads.virtual.enabled` configuration property to `true`. In this case, Spring Boot sets up a `SimpleAsyncTaskExecutor`, and creates a new virtual thread for every task. +If you want to use virtual threads, you can enable them by setting the `spring.threads.virtual.enabled` configuration property to `true`. In this case, Spring Boot sets up a `SimpleAsyncTaskExecutor`, and creates a new virtual thread for every task. -In practice, you inject an instance of `TaskExecutor` into your code, and submit work to it. Here is an example of a class that uses the `TaskExecutor`: +You can interact with the `TaskExecutor` either directly, or declaratively through annotations. + +When interacting with it directly, you inject an instance of `TaskExecutor` into your code, and submit work to it. Here is an example of a class that uses the `TaskExecutor`: [source,java] ---- @@ -54,7 +56,26 @@ public class MyWorker { } ---- -You can also use the `@Async` annotation to tell Spring to execute your code using the `TaskExecutor`: +[IMPORTANT] +When you inject the `TaskExecutor`, you have to name the parameter `taskExecutor`. The application context may contain more than one bean that implements the `TaskExecutor` interface. If the parameter name does not match the name of the bean, Spring does not know which instance to inject. + +If you want to use annotations, you have to enable them before you can use them. Do this by adding the `@EnableAsync` annotation to your main application class, or any other `@Configuration` class: + +[source,java] +---- +import org.springframework.scheduling.annotation.EnableAsync; + +@SpringBootApplication +@EnableAsync +public class Application{ + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } +} +---- + +You can now use the `@Async` annotation to tell Spring to execute your code in a background thread: [source,java] ---- @@ -70,41 +91,70 @@ public class MyWorker { } ---- -The annotated version is more concise, but has some pitfalls you need to be aware of. +See the https://docs.spring.io/spring-framework/reference/integration/scheduling.html[Spring Documentation] for more information about task execution. -First, support for the `@Async` annotation must be explicitly enabled in the Spring application context. If you forget to do this, and you call `performTask()`, the method executes in the calling thread, and not in a background thread. +=== Caveats -Second, you cannot call the `performTask()` method from within the `MyWorker` class itself. This is because Spring by default uses proxies to process `@Async` annotations, and local method calls bypass the proxy. +Using annotations makes the code more concise. However, they come with some caveats you need to be aware of. -== Task Scheduling +First, if you forget to add `@EnableAsync` to your application, and you call an `@Async` method, it executes in the calling thread, not in a background thread. + +Second, you cannot call an `@Async` method from within the bean itself. This is because Spring by default uses proxies to process `@Async` annotations, and local method calls bypass the proxy. In the following example, `performTask()` is executed in a background thread, and `performAnotherTask()` in the calling thread: + +[source,java] +---- +@Component +public class MyWorker { + + @Async + public void performTask() { + System.out.println("Hello, I'm running inside thread " + Thread.currentThread()); + } -// TODO Continue here + public void performAnotherTask() { + performTask(); // This call runs in the calling thread + } +} +---- -Spring also has built in support for scheduling tasks. You can schedule jobs programmatically by injecting a `TaskScheduler` into your code, like this: +If you interact with `TaskExecutor` directly, you avoid this problem: [source,java] ---- -public class MyScheduler { +@Component +public class MyWorker { + private final TaskExecutor taskExecutor; + MyWorker(TaskExecutor taskExecutor) { + this.taskExecutor = taskExecutor; + } + + public void performTask() { + taskExecutor.execute(() -> { + System.out.println("Hello, I'm running inside thread " + Thread.currentThread()); + }); + } + + public void performAnotherTask() { + performTask(); // This call runs in a background thread + } } ---- -For this, it sets up a separate thread pool. You can interact with it through a `TaskScheduler`. +In this case, both `performTask()` and `performAnotherTask()` execute in a background thread. -== Spring Configuration +== Task Scheduling -Before you can schedule tasks, you have to enable scheduling and asynchronous execution. To do this, add the `@EnableAsync` and the `@EnableScheduling` annotations to your main application class: +Spring also has built in support for scheduling tasks through a `TaskScheduler`. You can interact with it either directly, or through annotations. In both cases, you have to enable it by adding the `@EnableScheduling` annotation to your main application class, or any other `@Configuration` class: [source,java] ---- -import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.annotation.EnableScheduling; @SpringBootApplication -@EnableAsync @EnableScheduling -public class Application implements AppShellConfigurator { +public class Application{ public static void main(String[] args) { SpringApplication.run(Application.class, args); @@ -112,13 +162,103 @@ public class Application implements AppShellConfigurator { } ---- -In addition to using the `TaskExecutor` and `TaskScheduler` interfaces, you can also use the `@Async` and `@Scheduled` annotations to execute and schedule tasks. You'll see examples of both later on this page. +When interacting with the `TaskScheduler` directly, you inject it into your code, and schedule wok with it. Here is an example class that uses the `TaskScheduler`: -[IMPORTANT] -Because both the task executor and the task scheduler implement the `TaskExecutor` interface, you have to use the names `taskExecutor` and `taskScheduler` in the constructor parameters. Otherwise, Spring does not know which instance to inject. +[source,java] +---- +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.ApplicationListener; +import org.springframework.scheduling.TaskScheduler; -See the https://docs.spring.io/spring-framework/reference/integration/scheduling.html[Spring Documentation] for more information about task execution. +@Component +public class MyScheduler implements ApplicationListener { + + private final TaskScheduler taskScheduler; + + public MyScheduler(TaskScheduler taskScheduler) { + this.taskScheduler = taskScheduler; + } + + @Override + public void onApplicationEvent(ApplicationReadyEvent event) { + taskScheduler.scheduleAtFixedRate(this::performTask, Duration.ofMinutes(5)); + } + + private void performTask() { + System.out.println("Hello, I'm running inside thread " + Thread.currentThread()); + } +} +---- + +This example starts to call `performTask()` every 5 minutes after the application has started up. + +You can achieve the same using the `@Scheduled` annotation, like this: + +[source,java] +---- +import org.springframework.scheduling.annotation.Scheduled; + +@Component +public class MyScheduler { + + @Scheduled(fixedRate = 5, timeUnit = TimeUnit.MINUTES) + public void performTask() { + System.out.println("Hello, I'm running inside thread " + Thread.currentThread()); + } +} +---- + +See the https://docs.spring.io/spring-framework/reference/integration/scheduling.html[Spring Documentation] for more information about task scheduling. + +=== Caveats + +Spring uses a separate thread pool for task scheduling. The tasks themselves are also executed in this thread pool. If you have a small number of short tasks, this is not a problem. However, if you have many tasks, or long-running tasks, you may run into problems. For instance, your scheduled jobs may stop running because the thread pool has become exhausted. + +To avoid problems, you should use the scheduling thread pool to schedule jobs, and then hand them over to the task execution thread pool for execution. You can combine the `@Async` and `@Scheduled` annotations, like this: + +[source,java] +---- +@Component +public class MyScheduler { + + @Scheduled(fixedRate = 5, timeUnit = TimeUnit.MINUTES) + @Async + public void performTask() { + System.out.println("Hello, I'm running inside thread " + Thread.currentThread()); + } +} +---- + +You can also interact with the `TaskScheduler` and `TaskExecutor` directly, like this: + +[source,java] +---- +@Component +public class MyScheduler implements ApplicationListener { + + private final TaskScheduler taskScheduler; + private final TaskExecutor taskExecutor; + + public MyScheduler(TaskScheduler taskScheduler, TaskExecutor taskExecutor) { + this.taskScheduler = taskScheduler; + this.taskExecutor = taskExecutor; + } + + @Override + public void onApplicationEvent(ApplicationReadyEvent event) { + taskScheduler.scheduleAtFixedRate(this::performTask, Duration.ofMinutes(5)); + } + + private void performTask() { + taskExecutor.execute(() -> { + System.out.println("Hello, I'm running inside thread " + Thread.currentThread()); + }); + } +} +---- == Building +// TODO Come up with a better heading, and maybe a short intro to this section. + section_outline::[] From cb648098626926873d6a39b22fc1bd11e6464895 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petter=20Holmstr=C3=B6m?= Date: Thu, 3 Oct 2024 11:46:23 +0300 Subject: [PATCH 04/30] Fix typo --- articles/building-apps/index.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/articles/building-apps/index.adoc b/articles/building-apps/index.adoc index 3fdf237b02..2a57673a26 100644 --- a/articles/building-apps/index.adoc +++ b/articles/building-apps/index.adoc @@ -7,7 +7,7 @@ section-nav: flat expanded // TODO Change order once there is more material -= Building Apps The Vaadin Way += Building Apps the Vaadin Way .Work in progress [IMPORTANT] From daa4b13ee9ce675f87c63d5a6c2ec70e000704f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petter=20Holmstr=C3=B6m?= Date: Thu, 3 Oct 2024 14:21:29 +0300 Subject: [PATCH 05/30] First draft of implementing jobs --- .../background-jobs/index.adoc | 12 +++--- .../background-jobs/jobs.adoc | 41 ++++++++++++++++--- 2 files changed, 42 insertions(+), 11 deletions(-) diff --git a/articles/building-apps/application-layer/background-jobs/index.adoc b/articles/building-apps/application-layer/background-jobs/index.adoc index 06145f29dd..504b93cdf0 100644 --- a/articles/building-apps/application-layer/background-jobs/index.adoc +++ b/articles/building-apps/application-layer/background-jobs/index.adoc @@ -171,11 +171,11 @@ import org.springframework.context.ApplicationListener; import org.springframework.scheduling.TaskScheduler; @Component -public class MyScheduler implements ApplicationListener { +class MyScheduler implements ApplicationListener { private final TaskScheduler taskScheduler; - public MyScheduler(TaskScheduler taskScheduler) { + MyScheduler(TaskScheduler taskScheduler) { this.taskScheduler = taskScheduler; } @@ -199,7 +199,7 @@ You can achieve the same using the `@Scheduled` annotation, like this: import org.springframework.scheduling.annotation.Scheduled; @Component -public class MyScheduler { +class MyScheduler { @Scheduled(fixedRate = 5, timeUnit = TimeUnit.MINUTES) public void performTask() { @@ -219,7 +219,7 @@ To avoid problems, you should use the scheduling thread pool to schedule jobs, a [source,java] ---- @Component -public class MyScheduler { +class MyScheduler { @Scheduled(fixedRate = 5, timeUnit = TimeUnit.MINUTES) @Async @@ -234,12 +234,12 @@ You can also interact with the `TaskScheduler` and `TaskExecutor` directly, like [source,java] ---- @Component -public class MyScheduler implements ApplicationListener { +class MyScheduler implements ApplicationListener { private final TaskScheduler taskScheduler; private final TaskExecutor taskExecutor; - public MyScheduler(TaskScheduler taskScheduler, TaskExecutor taskExecutor) { + MyScheduler(TaskScheduler taskScheduler, TaskExecutor taskExecutor) { this.taskScheduler = taskScheduler; this.taskExecutor = taskExecutor; } diff --git a/articles/building-apps/application-layer/background-jobs/jobs.adoc b/articles/building-apps/application-layer/background-jobs/jobs.adoc index efc35981a3..388f235775 100644 --- a/articles/building-apps/application-layer/background-jobs/jobs.adoc +++ b/articles/building-apps/application-layer/background-jobs/jobs.adoc @@ -10,7 +10,9 @@ When you implement a background job, you should decouple its implementation from For instance, you may want to run the job every time the application starts up. In this case, you may want to run it in the main thread, blocking the initialization of the rest of the application until the job is finished. You may also want to run the job in a background thread every day at midnight, or whenever a certain application event is published. -In code, a job is a Spring bean, annotated with the `@Component` annotation. It contains one or more methods, that when called, execute the job in the calling thread, like this: +image::images/job-and-triggers.png[A job with three triggers] + +In code, a job is a Spring bean, annotated with the `@Component` or `@Service` annotation. It contains one or more methods, that when called, execute the job in the calling thread, like this: [source,java] ---- @@ -25,6 +27,8 @@ public class MyBackgroundJob { } ---- +If the job is <> from within the same package, the class should be package private. Otherwise, it has to be public. + == Transactions If the job works on the database, it should manage its own transactions. Because a job is a Spring bean, you can use either declarative, or programmatic transaction management. Here is the earlier example, with declarative transactions: @@ -49,12 +53,39 @@ This guarantees that the job runs inside a new transaction, regardless of how it == Security -Unlike <>, background jobs should _not_ use method security. The reason is that Spring Security uses the `SecurityContext` to access information about the current user. This context is typically thread local, which means it is not available in a background thread. Therefore, whenever the job is executed by a background thread, Spring would deny access. +Unlike <<../application-services#,application services>>, background jobs should _not_ use method security. The reason is that Spring Security uses the `SecurityContext` to access information about the current user. This context is typically thread local, which means it is not available in a background thread. Therefore, whenever the job is executed by a background thread, Spring would deny access. + +If the background job needs information about the current user, this information should be passed to it by the <>, as an immutable method parameter. == Batch Jobs -// TODO Write me (copy from my blog post) +If you are writing a batch job that processes multiple inputs, you should consider implementing two versions of it: one that processes all applicable inputs, and another that processes a given set of inputs. For example, a batch job that generates invoices for shipped orders could look like this: + +[source,java] +---- +@Component +public class InvoiceCreationJob { + + @Transactional(propagation = Propagation.REQUIRES_NEW) + public void createInvoicesForOrders(Collection orders) { + ... + } + + @Transactional(propagation = Propagation.REQUIRES_NEW) + public void createInvoicesForAllApplicableOrders() { + ... + } +} +---- + +In this example, the first method creates invoices for the orders whose ID:s have been passed as parameters. The second method creates invoices for all orders that have been shipped and not yet invoiced. + +Implementing batch jobs like this does not require much effort if done from the start, but allows for flexibility that may be useful. Continuing on the invoice generation example, you may discover a bug in production. This bug has caused some orders to have bad data in the database. As a result, the batch job has not been able to generate invoices for them. Fixing the bug is easy, but your users do not want to wait for the next batch run to occur. Instead, as a part of the fix, you can add a button to the user interface that allows a user to trigger invoice generation for an individual order. + +== Idempotent Jobs + +Whenever you build a background job that updates, or generates data, you should consider making the job _idempotent_. An idempotent job leaves the database in the same state regardless of how many times it has been executed on the same input. -== Simultaneous Executions +For example, a job that generates invoices for shipped orders should always check that no invoice already exists before it generates a new one. Otherwise, some customers may end up getting multiple invoices because of an error somewhere. -// TODO Write me (copy from my blog post) +How to make a job idempotent depends on the job itself. It is therefore outside the scope of this documentation page. From ed473f1dc285f7383cc1671cd996c9d7cb7a28e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petter=20Holmstr=C3=B6m?= Date: Thu, 3 Oct 2024 17:22:33 +0300 Subject: [PATCH 06/30] WIP --- .../background-jobs/concurrency.adoc | 9 + .../images/job-and-triggers.png | Bin 0 -> 23622 bytes .../background-jobs/triggers.adoc | 198 +++++++++++------- .../background-jobs/ui-interaction.adoc | 9 - .../application-layer/domain-primitives.adoc | 2 +- .../application-layer/persistence/index.adoc | 2 +- .../presentation-layer/index.adoc | 5 + .../presentation-layer/server-push/index.adoc | 9 + 8 files changed, 144 insertions(+), 90 deletions(-) create mode 100644 articles/building-apps/application-layer/background-jobs/concurrency.adoc create mode 100644 articles/building-apps/application-layer/background-jobs/images/job-and-triggers.png delete mode 100644 articles/building-apps/application-layer/background-jobs/ui-interaction.adoc create mode 100644 articles/building-apps/presentation-layer/index.adoc create mode 100644 articles/building-apps/presentation-layer/server-push/index.adoc diff --git a/articles/building-apps/application-layer/background-jobs/concurrency.adoc b/articles/building-apps/application-layer/background-jobs/concurrency.adoc new file mode 100644 index 0000000000..eb97fd98ff --- /dev/null +++ b/articles/building-apps/application-layer/background-jobs/concurrency.adoc @@ -0,0 +1,9 @@ +--- +title: Concurrent Jobs +description: How to handle concurrent job executions. +order: 30 +--- + += Concurrent Jobs + +// TODO Write about running the same job concurrently inside the same VM and on different VMs (but do this after you have written about server push in the presentation layer) diff --git a/articles/building-apps/application-layer/background-jobs/images/job-and-triggers.png b/articles/building-apps/application-layer/background-jobs/images/job-and-triggers.png new file mode 100644 index 0000000000000000000000000000000000000000..a4448abc82ea3f4f9f8d33f54c2e59f22da3e3a1 GIT binary patch literal 23622 zcmeFZ2T)X9n=ZNuYAZ-i5(Pza5)dRy5F{heLkIR&f#_xshur& zUSyp?Oie_Xw6;EFel0!4DZ5wR`L#jH!3@Izdj|e{2L5{n{twK6Tpl}uBqt~5j`K*_BW0fh>(M(pg+uL? z;~JNjem`z9Pw#Zetwi^13ETdZx^wrg|7|=-E}fhV&slow8|B>2&W^9Gg_G~Cr)0Z| zG<4D)`)i}YM%p5=5R{<>dx_XlMD<>vH7yfs2=v8=cRwGmcE&j2LkOAc*YRm03o-+m zuwFjCQh0^V3qL*x4;S9Thb)_-ZX!IRnWZII8J~%YhJ3j|E5(%F1kB{B$_4V?3=i0^ zcvUtC`W26WeGep&P7G5hOJloPKgMkfK}DBze;~X{Soi$!e+?Ygp~3Hp?oumiyD#P> zH1+q4L@|iv95f#^MLBB8DsHA~=Q9AjtCS<(C;_cjBeF zu0IZLZjFbGUTjnY64B4|M<vRCM#NV%+lX4vsYi`gXhb zERrvR$Yhs8`X!>7Z0J_8qEe6GD|@fNgAEM>Q+K5su!e)FK;4#C<`2lp=1nBsWrWQ5 zWovxyQ9w``R*wuXIMb|`j9O9Wfe5i~$ynV&k|X`NAboY3Dg+4@f%B{U+z*jW4p|@E z`P(u%(u2SzgvtVNCrao@!joOa}iZ`&h<_|Fa~hDE1s-9x_` zmtVLHS$-f?Y^{GaRO~Tq9_Tzr6cY3lO^{W)F4DvM3}Hn_p4k&g$AaW@ZodhHYHR%3bB@5Z+k^YS(sTRk#3sHWk&u< z>g3N?KJ*RR;f29OH1(cdcT1WR5tc{_{ zzr8<{_G&;^Ogpa$lq@=&HiKQ(n|B@mm%h+pq#S4IQe?iIPL*yEn=!Jn{}wkKsoR=l zFoh(`92m?Ex?(8f&Bc;zz&C$Sb>cpvZ*{1iZG`Iu(~_WosW_W0b|{c?w#A@|?Gp~w#MMLlAj z*;?e#?eZ8uGxwIGIjJ)Hv@rwXc4R_eNzVZO936AOQl?V8_4-e~W4)^6pR^r}c-Cu$ zeyQ8j!R7rtC9iNsM_q{EmkuErsw}ha;Ae!)Zl)5e+*DXCgMUmWoXv|EuN!Fyx-G>6hZT_)h&d&U_*iUn-{%qRjm{9bDCClo~FZ&DC z+2(T<1vv|=?@HeT+o1mhd9BO^X|X*XoEal{gY?~Y!F!h+5pR0R8Qm}vf9RpuG=h}2uD?uKe_0iYr!y+@(e7~K0imXn|8!Qog!}BbchMhXVTM^A z94-c)$A%m|EvqgiL%_SC$0KZ3@SqGa+}^d@7#MJ|-+!VrQ{gyVl4kI<$SbMO;R1Ej zS`20Px5T}TNJo0s9|45QlP5CzSO{ue0r7w%KeudxWIPvste3W`7yY9uP!xi^xCuWslMs3T~`UA!XW7y4|3M<&|dOB8G70i!cY89@V zyONX5ndTcNmKuEnKP#yFYubWf9!LF~N!n;h*=X@nSm-QWQp^o_K(|s$y7)qn`Uy!^J$H$Y6zwp( z#c}fpRYU4wDmCU33Ol=3UMhZ;9VP}Gq-#B}sq_cdp}?IRQ6Z}&YHFE5py7J?B|*&Xfz>8LeDcvSPKgT=TR~w$Iw*>c>5{Hn(d_a?=C2N7 za{4g`n-;Yp?&BpM905ln7KJc1ht7hB{ZCoe{#(X$uYp5 zMbx|aFg}8Z@fNpz8ZjUgPvw3KXS49<{h4bH9ys7 z*!nI{F)FZqGi$OOb=2-@V-22sd_H?dMKqHnTp%#S%8KnOGEY46$_LY;*9j4{{@!X% zM_z@RYKoEv2_JJk-p9FnLdISorNjGT|Kg7g0!Pyer(FWWmomVWtCC}yL2#LcN5O&$P zcx<;JKRT3+hBG56fUpL;Cl*Dw5&V5*O9q__O z=(AnNN8ow!GY0Tqeqs6EsjDqB9PLFzVn$mtYTLAR`2Fn-az+i?FMS4;>wYwJOj=Oy z4G_Q5$&J#squFQ3^fZDNN?-lOIlh12GVX$EM9V?brgTEMQUoPb1hyD-aUhydZ!!m@ zAj`ahST9yyiBAQ@cyHEAG!zQ)^*&&xI(<@v zcn@WS8{>1Yj+9&PRV;Gcx^zx?>51!lVV3`hG zEv}Jy59LWogSg%jp=e1JFKbk`^BsJK%7<3>6zrGzf8yt$ZCLvRZb95yxQu~d1w`!8 z&H*ki8v|el5(PLoyX3Rb%y&J0Gte6n=q2NP`9)K6{m;)bmR7*iFNH-y1!}lU6|%TE z$KG8~5?1p&nwMuotWbcQqxH>QTB@dpg+u&kW`3Iofvn_mN+8Wpacw?&5-<}^O96eg z0hZ7njINm-PI);J;{Ja7KyF@1d>aJypA0O5tIET>`5So6+)>oa1%)N5v}Rs9^iR+- z7^iw*`WS{;e_}EtnCXu_FXp|=yVcc}X0WiZkcI9I9*vP1Ky`1wS^7GhL6h#_AJ;Y?f}Km8pBF&Exo;#Rl( zA%_E-aoKf`!@U?;>>-%p7v_9;@!o4Am6NBmL~*rTAxw-s;j?2j(-$3~Kap;EMHJlf?`IDu8+>bmOD4Bafm5Mpr?1*R zWr2=KmTy)dIDRfZy~NDfq}fNbtP7k-?_MDOlJaZy9VxgY+S86-MVwYAAqen~c8(y*aA`I0Pqvdf=}!ehDOuda7D4lc)em3EY955LMGqdP%gpQt1U47Aj62KEKHy- z)MYb`EC7L$CLv89LEiYXhR$;ACeOb6U#(A-@YI=AQO+{)Jlv1Fa?) zwdpRmgDA)!6B}!Eu=Uw5TXX10s*-NeF47M3;-{WSC&zI7l%CeA8RnXjjP}i^)`ED= zPR`Z@f|u1wANw8*ukKm!UZXGMy-(e@jZLJ6j^A-Tk(*b1Lq5dU^F=L;wp`UHOgVIO z^R%hm*;Bl~$Y1|+Q{X}MRj-74Hsf4#86EF|9+Aqt`(5+Y57czlw5Wzl%wn>Z81$sl zW<@F+B{*p@OZyVuNxv@2ZBBS}^KlVHF#p(K-W2qbB8v3^d-=r`wv2adDdJvB+BZIP z!mFv2{!p4U&l(h3exhssCQ_z3Fqo7ZUPXCNQ%qmj-841l19fhqlKAJicJ$GTeVtaw zc5L?Cjq8p>hYfxuDC)s6F7w>lcAdQotx5T|<5;JLlE>o z)p&WgVqxKzNsS916H7-wH-Z~Z3HI@_1a-U_2NWOx7nSt*{e5d~W3h5wm|cNEU$JJw zK}iq?&M!ffp@?Bk;h96xeFCfBuE8ymLC>zu6&~XqO305u$+@jlT)bF zs!#k9*%5niXoR%V{>6W?d|&8O7sGRbJ#k)t(lVbXwzeOUcGDvxhUzyQhsz`iUe5aC zk++idK8Iz5!yaZCgM~pIIIl=H;Hp-tOs&J8{v^TCyvd23?X|H$aZWK_S-@x73r6r#( zeNSKU7I(0Z@hvSu$g3Jj#$H+w`DPYi984Ai6Fhk%M7ebJiZ*=# zdtF=?`Hw^@tm9y z#fM6Sg1x(7X=LA3iIn>o+lc2?Qrl+eu3rX|PU^T5csLWmFfcS*oqVtXPchhuuS(#RiIis<1xOy=HNRbyaFk97 z5opK}>UDu2mPUH2#?|7+4?F6B--XW91V~s|Qf@qTp{JF_x_s72-H7YNe*Kn5xsmr4 z9ot_<4@lFkP}-)swYPfo^|y8a$YOmV8u1C z@#}l0%ZR5SvDOA@;vUM4t<;YT-lf>v$M{S{z&#!Fn?rUKCbYe!vqyoaB2w`)S-InL zp_P)B`>}rMH|%Va$!QS#Y`tP_Y=oveKgdL#QY_S&UhPEh2)uCGO6$>ABmYUAn}HII z5zHI#HjZhZh8nG1T=-p>uQ4!SQ12ckTZvWzbI76v5#Pik*S_ojSFh=phs3v%l(flD zk;eM7ir(r{21P}Y<390y|JBK^3Jb_kuu*%PPPa-c4c2us{-}wsewE9j)5hCcO5(b9krUWn~AN#Y5j6BT0 zZ1*aV#$Tk~GF=mBuV$d#9O!9=XVn?!4IJ?)g()`pZki81&*z-tXjjY|*pS{?rNS7Y zR|#ADAJ1q>k2f3NdGh%2Zw?BGpfCZ^CE>GX*tB*O85yZZebi;)Z;_Gb6S=J0eU;Oy zU=V~Izec824~Am4x%|qDf1k`spuEusp3B*Z)mJlK^z80>E$;w?e@Bddi9tD(mHv0_@;XJw{%WGsbdTtSf#O`f*P=>|PVBv4G+t9&!EX=xFT+W(J<7u1#4<6GlQAK5A-Cw zgqSWa$p-Z_q!M%o z*U!vyLcKAN5sTBUkKW*(7<6h-I?(62MeqS*<@3@a+ zE35%qQL7a+ODU`z!+aWqOFeSSw`>lyu)noH{!>x5zY`l9D|{J{I`4sFCX_v3?{@ea zkbL*Rh!8@E0^WhX=_Po|dk<6yS8dLjqE%@^s5JltbytOkip)#3Zn2UVRkhoq2XBK$ zVgZ2Sw$me~Re1B6=ujRb5^WARCvWh4X(Cm#)`(Vz`gq0g3UA3+ZyD*{C4}T;LDJ`z z<{7NL5i%Re!Th87+f?G@^aq5w_=n&el2qxy{d-@?Am~(4j_@<}km6`hE(q#2rf}qq zaj}ot{=AQ)k7U|ukS@MNq@3NYhA6Jb7Cp~0ztZavK)o@*ODHck_sV#hmuE&o^Fl|c;vjoR;%l90(7qj!XE-}ab1IXT${h}Db;V%zf?6Vs$l>2SxTBU9+& zJwCvX5Nw)ec=*%g1!cnvqiE9t*oj_+;L_DJO&(mDaEKMOxcvzjrhRJkL-BxJy9rZi zF|mW>o<)E{1S-%__1uI4x~PO9hz{RKP|uN^B)ofZak1rHI6tL5^m|5|(X-GK=Ll5s zGe8^wh?>8cLr=D9NBh{cJI$nEtU5xBs;*k`jy5L%s2FC40?w@mL5X>3M|IWLGFW8V z#_*fGZa2=s3ZehuG`tYC-(f~PIT#bKs0NFWmmW`3gLdQ26X^*+HabPD?j5Vse#JP{ zk5YWi6!aW-u8~fFmbh?u9&s(e96mq#`a%q_mZ98%eig40ca>B=zjj-SGJMO2*XxNR z_~`J(rh_~uq!KT1ihaU^MJ8M$nS7JF==INm+`PzslY#_qm*u?4w;`mFVG7Xcxy@C` zu~1-}A(J~h&(_ISYTf|ivUmD?{GX2hzD)M~vhqC=+-1-2ohSPxWE5uUzXWm35|iN0 z>&b;Fk~bFVv%htNS(bs&5y~)z2iJc9|NR*Rw3s$m_f@xwEwt^yqFVb!LkGGcJ{9C4wLisEtvbbQWz8h+})ow9^*9YpNxa-I(W zhHJ04{|asOuQulGKPjKj%PQA|(_~#gXj3I~vPyGMI`Jde*k$!vO zeoTsVppOM^!~7-HP+`d_EF*T-)UbNvhU5kLUYOZsC0fiOEk&Y6V(T zw=Yv`8I%x5$>MI^^%yRoXv3yTOe_+yXHE+dTS$ z;Z-eL-ww*Mu_qO8mI8(p^(ISdzGikW4y+keoJlchwQ?7>+)iqD(bgcO`u8OS)O(di z;v$LtLv5^vYE|8f73c}HOmOQoi{(?0pWT}$E8>Y))O**(YUs)xiSPybg~r8RPq6Y< zx#Y77_iko!|J-A&KP(o(T%92+%01?pRE-fzud@-XS^s z*w*ajc!QSI3rc69NvX+vfe)NrLNM8FwHU4Hcq~JU+t&*@@%LiPLPjwI?4fhgHVO{*kFgvS+D8AL z#9|XodHIF@E>ppf>KQiZ4}okPVya)^7h>2 z7kcaK&W%n#0{PbrZ7%VCUC>DMqSIl}lC$|Yf(*-A(4$ba(x#@|rkUTD3#l&-F$C1N zaAE2HX4wU3@INQ^h+>C~aR_X`hpEq~O%!0jEVY4AH1^vYG8jmc4(#`s2*IrX%W=cE z5iB9%sS@4=?q_3;r5ZSN4~*g>Aa^r})IQvSS1sG1rFPUm&T@aNBLc?N@(*aOo=msh zrE#krSaiYHIG>{M7$gI_KEnUX-4jB+PeJ%ijl)uj21;?#j<{eQ9wrv+InlbMCP>$aZT>%qk6j{^`fyz6v@`&aJSk>dRvig|d#L+J`r`si4hV4vbg+;UKi$f* zpWm%hNj81<5I!)*_E)5k|M!6t)3p{S)d?I&w?~YGY|pT#QLXt#A{j1K+$Nr6dMEzO zjPo4=*Pm7C5=UHV2Z2SpOu+8ao;(|rk@}XfwP3W|x+ALtwUPn<8;c9kBM^;vK;ETj zH^9x~lp5}|DP@a7WgmlZNJ2nLnc@)IG&oa=6)84f2_3o@4P_*oUK$$Auc(E9l!%zVK7 zEG|+m&tFxaPymJC+#hln(+|6?H65Ug5`~@^eC4#emG1iQDI2HyODqbL$qh(W! zZ2c7LW1XeSkrNUQANYZ6iUf<~N>-MY9~C-r@3sTn^hL8#^S5A0`1R*sD5&r0T%(%N z+IQ29nBfL}xq#1x;Qtm^;?v#kdxJZG&BIUo&&YkK!o`52JOAXV138$I?nlUL zt@M%ltsW?Q3;*`rYTPfOUn)h!38h!yDNlY$H5CnTTk81h zRo^xe9{84#l%VrqH{R*wD!6Gz5?I@gH(mKtwT`-SaZqGcAv1UL24FF&;-q@x*4MF5gfAuo8x~EMIzs(NdLvYMqXb z7lyr!QELG8b|^~P|MQhpz!=ZDiaObz;8a0{5b^?~pP>R^MNCDcdGS+BjXYNt{wx8c z-L6i;h)`t9G)$hHO$q(cxH#dLDxo=C<2m-`O?;>eJh9$zn3>h;eL`sfdAMMLMBcmT z;|=DQ_5yv#c-|Ex8K?aFR7u}E=CLjOAVjQ+-ok*Z*#eVU=WWOdJ10k=5{YXnTg`nGv4y5Ob>`hJK$tPAp4)g zED_!-CZB(?fl%KXe{3N3<#h1u>T;)m%&%IOSM(<=EvK9P4o6L;!8cCZ1%e)FOc5cn zV@U`gz4yycAUS~gLwXZ+ZB=eDKfDUbDTFB+ z{WSoKWIK64Z!mBZ&W1@92$en^WofFUA3bmm@;#PhX$lxEd?&goynlct#kw7T=LnI) zEAs)1RWJicSlc_=+ne7{MqP<^N#Kfzu4v`pd;2C2pY7YxSZE0M>3(Ps_vv1K&Cfur zxLu1oUX04eHjL_AaL72T=mXMk0 zs!7}C7TmO5MhxXX1H7{~4M{Mdw3h^EZO`~y-T<1Aj@jtl&SA^Z-tIc>nb(Z6f%=Kc z@sjMJ-*IcaY)W%%xOlme-)?(ki=7k;ULQ4OXhTq!QBzvTk?%5$kUgzA0zF~-DR*X4YwQ!Y+ zUUqKT5;!wE?npm7B5Yj|LJ>n3b;SzRzZmNd-BDlJyFZ<28g>uN?%ANn61(hSv)8z+ z|6-k(X5$nru}cv)$Vf6DG8;)_?2ot|v zqTCI%2y~g*nm%eU5Kt#@oL2_jmI>V2l-W|}-sx)zwZ?Ebas~h9AS!%s^GH9p^)ezJ zY`In_DK+(U`4jRbcDo(q&9;;ZbMuSXKdG0DEGgAS6Z37(4nBqpO$ThnbYILRH9bS^y=2;ZJ|#N%kO$NS4D0z z>}cLt9>~SkKej6tLKVesj{D8Doi<*+0~IR7IzV*Vi_NdKZY^bgRE}xy87g-QUMb1d zzQI@zq7#ik#@po*qpB&UGpl%pCcrvFc4Q~MZ;`fafAUQaXsrx|8(0G>~euCZxbjD z@p`3oe?SndEt6FT{VXYTOLoJ^?*vSn?r|iX=Y{A6i5_ZMTHBb954@QBPq4!+Z)Gah zJ9H(dJMmK!uS6A3Tl@;EPouXktvyyABk{KnaF1O(2rX;O7ln9$X$0cnzm?kbxA*c| zk6yL!W6lo5#;!h?=GNN^`s;b9XWvehG2iJ3$JFJpkWI&dD6+iN=~LNsu4eJzU6wdr z>vY`aELg`*9e0Zj#99P7&1|8Prw!**4KD_(e=#3ub0#f3L6QY|A$xsdRWiAap^lTYC|=(|3Q1z#0s8zXSSeGU_I}?)k={KUVFE;D2Dyvyu1~9x*FBdN4sQk@?6>o^7alm ziSc|a@o@3h6J&UHQyerEG6=WRMzc6D1b(vnp z2`(qgCY8ZKA9c6}9V{jp8K5SxcA-4F8Ew&%k?NM@>Yzhq`XL1!jlng{o#qB|tYT?K zxcy^i#S7>dXUrCuLkw-d+CXSoR&cm+brZ(bG!DBZa2nJ-XR-qNSNE^;MS?wQ%ZN=>(rp6MLFF#kCR$aj$I1m1j|6pZRU3}FA!R{IxMsE zLwFg44~Bk0OTC$yL@)ONV?&lPiAz?B?a*ny32IY7!(G4C&Oj;Xr$?f15XRSJe1Ug* zm^$B=tqfV5h#|DJ|MV4LOxGDfoPh&^UZl9epe29>LxSG`vLgpXlOR=56$jCaTC4+t z`4x2wAYu)#@zNHCpt|gB3Ug7b+e2+uK~pe^Gyf^Y0HA1(IyLp|5w+*)*j=kc^Bd>$ z?zhkvzC{(d_Kh^u-Zp0RWOU6PnnOl?+nEE5)UCNAY}V8Lhw#kj1;$q5UJt>&hCp7p z^cEJ*j8ihGwTZoIodZlE-}4WCX3TFCAqu<)Qv5G&)dAU6e=v27J;*?aW*+LS)eIqR zc*r}bF;Q>wCWSV~stTQJ^`(YuFS=`xFiX|ig%-<*bCg{XX5+jet$i^M2~0GFn8IV7wB{x z!<16Ljf~iiVi7w4Q$x&agSc9r{4rIbDK2XR#K)QB6&*eCSvm8Lcu(trGA?*lcYk#?4r(HJz|3_HjaI2%;oaZkqsbd{QQty1l620 z?FeGIsGS%&-uB;*^a&rhYuf0dwpXb;0tYn2ME9vnEu!q*1$n^(YxTgYWY&F+qZ9_nTlrX+v~i?m?hsW4Ob-aj%kw<)^oJe_y}n$W^KM@H zZI-Gn`)n{WUbAO2K0UP2pgfDqT$kn@>PYE3jc)(8HW8;7_~;i7;coX1ce}AVPurDv zm_nBEt%4A?6V4Y3O8#1zrmtOT$>uT6`h1j_7-UjV61)(RcUU>1+#sXit}awaDcn2dVEDcfFw%m|X|P3>R_4N|5diSiZF1B)R=nUoE|{PId-W4TwUDd6>; z(;Z0?kVEo19|ldO^`2yRn6BjxE;r56QxjxXq-iuYCTL&S3kc47^$0xX40hb!;meNE zP`_%T`SCZ*ew$A|PV;KfD>gUoWVpW1NzcIIn7+t7yy9_?X>wjz7s`9ZE3N!@aL8*L z-P2wAXW5<4(&t8drrxpQngjAT?FSJgOe+CS>MbidT3UoR%GnGjXcS`SUy|9>X{Lym zFS(D3RGWqgbsNbv-xcp>Q1k76x7RBPL}UXWKYkQ;&iQ~_Fb_fU!~9aet}DSM#vvo` zw~;D!pGWb0#h-+X2&@pU8VPfzFl@`(H=x1Vr%pqnaRpp-8(c(4cf^5+Ja+Ja}}1NuAx0Kzk zTBVa?KabglFbd7{6(r0C@YWpQ5N(qHkgCY(ttu+4G?MlK?1DkJvjh~Qw0n{xeydFj z_;MZ1zeI3GbEV7BWd8V>D#_516meIlk1A6`4-(aiH$jS{L1x4OENtnP_y+dw zLFLT-A+)OEb(%$5>IbL;2eD!o5047>Nia`XOOWYaQTk57lSrNGO+C+(tn{)XA**7s zH&elx$2-e|GrG_~UrK0*cdB&Ko3q(gWCxJA$IAaoP8XRS$)zWh%2PTP$pt4T+k2kp z(3R_laxY%kEjf|+=B!@X&0t$&GEO>TyW;WZ5_$%({Tstip}J?Uz1JZ|@;=nYII$l5-;L-+z{IdS!9(iZ!T2pGHr z6!EC0qua)p3BMMY#}w~wq9bEN-y)_kA6?Jvb6K%ixp07Q{?FKHx`#V^Zv(1>tDJuL zVpjwPeniTu$!#`nucUh?H5-*i4XVl}x4KcDSF=4N%_Z^d`PiTNYFSW@cD8abKY%mk zuIEBIp@Kum>fMO^36uxFapY zXN9S7<#zF%GO;n=Y=ytP3HW`C&gna>uqOT;g`(#ft|bgKf+G@LgdW>sg}pn&!RYcG`)aWU<_HupMvSirZ*+8_AdB z2z2mc3L)=y=i-_X5iBFnZmL(t$Ey(XP?q#&Vyec zgqMe6ryrCyZ_!3^C7b2Ns_ofWZIve!3=M5M^*?F87Nmy5EdmJ zzDC(6~UUG$RG`cT(mCv&(q~vs*P4?F$~AOuf7m9y@Bg z(hIOs7AoR`y)Xa^>h_PqH!or?Zj4wN39B7D{e!Zlx_dFOlKqZc8xMurq<_$XzXB#^^?$2!{ zwaE7B5Xz7CsPhfwF7|&nn2yD+l-(cv50QGfABo^tP`O0Vk=cc9oNoSc4drMxSBMnQ z$ZK(MaM%LH&fhYad9WPZJ@)_`ZqPseMZ1pyh-Abjf_8cafTjhP2|yAnWF+%p{}iyq zt=wkp0x;*rH#kz+&M|UETT_Kl&=MEL%Bgh|M|jAyA@BeW2SGn8+uS%gZsOCC;c`#Q zx6I)RcUj3>arI&lL=*s$c+hI_dGV-7(L?b#0PXAw;V96uvGYc8^^-WJ!IA3kU#ftZ z8KSOk|NErveH_vIpc@vUxo`IawUsuC(Ygs8Kf`s4+wR(o_Q#dnukVlRgmC@A4FrKS zauJV<+I*rq;t7s@IX`v<92<0Q$#62A0OInCK$!zU1vW!%nJI4omyCI-@aU`xCq45h!T2n3<7<6mym@|hDfIlT{3*rxAWnh2 z4TSqS)8}`{2*n{;mM?c5{~kRDZuG>FA*AQ`!k+h#Rgm-V0bAf^Ly!;z6%M2O$@WlN z=EQA>q_q-==djZnb z#b(6PqgpfCE|IJ{e3HXkgmPNvJ!!T1qUvO^F38IrQL} zXMo$zbSW0YlRM=8Z*y@_IjhBTFx-@dKa$E;wH^ zW=5+B7@}(K%pX@(@?TM43~uwEW4ds&_+RR+!QK9Y#nkN-a+g3YZ}J_dk+K^Nm~yA& zydj6V(e)fcFOLe~#~;@Wj4m!5eh{{0PEJXY0zqX48d$UHG{4!%t)ru(hDw2I$yhm9 zg0=4c(TDFY)|g#Toc{(OILF_CaXhfO*QX8^i45!CH2o;;XcJg!ZJC6-n)@G+8A}5l z(M*AVK%qWHj|va!B?V$qToM0T_p0aUWCLYc(nd7enC5#lPIzVuSo<%1AC);)_KpWe zC$|WT!eNioqyI&TMW5x72QSnBbviiRmKHnMHmks%i3E`3Gd}Yk%Dag~TY+YpkbcL*ORsG)&YRw9v=a-0(kLUt#lVB4$Tx5TXfp` zim2_+l)Aswgu9tHl%u~LL>#?i-2)LSh9wj4;v1Wd%-R1;nZGEU>@)H@DDzjXC#Ci& zneO~qAw*7M=06_~I+%G#jI!mD(X|0mHO(flj0_!d9P+>L*ID@ZhINO$DBFVP8k0eH zK)Gcip`!MqD&zI~ip7uUH(jht#fR!jvZW+4|DwwSgZx#o*pTN1C9u*rC3d!rM*{2P z2n;E8LhxlKfxxN3+lEZ3_*p;vz-)=$y#CCA$2pAWA2t5HAm~Pj;&%T33V|%A3>F)x zweiu7cmza>|8O^;dPnfnxv!{cmy{B?j!&m;jT5sx@9XQU)wu^w;#4{i6DS!PsZNOc zke(hYgD3dr?}JgVUk_F!19hf(;Sg5*pNA*u1p>kPZFvXLso*SJhTfw{Ok{;Ss0#zI z=*4sK(^9))#G;la_;5fM-!TJ2Dwpqh8sV}ks1B4tt>+!e=e>?hj)}+J^K6~4wG{{G zK_Cn`%iPkFK^3Kit8{>2s9nG%JHM8efaGiV(MTs)3^)ldFV6b*!uet+PfR0(K#>>$ zDER|@$p+Z%{j<#X#&C%yCtwxJ30viKaiXKoaTjEDq<25mym~n%;SXe3z;awnxdQtC zmsDSh1kua(N;f;(QSpIp=*9ED4};WJ=g#MvuVn!X2H*x-xSo`$)C?(nurEx+Bff-F zsD_peuUP?h7WkHSI46gA zfm_}h>U<2Ff_i&ot#5FcJY=WNmz2xO0e~kG>WbOe$ZaS#@VZu#{o?+%@@1z;=v@8w{hv+0gNfRWelu8uAMz=EP z%h^}~RCE{$NZ~vlq2A3F2<8HcRS@0D4x|2v0V%($3Pg;niG%cjHspgAsK*fS5t4%Q z30DzYe#aTy4EY4`A_Md^sGT4M;=2<7vy)8*rBrZGi-=$q6BVTf z0+Lm;&0`cV)hd};u`R0S-gdZKxN{aC%X z<5-1_nlX_(@zxaURA}*3P2%}$D`?;tMsvTJ@Ni6`nU9e*fu(m!A$>jn_8+x9UW0B z-4EN%666#>9H0@E0fS!L%QUvL^=Y`lSKI-jP@}xhX9*@Q{(^Yg{`hPeo6?xI%2&)a zL=o~QQpe-JPhF-!h`RebR;z#w6g4l;mHBYkRW5^ zjYLkIxA7@=dO(5!ISN!xZIf}q*}G9lb093~$>MUFL?UxxRY;x`K5vWi81cVWFv-qH?bkPi(`ANOkOCsuTc+7mKNed&-rE4Eak8Ol!m= z#1n%;`;r~*wwNG>9>Si|j{^w^h;ss6t2IE96S-zfneV&rJl%YXtHJ~iAmjI=dmNJ` z?bMItPn7)$DSBoX$-RJ{zmQzrivsg9zQNjZo!y1;7hpVX(S#Oz)7!j_PT)eoOKQ^r3+KMph8H~l!tw3 zZ@2#Jy(!^k94bpFnVOSNo%q_79X}3MI~H!xa~eC=sgOLPRrrSU*&9%&Sm`BO9uuk)LAK}vs^;2%X|d{fw$dyz<7umt zzp~_O`$89+ZgN60m(tbN)>!ACVk^Rf&Q0b!nAVW(Y|F$|0c&r>8y8n^f|YnT4$2_F z^>;fXqxPK<;RS!YolMeTPUp$`s;&7J268g018Uv)4WLg^di0>tc1@DoovksF4?jN7 z*uDl0<6$eapVA7u=RdI4%Y7X=*ZDlgFL;fE)JvX_-rqGF_Ch{Pqi0CZ|4kftpNYH6 zq#jRk{DNvatOJx)9xXoV#l7+*fC=T;l|$LS$(ODv7REEWt^yS^p`T?c6DmNwC>4A< zi2Mf?!E;mfair=yn1|2Cgwa?F$MD3*95X*MG+Q_>bi|eJ{}M0LM-` z+NE?Fh@WvFe}d`-azVjudzxrYN`k#5%35Knb()1)pzuqha$XY?$(=1#>)_Hn z!RYZ#FYh*r%eUu|uKQ4>CQlklNZGBw;M%S+53{d&1dnct5AE!66^5z!F?$uBU@E-% zhp6C)CO|8`?ggCo0->wdzL=UDacczt#h<=22>V9E<2A3ADRzmSoLNy{NAD7~!ZSJ( zVnvAyF!GGutmB&rcCNX8YexkpHpoX0Q`E|HHMgpS_DW)5ZzV*-&7DC(s^7$DVrsO$ zr8_8Y(I!qzx~fo`=~E%_T(>hd@kZ zknRXCW+5kzhy~$s!80~kQsnsBmRI%Z7GRn`&oAQ{Y5>c?sYv(kV6l_IFha8@87R-x#72xTDuzC-5p31Wj-d#}TJ4>Bp7+p|+woZb0O*9B)z;`XvlV)tf1-y8JA* zfD7>;r}Y~=9c5V-@3n&65njb7n{`ZPsF^*)_uNk8`YIO>)>2065(1*RjQvW zn6K-(ej6p1muyJ{U})gsdxw(@W{(ffu+zRpdXX7fbkQPO2HYj4@D$`PRUyU|s#;zH zJwfW2a$Pd&%wJ}{lkxkT)9C=@ppiBpHfHO!4{7EM5C(7Sz`|)bsgs}oKht;rxMMzW zA=stbqs4{)E}pTzw_Sc2ClkYg1;8V!YIT{qk`@R*^w){Mzv`4qQxgZztZ>HcxKnk& zeoFVMu0=*GLlp#nYlTVp`mKx8e_*jJW}j5F_Q#ktpQg4aoV~vA=bc-u(H}o=`k*m= zz0maT?9e@Gg=gh~^J${t;iCQ4-z4`R4%>IazTL_^ zCzzXe0B6xGVs)CDfvb1tg!>9D^eeWX%`w;R>q>uJo#k@BZv$t2l4Ei<_S-a^IQrY= z|Kh7cieW64p#A8N3+4Qdeth!$M$B80MlX{mde+-*0y_V?Obu$$6x2HU^XTix&wo~L zn-o9!PwnFBgTP5Q90 zDzZhtGC}zHwRP@6GTt)5s|%LgF77T+Jv41ogNFS&jwf9QeeRr>KMp+H$nWnQtIn*Y zvB2Y*TA~%17M2D7e)Rpe%miSI|8#Pfq=A4Ehb3r}YllWOlkNJh&JRm~M=Es}D1N({ zcXZ7jr{}=U_feCzrg9wm_U>+URbQcxV9yc5sj=-A?|`+>ze}uB^}Y$dzitz% z|K$3v@YQ{;J4E+S4tsy~&%C*H`+?2=?^&EH)+8VAyLx)w{24Pm{%Wn4(+U$f8nOg9 z?yA4~lKT9xY47t@f4`dgtT^VK;n7N9p3d%Fwdh&f4ezP(%Prcj?z8(@eKc35nkBUG z&Fi1mFPFZ2`SQ~9?YXy~<(mNeR>$gVoLYH{99Wt`16vOE;Kng+PeB8A&xZ0Kh2z$_ zI@O=v+}zChVlQwld%08*@PIPNL3F@g;LJ_W_y4~8UTwkmT^t>0z#%`#W;bA)YTuhm zM!_|Gz`QfXZhq_b%b*Qlz(GkZo!>%Ld*1vEUhcQ_dVKxg{aeeA@5ppjQfW2vMv-#)cXD!=l{q0=c{{FLR$&)zZR$d8Mxil#F_>)+j39w&&xg~2>nw*o;i(3o7ZPEgot6nC+cz(_G*PeM{_0NxcGQ5AAS+s%+ zXv#!Rr3W*OJf}!s^G}V;V|c&pO2kUjhd_g#A7Byuu;_T^qT7Fdm^N(xFd_R@-uBzN zz}5)E9ARK_p0zV3j<@}=zUI>NJ9qw_@qY53&!G&tUJ4;kfA6pPd-mXky{^O$X#-RF pC>RZa(GVC70jh>T!M=aYen!8&eVU?y>jM}VJYD@<);T3K0RTVxeKP<6 literal 0 HcmV?d00001 diff --git a/articles/building-apps/application-layer/background-jobs/triggers.adoc b/articles/building-apps/application-layer/background-jobs/triggers.adoc index 29b02227d5..8aacc65b8c 100644 --- a/articles/building-apps/application-layer/background-jobs/triggers.adoc +++ b/articles/building-apps/application-layer/background-jobs/triggers.adoc @@ -1,156 +1,196 @@ --- title: Triggering Jobs -description: How to trigger backgorund jobs +description: How to trigger backgorund jobs. order: 20 --- = Triggering Jobs -A trigger is an object that starts a job. It decides which thread the job should execute in, executes it, and handles any errors that occurred. +A trigger is an object that starts a job. It decides which thread the job should execute in, executes it, and handles any exceptions that occurred. -Business applications have many different triggers. You may want to trigger some jobs on application startup. Other jobs may run once a week, every day at midnight, or every five minutes. Some jobs may run in response to application events, and other in response to user input. You might even have some jobs that can be triggered through a REST API, or through Java Management Extensions (JMX). +image::images/job-and-triggers.png[A job with three triggers] -The same job can have more than one trigger. +Business applications have many different triggers. You may want to trigger some jobs on application startup. Other jobs may run once a week, every day at midnight, or every five minutes. Some jobs may run in response to application events, and others in response to user input. You might even have some jobs that can be triggered through a REST API, or through Java Management Extensions (JMX). You can even create multiple triggers for the same job. -== Startup Jobs - -// TODO Write me - -== Scheduled Jobs - -For scheduled jobs, you should create a _scheduler_ that uses Spring's scheduling mechanism to trigger the job. +== User Triggered Jobs -Spring uses a separate thread pool for scheduled tasks. You should not use this thread pool to execute the jobs. Instead, your schedulers should hand over the jobs to the `TaskExecutor`. +For user triggered jobs, an <<../application-services#,application service>> acts as the trigger. You can create a dedicated service class for this, or add a method to a suitable, existing application service. Like all other application service methods, it should be protected using method security. -Since the scheduler is not intended to be called by any other objects, you should make it package private. +However, unlike a normal application service method, the service should let the job handle its own transactions. -A declarative scheduler looks like this: +Here is an example of an application service that executes a job in a background thread when called: [source,java] ---- -@Component -class MyBackgroundJobScheduler { - - private static final Logger log = LoggerFactory.getLogger(MyBackgroundJobScheduler.class); +@Service +public class MyApplicationService { + private static final Logger log = LoggerFactory.getLogger(MyApplicationService.class); private final MyBackgroundJob job; - MyBackgroundJobScheduler(MyBackgroundJob job) { + MyApplicationService(MyBackgroundJob job) { this.job = job; } - @Scheduled(fixedRate = 5, timeUnit = TimeUnit.MINUTES) // <1> + @PreAuthorize("hasAuthority('permission:startjob')") // <1> @Async // <2> - public void performBackgroundJob() { + public void startJob(MyJobParameters params) { try { - job.performBackgroundJob(); // <3> + job.executeJob(params); // <3> } catch (Exception ex) { - log.error("Error performing background job", ex); // <4> + log.error("Error executing background job", ex); // <4> } } } ---- -<1> The job is executed every 5 minutes. -<2> The job is executed by the `TaskExecutor`, not by the scheduling thread pool. -<3> The scheduler object delegates to the job object. -<4> Log any errors, or handle them in some other way. +<1> Spring ensures the current user has permission to start the job. +<2> Spring executes the method using its task executor thread pool. +<3> The application service delegates to the job, and passes data from the client. +<4> The application service logs any exceptions that may occur. + +This example uses the `@Async` annotation, but you can also execute the job <<../background-jobs#task-execution,programmatically>>. + +Sometimes, the background job needs to interact with the user interface. You may want to update a progress bar, do something with the results, or show an error message. To do that, you have to use server push, and design your service method in a particular way. You can find more information about this on the <<{articles}/building-apps/presentation-layer/server-push#,Server Push>> documentation page. + +== Event Triggered Jobs -If you prefer a more explicit, programmatic approach, you could do the following: +For event triggered jobs, you should create an event listener that receives events from Spring's event publisher. By default, the event publisher calls each listener in the same thread that published the event. You should therefore hand over the job to the `TaskExecutor`. + +Since the listener is not intended to be called by other objects, you should make it package private. + +Here is an example of a listener that executes a job in a background thread whenever a `MyEvent` is published: [source,java] ---- @Component -class MyBackgroundJobScheduler { - - private static final Logger log = LoggerFactory.getLogger(MyBackgroundJobScheduler.class); +class PerformBackgroundJobOnMyEventTrigger { // <1> + private static final Logger log = LoggerFactory.getLogger(PerformBackgroundJobOnMyEventTrigger.class); private final MyBackgroundJob job; - private final TaskExecutor taskExecutor; - private final TaskScheduler taskScheduler; - MyBackgroundJobScheduler(MyBackgroundJob job, - TaskExecutor taskExecutor, - TaskScheduler taskScheduler) { + PerformBackgroundJobOnMyEventTrigger(MyBackgroundJob job) { this.job = job; - this.taskExecutor = taskExecutor; - this.taskScheduler = taskScheduler; - } - - @EventListener - public void onApplicationReadyEvent(ApplicationReadyEvent event) { // <1> - taskScheduler.scheduleAtFixedRate( - () -> taskExecutor.execute(this::performBackgroundJob), - Duration.ofMinutes(5) - ); } - private void performBackgroundJob() { + @EventListener // <2> + @Async // <3> + public void onMyEvent(MyEvent event) { try { - job.performBackgroundJob(); + job.executeJob(event.someDataOfInterestToTheJob()); // <4> } catch (Exception ex) { - log.error("Error performing background job", ex); + log.error("Error executing background job", ex); // <5> } } } ---- -<1> This event is fired once, when the application has started up and is ready to serve requests. +<1> Trigger is package private. +<2> Spring calls the trigger when the `MyEvent` is published. +<3> Spring executes the method using its task executor thread pool. +<4> The trigger delegates to the job, and passes data from the event. +<5> The trigger logs any exceptions that may occur. -Programmatic schedulers are more verbose, but they are easier to debug. You should start with declarative schedulers, and switch to programmatic ones if you need more control over the scheduling, or run into problems that are difficult to debug. +This example uses the `@Async` annotation, but you can also execute the job <<../background-jobs#task-execution,programmatically>>. -== Event Triggered Jobs +== Scheduled Jobs -For event triggered jobs, you should create an _event listener_ that receives events from Spring's event publisher. By default, the event publisher calls each listener in the same thread that published the event. This can be overridden, but it is easier to handle it in the listener implementation itself. +For scheduled jobs, you should create a scheduler that uses Spring's scheduling mechanism to trigger the job. + +Spring uses a separate thread pool for scheduled tasks. You should not use this thread pool to execute the jobs. Instead, your schedulers should hand over the jobs to the `TaskExecutor`. -Since the listener is not intended to be called by any other objects, you should make it package private. +Since the scheduler is not intended to be called by other objects, you should make it package private. -A listener looks like this: +Here is an example of a scheduler that schedules a job to execute every five minutes in a background thread: [source,java] ---- @Component -class PerformBackgroundJobOnMyEventTrigger { - private static final Logger log = LoggerFactory.getLogger(PerformBackgroundJobOnMyEventTrigger.class); +class MyBackgroundJobScheduler { // <1> + + private static final Logger log = LoggerFactory.getLogger(MyBackgroundJobScheduler.class); private final MyBackgroundJob job; - private final TaskExecutor taskExecutor; - - PerformBackgroundJobOnMyEventTrigger(MyBackgroundJob job, - TaskExecutor taskExecutor) { - this.job = job; - this.taskExecutor = taskExecutor; - } - @EventListener - public void onMyEvent(MyEvent event) { - taskExecutor.execute(this::performBackgroundJob); + MyBackgroundJobScheduler(MyBackgroundJob job) { + this.job = job; } - private void performBackgroundJob() { + @Scheduled(fixedRate = 5, timeUnit = TimeUnit.MINUTES) // <2> + @Async // <3> + public void executeJob() { try { - job.performBackgroundJob(); + job.executeJob(); // <4> } catch (Exception ex) { - log.error("Error performing background job", ex); - } + log.error("Error executing scheduled job", ex); // <5> + } } } ---- +<1> Scheduler is package private. +<2> Spring calls the trigger every 5 minutes. +<3> Spring executes the method using its task executor thread pool. +<4> The scheduler delegates to the job. +<5> The scheduler logs any exceptions that may occur. -== User Triggered Jobs +This example uses the `@Scheduled` and `@Async` annotations, but you can also execute the job using the task scheduler and task executor <<../background-jobs#task-scheduling,programmatically>>. -For user triggered jobs, an <> acts as the trigger. +Programmatic schedulers are more verbose, but they are easier to debug. Therefore, you should start with annotations when you implement schedulers. If you later need more control over the scheduling, or run into problems that are difficult to debug, you should switch to a programmatic approach. -// TODO Continue here +== Startup Jobs + +For startup jobs, you should create a startup trigger that executes the job when the application starts. + +Since the trigger is not intended to be called by other objects, you should make it package private. + +If you want the initialization of the application to block until the job is finished, you should start the job inside the constructor of your trigger. Furthermore, you should execute the job in the calling thread, which in this case is Spring's main thread. If an error occurs during a job like this, you probably want the application to exit. Therefore, you can leave any exceptions unhandled. + +Here is an example of a trigger that blocks initialization until the job is finished: [source,java] ---- -@Service -public class MyApplicationService { +@Component +class MyStartupTrigger { // <1> - @Test - public void startBackgroundJob() { + MyStartupTrigger(MyBackgroundJob job) { + job.executeJob(); // <2> + } +} +---- +<1> Trigger is package private. +<2> The trigger delegates to the job, and executes in the calling thread. + +[IMPORTANT] +Whenever you implement a startup trigger like this, you have to remember that the application is still being initialized. That means that not all services may be available for your job to use. + +If you want to trigger a job after the application has started, you should start the job in response to the `ApplicationReadyEvent` event. This event is published by Spring Boot when the application has started up and is ready to serve requests. Here is an example of a trigger that executes a job in a background thread after the application has started up: + +[source,java] +---- +import org.springframework.boot.context.event.ApplicationReadyEvent; +@Component +class MyStartupTrigger { // <1> + + private static final Logger log = LoggerFactory.getLogger(MyStartupTrigger.class); + private final MyBackgroundJob job; + + MyStartupTrigger(MyBackgroundJob job) { + this.job = job; } + @EventListener // <2> + @Async // <3> + public void onApplicationReady(ApplicationReadyEvent event) { + try { + job.executeJob(); // <4> + } catch (Exception ex) { // <5> + log.error("Error executing job on startup", ex); + } + } } ---- +<1> Trigger is package private. +<2> Spring calls the trigger when the `ApplicationReadyEvent` is published. +<3> Spring executes the method using its task executor thread pool. +<4> The trigger delegates to the job. +<5> The trigger logs any exceptions that may occur. -// TODO If the job needs to interact with the user interface in some way, either while running, or after it has finished, it becomes a bit more involved. This is explained in the next section. +This example uses the `@Async` annotation, but you can also execute the job <<../background-jobs#task-execution,programmatically>>. // TODO How to trigger jobs using Control Center? diff --git a/articles/building-apps/application-layer/background-jobs/ui-interaction.adoc b/articles/building-apps/application-layer/background-jobs/ui-interaction.adoc deleted file mode 100644 index 37e1a068c9..0000000000 --- a/articles/building-apps/application-layer/background-jobs/ui-interaction.adoc +++ /dev/null @@ -1,9 +0,0 @@ ---- -title: UI Interaction -description: How to interact with the user interface from background jobs. -order: 30 ---- - -= UI Interaction - -// TODO Write me \ No newline at end of file diff --git a/articles/building-apps/application-layer/domain-primitives.adoc b/articles/building-apps/application-layer/domain-primitives.adoc index 65781791c6..cb6a662fc0 100644 --- a/articles/building-apps/application-layer/domain-primitives.adoc +++ b/articles/building-apps/application-layer/domain-primitives.adoc @@ -1,7 +1,7 @@ --- title: Domain Primitives description: Learn what domain primitives are and how to use them in your applications. -order: 50 +order: 28 --- diff --git a/articles/building-apps/application-layer/persistence/index.adoc b/articles/building-apps/application-layer/persistence/index.adoc index 312aa07d25..4456d50968 100644 --- a/articles/building-apps/application-layer/persistence/index.adoc +++ b/articles/building-apps/application-layer/persistence/index.adoc @@ -1,7 +1,7 @@ --- title: Persistence description: How do handle persistence in Vaadin applications. -order: 20 +order: 40 --- = Persistence diff --git a/articles/building-apps/presentation-layer/index.adoc b/articles/building-apps/presentation-layer/index.adoc new file mode 100644 index 0000000000..0ae0b36896 --- /dev/null +++ b/articles/building-apps/presentation-layer/index.adoc @@ -0,0 +1,5 @@ +--- +title: Presentation Layer +description: How to build the presentation layer of Vaadin applications. +order: 30 +--- \ No newline at end of file diff --git a/articles/building-apps/presentation-layer/server-push/index.adoc b/articles/building-apps/presentation-layer/server-push/index.adoc new file mode 100644 index 0000000000..4a0c87881e --- /dev/null +++ b/articles/building-apps/presentation-layer/server-push/index.adoc @@ -0,0 +1,9 @@ +--- +title: Server Push +description: How to use server push in your user interfaces. +order: 50 +--- + += Server Push + +// TODO Write about server push (and move some of the other documentation here as well) From 6387bbc2c0baafa7a5bba043c9250f5331cd369b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petter=20Holmstr=C3=B6m?= Date: Fri, 4 Oct 2024 16:49:16 +0300 Subject: [PATCH 07/30] WIP --- .../background-jobs/interaction.adoc | 157 ++++++++++++++++++ .../background-jobs/jobs.adoc | 2 +- .../background-jobs/triggers.adoc | 3 + .../server-push/callbacks.adoc | 122 ++++++++++++++ .../server-push/futures.adoc | 10 ++ .../presentation-layer/server-push/index.adoc | 3 + .../server-push/reactive.adoc | 9 + .../server-push/ui-threads.adoc | 10 ++ 8 files changed, 315 insertions(+), 1 deletion(-) create mode 100644 articles/building-apps/application-layer/background-jobs/interaction.adoc create mode 100644 articles/building-apps/presentation-layer/server-push/callbacks.adoc create mode 100644 articles/building-apps/presentation-layer/server-push/futures.adoc create mode 100644 articles/building-apps/presentation-layer/server-push/reactive.adoc create mode 100644 articles/building-apps/presentation-layer/server-push/ui-threads.adoc diff --git a/articles/building-apps/application-layer/background-jobs/interaction.adoc b/articles/building-apps/application-layer/background-jobs/interaction.adoc new file mode 100644 index 0000000000..29a5687f1b --- /dev/null +++ b/articles/building-apps/application-layer/background-jobs/interaction.adoc @@ -0,0 +1,157 @@ +--- +title: Interacting with Jobs +description: How to interact with jobs from the user interface. +order: 25 +--- + +// TODO Re-write this page once the pages about server push are finished. This page should explain how to adapt the background jobs to the different patterns of updating the UI from a background thread. + += Interacting with Jobs + +// TODO + +== Awaiting Completion + +// TODO + +== Handling Errors + +// TODO + +== Reporting Progress + +// TODO + +== Cancelling + +A background job needs to know whether it has been cancelled, so that it can stop what it is doing. This is typically done through a boolean flag that the job checks, for instance at the beginning of every loop iteration. If you have created a separate <>, you can do this with a `Supplier`, like this: + +[source,java] +---- +@Component +public class MyBackgroundJob { + + public void performBackgroundJob(Supplier isCancelled) { + var thingsToProcess = fetchThingsToProcess(); + for (var thing: thingsToProcess) { + if (isCancelled.get()) { + return; + } + processThing(thing); + } + } + + private List fetchThingsToProcess() { + ... + } + + private void processThing(Thing thing) { + ... + } +} +---- + +The caller must make sure the supplier is thread-safe, for instance by using an `AtomicBoolean`. + +You can implement the application service method in different ways, depending on whether you are using Flow or Hilla for the user interface. However, regardless of which way you choose, you have to call the `TaskExecutor` directly to execute the job. _Do not use the `@Async` annotation._ + +=== Return a Handle [badge-flow]#Flow# + +First, create a handle interface that the user interface can use to cancel the job: + +[source,java] +---- +@FunctionalInterface +public interface CancellableJob { + void cancel(); +} +---- + +Next, implement the service method like this: + +[source,java] +---- +public CancellableJob startJob() { + var cancelled = new AtomicBoolean(false); + taskExecutor.execute(() -> { + try { + myBackgroundJob.performBackgroundJob(cancelled::get); + } catch (Exception ex) { + log.error("Error performing background job", ex); + } + }); + return () -> cancelled.set(true); +} +---- + +When you call the `startJob()` method from the user interface, you should store the returned handle in a variable. If you need to cancel the job, call the `cancel()` method on the handle. + +// TODO Mention something about + +=== Return a CompletableFuture [badge-flow]#Flow# + +Implement the service method like this: + +[source,java] +---- +public CompletableFuture startJob() { + var future = new CompletableFuture(); + taskExecutor.execute(() -> { + try { + myBackgroundJob.performBackgroundJob(future::isCancelled); + future.complete(null); + } catch (Exception ex) { + future.completeExceptionally(ex); + } + }); + return future; +} +---- + +Note, that the `CompletableFuture` itself completes exceptionally with a `CompletionException` caused by a `CancellationException`. + +The user interface can use the `CompletableFuture`` like this: + +[source,java] +---- +public class MyJobUI extends HorizontalLayout { + private CompletableFuture job; + + public MyView(MyService service) { + var startButton = new Button("Start", event -> { + job = service.startJob(); + }); + var cancelButton = new Button("Cancel", event -> { + if (job != null) { + job.cancel(true); + } + }); + add(startButton, cancelButton); + } +} +---- + +// TODO Add link to how to handle errors and completion. + +=== Return a Flux [badge-flow]#Flow# [badge-hilla]#Hilla# + +Implement the service method like this: + +[source,java] +---- +public Flux startJob() { + var cancelled = new AtomicBoolean(false); + return Mono.fromRunnable(() -> myBackgroundJob.performBackgroundJob(cancelled::get)) // <1> + .doOnCancel(() -> cancelled.set(true)) // <2> + .subscribeOn(Schedulers.fromExecutor(taskExecutor)) // <3> + .then() // <4> + .flux(); // <5> +} +---- +<1> Create a `Mono` that completes empty when the job has finished. +<2> If the `Mono` is cancelled, set the `cancelled` flag to `true`. +<3> Execute the job in the `TaskExecutor` when subscribed to. +<4> This is needed because `subscribeOn` returns `Mono` and this method expects `Mono`. +<5> Turn the `Mono` into a `Flux`, because Hilla only works with `Flux`. + +If the user interface cancels the subscription, the `cancelled` flag becomes `true`, and the job stops executing at its next iteration. diff --git a/articles/building-apps/application-layer/background-jobs/jobs.adoc b/articles/building-apps/application-layer/background-jobs/jobs.adoc index 388f235775..0f30e5ff82 100644 --- a/articles/building-apps/application-layer/background-jobs/jobs.adoc +++ b/articles/building-apps/application-layer/background-jobs/jobs.adoc @@ -6,7 +6,7 @@ order: 10 = Implementing Jobs -When you implement a background job, you should decouple its implementation from how it is triggered, and where it is executed. This makes it possible to trigger the job in multiple ways. +When you implement a background job, you should consider decoupling its implementation from how it is triggered, and where it is executed. This makes it possible to trigger the job in multiple ways. For instance, you may want to run the job every time the application starts up. In this case, you may want to run it in the main thread, blocking the initialization of the rest of the application until the job is finished. You may also want to run the job in a background thread every day at midnight, or whenever a certain application event is published. diff --git a/articles/building-apps/application-layer/background-jobs/triggers.adoc b/articles/building-apps/application-layer/background-jobs/triggers.adoc index 8aacc65b8c..965d357bc2 100644 --- a/articles/building-apps/application-layer/background-jobs/triggers.adoc +++ b/articles/building-apps/application-layer/background-jobs/triggers.adoc @@ -12,6 +12,9 @@ image::images/job-and-triggers.png[A job with three triggers] Business applications have many different triggers. You may want to trigger some jobs on application startup. Other jobs may run once a week, every day at midnight, or every five minutes. Some jobs may run in response to application events, and others in response to user input. You might even have some jobs that can be triggered through a REST API, or through Java Management Extensions (JMX). You can even create multiple triggers for the same job. +[IMPORTANT] +On this page, all the trigger examples are delegating to a separate <>. However, if your job is simple, and you know it only needs one trigger, you can implement the job itself inside the trigger. + == User Triggered Jobs For user triggered jobs, an <<../application-services#,application service>> acts as the trigger. You can create a dedicated service class for this, or add a method to a suitable, existing application service. Like all other application service methods, it should be protected using method security. diff --git a/articles/building-apps/presentation-layer/server-push/callbacks.adoc b/articles/building-apps/presentation-layer/server-push/callbacks.adoc new file mode 100644 index 0000000000..518a2933fc --- /dev/null +++ b/articles/building-apps/presentation-layer/server-push/callbacks.adoc @@ -0,0 +1,122 @@ +--- +title: Callbacks +description: How to use server push with callbacks. +order: 20 +section-nav: badge-flow +--- + += Callbacks [badge-flow]#Flow# + +If you are building the user interface with Flow, you can use callbacks to allow a background thread to update the user interface. This approach is easy to implement and understand. + +== Callback Interfaces + +You can use `Consumer` and `Runnable` as callback interfaces, depending on what information you want the background thread to send to your user interface. + +[cols="1,1"] +|=== +|Event |Callback + +|Completed without a result +|`Runnable` + +|Completed with a result of type `T` +|`Consumer` + +|Completed with an exception +|`Consumer` + +|Reported percentage done +|`Consumer` + +|=== + +For example, a background job that returns a string or an exception could be implemented like this: + +[source,java] +---- +@Async +public void startBackgroundJob(Consumer onComplete, + Consumer onError) { + try { + var result = doSomethingThatTakesALongTime(); + onComplete.accept(result); + } catch (Exception ex) { + onError.accept(result); + } +} +---- + +If the background job is also reporting its progress, for instance as a percentage number, it could look like this: + +[source,java] +---- +@Async +public void startBackgroundJob(Consumer onComplete, + Consumer onProgress, + Consumer onError) { + try { + onProgress.apply(0.0); + + var step1Result = performStep1(); + onProgress.apply(0.25); + + var step2Result = performStep2(step1Result); + onProgress.apply(0.50); + + var step3Result = performStep3(step2Result); + onProgress.apply(0.75); + + var result = performStep4(step3Result); + onProgress.apply(1.0); + + onComplete.accept(result); + } catch (Exception ex) { + onError.accept(ex); + } +} +---- + +You can find more information about working with background threads on the <<{articles}/building-apps/application-layer/background-jobs#,Background Jobs>> and <> documentation pages. + +== Callback Implementations + +// This text assumes the logic behind `UI.access()` and `UI.accessLater()` has been explained earlier, including how to get the `UI` instance itself. + +Whenever you implement a callback, you have remember that the callback is called by the background thread. This means that any updates to the user interface, must happen inside a call to `UI.access()`. However, since you are using `Runnable` and `Consumer` callbacks, you can use `UI.accessLater()`. + +For every callback, you should create a private method in your user interface that you can pass as a method reference to `UI.accessLater`. For example, a method for handling successful completion could look like this: + +[source,java] +---- +private void onJobCompleted(String result) { + Notification.show("Job completed: " + result); +} +---- + +Likewise, a method for handling errors could look like this: + +[source,java] +---- +private void onJobFailed(Exception error) { + Notification.show("Job failed: " + error.getMessage()); +} +---- + +For reporting progress, you can use a `<<{articles}/components/progress-bar#,ProgressBar>>`. If the background jobs reports the progress as a floating point value between 0.0 and 1.0, you can pass it directly to the `setValue` method of the progress bar. + +With these methods in place, the method that starts the background job could look like this: + +[source,java] +---- +private void startJob() { + var ui = UI.getCurrent(); + service.startBackgroundJob( + ui.accessLater(this::onJobCompleted, null), + ui.accessLater(progressBar::setValue, null), + ui.accessLater(this::onJobFailed, null) + ); +} +---- + +You would then call the `startJob()` method when a user clicks a button, for instance. diff --git a/articles/building-apps/presentation-layer/server-push/futures.adoc b/articles/building-apps/presentation-layer/server-push/futures.adoc new file mode 100644 index 0000000000..bc409958de --- /dev/null +++ b/articles/building-apps/presentation-layer/server-push/futures.adoc @@ -0,0 +1,10 @@ +--- +title: Futures +description: How to use server push with `CompletableFuture`. +order: 30 +section-nav: badge-flow +--- + += Futures [badge-flow]#Flow# + +// TODO Write me \ No newline at end of file diff --git a/articles/building-apps/presentation-layer/server-push/index.adoc b/articles/building-apps/presentation-layer/server-push/index.adoc index 4a0c87881e..b1245335c3 100644 --- a/articles/building-apps/presentation-layer/server-push/index.adoc +++ b/articles/building-apps/presentation-layer/server-push/index.adoc @@ -7,3 +7,6 @@ order: 50 = Server Push // TODO Write about server push (and move some of the other documentation here as well) +// Explain setting up server push +// Explain UI.access() and UI.accessLater(). +// Explain push for Hilla diff --git a/articles/building-apps/presentation-layer/server-push/reactive.adoc b/articles/building-apps/presentation-layer/server-push/reactive.adoc new file mode 100644 index 0000000000..82f0ceae12 --- /dev/null +++ b/articles/building-apps/presentation-layer/server-push/reactive.adoc @@ -0,0 +1,9 @@ +--- +title: Reactive Streams +description: How to use server push with reactive streams. +order: 40 +--- + += Reactive Streams + +// TODO Write me \ No newline at end of file diff --git a/articles/building-apps/presentation-layer/server-push/ui-threads.adoc b/articles/building-apps/presentation-layer/server-push/ui-threads.adoc new file mode 100644 index 0000000000..1f979cf7a6 --- /dev/null +++ b/articles/building-apps/presentation-layer/server-push/ui-threads.adoc @@ -0,0 +1,10 @@ +--- +title: UI Threads +description: How to use server push with callbacks. +order: 50 +section-nav: badge-flow +--- + += User Interface Threads [badge-flow]#Flow# + +// TODO This is about the use case when a Flow UI itself requires another thread to do UI-stuff. In other words, it is not starting a background job. \ No newline at end of file From 48efae6de0b67a13ea12f98a5277f272deea6386 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petter=20Holmstr=C3=B6m?= Date: Fri, 4 Oct 2024 17:07:54 +0300 Subject: [PATCH 08/30] WIP --- .../application-layer/background-jobs/triggers.adoc | 1 + .../presentation-layer/server-push/callbacks.adoc | 6 ++++-- .../presentation-layer/server-push/ui-threads.adoc | 4 ++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/articles/building-apps/application-layer/background-jobs/triggers.adoc b/articles/building-apps/application-layer/background-jobs/triggers.adoc index 965d357bc2..71d06c4d61 100644 --- a/articles/building-apps/application-layer/background-jobs/triggers.adoc +++ b/articles/building-apps/application-layer/background-jobs/triggers.adoc @@ -20,6 +20,7 @@ On this page, all the trigger examples are delegating to a separate <> acts as the trigger. You can create a dedicated service class for this, or add a method to a suitable, existing application service. Like all other application service methods, it should be protected using method security. However, unlike a normal application service method, the service should let the job handle its own transactions. +// TODO What if the service method does not delegate to a job? Can it handle the transaction then? How? Here is an example of an application service that executes a job in a background thread when called: diff --git a/articles/building-apps/presentation-layer/server-push/callbacks.adoc b/articles/building-apps/presentation-layer/server-push/callbacks.adoc index 518a2933fc..b920fe8584 100644 --- a/articles/building-apps/presentation-layer/server-push/callbacks.adoc +++ b/articles/building-apps/presentation-layer/server-push/callbacks.adoc @@ -83,9 +83,9 @@ You can find more information about working with background threads on the <<{ar // This text assumes the logic behind `UI.access()` and `UI.accessLater()` has been explained earlier, including how to get the `UI` instance itself. -Whenever you implement a callback, you have remember that the callback is called by the background thread. This means that any updates to the user interface, must happen inside a call to `UI.access()`. However, since you are using `Runnable` and `Consumer` callbacks, you can use `UI.accessLater()`. +Whenever you implement a callback, you have remember that the callback is called by the background thread. This means that any updates to the user interface must happen inside a call to `UI.access()`. However, since you are using `Runnable` and `Consumer` callbacks, you can use `UI.accessLater()`. -For every callback, you should create a private method in your user interface that you can pass as a method reference to `UI.accessLater`. For example, a method for handling successful completion could look like this: +For every callback, you should create a private method in your user interface that you can pass as a method reference to `UI.accessLater()`. For example, a method for handling successful completion could look like this: [source,java] ---- @@ -120,3 +120,5 @@ private void startJob() { ---- You would then call the `startJob()` method when a user clicks a button, for instance. + +// TODO Explain why no detach handler is needed here diff --git a/articles/building-apps/presentation-layer/server-push/ui-threads.adoc b/articles/building-apps/presentation-layer/server-push/ui-threads.adoc index 1f979cf7a6..2553bb44fa 100644 --- a/articles/building-apps/presentation-layer/server-push/ui-threads.adoc +++ b/articles/building-apps/presentation-layer/server-push/ui-threads.adoc @@ -1,7 +1,7 @@ --- title: UI Threads -description: How to use server push with callbacks. -order: 50 +description: How to use threads in your Flow user interface. +order: 10 section-nav: badge-flow --- From 08b14d6a26ad10e7f1416d3519b49be23d20d622 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petter=20Holmstr=C3=B6m?= Date: Mon, 7 Oct 2024 15:29:40 +0300 Subject: [PATCH 09/30] Text about futures --- .../server-push/callbacks.adoc | 4 +- .../server-push/futures.adoc | 84 ++++++++++++++++++- 2 files changed, 84 insertions(+), 4 deletions(-) diff --git a/articles/building-apps/presentation-layer/server-push/callbacks.adoc b/articles/building-apps/presentation-layer/server-push/callbacks.adoc index b920fe8584..1749659cdc 100644 --- a/articles/building-apps/presentation-layer/server-push/callbacks.adoc +++ b/articles/building-apps/presentation-layer/server-push/callbacks.adoc @@ -119,6 +119,4 @@ private void startJob() { } ---- -You would then call the `startJob()` method when a user clicks a button, for instance. - -// TODO Explain why no detach handler is needed here +You would then call the `startJob()` method when a user clicks a button, for instance. If the UI has already been detached when the background thread completes, you do not have to do anything. Because of this, no detach handler is passed to `ui.accessLater()`. diff --git a/articles/building-apps/presentation-layer/server-push/futures.adoc b/articles/building-apps/presentation-layer/server-push/futures.adoc index bc409958de..50ea828e88 100644 --- a/articles/building-apps/presentation-layer/server-push/futures.adoc +++ b/articles/building-apps/presentation-layer/server-push/futures.adoc @@ -7,4 +7,86 @@ section-nav: badge-flow = Futures [badge-flow]#Flow# -// TODO Write me \ No newline at end of file +If you are building the user interface with Flow, a background thread can use the standard Java `CompletableFuture` to inform the user interface of results and errors. This is a good approach if you are already used to working with `CompletableFuture`. + +For example, a background job that returns a string or an exception could be implemented like this: + +[source,java] +---- +@Async +public CompletableFuture startBackgroundJob() { + return CompletableFuture.completedFuture(doSomethingThatTakesALongTime()); +} +---- + +If `doSomethingThatTakesALongTime()` throws an exception, Spring automatically returns a `CompletableFuture` that is completed with the exception. + +If you also need to report progress, you can combine this approach with <>. + +// This text assumes the logic behind `UI.access()` and `UI.accessLater()` has been explained earlier, including how to get the `UI` instance itself. + +In fact, you are using callbacks in this approach as well, but instead of calling them directly from your background thread, you are registering them with `CompletableFuture`. However, the principle is the same: create private methods that can be passed as `Consumers` to `UI.accessLater()`. For example, a method for handling successful completion could look like this: + +[source,java] +---- +private void onJobCompleted(String result) { + Notification.show("Job completed: " + result); +} +---- + +Likewise, a method for handling errors could look like this: + +[source,java] +---- +private void onJobFailed(Throwable error) { + Notification.show("Job failed: " + error.getMessage()); +} +---- + +Note, that the error handler must accept a `Throwable` and not an `Exception` when you are working with `CompletableFuture`. + +== Successful Completion + +If a `CompletableFuture` completes successfully, you can instruct it to perform a specific operation by calling the `thenAccept()` method on it. This method takes a `Consumer` object as its input. When the `CompletableFuture` completes, it calls this object with the result. + +You use it to update your user interface like this: + +[source,java] +---- +private void startJob() { + var ui = UI.getCurrent(); + service.startBackgroundJob().thenAccept(ui.accessLater(this::onJobCompleted, null)); +} +---- + +However, this version does not yet handle any exceptions. + +== Exceptional Completion + +If a `CompletableFuture` is completed with an exception, you can instruct it to perform a specific operation by calling the `exceptionally()` method on it. However, this method works in a different way than `thenAccept()`. It takes a `Function` instead of a `Consumer` object as input. The exception is passed to the function as input. The function output is used as the result of the `CompletableFuture` that is returned by `exceptionally()`. + +Flow has no version of `UI.accessLater()` that works with `Function`. However, since you are not interested in returning a result, you can create a helper function that adapts a `Consumer` to a `Function`, like this: + +[source,java] +---- +public static Function consumerToFunction(Consumer consumer) { + return input -> { + consumer.accept(input); + return null; + }; +} +---- + +With this helper function in place, the code for starting the job with error handling now becomes: + +[source,java] +---- +private void startJob() { + var ui = UI.getCurrent(); + service.startBackgroundJob() + .thenAccept(ui.accessLater(this::onJobCompleted, null)) + .exceptionally(consumerToFunction(ui.accessLater(this::onJobFailed, null))) +} +---- + +You would then call the `startJob()` method when a user clicks a button, for instance. If the UI has already been detached when the background thread completes, you do not have to do anything. Because of this, no detach handler is passed to `ui.accessLater()`. \ No newline at end of file From 7bede80af9097b8b41e79e878e7681ef5ff5ad2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petter=20Holmstr=C3=B6m?= Date: Tue, 8 Oct 2024 18:07:13 +0300 Subject: [PATCH 10/30] WIP --- .../background-jobs/interaction.adoc | 273 +++++++++++++----- .../background-jobs/triggers.adoc | 7 +- .../server-push/callbacks.adoc | 74 +---- .../server-push/futures.adoc | 10 +- .../server-push/reactive.adoc | 19 +- 5 files changed, 237 insertions(+), 146 deletions(-) diff --git a/articles/building-apps/application-layer/background-jobs/interaction.adoc b/articles/building-apps/application-layer/background-jobs/interaction.adoc index 29a5687f1b..1b6621b0d9 100644 --- a/articles/building-apps/application-layer/background-jobs/interaction.adoc +++ b/articles/building-apps/application-layer/background-jobs/interaction.adoc @@ -1,63 +1,138 @@ --- -title: Interacting with Jobs +title: UI Interaction description: How to interact with jobs from the user interface. order: 25 --- -// TODO Re-write this page once the pages about server push are finished. This page should explain how to adapt the background jobs to the different patterns of updating the UI from a background thread. += UI Interaction -= Interacting with Jobs +Some background jobs execute business processes in the background. The end user may see the result of the background job, but does not have to interact with the directly. Scheduled jobs and event triggered jobs typically fall in this category. -// TODO +Then there are jobs that need to interact with the user interface. For instance, the job may want to update a progress indicator while running, and notify the user when the job has completed, or an error has occurred. Furthermore, the user may want to be able to cancel a running job before it has completed. -== Awaiting Completion +On this page, you'll learn different ways of doing this that work with both Flow and Hilla. -// TODO +== Callbacks [badge-flow]#Flow# -== Handling Errors +If you are using a Flow user interface, the simplest way of allowing your background jobs to interact with it is through callbacks. -// TODO +You can use `Consumer`, `Runnable`, and `Supplier` as callback interfaces, depending on how you want to interact with the background job. -== Reporting Progress +[cols="1,1"] +|=== +|Event |Callback -// TODO +|Completed without a result +|`Runnable` -== Cancelling +|Completed with a result of type `T` +|`Consumer` -A background job needs to know whether it has been cancelled, so that it can stop what it is doing. This is typically done through a boolean flag that the job checks, for instance at the beginning of every loop iteration. If you have created a separate <>, you can do this with a `Supplier`, like this: +|Completed with an exception +|`Consumer` + +|Reported percentage done +|`Consumer` + +|Cancelled by user +|`Supplier` + +|=== + +For example, a background job that returns a string or an exception could be implemented like this: [source,java] ---- -@Component -public class MyBackgroundJob { - - public void performBackgroundJob(Supplier isCancelled) { - var thingsToProcess = fetchThingsToProcess(); - for (var thing: thingsToProcess) { - if (isCancelled.get()) { - return; - } - processThing(thing); - } - } - - private List fetchThingsToProcess() { - ... +@Async +public void startBackgroundJob(Consumer onComplete, + Consumer onError) { + try { + var result = doSomethingThatTakesALongTime(); + onComplete.accept(result); + } catch (Exception ex) { + onError.accept(result); } +} +---- + +If the background job is also reporting its progress, for instance as a percentage number, it could look like this: - private void processThing(Thing thing) { - ... +[source,java] +---- +@Async +public void startBackgroundJob(Consumer onComplete, + Consumer onProgress, + Consumer onError) { + try { + onProgress.apply(0.0); + + var step1Result = performStep1(); + onProgress.apply(0.25); + + var step2Result = performStep2(step1Result); + onProgress.apply(0.50); + + var step3Result = performStep3(step2Result); + onProgress.apply(0.75); + + var result = performStep4(step3Result); + onProgress.apply(1.0); + + onComplete.accept(result); + } catch (Exception ex) { + onError.accept(ex); } } ---- -The caller must make sure the supplier is thread-safe, for instance by using an `AtomicBoolean`. +Furthermore, if the job can also be cancelled, it could look like this: + +[source,java] +---- +@Async +public void startBackgroundJob(Consumer onComplete, + Consumer onProgress, + Consumer onError, + Supplier isCancelled) { + try { + onProgress.apply(0.0); + + if (isCancelled.get()) { + return; + } + var step1Result = performStep1(); + onProgress.apply(0.25); + + if (isCancelled.get()) { + return; + } + var step2Result = performStep2(step1Result); + onProgress.apply(0.50); -You can implement the application service method in different ways, depending on whether you are using Flow or Hilla for the user interface. However, regardless of which way you choose, you have to call the `TaskExecutor` directly to execute the job. _Do not use the `@Async` annotation._ + if (isCancelled.get()) { + return; + } + var step3Result = performStep3(step2Result); + onProgress.apply(0.75); -=== Return a Handle [badge-flow]#Flow# + if (isCancelled.get()) { + return; + } + var result = performStep4(step3Result); + onProgress.apply(1.0); -First, create a handle interface that the user interface can use to cancel the job: + onComplete.accept(result); + } catch (Exception ex) { + onError.accept(ex); + } +} +---- + +All the callbacks have to be thread-safe, as they are called from the background thread, but owned and created by the user interface. For more information about how to implement these callbacks, see the <<{articles}/building-apps/presentation-layer/server-push/callbacks#,Server Push - Callbacks>> documentation page. + +=== Improving Cancel API + +If you want to make the cancelling API nicer, you can replace the callback with a handle. First, create a handle interface that the user interface can use to cancel the job: [source,java] ---- @@ -71,35 +146,102 @@ Next, implement the service method like this: [source,java] ---- -public CancellableJob startJob() { +public CancellableJob startBackgroundJob(Consumer onComplete, + Consumer onProgress + Consumer onError) { var cancelled = new AtomicBoolean(false); taskExecutor.execute(() -> { try { - myBackgroundJob.performBackgroundJob(cancelled::get); + onProgress.apply(0.0); + + if (cancelled.get()) { + return; + } + var step1Result = performStep1(); + onProgress.apply(0.25); + + if (cancelled.get()) { + return; + } + var step2Result = performStep2(step1Result); + onProgress.apply(0.50); + + if (cancelled.get()) { + return; + } + var step3Result = performStep3(step2Result); + onProgress.apply(0.75); + + if (cancelled.get()) { + return; + } + var result = performStep4(step3Result); + onProgress.apply(1.0); + + onComplete.accept(result); } catch (Exception ex) { - log.error("Error performing background job", ex); + onError.accept(result); } }); return () -> cancelled.set(true); } ---- -When you call the `startJob()` method from the user interface, you should store the returned handle in a variable. If you need to cancel the job, call the `cancel()` method on the handle. +The user interface would have to store the handle while the job is running, and call the `cancel()` method to cancel it. Note, that you cannot use the `@Async` annotation in this case. This is because `@Async` methods can only return `void` or future-like types. In this case, you want to return neither. -// TODO Mention something about +The handle itself is thread safe because you are using an `AtomicBoolean`. You do not need to take any special precautions to call it from the user interface. -=== Return a CompletableFuture [badge-flow]#Flow# +== CompletableFuture [badge-flow]#Flow# -Implement the service method like this: +If you are using a Flow user interface, you can use a standard Java `CompletableFuture` to report results and errors to the user interface. You can also use it to cancel the job from the user interface. For reporting progress, however, you still need to use a callback. + +The advantage of working with `CompletableFuture` is that Spring has built-in support for them when using the `@Async` annotation. For example, a background job that completes with either a string or an exception could be implemented like this: + +[source,java] +---- +@Async +public CompletableFuture startBackgroundJob() { + return CompletableFuture.completedFuture(doSomethingThatTakesALongTime()); +} +---- + +If the `doSomethingThatTakesALongTime()` method throws an exception, Spring automatically returns a `CompletableFuture` with the exception in question. + +To update the user interface, you have to add special completion stages that execute after the `CompletableFuture` completes. For more information about how to add these, see the <<{articles}/building-apps/presentation-layer/server-push/futures#,Server Push - Futures>> documentation page. + +=== Cancelling + +You can cancel a Java `Future` by calling its `cancel()` method. The method has a `boolean` parameter that indicates whether the thread should be interrupted or not. However, `CompletableFuture`, which implements `Future`, does not use this parameter. It therefore does not make any difference whether you pass in `true` or `false`. + +When you cancel a `CompletableFuture`, it completes with a `CompletionException` caused by a `CancellationException`. However, the job continues to silently run in the background until it has finished. If you want to notify the job itself that it has been cancelled, and should stop running at the next suitable moment, you have to make some changes. + +`CompletableFuture` has an `isCancelled()` method that you can use to query whether the job has been cancelled or not. However, do to this, you cannot use the `@Async` annotation anymore. Instead, you have to manually execute the job using the `TaskExecutor`, and manage the state of the returned `CompletableFuture`. The principle is the same as when you are using callbacks or handles. + +The earlier example would look like this when implemented using a `CompletableFuture`: [source,java] ---- -public CompletableFuture startJob() { - var future = new CompletableFuture(); +public CompletableFuture startBackgroundJob() { + var future = new CompletableFuture(); taskExecutor.execute(() -> { try { - myBackgroundJob.performBackgroundJob(future::isCancelled); - future.complete(null); + var step1Result = performStep1(); + + if (future.isCancelled()) { + return; + } + var step2Result = performStep2(step1Result); + + if (future.isCancelled()) { + return; + } + var step3Result = performStep3(step2Result); + + if (future.isCancelled()) { + return; + } + var result = performStep4(step3Result); + future.complete(result); } catch (Exception ex) { future.completeExceptionally(ex); } @@ -108,32 +250,33 @@ public CompletableFuture startJob() { } ---- -Note, that the `CompletableFuture` itself completes exceptionally with a `CompletionException` caused by a `CancellationException`. +You do not need to do anything with the `future` after it has been cancelled, as it has already been completed. Returning is enough. + +== Flux and Mono + +If you are using Flow or Hilla to build your user interface, you can use `Flux` or `Mono` from https://projectreactor.io/[Reactor] to allow your background jobs to interact with them. + +// TODO Implement me + +[IMPORTANT] +Hilla only supports `Flux`, so if your job is returning a `Mono`, you have to convert it to a `Flux` inside your `@BrowserCallable` endpoint. You can do this by calling the `Mono.flux()` method. + + + +=== Reporting Progress + +=== Cancelling + + + + + -The user interface can use the `CompletableFuture`` like this: -[source,java] ----- -public class MyJobUI extends HorizontalLayout { - private CompletableFuture job; - public MyView(MyService service) { - var startButton = new Button("Start", event -> { - job = service.startJob(); - }); - var cancelButton = new Button("Cancel", event -> { - if (job != null) { - job.cancel(true); - } - }); - add(startButton, cancelButton); - } -} ----- -// TODO Add link to how to handle errors and completion. -=== Return a Flux [badge-flow]#Flow# [badge-hilla]#Hilla# +=== Cancelling Implement the service method like this: diff --git a/articles/building-apps/application-layer/background-jobs/triggers.adoc b/articles/building-apps/application-layer/background-jobs/triggers.adoc index 71d06c4d61..bec7da5fdc 100644 --- a/articles/building-apps/application-layer/background-jobs/triggers.adoc +++ b/articles/building-apps/application-layer/background-jobs/triggers.adoc @@ -19,9 +19,6 @@ On this page, all the trigger examples are delegating to a separate <> acts as the trigger. You can create a dedicated service class for this, or add a method to a suitable, existing application service. Like all other application service methods, it should be protected using method security. -However, unlike a normal application service method, the service should let the job handle its own transactions. -// TODO What if the service method does not delegate to a job? Can it handle the transaction then? How? - Here is an example of an application service that executes a job in a background thread when called: [source,java] @@ -53,7 +50,7 @@ public class MyApplicationService { This example uses the `@Async` annotation, but you can also execute the job <<../background-jobs#task-execution,programmatically>>. -Sometimes, the background job needs to interact with the user interface. You may want to update a progress bar, do something with the results, or show an error message. To do that, you have to use server push, and design your service method in a particular way. You can find more information about this on the <<{articles}/building-apps/presentation-layer/server-push#,Server Push>> documentation page. +Sometimes, the background job needs to interact with the user interface. You may want to update a progress bar, do something with the results, or show an error message. To do that, you have to use server push, and design your service method in a particular way. You can find more information about this on the <> documentation page. == Event Triggered Jobs @@ -164,6 +161,8 @@ Whenever you implement a startup trigger like this, you have to remember that th If you want to trigger a job after the application has started, you should start the job in response to the `ApplicationReadyEvent` event. This event is published by Spring Boot when the application has started up and is ready to serve requests. Here is an example of a trigger that executes a job in a background thread after the application has started up: +// TODO Is CommandLineRunner a simpler approach? + [source,java] ---- import org.springframework.boot.context.event.ApplicationReadyEvent; diff --git a/articles/building-apps/presentation-layer/server-push/callbacks.adoc b/articles/building-apps/presentation-layer/server-push/callbacks.adoc index 1749659cdc..3d21a51511 100644 --- a/articles/building-apps/presentation-layer/server-push/callbacks.adoc +++ b/articles/building-apps/presentation-layer/server-push/callbacks.adoc @@ -7,79 +7,9 @@ section-nav: badge-flow = Callbacks [badge-flow]#Flow# -If you are building the user interface with Flow, you can use callbacks to allow a background thread to update the user interface. This approach is easy to implement and understand. +// TODO Re-write this page once the page about UI threads is completed. -== Callback Interfaces - -You can use `Consumer` and `Runnable` as callback interfaces, depending on what information you want the background thread to send to your user interface. - -[cols="1,1"] -|=== -|Event |Callback - -|Completed without a result -|`Runnable` - -|Completed with a result of type `T` -|`Consumer` - -|Completed with an exception -|`Consumer` - -|Reported percentage done -|`Consumer` - -|=== - -For example, a background job that returns a string or an exception could be implemented like this: - -[source,java] ----- -@Async -public void startBackgroundJob(Consumer onComplete, - Consumer onError) { - try { - var result = doSomethingThatTakesALongTime(); - onComplete.accept(result); - } catch (Exception ex) { - onError.accept(result); - } -} ----- - -If the background job is also reporting its progress, for instance as a percentage number, it could look like this: - -[source,java] ----- -@Async -public void startBackgroundJob(Consumer onComplete, - Consumer onProgress, - Consumer onError) { - try { - onProgress.apply(0.0); - - var step1Result = performStep1(); - onProgress.apply(0.25); - - var step2Result = performStep2(step1Result); - onProgress.apply(0.50); - - var step3Result = performStep3(step2Result); - onProgress.apply(0.75); - - var result = performStep4(step3Result); - onProgress.apply(1.0); - - onComplete.accept(result); - } catch (Exception ex) { - onError.accept(ex); - } -} ----- - -You can find more information about working with background threads on the <<{articles}/building-apps/application-layer/background-jobs#,Background Jobs>> and <> documentation pages. - -== Callback Implementations +If you are building the user interface with Flow, you can use callbacks to allow a background thread to update the user interface. // This text assumes the logic behind `UI.access()` and `UI.accessLater()` has been explained earlier, including how to get the `UI` instance itself. diff --git a/articles/building-apps/presentation-layer/server-push/futures.adoc b/articles/building-apps/presentation-layer/server-push/futures.adoc index 50ea828e88..321c660e93 100644 --- a/articles/building-apps/presentation-layer/server-push/futures.adoc +++ b/articles/building-apps/presentation-layer/server-push/futures.adoc @@ -11,6 +11,8 @@ If you are building the user interface with Flow, a background thread can use th For example, a background job that returns a string or an exception could be implemented like this: +// TODO Replace with method signature only and a link to the background jobs page + [source,java] ---- @Async @@ -19,7 +21,7 @@ public CompletableFuture startBackgroundJob() { } ---- -If `doSomethingThatTakesALongTime()` throws an exception, Spring automatically returns a `CompletableFuture` that is completed with the exception. +Because of the `@Async` annotation, if `doSomethingThatTakesALongTime()` throws an exception, Spring automatically returns a `CompletableFuture` that is completed with the exception. If you also need to report progress, you can combine this approach with <>. @@ -47,7 +49,7 @@ Note, that the error handler must accept a `Throwable` and not an `Exception` wh == Successful Completion -If a `CompletableFuture` completes successfully, you can instruct it to perform a specific operation by calling the `thenAccept()` method on it. This method takes a `Consumer` object as its input. When the `CompletableFuture` completes, it calls this object with the result. +If a `CompletableFuture` completes successfully, you can instruct it to perform a specific operation by calling the `thenAccept()` method on it. This method takes a `Consumer` as its input. When the `CompletableFuture` completes, it calls this consumer with the result. You use it to update your user interface like this: @@ -63,7 +65,9 @@ However, this version does not yet handle any exceptions. == Exceptional Completion -If a `CompletableFuture` is completed with an exception, you can instruct it to perform a specific operation by calling the `exceptionally()` method on it. However, this method works in a different way than `thenAccept()`. It takes a `Function` instead of a `Consumer` object as input. The exception is passed to the function as input. The function output is used as the result of the `CompletableFuture` that is returned by `exceptionally()`. +If a `CompletableFuture` is completed with an exception, you can instruct it to perform a specific operation by calling the `exceptionally()` method on it. However, this method works in a different way than `thenAccept()`. + +The `exceptionally()` method takes a `Function` instead of a `Consumer` as input. The exception is passed to the function as input. The function output is used as the result of the `CompletableFuture` that is returned by `exceptionally()`. Flow has no version of `UI.accessLater()` that works with `Function`. However, since you are not interested in returning a result, you can create a helper function that adapts a `Consumer` to a `Function`, like this: diff --git a/articles/building-apps/presentation-layer/server-push/reactive.adoc b/articles/building-apps/presentation-layer/server-push/reactive.adoc index 82f0ceae12..0d77f425f1 100644 --- a/articles/building-apps/presentation-layer/server-push/reactive.adoc +++ b/articles/building-apps/presentation-layer/server-push/reactive.adoc @@ -4,6 +4,21 @@ description: How to use server push with reactive streams. order: 40 --- -= Reactive Streams +// TODO Write the text first, and then worry about how to group the texts later. Hilla and Flow on the same page, or on different pages? + += Reactive Streams [badge-flow]#Flow# [badge-hilla]#Hilla# + +If you are building the user interface with either Flow or Hilla, you can use reactive streams to allow a background thread to update the user interface. Reactive streams are also useful for broadcasts, where .... CONTINUE HERE + +== Background Threads + +=== Flow + +=== Hilla + +== Broadcasts + +=== Flow + +=== Hilla -// TODO Write me \ No newline at end of file From fac537779cdba3a992c9fca74506c1754e5e8a50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petter=20Holmstr=C3=B6m?= Date: Wed, 9 Oct 2024 14:44:29 +0300 Subject: [PATCH 11/30] WIP --- .../background-jobs/concurrency.adoc | 9 -- .../background-jobs/interaction.adoc | 106 +++++++++++++++--- 2 files changed, 89 insertions(+), 26 deletions(-) delete mode 100644 articles/building-apps/application-layer/background-jobs/concurrency.adoc diff --git a/articles/building-apps/application-layer/background-jobs/concurrency.adoc b/articles/building-apps/application-layer/background-jobs/concurrency.adoc deleted file mode 100644 index eb97fd98ff..0000000000 --- a/articles/building-apps/application-layer/background-jobs/concurrency.adoc +++ /dev/null @@ -1,9 +0,0 @@ ---- -title: Concurrent Jobs -description: How to handle concurrent job executions. -order: 30 ---- - -= Concurrent Jobs - -// TODO Write about running the same job concurrently inside the same VM and on different VMs (but do this after you have written about server push in the presentation layer) diff --git a/articles/building-apps/application-layer/background-jobs/interaction.adoc b/articles/building-apps/application-layer/background-jobs/interaction.adoc index 1b6621b0d9..b5080207d4 100644 --- a/articles/building-apps/application-layer/background-jobs/interaction.adoc +++ b/articles/building-apps/application-layer/background-jobs/interaction.adoc @@ -254,47 +254,119 @@ You do not need to do anything with the `future` after it has been cancelled, as == Flux and Mono -If you are using Flow or Hilla to build your user interface, you can use `Flux` or `Mono` from https://projectreactor.io/[Reactor] to allow your background jobs to interact with them. +If you are using Flow or Hilla to build your user interface, you can use `Flux` or `Mono` from https://projectreactor.io/[Reactor] to allow your background jobs to interact with them. Reactor has an extensive API, which means you can do a lot of things with it. This also means that it has a steeper learning curve than using callbacks or `CompletableFuture`. If you are new to reactive programming, you should read Reactor's https://projectreactor.io/docs/core/release/reference/#intro-reactive[Introduction to Reactive Programming] before continuing. -// TODO Implement me +When you are using Reactor, you cannot use the `@Async` annotation. Instead, you have to manually instruct your `Mono` or `Flux` to execute using the Spring `TaskExecutor`. Otherwise, your job executes in the thread that subscribes to the `Mono` or `Flux`. -[IMPORTANT] -Hilla only supports `Flux`, so if your job is returning a `Mono`, you have to convert it to a `Flux` inside your `@BrowserCallable` endpoint. You can do this by calling the `Mono.flux()` method. +For example, a background job that returns a string or an exception could be implemented like this: + +[source,java] +---- +public Mono startBackgroundJob() { + return Mono.fromSupplier(this::doSomethingThatTakesALongTime) + .subscribeOn(Schedulers.fromExecutor(taskExecutor)); +} +---- + +If the `doSomethingThatTakesALongTime()` method throws an exception, the `Mono` terminates with an error. +To update the user interface, you have to subscribe to the `Mono` or `Flux`. For more information about how to do this, see the <<{articles}/building-apps/presentation-layer/server-push/reactive#,Server Push - Reactive Streams>> documentation page. +[IMPORTANT] +Hilla only supports `Flux`, so if your job is returning a `Mono`, you have to convert it to a `Flux` inside your `@BrowserCallable` endpoint. You can do this by calling the `Mono.flux()` method. === Reporting Progress -=== Cancelling +If your background job only needs to report its progress without actually returning a result, you can return a `Flux`. Your job should then emit progress updates, and complete the stream when done. However, you often also want to return a result. Since Hilla only supports returning a single `Flux`, you have to use the same stream for emitting both progress updates and the end result. The code may be a bit messy, but it works. +You first need to create a data type that can contain both progress updates and the result. For a job that returns a string, it could look like this: +[source,java] +---- +import com.vaadin.hilla.Nullable; + +public record BackgroundJobOutput( + @Nullable Double progressUpdate, + @Nullable String result +) { + public static BackgroundJobOutput progressUpdate(double progressUpdate) { + return new BackgroundJobOutput(progressUpdate, null); + } + public static BackgroundJobOutput finished(String result) { + return new BackgroundJobOutput(null, result); + } +} +---- +The two factory methods `progressUpdate()` and `finished()` make the code look better when it is time to create instances of `BackgroundJobOutput`. +[NOTE] +If you have worked with sealed classes, you may be tempted to create a sealed interface called `BackgroundJobOutput`, and then create two records that implement that interface: one for progress updates, and another for the result. However, Hilla does not support this at the moment. +Next, you have to implement the background job like this: +[source,java] +---- +private String doSomethingThatTakesALongTime(Consumer onProgress) { + ... +} +public Flux startBackgroundJob() { + Sinks.Many progressUpdates = Sinks // <1> + .many() + .unicast() + .onBackpressureError(); + + var result = Mono // <2> + .fromSupplier(() -> doSomethingThatTakesALongTime( + progressUpdates::tryEmitNext + )) + .subscribeOn(Schedulers.fromExecutor(taskExecutor)); + + return Flux.merge( // <3> + progress.asFlux().map(BackgroundJobOutput::progressUpdate), + result.map(BackgroundJobOutput::finished) + ); +} +---- +<1> Create a sink that you can emit progress updates to. +<2> Create a `Mono` that emits the result of the background job. +<3> Map both streams to `BackgroundJobOutput` and merge them. +When your user interface subscribes to this `Flux`, it needs to check the state of the returned `BackgroundJobOutput` objects. If `progressUpdate` contains a value, it should update the progress indicator. If `result` contains a value, the operation is finished. === Cancelling -Implement the service method like this: +You can cancel a subscription to a `Flux` or `Mono` at any time. However, as with `CompletableFuture`, cancelling the subscription does not stop the background job itself. To fix this, you need to tell the background job when it has been cancelled, so that it can stop itself. Continuing on the earlier example, adding support for cancelling could look like this: [source,java] ---- -public Flux startJob() { +private String doSomethingThatTakesALongTime( + Consumer onProgress, + Supplier isCancelled) { + ... +} + +public Flux startBackgroundJob() { var cancelled = new AtomicBoolean(false); - return Mono.fromRunnable(() -> myBackgroundJob.performBackgroundJob(cancelled::get)) // <1> - .doOnCancel(() -> cancelled.set(true)) // <2> - .subscribeOn(Schedulers.fromExecutor(taskExecutor)) // <3> - .then() // <4> - .flux(); // <5> + Sinks.Many progressUpdates = Sinks + .many() + .unicast() + .onBackpressureError(); + + var result = Mono + .fromSupplier(() -> doSomethingThatTakesALongTime( + progressUpdates::tryEmitNext, cancelled::get + )) + .doOnCancel(() -> cancelled.set(true)) + .subscribeOn(Schedulers.fromExecutor(taskExecutor)); + + return Flux.merge( + progress.asFlux().map(BackgroundJobOutput::progressUpdate), + result.map(BackgroundJobOutput::finished) + ); } ---- -<1> Create a `Mono` that completes empty when the job has finished. -<2> If the `Mono` is cancelled, set the `cancelled` flag to `true`. -<3> Execute the job in the `TaskExecutor` when subscribed to. -<4> This is needed because `subscribeOn` returns `Mono` and this method expects `Mono`. -<5> Turn the `Mono` into a `Flux`, because Hilla only works with `Flux`. If the user interface cancels the subscription, the `cancelled` flag becomes `true`, and the job stops executing at its next iteration. From 7933ec74a614ab48a45cfdfcb06ff8baa7c8ceab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petter=20Holmstr=C3=B6m?= Date: Thu, 10 Oct 2024 10:48:56 +0300 Subject: [PATCH 12/30] WIP --- .../background-jobs/index.adoc | 2 +- .../background-jobs/triggers.adoc | 2 +- .../presentation-layer/server-push/index.adoc | 227 +++++++++++++++++- .../flow/advanced/long-running-tasks.adoc | 3 + articles/flow/advanced/server-push.adoc | 3 + .../hilla/lit/guides/reactive-endpoints.adoc | 3 + 6 files changed, 234 insertions(+), 6 deletions(-) diff --git a/articles/building-apps/application-layer/background-jobs/index.adoc b/articles/building-apps/application-layer/background-jobs/index.adoc index 504b93cdf0..043a1bc9ec 100644 --- a/articles/building-apps/application-layer/background-jobs/index.adoc +++ b/articles/building-apps/application-layer/background-jobs/index.adoc @@ -1,6 +1,6 @@ --- title: Background Jobs -description: How to handle background jobs in Vaadin applications. +description: How to handle background jobs. order: 11 --- diff --git a/articles/building-apps/application-layer/background-jobs/triggers.adoc b/articles/building-apps/application-layer/background-jobs/triggers.adoc index bec7da5fdc..23b349ef51 100644 --- a/articles/building-apps/application-layer/background-jobs/triggers.adoc +++ b/articles/building-apps/application-layer/background-jobs/triggers.adoc @@ -12,7 +12,7 @@ image::images/job-and-triggers.png[A job with three triggers] Business applications have many different triggers. You may want to trigger some jobs on application startup. Other jobs may run once a week, every day at midnight, or every five minutes. Some jobs may run in response to application events, and others in response to user input. You might even have some jobs that can be triggered through a REST API, or through Java Management Extensions (JMX). You can even create multiple triggers for the same job. -[IMPORTANT] +[NOTE] On this page, all the trigger examples are delegating to a separate <>. However, if your job is simple, and you know it only needs one trigger, you can implement the job itself inside the trigger. == User Triggered Jobs diff --git a/articles/building-apps/presentation-layer/server-push/index.adoc b/articles/building-apps/presentation-layer/server-push/index.adoc index b1245335c3..014646f4ef 100644 --- a/articles/building-apps/presentation-layer/server-push/index.adoc +++ b/articles/building-apps/presentation-layer/server-push/index.adoc @@ -6,7 +6,226 @@ order: 50 = Server Push -// TODO Write about server push (and move some of the other documentation here as well) -// Explain setting up server push -// Explain UI.access() and UI.accessLater(). -// Explain push for Hilla +Server push is based on a client-server connection established by the client. The server can then use the connection to send updates to the client. For example, it could send a new chat message to all participants without delay. + +The server-client communication uses a WebSocket connection, if supported by the browser and the server. If not, the connection resorts to whatever method is supported by the browser. Vaadin uses the link:https://github.com/Atmosphere/atmosphere[Atmosphere framework], internally. + +In Hilla views, push is always enabled when you subscribe to a _reactive endpoint_. For Flow views, you have to enable it explicitly. + +== Enabling Push [badge-flow]#Flow# + +Before you can use server push in Flow, you have to enable it. You do this by adding the `@Push` annotation to the application shell class, like this: + +[source,java] +---- +import com.vaadin.flow.component.page.AppShellConfigurator; +import com.vaadin.flow.component.page.Push; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +@Push +public class Application implements AppShellConfigurator { + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } +} +---- + +// TODO Add link to page about the application shell, once is has been written (currently, the contents is scattered all over the documentation) + +// TODO Transport modes? Or is that something for the reference material. + +== Updating the UI [badge-flow]#Flow# + +Whenever you are using server push in Flow, you are triggering it from another thread than the normal HTTP request thread. Making changes to a UI from another thread and pushing them to the browser requires locking the user session. Otherwise, the UI update done from another thread could conflict with a regular event-driven update and cause either data corruption or deadlocks. Because of this, you may only access a UI using the `UI.access()` method, which locks the session to prevent conflicts. You use it like this: + +[source,java] +---- +ui.access(() -> { + // Update your UI here +}); +---- + +By default, Flow uses automatic pushing. This means that any pending changes are pushed to the browser automatically after `UI.access()` finishes. You can also configure Flow to use manual pushing. This gives you more control over when changes are pushed to the browser. For example, you can push multiple times inside a single call to `UI.access()`. + +To enable manual pushing, you have to make an addition to the `@Push` annotation, like this: + +[source,java] +---- +@Push(PushMode.MANUAL) +public class Application implements AppShellConfigurator { + ... +} +---- + +After this, you have to call the `UI.push()` method whenever you want to push your changes to the browser, like this: + +[source,java] +---- +ui.access(() -> { + // Update your UI here + ui.push(); +}); +---- + +=== Getting the UI Instance + +// TODO This assumes that the UI has been explained earlier, and what attach and detach means. + +Before you can call `access()`, you need to get the `UI` instance. You typically use `Component.getUI()` or `UI.getCurrent()` for this. However, both are problematic when it comes to server push. + +`Component.getUI()` is not thread-safe, which means you should only call it while the user session is locked. Therefore, you cannot use it to call `access()`. + +`UI.getCurrent()` only returns a UI whenever the session is locked. Therefore, you cannot use it to call `access()`, either. + +Whenever you are planning to use server push, you have to get a hold of the `UI` instance _while the user session is locked_. This typically happens right before you start your background thread, for example: + +[source,java] +---- +var ui = UI.getCurrent(); // <1> +taskExecutor.execute(() -> { + // Do your work here + ui.access(() -> { // <2> + // Update your UI here + }); +}); +---- +<1> This is executed in an HTTP request thread. The user session is locked and `UI.getCurrent()` returns the current `UI`-instance. +<2> This is executed in the background thread. `UI.getCurrent()` returns `null`, but the `UI` instance is stored in a local variable. + +=== Access Later + +You often use server push in various types of event listeners and <>. A background job might inform you that it has finished processing, like this: + +[source,java] +---- +var ui = UI.getCurrent(); +myService.startBackgroundJob(() -> ui.access(() -> { + // Update your UI here when the job is finished +})); +---- + +Or an event bus might inform you that a new message has arrived, like this: + +[source,java] +---- +var ui = UI.getCurrent(); +var subscription = myEventBus.subscribe((message) -> ui.access(() -> { + // Update your UI here when a message has arrived +})); +---- + +Whenever these event listeners or callbacks conform to the `Runnable` or `Consumer` functional interfaces, you should consider using `UI.accessLater()` instead of `UI.access()`. + +`UI.accessLater()` exists in two versions: one that wraps a `Runnable`, and another that wraps a `Consumer`. It stores the `UI` instance, and runs the wrapped delegate inside a call to `UI.access()`. It also takes a second parameter, which is a _detach handler_. The detach handler is a `Runnable` that runs if the `UI` has been detached when `UI.access()` is called. The detach handler can be `null` if no special actions are needed. + +Rewritten with `accessLater()`, the thread completion example becomes: + +[source,java] +---- +myService.startBackgroundJob(UI.getCurrent().accessLater(() -> { + // Update your UI here when the job is finished. +}, null)); // <1> +---- +<1> No detach handler is needed. + +Likewise, the event listener becomes: + +[source,java] +---- +var subscription = myEventBus.subscribe(UI.getCurrent().accessLater((message) -> { + // Update your UI here when a message has arrived +}, null)); // <1> +---- +<1> No detach handler is needed. + +=== Avoiding Memory Leaks + +When you are using server push to update the user interface when an event has occurred, you typically subscribe to some broadcaster or event bus. When you do this, you have to remember to always unsubscribe when the UI is detached. Otherwise, you end up with a memory leak that prevents your UI from being garbage collected. + +It is recommended to always subscribe when your view is attached to a UI, and unsubscribe when it is detached. You can do this by overriding the `Component.onAttach()` method, like this: + +[source,java] +---- +@Override +protected void onAttach(AttachEvent attachEvent) { // <1> + var subscription = myEventBus.subscribe(attachEvent.getUI().accessLater((message) -> { // <2> + // Update your UI here when a message has arrived + }, null)); + addDetachListener(detachEvent -> subscription.unsubscribe()); // <3> +} +---- +<1> Subscribe when the view is attached to a UI. +<2> Get the `UI` from the `AttachEvent`. +<3> Unsubscribe when the view is detached from the UI. + +=== Avoiding Floods + +Another risk you have to manage when updating the user interface in response to events is flooding the user interface with updates. As a rule of thumb, you should not push more than 2--4 times per second. Pushing more often than that can cause performance issues. Also, there is a limit to how many updates the human eye is able to detect per second. + +If you know the events are coming in at a pace no faster than 2--4 events per second, you can push on every event. However, if they are more frequent, you have to start buffering events and update the user interface in batches. This is quite easy to do if you are using a `Flux` from https://projectreactor.io/[Reactor] to emit the events: + +[source,java] +---- +@Override +protected void onAttach(AttachEvent attachEvent) { + var subscription = myEventBus + .asFlux() // <1> + .buffer(Duration.ofMillis(250)) // <2> + .subscribe(attachEvent.getUI().accessLater((messageList) -> { // <3> + // Update your UI here when a list of messages has arrived + }, null)); + addDetachListener(detachEvent -> subscription.dispose()); +} +---- +<1> This assumes you can access the stream of messages through a `Flux`. +<2> Buffer incoming messages for 250 milliseconds before pushing. +<3> Instead of reacting to a single message, you are now reacting to a list of messages. + +The buffering duration depends on the size of the UI update, and the network latency. In some applications, you may need to use a longer buffer duration, in others, a shorter one might work. You should try various durations and see what works best for your application. + +// TODO Add link to subpage about reactive streams + +=== Avoiding Unnecessary Pushes + +The `UI.access()` method updates the user interface asynchronously. The update operation is not executed right away, but added to a queue and executed at some point in the future. If this is combined with regular event-driven updates in the HTTP request thread, you may end up in a situation where the user interface is updated out-of-order. Look at this example: + +[source,java] +---- +var button = new Button("Test Me", event -> { + UI.getCurrent().access(() -> { + add(new Div("This
is added from within a call to UI.access()")); + }); + add(new Div("This
is added from an event listener")); +}); +add(button); +---- + +If you click the button, the user interface looks like this: + +[source] +---- +This
is added from an event listener +This
is added from within a call to UI.access() +---- + +In this particular case, the call to `UI.access()` would not have been needed. Sometimes, you can deduce this by looking at the code. However, there are situations where this is not clear. You may have code that sometimes is executed by the HTTP request thread, and sometimes by another thread. In this case, you can check whether the current thread has locked the user session or not, like this: + +[source,java] +---- +if (ui.getSession().hasLock()) { + // Update the UI without calling UI.access() +} else { + ui.access(() -> { + // Update the UI inside UI.access() + }); +} +---- + +// TODO Consider showing an example of a UIRunner that takes a Runnable or Consumer, performs the check, and calls it directly or inside UI.access(). + +== Reactive Endpoints [badge-hilla]#Hilla# + +TODO \ No newline at end of file diff --git a/articles/flow/advanced/long-running-tasks.adoc b/articles/flow/advanced/long-running-tasks.adoc index df3b39c535..fd156e5d23 100644 --- a/articles/flow/advanced/long-running-tasks.adoc +++ b/articles/flow/advanced/long-running-tasks.adoc @@ -7,6 +7,9 @@ order: 175 = Handling Long-Running Tasks +[IMPORTANT] +This page is being migrated to the new <<{articles}/building-apps#,Building Apps>> section. See the <<{articles}/building-apps/application-layer/background-jobs#,Background Jobs>> and <<{articles}/building-apps/presentation-layer/server-push#,Server Push>> documentation pages. + Often, server-side tasks can take a long time to complete. For example, a task that requires fetching a large amount of data from the database can take a long time to finish. In such cases, a poorly designed application can freeze the UI and prevent the user from interacting with the application. This guide shows you how to handle long-running tasks in Vaadin applications in a way that: diff --git a/articles/flow/advanced/server-push.adoc b/articles/flow/advanced/server-push.adoc index 7adc03a522..bd800d4782 100644 --- a/articles/flow/advanced/server-push.adoc +++ b/articles/flow/advanced/server-push.adoc @@ -8,6 +8,9 @@ order: 620 [[push.configuration]] = Server Push Configuration +[IMPORTANT] +This page is being migrated to the new <<{articles}/building-apps#,Building Apps>> section. See the <<{articles}/building-apps/application-layer/background-jobs#,Background Jobs>> and <<{articles}/building-apps/presentation-layer/server-push#,Server Push>> documentation pages. + Server push is based on a client-server connection established by the client. The server can then use the connection to send updates to the client. For example, it could send a new chat message to all participants without delay. The server-client communication uses a WebSocket connection, if supported by the browser and the server. If not, the connection resorts to whatever method is supported by the browser. Vaadin uses the link:https://github.com/Atmosphere/atmosphere[Atmosphere framework], internally. diff --git a/articles/hilla/lit/guides/reactive-endpoints.adoc b/articles/hilla/lit/guides/reactive-endpoints.adoc index d49dd63056..5ef140447c 100644 --- a/articles/hilla/lit/guides/reactive-endpoints.adoc +++ b/articles/hilla/lit/guides/reactive-endpoints.adoc @@ -10,6 +10,9 @@ order: 35 = [since:dev.hilla:hilla@v1.2]#Reactive Endpoints# +[IMPORTANT] +This page is being migrated to the new <<{articles}/building-apps#,Building Apps>> section. See the <<{articles}/building-apps/application-layer/background-jobs#,Background Jobs>> and <<{articles}/building-apps/presentation-layer/server-push#,Server Push>> documentation pages. + Although traditional server calls work fine in most cases, sometimes you need different tools. https://projectreactor.io/[Reactor] is one of these, and can help you stream data to clients -- and it fits well into a non-blocking application. Whenever the simple request-response pattern doesn't suit your needs, you might consider Reactor. Multi-user applications, infinite data streaming, and retries are all good examples of what you can do with it. If you want to know more about Reactor, see their curated https://projectreactor.io/learn[learning page]. From 7f9dd0ef6fdff90488a2125f6784e39e344f05a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petter=20Holmstr=C3=B6m?= Date: Thu, 10 Oct 2024 14:21:06 +0300 Subject: [PATCH 13/30] WIP --- .../server-push/callbacks.adoc | 8 +- .../server-push/futures.adoc | 24 +----- .../presentation-layer/server-push/index.adoc | 25 +++++- .../server-push/reactive.adoc | 4 +- .../server-push/ui-threads.adoc | 81 ++++++++++++++++++- 5 files changed, 110 insertions(+), 32 deletions(-) diff --git a/articles/building-apps/presentation-layer/server-push/callbacks.adoc b/articles/building-apps/presentation-layer/server-push/callbacks.adoc index 3d21a51511..df9104d921 100644 --- a/articles/building-apps/presentation-layer/server-push/callbacks.adoc +++ b/articles/building-apps/presentation-layer/server-push/callbacks.adoc @@ -7,11 +7,7 @@ section-nav: badge-flow = Callbacks [badge-flow]#Flow# -// TODO Re-write this page once the page about UI threads is completed. - -If you are building the user interface with Flow, you can use callbacks to allow a background thread to update the user interface. - -// This text assumes the logic behind `UI.access()` and `UI.accessLater()` has been explained earlier, including how to get the `UI` instance itself. +If you are building the user interface with Flow, you can use <<{articles}/building-apps/application-layer/background-jobs/interaction#callbacks-flow,callbacks>> to allow a background thread to update the user interface. Whenever you implement a callback, you have remember that the callback is called by the background thread. This means that any updates to the user interface must happen inside a call to `UI.access()`. However, since you are using `Runnable` and `Consumer` callbacks, you can use `UI.accessLater()`. @@ -49,4 +45,4 @@ private void startJob() { } ---- -You would then call the `startJob()` method when a user clicks a button, for instance. If the UI has already been detached when the background thread completes, you do not have to do anything. Because of this, no detach handler is passed to `ui.accessLater()`. +You would then call the `startJob()` method when a user clicks a button, for instance. diff --git a/articles/building-apps/presentation-layer/server-push/futures.adoc b/articles/building-apps/presentation-layer/server-push/futures.adoc index 321c660e93..2838a0e660 100644 --- a/articles/building-apps/presentation-layer/server-push/futures.adoc +++ b/articles/building-apps/presentation-layer/server-push/futures.adoc @@ -7,27 +7,9 @@ section-nav: badge-flow = Futures [badge-flow]#Flow# -If you are building the user interface with Flow, a background thread can use the standard Java `CompletableFuture` to inform the user interface of results and errors. This is a good approach if you are already used to working with `CompletableFuture`. +If you are building the user interface with Flow, a background thread can use the standard Java <<{articles}/building-apps/application-layer/background-jobs/interaction#completablefuture-flow,`CompletableFuture`>> to inform the user interface of results and errors. -For example, a background job that returns a string or an exception could be implemented like this: - -// TODO Replace with method signature only and a link to the background jobs page - -[source,java] ----- -@Async -public CompletableFuture startBackgroundJob() { - return CompletableFuture.completedFuture(doSomethingThatTakesALongTime()); -} ----- - -Because of the `@Async` annotation, if `doSomethingThatTakesALongTime()` throws an exception, Spring automatically returns a `CompletableFuture` that is completed with the exception. - -If you also need to report progress, you can combine this approach with <>. - -// This text assumes the logic behind `UI.access()` and `UI.accessLater()` has been explained earlier, including how to get the `UI` instance itself. - -In fact, you are using callbacks in this approach as well, but instead of calling them directly from your background thread, you are registering them with `CompletableFuture`. However, the principle is the same: create private methods that can be passed as `Consumers` to `UI.accessLater()`. For example, a method for handling successful completion could look like this: +In fact, you are using <> in this approach as well, but instead of calling them directly from your background thread, you are registering them with `CompletableFuture`. However, the principle is the same: create private methods that can be passed as `Consumers` to `UI.accessLater()`. For example, a method for handling successful completion could look like this: [source,java] ---- @@ -93,4 +75,4 @@ private void startJob() { } ---- -You would then call the `startJob()` method when a user clicks a button, for instance. If the UI has already been detached when the background thread completes, you do not have to do anything. Because of this, no detach handler is passed to `ui.accessLater()`. \ No newline at end of file +You would then call the `startJob()` method when a user clicks a button, for instance. \ No newline at end of file diff --git a/articles/building-apps/presentation-layer/server-push/index.adoc b/articles/building-apps/presentation-layer/server-push/index.adoc index 014646f4ef..b3fb629de7 100644 --- a/articles/building-apps/presentation-layer/server-push/index.adoc +++ b/articles/building-apps/presentation-layer/server-push/index.adoc @@ -186,7 +186,7 @@ protected void onAttach(AttachEvent attachEvent) { The buffering duration depends on the size of the UI update, and the network latency. In some applications, you may need to use a longer buffer duration, in others, a shorter one might work. You should try various durations and see what works best for your application. -// TODO Add link to subpage about reactive streams +// TODO Add link to subpage about reactive streams and move the code example there. === Avoiding Unnecessary Pushes @@ -228,4 +228,25 @@ if (ui.getSession().hasLock()) { == Reactive Endpoints [badge-hilla]#Hilla# -TODO \ No newline at end of file +// TODO This text assumes that browser callable endpoints have already been explained earlier. + +If you are building your user interface with Hilla, you use reactive endpoints to push messages from the server to the browser. A reactive endpoint is an endpoint that returns a `Flux` from https://projectreactor.io/[Reactor]. For example, an endpoint that emits the current date and time every second could look like this: + +[source,java] +---- +@BrowserCallable +public class TimeEndpoint { + + @AnonymousAllowed + public Flux<@Nonnull String> getClock() { + return Flux.interval(Duration.ofSeconds(1)) // <1> + .onBackpressureDrop() // <2> + .map(_interval -> Instant.now().toString()); // <3> + } +} +---- +<1> Emit a new message every second. +<2> Drop any messages that for some reason cannot be sent to the client in time. +<3> Output the current date and time as a string. + +Hilla generates the necessary TypeScript types to subscribe to this endpoint from the browser. The push connection is automatically established when the client subscribes. For more information about updating the UI using a reactive endpoint, see the <> documentation page. \ No newline at end of file diff --git a/articles/building-apps/presentation-layer/server-push/reactive.adoc b/articles/building-apps/presentation-layer/server-push/reactive.adoc index 0d77f425f1..3b5b6a9d84 100644 --- a/articles/building-apps/presentation-layer/server-push/reactive.adoc +++ b/articles/building-apps/presentation-layer/server-push/reactive.adoc @@ -1,12 +1,12 @@ --- -title: Reactive Streams +title: Rective Streams description: How to use server push with reactive streams. order: 40 --- // TODO Write the text first, and then worry about how to group the texts later. Hilla and Flow on the same page, or on different pages? -= Reactive Streams [badge-flow]#Flow# [badge-hilla]#Hilla# += Reactive Streams If you are building the user interface with either Flow or Hilla, you can use reactive streams to allow a background thread to update the user interface. Reactive streams are also useful for broadcasts, where .... CONTINUE HERE diff --git a/articles/building-apps/presentation-layer/server-push/ui-threads.adoc b/articles/building-apps/presentation-layer/server-push/ui-threads.adoc index 2553bb44fa..d7da6d6877 100644 --- a/articles/building-apps/presentation-layer/server-push/ui-threads.adoc +++ b/articles/building-apps/presentation-layer/server-push/ui-threads.adoc @@ -7,4 +7,83 @@ section-nav: badge-flow = User Interface Threads [badge-flow]#Flow# -// TODO This is about the use case when a Flow UI itself requires another thread to do UI-stuff. In other words, it is not starting a background job. \ No newline at end of file +You often use server push to update the user interface from background jobs. This is covered in the <<{articles}/building-apps/application-layer/background-jobs/interaction#,Background Jobs - UI Interaction>> documentation page. However, in Flow, there are also cases where you want to start up a separate thread for use by the user interface itself. You might, for instance, want to show the server date and time in "real time". + +If you have used Swing before, you might be tempted to use a `Timer`, or start a new `Thread` manually. In Flow, this is not a good idea. The reason is that Flow applications are multi-user applications, with potentially thousands of concurrent users. If each user creates its own `Timer`, or starts its own `Thread`, you may run out of threads. If that happens, the application crashes. + +Instead, you should use a shared `ExecutorService` or `ScheduledExecutorService`, or virtual threads. + +== Executor Service + +Although Spring has a `TaskExecutor`, you should create your own executor service for your user interface. Create a separate `@Configuration` class, like this: + +[source,java] +---- +@Configuration +class UIThreadConfiguration { + + @Bean(destroyMethod = "shutdown") + public ScheduledExecutorService uiExecutor() { + return Executors.newSingleThreadScheduledExecutor(); + } +} +---- + +This example uses a single thread. If you need more threads, you can use `Executors.newScheduledThreadPool()`. + +Next, you inject the executor into your view, like this: + +[source,java] +---- +@Route +public class MyView extends VerticalLayout { + + private final ScheduledExecutorService uiExecutor; + + public MyView(ScheduledExecutorService uiExecutor) { + this.uiExecutor = uiExecutor; + ... + } +} +---- + +Now, whenever you need to run a UI operation in a background thread, you can do this: + +[source,java] +---- +uiExecutor.submit(UI.getCurrent().accessLater(() -> { + // Perform the UI operation here. +}, null)); +---- + +Because of the call to `UI.accessLater()`, the user interface is automatically updated through a server push when the task finishes. + +You can also use the executor to schedule tasks. In this case, you have to schedule the task when the component is attached, and cancel it when it is detached, like this: + +[source,java] +---- +@Override +protected void onAttach(AttachEvent attachEvent) { + var task = uiExecutor.scheduleAtFixedRate( + attachEvent.getUI().accessLater(() -> { + currentTimeLabel.setText(Instant.now().toString()); + }, null), 0, 1, TimeUnit.SECONDS + ); + addDetachListener(detachEvent -> task.cancel(true)); +} +---- + +This example schedules a task to be executed every second. The task sets the text of `currentTimeLabel` to the current date and time of the server. When the component is detached, the task is cancelled. + +== Virtual Threads + +If you use a Java version that supports virtual threads, you do not need to worry about setting up a thread pool. Just start up a new virtual thread whenever you need one, like this: + +[source,java] +---- +Thread.ofVirtual().start(UI.getCurrent().accessLater(() -> { + // Perform the UI operation here. +}, null)); +---- + +For scheduled tasks, you should still use a `ScheduledExecutorService`. From 8fcc11f6a70c30636c1dc340cdb79964e97b481a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petter=20Holmstr=C3=B6m?= Date: Thu, 10 Oct 2024 16:27:46 +0300 Subject: [PATCH 14/30] WIP --- .../background-jobs/interaction.adoc | 372 ------------------ .../interaction/callbacks.adoc | 191 +++++++++ .../background-jobs/interaction/futures.adoc | 69 ++++ .../background-jobs/interaction/index.adoc | 15 + .../background-jobs/interaction/reactive.adoc | 128 ++++++ .../server-push/callbacks.adoc | 2 +- .../server-push/endpoints.adoc | 33 ++ .../server-push/futures.adoc | 2 +- .../presentation-layer/server-push/index.adoc | 214 +--------- .../server-push/reactive.adoc | 140 ++++++- .../{ui-threads.adoc => threads.adoc} | 2 +- .../server-push/updates.adoc | 174 ++++++++ 12 files changed, 746 insertions(+), 596 deletions(-) delete mode 100644 articles/building-apps/application-layer/background-jobs/interaction.adoc create mode 100644 articles/building-apps/application-layer/background-jobs/interaction/callbacks.adoc create mode 100644 articles/building-apps/application-layer/background-jobs/interaction/futures.adoc create mode 100644 articles/building-apps/application-layer/background-jobs/interaction/index.adoc create mode 100644 articles/building-apps/application-layer/background-jobs/interaction/reactive.adoc create mode 100644 articles/building-apps/presentation-layer/server-push/endpoints.adoc rename articles/building-apps/presentation-layer/server-push/{ui-threads.adoc => threads.adoc} (99%) create mode 100644 articles/building-apps/presentation-layer/server-push/updates.adoc diff --git a/articles/building-apps/application-layer/background-jobs/interaction.adoc b/articles/building-apps/application-layer/background-jobs/interaction.adoc deleted file mode 100644 index b5080207d4..0000000000 --- a/articles/building-apps/application-layer/background-jobs/interaction.adoc +++ /dev/null @@ -1,372 +0,0 @@ ---- -title: UI Interaction -description: How to interact with jobs from the user interface. -order: 25 ---- - -= UI Interaction - -Some background jobs execute business processes in the background. The end user may see the result of the background job, but does not have to interact with the directly. Scheduled jobs and event triggered jobs typically fall in this category. - -Then there are jobs that need to interact with the user interface. For instance, the job may want to update a progress indicator while running, and notify the user when the job has completed, or an error has occurred. Furthermore, the user may want to be able to cancel a running job before it has completed. - -On this page, you'll learn different ways of doing this that work with both Flow and Hilla. - -== Callbacks [badge-flow]#Flow# - -If you are using a Flow user interface, the simplest way of allowing your background jobs to interact with it is through callbacks. - -You can use `Consumer`, `Runnable`, and `Supplier` as callback interfaces, depending on how you want to interact with the background job. - -[cols="1,1"] -|=== -|Event |Callback - -|Completed without a result -|`Runnable` - -|Completed with a result of type `T` -|`Consumer` - -|Completed with an exception -|`Consumer` - -|Reported percentage done -|`Consumer` - -|Cancelled by user -|`Supplier` - -|=== - -For example, a background job that returns a string or an exception could be implemented like this: - -[source,java] ----- -@Async -public void startBackgroundJob(Consumer onComplete, - Consumer onError) { - try { - var result = doSomethingThatTakesALongTime(); - onComplete.accept(result); - } catch (Exception ex) { - onError.accept(result); - } -} ----- - -If the background job is also reporting its progress, for instance as a percentage number, it could look like this: - -[source,java] ----- -@Async -public void startBackgroundJob(Consumer onComplete, - Consumer onProgress, - Consumer onError) { - try { - onProgress.apply(0.0); - - var step1Result = performStep1(); - onProgress.apply(0.25); - - var step2Result = performStep2(step1Result); - onProgress.apply(0.50); - - var step3Result = performStep3(step2Result); - onProgress.apply(0.75); - - var result = performStep4(step3Result); - onProgress.apply(1.0); - - onComplete.accept(result); - } catch (Exception ex) { - onError.accept(ex); - } -} ----- - -Furthermore, if the job can also be cancelled, it could look like this: - -[source,java] ----- -@Async -public void startBackgroundJob(Consumer onComplete, - Consumer onProgress, - Consumer onError, - Supplier isCancelled) { - try { - onProgress.apply(0.0); - - if (isCancelled.get()) { - return; - } - var step1Result = performStep1(); - onProgress.apply(0.25); - - if (isCancelled.get()) { - return; - } - var step2Result = performStep2(step1Result); - onProgress.apply(0.50); - - if (isCancelled.get()) { - return; - } - var step3Result = performStep3(step2Result); - onProgress.apply(0.75); - - if (isCancelled.get()) { - return; - } - var result = performStep4(step3Result); - onProgress.apply(1.0); - - onComplete.accept(result); - } catch (Exception ex) { - onError.accept(ex); - } -} ----- - -All the callbacks have to be thread-safe, as they are called from the background thread, but owned and created by the user interface. For more information about how to implement these callbacks, see the <<{articles}/building-apps/presentation-layer/server-push/callbacks#,Server Push - Callbacks>> documentation page. - -=== Improving Cancel API - -If you want to make the cancelling API nicer, you can replace the callback with a handle. First, create a handle interface that the user interface can use to cancel the job: - -[source,java] ----- -@FunctionalInterface -public interface CancellableJob { - void cancel(); -} ----- - -Next, implement the service method like this: - -[source,java] ----- -public CancellableJob startBackgroundJob(Consumer onComplete, - Consumer onProgress - Consumer onError) { - var cancelled = new AtomicBoolean(false); - taskExecutor.execute(() -> { - try { - onProgress.apply(0.0); - - if (cancelled.get()) { - return; - } - var step1Result = performStep1(); - onProgress.apply(0.25); - - if (cancelled.get()) { - return; - } - var step2Result = performStep2(step1Result); - onProgress.apply(0.50); - - if (cancelled.get()) { - return; - } - var step3Result = performStep3(step2Result); - onProgress.apply(0.75); - - if (cancelled.get()) { - return; - } - var result = performStep4(step3Result); - onProgress.apply(1.0); - - onComplete.accept(result); - } catch (Exception ex) { - onError.accept(result); - } - }); - return () -> cancelled.set(true); -} ----- - -The user interface would have to store the handle while the job is running, and call the `cancel()` method to cancel it. Note, that you cannot use the `@Async` annotation in this case. This is because `@Async` methods can only return `void` or future-like types. In this case, you want to return neither. - -The handle itself is thread safe because you are using an `AtomicBoolean`. You do not need to take any special precautions to call it from the user interface. - -== CompletableFuture [badge-flow]#Flow# - -If you are using a Flow user interface, you can use a standard Java `CompletableFuture` to report results and errors to the user interface. You can also use it to cancel the job from the user interface. For reporting progress, however, you still need to use a callback. - -The advantage of working with `CompletableFuture` is that Spring has built-in support for them when using the `@Async` annotation. For example, a background job that completes with either a string or an exception could be implemented like this: - -[source,java] ----- -@Async -public CompletableFuture startBackgroundJob() { - return CompletableFuture.completedFuture(doSomethingThatTakesALongTime()); -} ----- - -If the `doSomethingThatTakesALongTime()` method throws an exception, Spring automatically returns a `CompletableFuture` with the exception in question. - -To update the user interface, you have to add special completion stages that execute after the `CompletableFuture` completes. For more information about how to add these, see the <<{articles}/building-apps/presentation-layer/server-push/futures#,Server Push - Futures>> documentation page. - -=== Cancelling - -You can cancel a Java `Future` by calling its `cancel()` method. The method has a `boolean` parameter that indicates whether the thread should be interrupted or not. However, `CompletableFuture`, which implements `Future`, does not use this parameter. It therefore does not make any difference whether you pass in `true` or `false`. - -When you cancel a `CompletableFuture`, it completes with a `CompletionException` caused by a `CancellationException`. However, the job continues to silently run in the background until it has finished. If you want to notify the job itself that it has been cancelled, and should stop running at the next suitable moment, you have to make some changes. - -`CompletableFuture` has an `isCancelled()` method that you can use to query whether the job has been cancelled or not. However, do to this, you cannot use the `@Async` annotation anymore. Instead, you have to manually execute the job using the `TaskExecutor`, and manage the state of the returned `CompletableFuture`. The principle is the same as when you are using callbacks or handles. - -The earlier example would look like this when implemented using a `CompletableFuture`: - -[source,java] ----- -public CompletableFuture startBackgroundJob() { - var future = new CompletableFuture(); - taskExecutor.execute(() -> { - try { - var step1Result = performStep1(); - - if (future.isCancelled()) { - return; - } - var step2Result = performStep2(step1Result); - - if (future.isCancelled()) { - return; - } - var step3Result = performStep3(step2Result); - - if (future.isCancelled()) { - return; - } - var result = performStep4(step3Result); - future.complete(result); - } catch (Exception ex) { - future.completeExceptionally(ex); - } - }); - return future; -} ----- - -You do not need to do anything with the `future` after it has been cancelled, as it has already been completed. Returning is enough. - -== Flux and Mono - -If you are using Flow or Hilla to build your user interface, you can use `Flux` or `Mono` from https://projectreactor.io/[Reactor] to allow your background jobs to interact with them. Reactor has an extensive API, which means you can do a lot of things with it. This also means that it has a steeper learning curve than using callbacks or `CompletableFuture`. If you are new to reactive programming, you should read Reactor's https://projectreactor.io/docs/core/release/reference/#intro-reactive[Introduction to Reactive Programming] before continuing. - -When you are using Reactor, you cannot use the `@Async` annotation. Instead, you have to manually instruct your `Mono` or `Flux` to execute using the Spring `TaskExecutor`. Otherwise, your job executes in the thread that subscribes to the `Mono` or `Flux`. - -For example, a background job that returns a string or an exception could be implemented like this: - -[source,java] ----- -public Mono startBackgroundJob() { - return Mono.fromSupplier(this::doSomethingThatTakesALongTime) - .subscribeOn(Schedulers.fromExecutor(taskExecutor)); -} ----- - -If the `doSomethingThatTakesALongTime()` method throws an exception, the `Mono` terminates with an error. - -To update the user interface, you have to subscribe to the `Mono` or `Flux`. For more information about how to do this, see the <<{articles}/building-apps/presentation-layer/server-push/reactive#,Server Push - Reactive Streams>> documentation page. - -[IMPORTANT] -Hilla only supports `Flux`, so if your job is returning a `Mono`, you have to convert it to a `Flux` inside your `@BrowserCallable` endpoint. You can do this by calling the `Mono.flux()` method. - -=== Reporting Progress - -If your background job only needs to report its progress without actually returning a result, you can return a `Flux`. Your job should then emit progress updates, and complete the stream when done. However, you often also want to return a result. Since Hilla only supports returning a single `Flux`, you have to use the same stream for emitting both progress updates and the end result. The code may be a bit messy, but it works. - -You first need to create a data type that can contain both progress updates and the result. For a job that returns a string, it could look like this: - -[source,java] ----- -import com.vaadin.hilla.Nullable; - -public record BackgroundJobOutput( - @Nullable Double progressUpdate, - @Nullable String result -) { - public static BackgroundJobOutput progressUpdate(double progressUpdate) { - return new BackgroundJobOutput(progressUpdate, null); - } - - public static BackgroundJobOutput finished(String result) { - return new BackgroundJobOutput(null, result); - } -} ----- - -The two factory methods `progressUpdate()` and `finished()` make the code look better when it is time to create instances of `BackgroundJobOutput`. - -[NOTE] -If you have worked with sealed classes, you may be tempted to create a sealed interface called `BackgroundJobOutput`, and then create two records that implement that interface: one for progress updates, and another for the result. However, Hilla does not support this at the moment. - -Next, you have to implement the background job like this: - -[source,java] ----- -private String doSomethingThatTakesALongTime(Consumer onProgress) { - ... -} - -public Flux startBackgroundJob() { - Sinks.Many progressUpdates = Sinks // <1> - .many() - .unicast() - .onBackpressureError(); - - var result = Mono // <2> - .fromSupplier(() -> doSomethingThatTakesALongTime( - progressUpdates::tryEmitNext - )) - .subscribeOn(Schedulers.fromExecutor(taskExecutor)); - - return Flux.merge( // <3> - progress.asFlux().map(BackgroundJobOutput::progressUpdate), - result.map(BackgroundJobOutput::finished) - ); -} ----- -<1> Create a sink that you can emit progress updates to. -<2> Create a `Mono` that emits the result of the background job. -<3> Map both streams to `BackgroundJobOutput` and merge them. - -When your user interface subscribes to this `Flux`, it needs to check the state of the returned `BackgroundJobOutput` objects. If `progressUpdate` contains a value, it should update the progress indicator. If `result` contains a value, the operation is finished. - -=== Cancelling - -You can cancel a subscription to a `Flux` or `Mono` at any time. However, as with `CompletableFuture`, cancelling the subscription does not stop the background job itself. To fix this, you need to tell the background job when it has been cancelled, so that it can stop itself. Continuing on the earlier example, adding support for cancelling could look like this: - -[source,java] ----- -private String doSomethingThatTakesALongTime( - Consumer onProgress, - Supplier isCancelled) { - ... -} - -public Flux startBackgroundJob() { - var cancelled = new AtomicBoolean(false); - Sinks.Many progressUpdates = Sinks - .many() - .unicast() - .onBackpressureError(); - - var result = Mono - .fromSupplier(() -> doSomethingThatTakesALongTime( - progressUpdates::tryEmitNext, cancelled::get - )) - .doOnCancel(() -> cancelled.set(true)) - .subscribeOn(Schedulers.fromExecutor(taskExecutor)); - - return Flux.merge( - progress.asFlux().map(BackgroundJobOutput::progressUpdate), - result.map(BackgroundJobOutput::finished) - ); -} ----- - -If the user interface cancels the subscription, the `cancelled` flag becomes `true`, and the job stops executing at its next iteration. diff --git a/articles/building-apps/application-layer/background-jobs/interaction/callbacks.adoc b/articles/building-apps/application-layer/background-jobs/interaction/callbacks.adoc new file mode 100644 index 0000000000..76b543422d --- /dev/null +++ b/articles/building-apps/application-layer/background-jobs/interaction/callbacks.adoc @@ -0,0 +1,191 @@ +--- +title: Callbacks +description: How to use callbacks to iteract with the user interface. +order: 10 +section-nav: badge-flow +--- + += Callbacks [badge-flow]#Flow# + +If you are using a Flow user interface, the simplest way of allowing your background jobs to interact with it is through callbacks. + +You can use `Consumer`, `Runnable`, and `Supplier` as callback interfaces, depending on how you want to interact with the background job. + +[cols="1,1"] +|=== +|Event |Callback + +|Completed without a result +|`Runnable` + +|Completed with a result of type `T` +|`Consumer` + +|Completed with an exception +|`Consumer` + +|Reported percentage done +|`Consumer` + +|Cancelled by user +|`Supplier` + +|=== + +== Returning a Result + +For example, a background job that returns a string or an exception could be implemented like this: + +[source,java] +---- +@Async +public void startBackgroundJob(Consumer onComplete, + Consumer onError) { + try { + var result = doSomethingThatTakesALongTime(); + onComplete.accept(result); + } catch (Exception ex) { + onError.accept(result); + } +} +---- + +== Reporting Progress + +If the background job is also reporting its progress, for instance as a percentage number, it could look like this: + +[source,java] +---- +@Async +public void startBackgroundJob(Consumer onComplete, + Consumer onProgress, + Consumer onError) { + try { + onProgress.apply(0.0); + + var step1Result = performStep1(); + onProgress.apply(0.25); + + var step2Result = performStep2(step1Result); + onProgress.apply(0.50); + + var step3Result = performStep3(step2Result); + onProgress.apply(0.75); + + var result = performStep4(step3Result); + onProgress.apply(1.0); + + onComplete.accept(result); + } catch (Exception ex) { + onError.accept(ex); + } +} +---- + +== Cancelling + +Furthermore, if the job can also be cancelled, it could look like this: + +[source,java] +---- +@Async +public void startBackgroundJob(Consumer onComplete, + Consumer onProgress, + Consumer onError, + Supplier isCancelled) { + try { + onProgress.apply(0.0); + + if (isCancelled.get()) { + return; + } + var step1Result = performStep1(); + onProgress.apply(0.25); + + if (isCancelled.get()) { + return; + } + var step2Result = performStep2(step1Result); + onProgress.apply(0.50); + + if (isCancelled.get()) { + return; + } + var step3Result = performStep3(step2Result); + onProgress.apply(0.75); + + if (isCancelled.get()) { + return; + } + var result = performStep4(step3Result); + onProgress.apply(1.0); + + onComplete.accept(result); + } catch (Exception ex) { + onError.accept(ex); + } +} +---- + +All the callbacks have to be thread-safe, as they are called from the background thread, but owned and created by the user interface. For more information about how to implement these callbacks, see the <<{articles}/building-apps/presentation-layer/server-push/callbacks#,Server Push - Callbacks>> documentation page. + +=== Improving Cancel API + +If you want to make the cancelling API nicer, you can replace the callback with a handle. First, create a handle interface that the user interface can use to cancel the job: + +[source,java] +---- +@FunctionalInterface +public interface CancellableJob { + void cancel(); +} +---- + +Next, implement the service method like this: + +[source,java] +---- +public CancellableJob startBackgroundJob(Consumer onComplete, + Consumer onProgress + Consumer onError) { + var cancelled = new AtomicBoolean(false); + taskExecutor.execute(() -> { + try { + onProgress.apply(0.0); + + if (cancelled.get()) { + return; + } + var step1Result = performStep1(); + onProgress.apply(0.25); + + if (cancelled.get()) { + return; + } + var step2Result = performStep2(step1Result); + onProgress.apply(0.50); + + if (cancelled.get()) { + return; + } + var step3Result = performStep3(step2Result); + onProgress.apply(0.75); + + if (cancelled.get()) { + return; + } + var result = performStep4(step3Result); + onProgress.apply(1.0); + + onComplete.accept(result); + } catch (Exception ex) { + onError.accept(result); + } + }); + return () -> cancelled.set(true); +} +---- + +The user interface would have to store the handle while the job is running, and call the `cancel()` method to cancel it. Note, that you cannot use the `@Async` annotation in this case. This is because `@Async` methods can only return `void` or future-like types. In this case, you want to return neither. + +The handle itself is thread safe because you are using an `AtomicBoolean`. You do not need to take any special precautions to call it from the user interface. diff --git a/articles/building-apps/application-layer/background-jobs/interaction/futures.adoc b/articles/building-apps/application-layer/background-jobs/interaction/futures.adoc new file mode 100644 index 0000000000..2d6d582006 --- /dev/null +++ b/articles/building-apps/application-layer/background-jobs/interaction/futures.adoc @@ -0,0 +1,69 @@ +--- +title: Futures +description: How to use `CompletableFuture` to iteract with the user interface. +order: 20 +section-nav: badge-flow +--- + += Futures [badge-flow]#Flow# + +If you are using a Flow user interface, you can use a standard Java `CompletableFuture` to report results and errors to the user interface. You can also use it to cancel the job from the user interface. For reporting progress, however, you still need to use a callback. + +== Returning a Result + +The advantage of working with `CompletableFuture` is that Spring has built-in support for them when using the `@Async` annotation. For example, a background job that completes with either a string or an exception could be implemented like this: + +[source,java] +---- +@Async +public CompletableFuture startBackgroundJob() { + return CompletableFuture.completedFuture(doSomethingThatTakesALongTime()); +} +---- + +If the `doSomethingThatTakesALongTime()` method throws an exception, Spring automatically returns a `CompletableFuture` with the exception in question. + +To update the user interface, you have to add special completion stages that execute after the `CompletableFuture` completes. For more information about how to add these, see the <<{articles}/building-apps/presentation-layer/server-push/futures#,Server Push - Futures>> documentation page. + +== Cancelling + +You can cancel a Java `Future` by calling its `cancel()` method. The method has a `boolean` parameter that indicates whether the thread should be interrupted or not. However, `CompletableFuture`, which implements `Future`, does not use this parameter. It therefore does not make any difference whether you pass in `true` or `false`. + +When you cancel a `CompletableFuture`, it completes with a `CompletionException` caused by a `CancellationException`. However, the job continues to silently run in the background until it has finished. If you want to notify the job itself that it has been cancelled, and should stop running at the next suitable moment, you have to make some changes. + +`CompletableFuture` has an `isCancelled()` method that you can use to query whether the job has been cancelled or not. However, do to this, you cannot use the `@Async` annotation anymore. Instead, you have to manually execute the job using the `TaskExecutor`, and manage the state of the returned `CompletableFuture`. The principle is the same as when you are using callbacks or handles. + +The earlier example would look like this when implemented using a `CompletableFuture`: + +[source,java] +---- +public CompletableFuture startBackgroundJob() { + var future = new CompletableFuture(); + taskExecutor.execute(() -> { + try { + var step1Result = performStep1(); + + if (future.isCancelled()) { + return; + } + var step2Result = performStep2(step1Result); + + if (future.isCancelled()) { + return; + } + var step3Result = performStep3(step2Result); + + if (future.isCancelled()) { + return; + } + var result = performStep4(step3Result); + future.complete(result); + } catch (Exception ex) { + future.completeExceptionally(ex); + } + }); + return future; +} +---- + +You do not need to do anything with the `future` after it has been cancelled, as it has already been completed. Returning is enough. \ No newline at end of file diff --git a/articles/building-apps/application-layer/background-jobs/interaction/index.adoc b/articles/building-apps/application-layer/background-jobs/interaction/index.adoc new file mode 100644 index 0000000000..8d3735930b --- /dev/null +++ b/articles/building-apps/application-layer/background-jobs/interaction/index.adoc @@ -0,0 +1,15 @@ +--- +title: UI Interaction +description: How to interact with jobs from the user interface. +order: 25 +--- + += UI Interaction + +Some background jobs execute business processes in the background. The end user may see the result of the background job, but does not have to interact with the directly. Scheduled jobs and event triggered jobs typically fall in this category. + +Then there are jobs that need to interact with the user interface. For instance, the job may want to update a progress indicator while running, and notify the user when the job has completed, or an error has occurred. Furthermore, the user may want to be able to cancel a running job before it has completed. + +== Options + +section_outline::[] diff --git a/articles/building-apps/application-layer/background-jobs/interaction/reactive.adoc b/articles/building-apps/application-layer/background-jobs/interaction/reactive.adoc new file mode 100644 index 0000000000..eb08919e2e --- /dev/null +++ b/articles/building-apps/application-layer/background-jobs/interaction/reactive.adoc @@ -0,0 +1,128 @@ +--- +title: Reactive Streams +description: How to use reactive streams to interact with the user interface. +order: 30 +--- + +// TODO This page is about returning results from background threads. You can also use reactive streams for broadcasting, but that is a different use case. This should be covered in another documentation page, and linked to from here. + += Reactive Streams + +If you are using Flow or Hilla to build your user interface, you can use `Flux` or `Mono` from https://projectreactor.io/[Reactor] to allow your background jobs to interact with them. Reactor has an extensive API, which means you can do a lot of things with it. This also means that it has a steeper learning curve than using callbacks or `CompletableFuture`. If you are new to reactive programming, you should read Reactor's https://projectreactor.io/docs/core/release/reference/#intro-reactive[Introduction to Reactive Programming] before continuing. + +== Returning a Result + +When you are using Reactor, you cannot use the `@Async` annotation. Instead, you have to manually instruct your `Mono` or `Flux` to execute using the Spring `TaskExecutor`. Otherwise, your job executes in the thread that subscribes to the `Mono` or `Flux`. + +For example, a background job that returns a string or an exception could be implemented like this: + +[source,java] +---- +public Mono startBackgroundJob() { + return Mono.fromSupplier(this::doSomethingThatTakesALongTime) + .subscribeOn(Schedulers.fromExecutor(taskExecutor)); +} +---- + +If the `doSomethingThatTakesALongTime()` method throws an exception, the `Mono` terminates with an error. + +To update the user interface, you have to subscribe to the `Mono` or `Flux`. For more information about how to do this, see the <<{articles}/building-apps/presentation-layer/server-push/reactive#,Server Push - Reactive Streams>> documentation page. + +[IMPORTANT] +Hilla only supports `Flux`, so if your job is returning a `Mono`, you have to convert it to a `Flux` inside your `@BrowserCallable` endpoint. You can do this by calling the `Mono.flux()` method. + +== Reporting Progress + +If your background job only needs to report its progress without actually returning a result, you can return a `Flux`. Your job should then emit progress updates, and complete the stream when done. However, you often also want to return a result. Since Hilla only supports returning a single `Flux`, you have to use the same stream for emitting both progress updates and the end result. The code may be a bit messy, but it works. + +You first need to create a data type that can contain both progress updates and the result. For a job that returns a string, it could look like this: + +[source,java] +---- +import com.vaadin.hilla.Nullable; + +public record BackgroundJobOutput( + @Nullable Double progressUpdate, + @Nullable String result +) { + public static BackgroundJobOutput progressUpdate(double progressUpdate) { + return new BackgroundJobOutput(progressUpdate, null); + } + + public static BackgroundJobOutput finished(String result) { + return new BackgroundJobOutput(null, result); + } +} +---- + +The two factory methods `progressUpdate()` and `finished()` make the code look better when it is time to create instances of `BackgroundJobOutput`. + +[NOTE] +If you have worked with sealed classes, you may be tempted to create a sealed interface called `BackgroundJobOutput`, and then create two records that implement that interface: one for progress updates, and another for the result. However, Hilla does not support this at the moment. + +Next, you have to implement the background job like this: + +[source,java] +---- +private String doSomethingThatTakesALongTime(Consumer onProgress) { + ... +} + +public Flux startBackgroundJob() { + Sinks.Many progressUpdates = Sinks // <1> + .many() + .unicast() + .onBackpressureError(); + + var result = Mono // <2> + .fromSupplier(() -> doSomethingThatTakesALongTime( + progressUpdates::tryEmitNext + )) + .subscribeOn(Schedulers.fromExecutor(taskExecutor)); + + return Flux.merge( // <3> + progress.asFlux().map(BackgroundJobOutput::progressUpdate), + result.map(BackgroundJobOutput::finished) + ); +} +---- +<1> Create a sink that you can emit progress updates to. +<2> Create a `Mono` that emits the result of the background job. +<3> Map both streams to `BackgroundJobOutput` and merge them. + +When your user interface subscribes to this `Flux`, it needs to check the state of the returned `BackgroundJobOutput` objects. If `progressUpdate` contains a value, it should update the progress indicator. If `result` contains a value, the operation is finished. + +== Cancelling + +You can cancel a subscription to a `Flux` or `Mono` at any time. However, as with `CompletableFuture`, cancelling the subscription does not stop the background job itself. To fix this, you need to tell the background job when it has been cancelled, so that it can stop itself. Continuing on the earlier example, adding support for cancelling could look like this: + +[source,java] +---- +private String doSomethingThatTakesALongTime( + Consumer onProgress, + Supplier isCancelled) { + ... +} + +public Flux startBackgroundJob() { + var cancelled = new AtomicBoolean(false); + Sinks.Many progressUpdates = Sinks + .many() + .unicast() + .onBackpressureError(); + + var result = Mono + .fromSupplier(() -> doSomethingThatTakesALongTime( + progressUpdates::tryEmitNext, cancelled::get + )) + .doOnCancel(() -> cancelled.set(true)) + .subscribeOn(Schedulers.fromExecutor(taskExecutor)); + + return Flux.merge( + progress.asFlux().map(BackgroundJobOutput::progressUpdate), + result.map(BackgroundJobOutput::finished) + ); +} +---- + +If the user interface cancels the subscription, the `cancelled` flag becomes `true`, and the job stops executing at its next iteration. diff --git a/articles/building-apps/presentation-layer/server-push/callbacks.adoc b/articles/building-apps/presentation-layer/server-push/callbacks.adoc index df9104d921..01fdc7f5e5 100644 --- a/articles/building-apps/presentation-layer/server-push/callbacks.adoc +++ b/articles/building-apps/presentation-layer/server-push/callbacks.adoc @@ -7,7 +7,7 @@ section-nav: badge-flow = Callbacks [badge-flow]#Flow# -If you are building the user interface with Flow, you can use <<{articles}/building-apps/application-layer/background-jobs/interaction#callbacks-flow,callbacks>> to allow a background thread to update the user interface. +If you are building the user interface with Flow, you can use <<{articles}/building-apps/application-layer/background-jobs/interaction/callbacks#,callbacks>> to allow a background thread to update the user interface. Whenever you implement a callback, you have remember that the callback is called by the background thread. This means that any updates to the user interface must happen inside a call to `UI.access()`. However, since you are using `Runnable` and `Consumer` callbacks, you can use `UI.accessLater()`. diff --git a/articles/building-apps/presentation-layer/server-push/endpoints.adoc b/articles/building-apps/presentation-layer/server-push/endpoints.adoc new file mode 100644 index 0000000000..3b11a01748 --- /dev/null +++ b/articles/building-apps/presentation-layer/server-push/endpoints.adoc @@ -0,0 +1,33 @@ +--- +title: Endpoints +description: How to create reactive endpoints for your Hilla user interface. +order: 39 +section-nav: badge-hilla +--- + += Reactive Endpoints [badge-hilla]#Hilla# + +// TODO This text assumes that browser callable endpoints have already been explained earlier. + +If you are building your user interface with Hilla, you use reactive endpoints to push messages from the server to the browser. A reactive endpoint is an endpoint that returns a `Flux` from https://projectreactor.io/[Reactor]. For example, an endpoint that emits the current date and time every second could look like this: + +[source,java] +---- +@BrowserCallable +public class TimeEndpoint { + + @AnonymousAllowed + public Flux<@Nonnull String> getClock() { + return Flux.interval(Duration.ofSeconds(1)) // <1> + .onBackpressureDrop() // <2> + .map(_interval -> Instant.now().toString()); // <3> + } +} +---- +<1> Emit a new message every second. +<2> Drop any messages that for some reason cannot be sent to the client in time. +<3> Output the current date and time as a string. + +Hilla generates the necessary TypeScript types to subscribe to this endpoint from the browser. + +For more information about updating the UI using a reactive endpoint, see the <> documentation page. \ No newline at end of file diff --git a/articles/building-apps/presentation-layer/server-push/futures.adoc b/articles/building-apps/presentation-layer/server-push/futures.adoc index 2838a0e660..52b6b7876e 100644 --- a/articles/building-apps/presentation-layer/server-push/futures.adoc +++ b/articles/building-apps/presentation-layer/server-push/futures.adoc @@ -7,7 +7,7 @@ section-nav: badge-flow = Futures [badge-flow]#Flow# -If you are building the user interface with Flow, a background thread can use the standard Java <<{articles}/building-apps/application-layer/background-jobs/interaction#completablefuture-flow,`CompletableFuture`>> to inform the user interface of results and errors. +If you are building the user interface with Flow, a background thread can use the standard Java <<{articles}/building-apps/application-layer/background-jobs/interaction/futures#,`CompletableFuture`>> to inform the user interface of results and errors. In fact, you are using <> in this approach as well, but instead of calling them directly from your background thread, you are registering them with `CompletableFuture`. However, the principle is the same: create private methods that can be passed as `Consumers` to `UI.accessLater()`. For example, a method for handling successful completion could look like this: diff --git a/articles/building-apps/presentation-layer/server-push/index.adoc b/articles/building-apps/presentation-layer/server-push/index.adoc index b3fb629de7..4db49ebee4 100644 --- a/articles/building-apps/presentation-layer/server-push/index.adoc +++ b/articles/building-apps/presentation-layer/server-push/index.adoc @@ -37,216 +37,6 @@ public class Application implements AppShellConfigurator { // TODO Transport modes? Or is that something for the reference material. -== Updating the UI [badge-flow]#Flow# +== Topics -Whenever you are using server push in Flow, you are triggering it from another thread than the normal HTTP request thread. Making changes to a UI from another thread and pushing them to the browser requires locking the user session. Otherwise, the UI update done from another thread could conflict with a regular event-driven update and cause either data corruption or deadlocks. Because of this, you may only access a UI using the `UI.access()` method, which locks the session to prevent conflicts. You use it like this: - -[source,java] ----- -ui.access(() -> { - // Update your UI here -}); ----- - -By default, Flow uses automatic pushing. This means that any pending changes are pushed to the browser automatically after `UI.access()` finishes. You can also configure Flow to use manual pushing. This gives you more control over when changes are pushed to the browser. For example, you can push multiple times inside a single call to `UI.access()`. - -To enable manual pushing, you have to make an addition to the `@Push` annotation, like this: - -[source,java] ----- -@Push(PushMode.MANUAL) -public class Application implements AppShellConfigurator { - ... -} ----- - -After this, you have to call the `UI.push()` method whenever you want to push your changes to the browser, like this: - -[source,java] ----- -ui.access(() -> { - // Update your UI here - ui.push(); -}); ----- - -=== Getting the UI Instance - -// TODO This assumes that the UI has been explained earlier, and what attach and detach means. - -Before you can call `access()`, you need to get the `UI` instance. You typically use `Component.getUI()` or `UI.getCurrent()` for this. However, both are problematic when it comes to server push. - -`Component.getUI()` is not thread-safe, which means you should only call it while the user session is locked. Therefore, you cannot use it to call `access()`. - -`UI.getCurrent()` only returns a UI whenever the session is locked. Therefore, you cannot use it to call `access()`, either. - -Whenever you are planning to use server push, you have to get a hold of the `UI` instance _while the user session is locked_. This typically happens right before you start your background thread, for example: - -[source,java] ----- -var ui = UI.getCurrent(); // <1> -taskExecutor.execute(() -> { - // Do your work here - ui.access(() -> { // <2> - // Update your UI here - }); -}); ----- -<1> This is executed in an HTTP request thread. The user session is locked and `UI.getCurrent()` returns the current `UI`-instance. -<2> This is executed in the background thread. `UI.getCurrent()` returns `null`, but the `UI` instance is stored in a local variable. - -=== Access Later - -You often use server push in various types of event listeners and <>. A background job might inform you that it has finished processing, like this: - -[source,java] ----- -var ui = UI.getCurrent(); -myService.startBackgroundJob(() -> ui.access(() -> { - // Update your UI here when the job is finished -})); ----- - -Or an event bus might inform you that a new message has arrived, like this: - -[source,java] ----- -var ui = UI.getCurrent(); -var subscription = myEventBus.subscribe((message) -> ui.access(() -> { - // Update your UI here when a message has arrived -})); ----- - -Whenever these event listeners or callbacks conform to the `Runnable` or `Consumer` functional interfaces, you should consider using `UI.accessLater()` instead of `UI.access()`. - -`UI.accessLater()` exists in two versions: one that wraps a `Runnable`, and another that wraps a `Consumer`. It stores the `UI` instance, and runs the wrapped delegate inside a call to `UI.access()`. It also takes a second parameter, which is a _detach handler_. The detach handler is a `Runnable` that runs if the `UI` has been detached when `UI.access()` is called. The detach handler can be `null` if no special actions are needed. - -Rewritten with `accessLater()`, the thread completion example becomes: - -[source,java] ----- -myService.startBackgroundJob(UI.getCurrent().accessLater(() -> { - // Update your UI here when the job is finished. -}, null)); // <1> ----- -<1> No detach handler is needed. - -Likewise, the event listener becomes: - -[source,java] ----- -var subscription = myEventBus.subscribe(UI.getCurrent().accessLater((message) -> { - // Update your UI here when a message has arrived -}, null)); // <1> ----- -<1> No detach handler is needed. - -=== Avoiding Memory Leaks - -When you are using server push to update the user interface when an event has occurred, you typically subscribe to some broadcaster or event bus. When you do this, you have to remember to always unsubscribe when the UI is detached. Otherwise, you end up with a memory leak that prevents your UI from being garbage collected. - -It is recommended to always subscribe when your view is attached to a UI, and unsubscribe when it is detached. You can do this by overriding the `Component.onAttach()` method, like this: - -[source,java] ----- -@Override -protected void onAttach(AttachEvent attachEvent) { // <1> - var subscription = myEventBus.subscribe(attachEvent.getUI().accessLater((message) -> { // <2> - // Update your UI here when a message has arrived - }, null)); - addDetachListener(detachEvent -> subscription.unsubscribe()); // <3> -} ----- -<1> Subscribe when the view is attached to a UI. -<2> Get the `UI` from the `AttachEvent`. -<3> Unsubscribe when the view is detached from the UI. - -=== Avoiding Floods - -Another risk you have to manage when updating the user interface in response to events is flooding the user interface with updates. As a rule of thumb, you should not push more than 2--4 times per second. Pushing more often than that can cause performance issues. Also, there is a limit to how many updates the human eye is able to detect per second. - -If you know the events are coming in at a pace no faster than 2--4 events per second, you can push on every event. However, if they are more frequent, you have to start buffering events and update the user interface in batches. This is quite easy to do if you are using a `Flux` from https://projectreactor.io/[Reactor] to emit the events: - -[source,java] ----- -@Override -protected void onAttach(AttachEvent attachEvent) { - var subscription = myEventBus - .asFlux() // <1> - .buffer(Duration.ofMillis(250)) // <2> - .subscribe(attachEvent.getUI().accessLater((messageList) -> { // <3> - // Update your UI here when a list of messages has arrived - }, null)); - addDetachListener(detachEvent -> subscription.dispose()); -} ----- -<1> This assumes you can access the stream of messages through a `Flux`. -<2> Buffer incoming messages for 250 milliseconds before pushing. -<3> Instead of reacting to a single message, you are now reacting to a list of messages. - -The buffering duration depends on the size of the UI update, and the network latency. In some applications, you may need to use a longer buffer duration, in others, a shorter one might work. You should try various durations and see what works best for your application. - -// TODO Add link to subpage about reactive streams and move the code example there. - -=== Avoiding Unnecessary Pushes - -The `UI.access()` method updates the user interface asynchronously. The update operation is not executed right away, but added to a queue and executed at some point in the future. If this is combined with regular event-driven updates in the HTTP request thread, you may end up in a situation where the user interface is updated out-of-order. Look at this example: - -[source,java] ----- -var button = new Button("Test Me", event -> { - UI.getCurrent().access(() -> { - add(new Div("This
is added from within a call to UI.access()")); - }); - add(new Div("This
is added from an event listener")); -}); -add(button); ----- - -If you click the button, the user interface looks like this: - -[source] ----- -This
is added from an event listener -This
is added from within a call to UI.access() ----- - -In this particular case, the call to `UI.access()` would not have been needed. Sometimes, you can deduce this by looking at the code. However, there are situations where this is not clear. You may have code that sometimes is executed by the HTTP request thread, and sometimes by another thread. In this case, you can check whether the current thread has locked the user session or not, like this: - -[source,java] ----- -if (ui.getSession().hasLock()) { - // Update the UI without calling UI.access() -} else { - ui.access(() -> { - // Update the UI inside UI.access() - }); -} ----- - -// TODO Consider showing an example of a UIRunner that takes a Runnable or Consumer, performs the check, and calls it directly or inside UI.access(). - -== Reactive Endpoints [badge-hilla]#Hilla# - -// TODO This text assumes that browser callable endpoints have already been explained earlier. - -If you are building your user interface with Hilla, you use reactive endpoints to push messages from the server to the browser. A reactive endpoint is an endpoint that returns a `Flux` from https://projectreactor.io/[Reactor]. For example, an endpoint that emits the current date and time every second could look like this: - -[source,java] ----- -@BrowserCallable -public class TimeEndpoint { - - @AnonymousAllowed - public Flux<@Nonnull String> getClock() { - return Flux.interval(Duration.ofSeconds(1)) // <1> - .onBackpressureDrop() // <2> - .map(_interval -> Instant.now().toString()); // <3> - } -} ----- -<1> Emit a new message every second. -<2> Drop any messages that for some reason cannot be sent to the client in time. -<3> Output the current date and time as a string. - -Hilla generates the necessary TypeScript types to subscribe to this endpoint from the browser. The push connection is automatically established when the client subscribes. For more information about updating the UI using a reactive endpoint, see the <> documentation page. \ No newline at end of file +section_outline::[] diff --git a/articles/building-apps/presentation-layer/server-push/reactive.adoc b/articles/building-apps/presentation-layer/server-push/reactive.adoc index 3b5b6a9d84..3395cdfa87 100644 --- a/articles/building-apps/presentation-layer/server-push/reactive.adoc +++ b/articles/building-apps/presentation-layer/server-push/reactive.adoc @@ -4,21 +4,143 @@ description: How to use server push with reactive streams. order: 40 --- -// TODO Write the text first, and then worry about how to group the texts later. Hilla and Flow on the same page, or on different pages? - = Reactive Streams -If you are building the user interface with either Flow or Hilla, you can use reactive streams to allow a background thread to update the user interface. Reactive streams are also useful for broadcasts, where .... CONTINUE HERE +If you are building the user interface with either Flow or Hilla, you can use <<{articles}/building-apps/application-layer/background-jobs/interaction/reactive#,reactive streams>> to allow a background thread to update the user interface. + +== Subscribing + +Background threads typically use cold streams for output. A cold stream starts emitting values when the client subscribes to it, and then completes. + +Broadcasts typically use hot streams for output. A hot stream emits values regardless of whether a client is subscribed or not. A subscriber only receives the values that were emitted while it was subscribed. + +In your user interfaces, you typically do not need to worry about unsubscribing from cold streams, as they are often short lived. However, if you subscribe to a hot stream, it is important that you remember to unsubscribe when no longer needed. + +=== Subscribing in Flow [badge-flow]#Flow# + +In Flow, you can use both `Flux` and `Mono`. You subscribe to both by calling the `subscribe()` method. The method takes a <> that is called for each emitted value. You should implement the callback as a private method, and then wrap it inside `UI.accessLater()` when you subscribe. + +For example, if you use a `Mono` to receive the output of a background job, you could do this: + +[source,java] +---- +private void onJobCompleted(String result) { + Notification.show("Job completed: " + result); +} + +private void startJob() { + var ui = UI.getCurrent(); + service.startBackgroundJob().subscribe(ui.accessLater(this::onJobCompleted, null)); +} +---- + +In this example, you are dealing with a cold stream and so, you do not need to explicitly unsubscribe from it. + +If you use a `Flux` to receive chat messages, you could do this: + +[source,java] +---- +private void onMessageReceived(ChatMessage message) { + // Add the message to a message list +} + +@Override +protected void onAttach(AttachEvent attachEvent) { + var subscription = service.messages() + .subscribe(attachEvent.getUI().accessLater(this::onMessageReceived, null)); + addDetachListener(detachEvent -> subscription.dispose()); +} +---- + +In this example, you are dealing with a hot stream. Therefore, you subscribe to it when your component is attached, and unsubscribe when it is detached. + +=== Subscribing in React [badge-hilla]#Hilla# + +In Hilla, you can only use a `Flux`, even if you are only emitting a single value. You subscribe to it by calling the generated TypeScript endpoint method. You then use the returned `Subscription` object to register a function that gets called whenever the `Flux` emits a value. + +For example, if you use a `Flux` to receive the single output of a background job, you could do this: + +[source,typescript] +---- +const onJobCompleted = (result: string) => { + // Update the UI state +} + +const startJob = () => { + MyEndpoint.startBackgroundJob().onNext(onJobCompleted) +} +---- + +In this example, you are dealing with a cold stream and so, you do not need to explicitly unsubscribe from it. + +If you use a `Flux` to receive chat messages, you could do this: + +[source,typescript] +---- +const onMessageReceived = (message: ChatMessage) => { + // Update the UI state +} + +useEffect(() => { + const subscription = MyEndpoint.messages().onNext(onMessageReceived) + return subscription.cancel +}, []) +---- + +In this example, you are dealing with a hot stream. Therefore, you subscribe to it inside a React effect. As a cleanup function, you return the `cancel` method of the subscription object. This ensures that the subscription is cancelled whenever your component is removed from the DOM. + +== Handling Errors + +== Buffering + +You should not push updates to the browser more than 2--4 times per second. If your `Flux` is emitting events faster than that, you should buffer them and update the user interface in batches. + +Buffering a `Flux` is easy, as it has built-in support for it: + +[source,java] +---- +private Flux eventStream() { + ... +} + +public Flux> bufferedEventStream() { + return eventStream().buffer(Duration.ofMillis(250)); +} +---- + +In this example, the buffered stream buffers events for 250 milliseconds before it emits them in batches. Because of this, the user interface is receiving a `List` instead of an `Event`. -== Background Threads +If you are using Flow, you can do the buffering in your user interface, before you subscribe to the stream: -=== Flow +[source,java] +---- +@Override +protected void onAttach(AttachEvent attachEvent) { + var subscription = myService.eventStream() + .buffer(Duration.ofMillis(250)) + .subscribe(attachEvent.getUI().accessLater((eventList) -> { + // Update your UI here + }, null)); + addDetachListener(detachEvent -> subscription.dispose()); +} +---- -=== Hilla +If you are using Hilla, you have to do the buffering inside the reactive endpoint: -== Broadcasts +[source,java] +---- +@BrowserCallable +public class EventEndpoint { -=== Flow + private Flux eventStream() { + ... + } -=== Hilla + @AnonymousAllowed + public Flux<@Nonnull List<@Nonnull Event>> bufferedEventStream() { + return eventStream().buffer(Duration.ofMillis(250)); + } +} +---- +== Disconnects [badge-hilla]#Hilla# diff --git a/articles/building-apps/presentation-layer/server-push/ui-threads.adoc b/articles/building-apps/presentation-layer/server-push/threads.adoc similarity index 99% rename from articles/building-apps/presentation-layer/server-push/ui-threads.adoc rename to articles/building-apps/presentation-layer/server-push/threads.adoc index d7da6d6877..3f9d5ba74b 100644 --- a/articles/building-apps/presentation-layer/server-push/ui-threads.adoc +++ b/articles/building-apps/presentation-layer/server-push/threads.adoc @@ -1,5 +1,5 @@ --- -title: UI Threads +title: Threads description: How to use threads in your Flow user interface. order: 10 section-nav: badge-flow diff --git a/articles/building-apps/presentation-layer/server-push/updates.adoc b/articles/building-apps/presentation-layer/server-push/updates.adoc new file mode 100644 index 0000000000..8c10827cf8 --- /dev/null +++ b/articles/building-apps/presentation-layer/server-push/updates.adoc @@ -0,0 +1,174 @@ +--- +title: Pushing +description: How to push updates to your Flow user interface. +order: 1 +section-nav: badge-flow +--- + += Pushing UI Updates [badge-flow]#Flow# + +Whenever you are using server push in Flow, you are triggering it from another thread than the normal HTTP request thread. Making changes to a UI from another thread and pushing them to the browser requires locking the user session. Otherwise, the UI update done from another thread could conflict with a regular event-driven update and cause either data corruption or deadlocks. Because of this, you may only access a UI using the `UI.access()` method, which locks the session to prevent conflicts. You use it like this: + +[source,java] +---- +ui.access(() -> { + // Update your UI here +}); +---- + +By default, Flow uses automatic pushing. This means that any pending changes are pushed to the browser automatically after `UI.access()` finishes. You can also configure Flow to use manual pushing. This gives you more control over when changes are pushed to the browser. For example, you can push multiple times inside a single call to `UI.access()`. + +To enable manual pushing, you have to make an addition to the `@Push` annotation, like this: + +[source,java] +---- +@Push(PushMode.MANUAL) +public class Application implements AppShellConfigurator { + ... +} +---- + +After this, you have to call the `UI.push()` method whenever you want to push your changes to the browser, like this: + +[source,java] +---- +ui.access(() -> { + // Update your UI here + ui.push(); +}); +---- + +== Getting the UI Instance + +// This assumes that the UI has been explained earlier, and what attach and detach means. + +Before you can call `access()`, you need to get the `UI` instance. You typically use `Component.getUI()` or `UI.getCurrent()` for this. However, both are problematic when it comes to server push. + +`Component.getUI()` is not thread-safe, which means you should only call it while the user session is locked. Therefore, you cannot use it to call `access()`. + +`UI.getCurrent()` only returns a UI whenever the session is locked. Therefore, you cannot use it to call `access()`, either. + +Whenever you are planning to use server push, you have to get a hold of the `UI` instance _while the user session is locked_. This typically happens right before you start your background thread, for example: + +[source,java] +---- +var ui = UI.getCurrent(); // <1> +taskExecutor.execute(() -> { + // Do your work here + ui.access(() -> { // <2> + // Update your UI here + }); +}); +---- +<1> This is executed in an HTTP request thread. The user session is locked and `UI.getCurrent()` returns the current `UI`-instance. +<2> This is executed in the background thread. `UI.getCurrent()` returns `null`, but the `UI` instance is stored in a local variable. + +== Access Later + +You often use server push in various types of event listeners and <>. A background job might inform you that it has finished processing, like this: + +[source,java] +---- +var ui = UI.getCurrent(); +myService.startBackgroundJob(() -> ui.access(() -> { + // Update your UI here when the job is finished +})); +---- + +Or an event bus might inform you that a new message has arrived, like this: + +[source,java] +---- +var ui = UI.getCurrent(); +var subscription = myEventBus.subscribe((message) -> ui.access(() -> { + // Update your UI here when a message has arrived +})); +---- + +Whenever these event listeners or callbacks conform to the `Runnable` or `Consumer` functional interfaces, you should consider using `UI.accessLater()` instead of `UI.access()`. + +`UI.accessLater()` exists in two versions: one that wraps a `Runnable`, and another that wraps a `Consumer`. It stores the `UI` instance, and runs the wrapped delegate inside a call to `UI.access()`. It also takes a second parameter, which is a _detach handler_. The detach handler is a `Runnable` that runs if the `UI` has been detached when `UI.access()` is called. The detach handler can be `null` if no special actions are needed. + +Rewritten with `accessLater()`, the thread completion example becomes: + +[source,java] +---- +myService.startBackgroundJob(UI.getCurrent().accessLater(() -> { + // Update your UI here when the job is finished. +}, null)); +---- + +Likewise, the event listener becomes: + +[source,java] +---- +var subscription = myEventBus.subscribe(UI.getCurrent().accessLater((message) -> { + // Update your UI here when a message has arrived +}, null)); +---- + +== Avoiding Memory Leaks + +When you are using server push to update the user interface when an event has occurred, you typically subscribe to some broadcaster or event bus. When you do this, you have to remember to always unsubscribe when the UI is detached. Otherwise, you end up with a memory leak that prevents your UI from being garbage collected. + +It is recommended to always subscribe when your view is attached to a UI, and unsubscribe when it is detached. You can do this by overriding the `Component.onAttach()` method, like this: + +[source,java] +---- +@Override +protected void onAttach(AttachEvent attachEvent) { // <1> + var subscription = myEventBus.subscribe(attachEvent.getUI().accessLater((message) -> { // <2> + // Update your UI here when a message has arrived + }, null)); + addDetachListener(detachEvent -> subscription.unsubscribe()); // <3> +} +---- +<1> Subscribe when the view is attached to a UI. +<2> Get the `UI` from the `AttachEvent`. +<3> Unsubscribe when the view is detached from the UI. + +== Avoiding Floods + +Another risk you have to manage when updating the user interface in response to events is flooding the user interface with updates. As a rule of thumb, you should not push more than 2--4 times per second. Pushing more often than that can cause performance issues. Also, there is a limit to how many updates the human eye is able to detect per second. + +If you know the events are coming in at a pace no faster than 2--4 events per second, you can push on every event. However, if they are more frequent, you have to start buffering events and update the user interface in batches. This is quite easy to do if you are using a `Flux` from https://projectreactor.io/[Reactor]. See the <> documentation page for more information about this. + +The buffering duration depends on the size of the UI update, and the network latency. In some applications, you may need to use a longer buffer duration, in others, a shorter one might work. You should try various durations and see what works best for your application. + +== Avoiding Unnecessary Pushes + +The `UI.access()` method updates the user interface asynchronously. The update operation is not executed right away, but added to a queue and executed at some point in the future. If this is combined with regular event-driven updates in the HTTP request thread, you may end up in a situation where the user interface is updated out-of-order. Look at this example: + +[source,java] +---- +var button = new Button("Test Me", event -> { + UI.getCurrent().access(() -> { + add(new Div("This
is added from within a call to UI.access()")); + }); + add(new Div("This
is added from an event listener")); +}); +add(button); +---- + +If you click the button, the user interface looks like this: + +[source] +---- +This
is added from an event listener +This
is added from within a call to UI.access() +---- + +In this particular case, the call to `UI.access()` would not have been needed. Sometimes, you can deduce this by looking at the code. However, there are situations where this is not clear. You may have code that sometimes is executed by the HTTP request thread, and sometimes by another thread. In this case, you can check whether the current thread has locked the user session or not, like this: + +[source,java] +---- +if (ui.getSession().hasLock()) { + // Update the UI without calling UI.access() +} else { + ui.access(() -> { + // Update the UI inside UI.access() + }); +} +---- + +// TODO Consider showing an example of a UIRunner that takes a Runnable or Consumer, performs the check, and calls it directly or inside UI.access(). From b2e45e776eae63fe38e321c8aefcfcd24c20c777 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petter=20Holmstr=C3=B6m?= Date: Thu, 10 Oct 2024 17:08:40 +0300 Subject: [PATCH 15/30] Ready for first peer review --- .../server-push/callbacks.adoc | 2 +- .../server-push/reactive.adoc | 83 ++++++++++++++++++- .../server-push/threads.adoc | 2 +- 3 files changed, 82 insertions(+), 5 deletions(-) diff --git a/articles/building-apps/presentation-layer/server-push/callbacks.adoc b/articles/building-apps/presentation-layer/server-push/callbacks.adoc index 01fdc7f5e5..4bf3554c95 100644 --- a/articles/building-apps/presentation-layer/server-push/callbacks.adoc +++ b/articles/building-apps/presentation-layer/server-push/callbacks.adoc @@ -29,7 +29,7 @@ private void onJobFailed(Exception error) { } ---- -For reporting progress, you can use a `<<{articles}/components/progress-bar#,ProgressBar>>`. If the background jobs reports the progress as a floating point value between 0.0 and 1.0, you can pass it directly to the `setValue` method of the progress bar. +For reporting progress, you can use a <<{articles}/components/progress-bar#,progress bar>>. If the background jobs reports the progress as a floating point value between 0.0 and 1.0, you can pass it directly to the `setValue` method of the progress bar. With these methods in place, the method that starts the background job could look like this: diff --git a/articles/building-apps/presentation-layer/server-push/reactive.adoc b/articles/building-apps/presentation-layer/server-push/reactive.adoc index 3395cdfa87..70d702eb48 100644 --- a/articles/building-apps/presentation-layer/server-push/reactive.adoc +++ b/articles/building-apps/presentation-layer/server-push/reactive.adoc @@ -16,7 +16,7 @@ Broadcasts typically use hot streams for output. A hot stream emits values regar In your user interfaces, you typically do not need to worry about unsubscribing from cold streams, as they are often short lived. However, if you subscribe to a hot stream, it is important that you remember to unsubscribe when no longer needed. -=== Subscribing in Flow [badge-flow]#Flow# +=== Flow In Flow, you can use both `Flux` and `Mono`. You subscribe to both by calling the `subscribe()` method. The method takes a <> that is called for each emitted value. You should implement the callback as a private method, and then wrap it inside `UI.accessLater()` when you subscribe. @@ -54,7 +54,7 @@ protected void onAttach(AttachEvent attachEvent) { In this example, you are dealing with a hot stream. Therefore, you subscribe to it when your component is attached, and unsubscribe when it is detached. -=== Subscribing in React [badge-hilla]#Hilla# +=== Hilla In Hilla, you can only use a `Flux`, even if you are only emitting a single value. You subscribe to it by calling the generated TypeScript endpoint method. You then use the returned `Subscription` object to register a function that gets called whenever the `Flux` emits a value. @@ -91,6 +91,55 @@ In this example, you are dealing with a hot stream. Therefore, you subscribe to == Handling Errors +In a reactive stream, an error is a terminal event. This means that the subscription is cancelled and no more values are emitted. If you are dealing with a hot stream, you should therefore consider resubscribing to it as a part of error recovery. + +=== Flow + +In Flow, you can use the `doOnError()` method to attach a <> that gets called if an error occurs. + +If you add error handling to the earlier background job example, you end up with something like this: + +[source,java] +---- +private void onJobCompleted(String result) { + Notification.show("Job completed: " + result); +} + +private void onJobFailed(Throwable error) { + Notification.show("Job failed: " + error.getMessage()); +} + +private void startJob() { + var ui = UI.getCurrent(); + service.startBackgroundJob() + .doOnError(ui.accessLater(this::onJobFailed, null)) + .subscribe(ui.accessLater(this::onJobCompleted, null)); +} +---- + +=== Hilla + +In Hilla, you can use the `onError()` method of the `Subscription` object to register a function that gets called if an error occurs. + +If you add error handling to the earlier background job example, you end up with something like this: + +[source,typescript] +---- +const onJobCompleted = (result: string) => { + // Update the UI state +} + +const onJobFailed = () => { + // Handle the error +} + +const startJob = () => { + MyEndpoint.startBackgroundJob().onNext(onJobCompleted).onError(onJobFailed) +} +---- + +Note, that the error callback function does not get any information about the error itself. + == Buffering You should not push updates to the browser more than 2--4 times per second. If your `Flux` is emitting events faster than that, you should buffer them and update the user interface in batches. @@ -143,4 +192,32 @@ public class EventEndpoint { } ---- -== Disconnects [badge-hilla]#Hilla# +After this, the generated TypeScript endpoint method emits arrays of `Event` objects. + +== Lost Subscriptions [badge-hilla]#Hilla# + +In Hilla, you have to be prepared to handle the case where a subscription is lost without being cancelled. For instance, the user may close their laptop lid, or get temporarily disconnected from the network. Hilla automatically re-establishes the connection, but the subscription may no longer be valid. When this happen, Hilla calls the `onSubscriptionLost` callback function if one has been registered with the `Subscription` object. + +This function can return two values: + +`REMOVE`:: Remove the subscription. No more values are received until the client has explicitly resubscribed. + +`RESUBSCRIBE`:: Re-subscribe by calling the same server method again. + +If you add automatic re-subscription to the earlier chat example, you end up with something like this: + +[source,typescript] +---- +const onMessageReceived = (message: ChatMessage) => { + // Update the UI state +} + +useEffect(() => { + const subscription = MyEndpoint.messages() + .onNext(onMessageReceived) + .onSubscriptionLost(() => ActionOnLostSubscription.RESUBSCRIBE) + return subscription.cancel +}, []) +---- + +If no callback has been specified, `REMOVE` is the default action. diff --git a/articles/building-apps/presentation-layer/server-push/threads.adoc b/articles/building-apps/presentation-layer/server-push/threads.adoc index 3f9d5ba74b..a5f007247b 100644 --- a/articles/building-apps/presentation-layer/server-push/threads.adoc +++ b/articles/building-apps/presentation-layer/server-push/threads.adoc @@ -77,7 +77,7 @@ This example schedules a task to be executed every second. The task sets the tex == Virtual Threads -If you use a Java version that supports virtual threads, you do not need to worry about setting up a thread pool. Just start up a new virtual thread whenever you need one, like this: +If you use a Java version that supports virtual threads, you do not need to worry about setting up a thread pool. You can start up a new virtual thread whenever you need one, like this: [source,java] ---- From 683d61a3b0ac7a48f8161733dca6388eafdd565a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petter=20Holmstr=C3=B6m?= Date: Mon, 14 Oct 2024 13:40:22 +0300 Subject: [PATCH 16/30] First round of edits after peer review --- .../presentation-layer/server-push/futures.adoc | 2 +- .../presentation-layer/server-push/updates.adoc | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/articles/building-apps/presentation-layer/server-push/futures.adoc b/articles/building-apps/presentation-layer/server-push/futures.adoc index 52b6b7876e..1c60a15ced 100644 --- a/articles/building-apps/presentation-layer/server-push/futures.adoc +++ b/articles/building-apps/presentation-layer/server-push/futures.adoc @@ -7,7 +7,7 @@ section-nav: badge-flow = Futures [badge-flow]#Flow# -If you are building the user interface with Flow, a background thread can use the standard Java <<{articles}/building-apps/application-layer/background-jobs/interaction/futures#,`CompletableFuture`>> to inform the user interface of results and errors. +If you are building the user interface with Vaadin Flow, a background thread can use the standard Java <<{articles}/building-apps/application-layer/background-jobs/interaction/futures#,`CompletableFuture`>> to inform the user interface of results and errors. In fact, you are using <> in this approach as well, but instead of calling them directly from your background thread, you are registering them with `CompletableFuture`. However, the principle is the same: create private methods that can be passed as `Consumers` to `UI.accessLater()`. For example, a method for handling successful completion could look like this: diff --git a/articles/building-apps/presentation-layer/server-push/updates.adoc b/articles/building-apps/presentation-layer/server-push/updates.adoc index 8c10827cf8..fa581c4672 100644 --- a/articles/building-apps/presentation-layer/server-push/updates.adoc +++ b/articles/building-apps/presentation-layer/server-push/updates.adoc @@ -1,13 +1,13 @@ --- title: Pushing -description: How to push updates to your Flow user interface. +description: How to push updates to your Vaadin Flow user interface. order: 1 section-nav: badge-flow --- = Pushing UI Updates [badge-flow]#Flow# -Whenever you are using server push in Flow, you are triggering it from another thread than the normal HTTP request thread. Making changes to a UI from another thread and pushing them to the browser requires locking the user session. Otherwise, the UI update done from another thread could conflict with a regular event-driven update and cause either data corruption or deadlocks. Because of this, you may only access a UI using the `UI.access()` method, which locks the session to prevent conflicts. You use it like this: +Whenever you are using server push in Vaadin Flow, you are triggering it from another thread than the normal HTTP request thread. Making changes to a UI from another thread and pushing them to the browser requires locking the user session. Otherwise, the UI update done from another thread could conflict with a regular event-driven update and cause either data corruption, race conditions or deadlocks. Such errors are by nature hard to discover and fix, since they often occur randomly, under heavy load. Because of this, you may only access a UI using the `UI.access()` method, which locks the session to prevent race conditions. You use it like this: [source,java] ---- @@ -46,7 +46,7 @@ Before you can call `access()`, you need to get the `UI` instance. You typically `Component.getUI()` is not thread-safe, which means you should only call it while the user session is locked. Therefore, you cannot use it to call `access()`. -`UI.getCurrent()` only returns a UI whenever the session is locked. Therefore, you cannot use it to call `access()`, either. +`UI.getCurrent()` only returns a non-`null` value when the current thread owns the session lock. When called from a background thread, it returns `null`. Therefore, you cannot use it to call `access()`, either. Whenever you are planning to use server push, you have to get a hold of the `UI` instance _while the user session is locked_. This typically happens right before you start your background thread, for example: From 866526d8fa331ddf76c8b221d0d47925e330a387 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petter=20Holmstr=C3=B6m?= Date: Mon, 14 Oct 2024 14:01:21 +0300 Subject: [PATCH 17/30] Second round of edits after peer review --- .../presentation-layer/server-push/callbacks.adoc | 9 +++++---- .../presentation-layer/server-push/endpoints.adoc | 4 ++-- .../presentation-layer/server-push/futures.adoc | 6 ++++-- .../presentation-layer/server-push/reactive.adoc | 4 +++- .../presentation-layer/server-push/threads.adoc | 4 ++-- .../presentation-layer/server-push/updates.adoc | 4 ++-- 6 files changed, 18 insertions(+), 13 deletions(-) diff --git a/articles/building-apps/presentation-layer/server-push/callbacks.adoc b/articles/building-apps/presentation-layer/server-push/callbacks.adoc index 4bf3554c95..4395c85035 100644 --- a/articles/building-apps/presentation-layer/server-push/callbacks.adoc +++ b/articles/building-apps/presentation-layer/server-push/callbacks.adoc @@ -7,11 +7,11 @@ section-nav: badge-flow = Callbacks [badge-flow]#Flow# -If you are building the user interface with Flow, you can use <<{articles}/building-apps/application-layer/background-jobs/interaction/callbacks#,callbacks>> to allow a background thread to update the user interface. +If you are building the user interface with Vaadin Flow, you can use <<{articles}/building-apps/application-layer/background-jobs/interaction/callbacks#,callbacks>> to allow a background thread to update the user interface. -Whenever you implement a callback, you have remember that the callback is called by the background thread. This means that any updates to the user interface must happen inside a call to `UI.access()`. However, since you are using `Runnable` and `Consumer` callbacks, you can use `UI.accessLater()`. +Whenever you implement a callback, you have remember that the callback is called by the background thread. This means that any updates to the user interface must happen inside a call to `UI.access()`. -For every callback, you should create a private method in your user interface that you can pass as a method reference to `UI.accessLater()`. For example, a method for handling successful completion could look like this: +For every callback, you should create a private method in your user interface. The method is going to be called inside `UI.access()` so you can safely update the user interface inside it. For example, a method for handling successful completion could look like this: [source,java] ---- @@ -38,11 +38,12 @@ With these methods in place, the method that starts the background job could loo private void startJob() { var ui = UI.getCurrent(); service.startBackgroundJob( - ui.accessLater(this::onJobCompleted, null), + ui.accessLater(this::onJobCompleted, null), // <1> ui.accessLater(progressBar::setValue, null), ui.accessLater(this::onJobFailed, null) ); } ---- +<1> The `UI.accessLater()` method is explained on the <> documentation page. You would then call the `startJob()` method when a user clicks a button, for instance. diff --git a/articles/building-apps/presentation-layer/server-push/endpoints.adoc b/articles/building-apps/presentation-layer/server-push/endpoints.adoc index 3b11a01748..6c1bb6eb9f 100644 --- a/articles/building-apps/presentation-layer/server-push/endpoints.adoc +++ b/articles/building-apps/presentation-layer/server-push/endpoints.adoc @@ -1,6 +1,6 @@ --- title: Endpoints -description: How to create reactive endpoints for your Hilla user interface. +description: How to create reactive endpoints for your Vaadin Hilla user interface. order: 39 section-nav: badge-hilla --- @@ -9,7 +9,7 @@ section-nav: badge-hilla // TODO This text assumes that browser callable endpoints have already been explained earlier. -If you are building your user interface with Hilla, you use reactive endpoints to push messages from the server to the browser. A reactive endpoint is an endpoint that returns a `Flux` from https://projectreactor.io/[Reactor]. For example, an endpoint that emits the current date and time every second could look like this: +If you are building your user interface with Vaadin Hilla, you use reactive endpoints to push messages from the server to the browser. A reactive endpoint is an endpoint that returns a `Flux` from https://projectreactor.io/[Reactor]. For example, an endpoint that emits the current date and time every second could look like this: [source,java] ---- diff --git a/articles/building-apps/presentation-layer/server-push/futures.adoc b/articles/building-apps/presentation-layer/server-push/futures.adoc index 1c60a15ced..9afcace056 100644 --- a/articles/building-apps/presentation-layer/server-push/futures.adoc +++ b/articles/building-apps/presentation-layer/server-push/futures.adoc @@ -9,7 +9,7 @@ section-nav: badge-flow If you are building the user interface with Vaadin Flow, a background thread can use the standard Java <<{articles}/building-apps/application-layer/background-jobs/interaction/futures#,`CompletableFuture`>> to inform the user interface of results and errors. -In fact, you are using <> in this approach as well, but instead of calling them directly from your background thread, you are registering them with `CompletableFuture`. However, the principle is the same: create private methods that can be passed as `Consumers` to `UI.accessLater()`. For example, a method for handling successful completion could look like this: +In fact, you are using <> in this approach as well, but instead of calling them directly from your background thread, you are registering them with `CompletableFuture`. For example, a method for handling successful completion could look like this: [source,java] ---- @@ -39,9 +39,11 @@ You use it to update your user interface like this: ---- private void startJob() { var ui = UI.getCurrent(); - service.startBackgroundJob().thenAccept(ui.accessLater(this::onJobCompleted, null)); + service.startBackgroundJob() + .thenAccept(ui.accessLater(this::onJobCompleted, null)); // <1> } ---- +<1> The `UI.accessLater()` method is explained on the <> documentation page. However, this version does not yet handle any exceptions. diff --git a/articles/building-apps/presentation-layer/server-push/reactive.adoc b/articles/building-apps/presentation-layer/server-push/reactive.adoc index 70d702eb48..0c9b641705 100644 --- a/articles/building-apps/presentation-layer/server-push/reactive.adoc +++ b/articles/building-apps/presentation-layer/server-push/reactive.adoc @@ -6,7 +6,7 @@ order: 40 = Reactive Streams -If you are building the user interface with either Flow or Hilla, you can use <<{articles}/building-apps/application-layer/background-jobs/interaction/reactive#,reactive streams>> to allow a background thread to update the user interface. +If you are building the user interface with either Vaadin Flow or Hilla, you can use <<{articles}/building-apps/application-layer/background-jobs/interaction/reactive#,reactive streams>> to allow a background thread to update the user interface. == Subscribing @@ -20,6 +20,8 @@ In your user interfaces, you typically do not need to worry about unsubscribing In Flow, you can use both `Flux` and `Mono`. You subscribe to both by calling the `subscribe()` method. The method takes a <> that is called for each emitted value. You should implement the callback as a private method, and then wrap it inside `UI.accessLater()` when you subscribe. +The `UI.accessLater()` method is explained on the <> documentation page. + For example, if you use a `Mono` to receive the output of a background job, you could do this: [source,java] diff --git a/articles/building-apps/presentation-layer/server-push/threads.adoc b/articles/building-apps/presentation-layer/server-push/threads.adoc index a5f007247b..a03c74cae8 100644 --- a/articles/building-apps/presentation-layer/server-push/threads.adoc +++ b/articles/building-apps/presentation-layer/server-push/threads.adoc @@ -1,13 +1,13 @@ --- title: Threads -description: How to use threads in your Flow user interface. +description: How to use threads in your Vaadin Flow user interface. order: 10 section-nav: badge-flow --- = User Interface Threads [badge-flow]#Flow# -You often use server push to update the user interface from background jobs. This is covered in the <<{articles}/building-apps/application-layer/background-jobs/interaction#,Background Jobs - UI Interaction>> documentation page. However, in Flow, there are also cases where you want to start up a separate thread for use by the user interface itself. You might, for instance, want to show the server date and time in "real time". +You often use server push to update the user interface from background jobs. This is covered in the <<{articles}/building-apps/application-layer/background-jobs/interaction#,Background Jobs - UI Interaction>> documentation page. However, in Vaadin Flow, there are also cases where you want to start up a separate thread for use by the user interface itself. You might, for instance, want to show the server date and time in "real time". If you have used Swing before, you might be tempted to use a `Timer`, or start a new `Thread` manually. In Flow, this is not a good idea. The reason is that Flow applications are multi-user applications, with potentially thousands of concurrent users. If each user creates its own `Timer`, or starts its own `Thread`, you may run out of threads. If that happens, the application crashes. diff --git a/articles/building-apps/presentation-layer/server-push/updates.adoc b/articles/building-apps/presentation-layer/server-push/updates.adoc index fa581c4672..f63c4ec67c 100644 --- a/articles/building-apps/presentation-layer/server-push/updates.adoc +++ b/articles/building-apps/presentation-layer/server-push/updates.adoc @@ -85,9 +85,9 @@ var subscription = myEventBus.subscribe((message) -> ui.access(() -> { })); ---- -Whenever these event listeners or callbacks conform to the `Runnable` or `Consumer` functional interfaces, you should consider using `UI.accessLater()` instead of `UI.access()`. +In cases like these, you should consider using `UI.accessLater()` instead of `UI.access()`. -`UI.accessLater()` exists in two versions: one that wraps a `Runnable`, and another that wraps a `Consumer`. It stores the `UI` instance, and runs the wrapped delegate inside a call to `UI.access()`. It also takes a second parameter, which is a _detach handler_. The detach handler is a `Runnable` that runs if the `UI` has been detached when `UI.access()` is called. The detach handler can be `null` if no special actions are needed. +`UI.accessLater()` exists in two versions: one that wraps a `SerializableRunnable`, and another that wraps a `SerializableConsumer`. It stores the `UI` instance, and runs the wrapped delegate inside a call to `UI.access()`. It also takes a second parameter, which is a _detach handler_. The detach handler is a `Runnable` that runs if the `UI` has been detached when `UI.access()` is called. The detach handler can be `null` if no special actions are needed. Rewritten with `accessLater()`, the thread completion example becomes: From ddff43d5a58675cf7cce5c6276808a7eb6174d12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petter=20Holmstr=C3=B6m?= Date: Mon, 14 Oct 2024 14:26:43 +0300 Subject: [PATCH 18/30] Edits based on ChatGPT feedback --- .../background-jobs/index.adoc | 44 +++++++++++-------- 1 file changed, 26 insertions(+), 18 deletions(-) diff --git a/articles/building-apps/application-layer/background-jobs/index.adoc b/articles/building-apps/application-layer/background-jobs/index.adoc index 043a1bc9ec..e4843c33e6 100644 --- a/articles/building-apps/application-layer/background-jobs/index.adoc +++ b/articles/building-apps/application-layer/background-jobs/index.adoc @@ -6,22 +6,22 @@ order: 11 = Background Jobs -Many business applications need to perform in background threads. These tasks could be long-running tasks triggered by the user, or scheduled jobs that run automatically at a specific time of day, or at specific intervals. +Many business applications need to perform in background threads. These tasks might be long-running operations triggered by the user, or scheduled jobs that run automatically at specific times or intervals. Working with more than one thread increases the risk of bugs. Furthermore, there are many different ways of implementing background jobs. To reduce the risk, you should learn one way, and then apply it consistently in all your Vaadin applications. == Threads -Whenever you work with background threads in a Vaadin application, you should never create new `Thread` objects directly. First, new threads are expensive to start. Second, the number of concurrent threads in a Java application is limited. An exact number is impossible to give, but typically it is measured in thousands. +Whenever you work with background threads in a Vaadin application, you should never create new `Thread` objects directly. First, new threads are expensive to start. Second, the number of concurrent threads in a Java application is limited. While the exact limit depends on various factors, Java applications typically support thousands of threads. -Instead, you should use thread pools, or virtual threads. +Instead of creating threads manually, you should use either a thread pool, or virtual threads. A thread pool consists of a queue, and a pool of running threads. The threads pick tasks from the queue and execute them. When the thread pool receives a new job, it adds it to the queue. The queue has an upper size limit. If the queue is full, the thread pool rejects the job, and throws an exception. Virtual threads were added in Java 21. Whereas ordinary threads are managed by the operating system, virtual threads are managed by the Java virtual machine. They are cheaper to start and run, which means you can have a much higher number of concurrent virtual threads than ordinary threads. -See the https://docs.oracle.com/en/java/javase/21/core/virtual-threads.html[Java Documentation] for more information about virtual threads. +For more information on virtual threads, see the https://docs.oracle.com/en/java/javase/21/core/virtual-threads.html[Java Documentation]. == Task Execution @@ -33,7 +33,9 @@ If you want to use virtual threads, you can enable them by setting the `spring.t You can interact with the `TaskExecutor` either directly, or declaratively through annotations. -When interacting with it directly, you inject an instance of `TaskExecutor` into your code, and submit work to it. Here is an example of a class that uses the `TaskExecutor`: +When interacting with it directly, you inject an instance of `TaskExecutor` into your code, and submit work to it. + +Here is an example of a class that uses the `TaskExecutor`: [source,java] ---- @@ -59,7 +61,9 @@ public class MyWorker { [IMPORTANT] When you inject the `TaskExecutor`, you have to name the parameter `taskExecutor`. The application context may contain more than one bean that implements the `TaskExecutor` interface. If the parameter name does not match the name of the bean, Spring does not know which instance to inject. -If you want to use annotations, you have to enable them before you can use them. Do this by adding the `@EnableAsync` annotation to your main application class, or any other `@Configuration` class: +If you want to use annotations, you have to enable them before you can use them. Do this by adding the `@EnableAsync` annotation to your main application class, or any other `@Configuration` class. + +Here is an example that adds the annotation to the main application class: [source,java] ---- @@ -75,7 +79,9 @@ public class Application{ } ---- -You can now use the `@Async` annotation to tell Spring to execute your code in a background thread: +You can now use the `@Async` annotation to tell Spring to execute your code in a background thread. + +Here is a version of the earlier `MyWorker` example that uses `@Async` instead of the `TaskExecutor`: [source,java] ---- @@ -91,15 +97,17 @@ public class MyWorker { } ---- -See the https://docs.spring.io/spring-framework/reference/integration/scheduling.html[Spring Documentation] for more information about task execution. +For more information about task execution, see the https://docs.spring.io/spring-framework/reference/integration/scheduling.html[Spring Documentation]. === Caveats Using annotations makes the code more concise. However, they come with some caveats you need to be aware of. -First, if you forget to add `@EnableAsync` to your application, and you call an `@Async` method, it executes in the calling thread, not in a background thread. +It is important to remember that if you forget to add `@EnableAsync` to your application, your `@Async` methods run synchronously in the calling thread instead of in a background thread. + +Also, you cannot call an `@Async` method from within the bean itself. This is because Spring by default uses proxies to process `@Async` annotations, and local method calls bypass the proxy. -Second, you cannot call an `@Async` method from within the bean itself. This is because Spring by default uses proxies to process `@Async` annotations, and local method calls bypass the proxy. In the following example, `performTask()` is executed in a background thread, and `performAnotherTask()` in the calling thread: +In the following example, `performTask()` is executed in a background thread, and `performAnotherTask()` in the calling thread: [source,java] ---- @@ -117,7 +125,7 @@ public class MyWorker { } ---- -If you interact with `TaskExecutor` directly, you avoid this problem: +If you interact with `TaskExecutor` directly, you avoid this problem. In the following example, both `performTask()` and `performAnotherTask()` execute in a background thread: [source,java] ---- @@ -142,11 +150,11 @@ public class MyWorker { } ---- -In this case, both `performTask()` and `performAnotherTask()` execute in a background thread. - == Task Scheduling -Spring also has built in support for scheduling tasks through a `TaskScheduler`. You can interact with it either directly, or through annotations. In both cases, you have to enable it by adding the `@EnableScheduling` annotation to your main application class, or any other `@Configuration` class: +Spring also has built in support for scheduling tasks through a `TaskScheduler`. You can interact with it either directly, or through annotations. In both cases, you have to enable it by adding the `@EnableScheduling` annotation to your main application class, or any other `@Configuration` class. + +Here is an example that adds the annotation to the main application class: [source,java] ---- @@ -162,7 +170,9 @@ public class Application{ } ---- -When interacting with the `TaskScheduler` directly, you inject it into your code, and schedule wok with it. Here is an example class that uses the `TaskScheduler`: +When interacting with the `TaskScheduler` directly, you inject it into your code, and schedule work with it. + +Here is an example that uses the `TaskScheduler` to execute the `performTask()` method every five minutes: [source,java] ---- @@ -190,8 +200,6 @@ class MyScheduler implements ApplicationListener { } ---- -This example starts to call `performTask()` every 5 minutes after the application has started up. - You can achieve the same using the `@Scheduled` annotation, like this: [source,java] @@ -208,7 +216,7 @@ class MyScheduler { } ---- -See the https://docs.spring.io/spring-framework/reference/integration/scheduling.html[Spring Documentation] for more information about task scheduling. +For more information about task scheduling, see the https://docs.spring.io/spring-framework/reference/integration/scheduling.html[Spring Documentation]. === Caveats From 58bbe773577b8d7945f8873e28555bae18e3fc1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petter=20Holmstr=C3=B6m?= Date: Mon, 14 Oct 2024 15:36:21 +0300 Subject: [PATCH 19/30] More edits based on ChatGPT feedback --- .../background-jobs/interaction/index.adoc | 2 +- .../background-jobs/jobs.adoc | 22 ++-- .../background-jobs/triggers.adoc | 103 ++++++++---------- 3 files changed, 59 insertions(+), 68 deletions(-) diff --git a/articles/building-apps/application-layer/background-jobs/interaction/index.adoc b/articles/building-apps/application-layer/background-jobs/interaction/index.adoc index 8d3735930b..e65104b6a9 100644 --- a/articles/building-apps/application-layer/background-jobs/interaction/index.adoc +++ b/articles/building-apps/application-layer/background-jobs/interaction/index.adoc @@ -4,7 +4,7 @@ description: How to interact with jobs from the user interface. order: 25 --- -= UI Interaction += User Interface Interaction Some background jobs execute business processes in the background. The end user may see the result of the background job, but does not have to interact with the directly. Scheduled jobs and event triggered jobs typically fall in this category. diff --git a/articles/building-apps/application-layer/background-jobs/jobs.adoc b/articles/building-apps/application-layer/background-jobs/jobs.adoc index 0f30e5ff82..a57ac85f89 100644 --- a/articles/building-apps/application-layer/background-jobs/jobs.adoc +++ b/articles/building-apps/application-layer/background-jobs/jobs.adoc @@ -6,13 +6,17 @@ order: 10 = Implementing Jobs -When you implement a background job, you should consider decoupling its implementation from how it is triggered, and where it is executed. This makes it possible to trigger the job in multiple ways. +When implementing a background job, it's important to decouple its logic from how and where it is triggered. This ensures flexibility in triggering the job from different sources. For instance, you may want to run the job every time the application starts up. In this case, you may want to run it in the main thread, blocking the initialization of the rest of the application until the job is finished. You may also want to run the job in a background thread every day at midnight, or whenever a certain application event is published. +Here is a visual example of a job with three different triggers: + image::images/job-and-triggers.png[A job with three triggers] -In code, a job is a Spring bean, annotated with the `@Component` or `@Service` annotation. It contains one or more methods, that when called, execute the job in the calling thread, like this: +In code, a job is a Spring bean, annotated with the `@Component` or `@Service` annotation. It contains one or more methods, that when called, execute the job in the calling thread. + +Here is an example of a Spring bean that implements a single background job: [source,java] ---- @@ -27,11 +31,13 @@ public class MyBackgroundJob { } ---- -If the job is <> from within the same package, the class should be package private. Otherwise, it has to be public. +If the job is <> from within the same package, the class can be package-private. If triggered externally, it must be public. == Transactions -If the job works on the database, it should manage its own transactions. Because a job is a Spring bean, you can use either declarative, or programmatic transaction management. Here is the earlier example, with declarative transactions: +If the job works on the database, it should manage its own transactions. Because a job is a Spring bean, you can use either declarative, or programmatic transaction management. + +Here is an example of a background job that uses declarative transaction management to ensure the job runs inside a new transaction: [source,java] ---- @@ -49,17 +55,17 @@ public class MyBackgroundJob { } ---- -This guarantees that the job runs inside a new transaction, regardless of how it is triggered. - == Security -Unlike <<../application-services#,application services>>, background jobs should _not_ use method security. The reason is that Spring Security uses the `SecurityContext` to access information about the current user. This context is typically thread local, which means it is not available in a background thread. Therefore, whenever the job is executed by a background thread, Spring would deny access. +Unlike <<../application-services#,application services>>, background jobs should _not_ rely on method-level security. The reason is that Spring Security uses the `SecurityContext` to access information about the current user. This context is typically thread local, which means it is not available in a background thread. Therefore, whenever the job is executed by a background thread, Spring would deny access. If the background job needs information about the current user, this information should be passed to it by the <>, as an immutable method parameter. == Batch Jobs -If you are writing a batch job that processes multiple inputs, you should consider implementing two versions of it: one that processes all applicable inputs, and another that processes a given set of inputs. For example, a batch job that generates invoices for shipped orders could look like this: +Consider implementing two versions of your batch job: one for processing all applicable inputs and another for handling a specific set of inputs. This approach provides flexibility when you need to process individual cases or recover from errors. + +For example, a batch job that generates invoices for shipped orders could look like this: [source,java] ---- diff --git a/articles/building-apps/application-layer/background-jobs/triggers.adoc b/articles/building-apps/application-layer/background-jobs/triggers.adoc index 23b349ef51..3483dd000f 100644 --- a/articles/building-apps/application-layer/background-jobs/triggers.adoc +++ b/articles/building-apps/application-layer/background-jobs/triggers.adoc @@ -6,20 +6,26 @@ order: 20 = Triggering Jobs -A trigger is an object that starts a job. It decides which thread the job should execute in, executes it, and handles any exceptions that occurred. +A trigger is an object responsible for starting a job, determining the thread in which to execute it, and handling any exceptions that occur during execution. + +Jobs can be triggered in various ways, such as on application startup, at regular intervals (e.g., weekly, daily at midnight, or every five minutes), or in response to specific application events or user actions. The same job can have multiple triggers. + +Below is a visual example of a job with three different triggers: image::images/job-and-triggers.png[A job with three triggers] -Business applications have many different triggers. You may want to trigger some jobs on application startup. Other jobs may run once a week, every day at midnight, or every five minutes. Some jobs may run in response to application events, and others in response to user input. You might even have some jobs that can be triggered through a REST API, or through Java Management Extensions (JMX). You can even create multiple triggers for the same job. +Spring has support for plugging in an `AsyncUncaughtExceptionHandler` that gets called whenever an `@Async` method throws an exception. However, this moves the error handling outside of the method where the error occurred. To increase code readability, you should handle the error explicitly in every trigger. + +Some triggers, like event listeners and schedulers, are not intended to be invoked by other objects. You should make them package-private to limit their visibility. [NOTE] On this page, all the trigger examples are delegating to a separate <>. However, if your job is simple, and you know it only needs one trigger, you can implement the job itself inside the trigger. == User Triggered Jobs -For user triggered jobs, an <<../application-services#,application service>> acts as the trigger. You can create a dedicated service class for this, or add a method to a suitable, existing application service. Like all other application service methods, it should be protected using method security. +For user triggered jobs, an <<../application-services#,application service>> acts as the trigger. You can create a dedicated service class for this, or add a method to a suitable, existing application service. Like all other application service methods, it should be protected using method-level security to ensure only authorized users can trigger the job. -Here is an example of an application service that executes a job in a background thread when called: +The following example demonstrates how to trigger a background job from an application service using `@Async` and security annotations: [source,java] ---- @@ -36,34 +42,30 @@ public class MyApplicationService { @Async // <2> public void startJob(MyJobParameters params) { try { - job.executeJob(params); // <3> + job.executeJob(params); } catch (Exception ex) { - log.error("Error executing background job", ex); // <4> + log.error("Error executing background job", ex); } } } ---- <1> Spring ensures the current user has permission to start the job. <2> Spring executes the method using its task executor thread pool. -<3> The application service delegates to the job, and passes data from the client. -<4> The application service logs any exceptions that may occur. This example uses the `@Async` annotation, but you can also execute the job <<../background-jobs#task-execution,programmatically>>. -Sometimes, the background job needs to interact with the user interface. You may want to update a progress bar, do something with the results, or show an error message. To do that, you have to use server push, and design your service method in a particular way. You can find more information about this on the <> documentation page. +If the job needs to provide real-time updates to the user interface (e.g., showing a progress bar or error messages), you have to use server push. For more details, see the <> documentation page. == Event Triggered Jobs For event triggered jobs, you should create an event listener that receives events from Spring's event publisher. By default, the event publisher calls each listener in the same thread that published the event. You should therefore hand over the job to the `TaskExecutor`. -Since the listener is not intended to be called by other objects, you should make it package private. - -Here is an example of a listener that executes a job in a background thread whenever a `MyEvent` is published: +Here is an example of a listener that listens for `MyEvent` to be published. When the event occurs, it triggers the job in a background thread: [source,java] ---- -@Component -class PerformBackgroundJobOnMyEventTrigger { // <1> +@Service +class PerformBackgroundJobOnMyEventTrigger { private static final Logger log = LoggerFactory.getLogger(PerformBackgroundJobOnMyEventTrigger.class); private final MyBackgroundJob job; @@ -71,22 +73,19 @@ class PerformBackgroundJobOnMyEventTrigger { // <1> this.job = job; } - @EventListener // <2> - @Async // <3> + @EventListener // <1> + @Async // <2> public void onMyEvent(MyEvent event) { try { - job.executeJob(event.someDataOfInterestToTheJob()); // <4> + job.executeJob(event.someDataOfInterestToTheJob()); } catch (Exception ex) { - log.error("Error executing background job", ex); // <5> + log.error("Error executing background job", ex); } } } ---- -<1> Trigger is package private. -<2> Spring calls the trigger when the `MyEvent` is published. -<3> Spring executes the method using its task executor thread pool. -<4> The trigger delegates to the job, and passes data from the event. -<5> The trigger logs any exceptions that may occur. +<1> Spring calls the trigger when the `MyEvent` is published. +<2> Spring executes the method using its task executor thread pool. This example uses the `@Async` annotation, but you can also execute the job <<../background-jobs#task-execution,programmatically>>. @@ -94,16 +93,14 @@ This example uses the `@Async` annotation, but you can also execute the job <<.. For scheduled jobs, you should create a scheduler that uses Spring's scheduling mechanism to trigger the job. -Spring uses a separate thread pool for scheduled tasks. You should not use this thread pool to execute the jobs. Instead, your schedulers should hand over the jobs to the `TaskExecutor`. - -Since the scheduler is not intended to be called by other objects, you should make it package private. +Spring uses a separate thread pool for scheduled tasks. It is important not to use the scheduling thread pool for executing jobs directly. Instead, schedule tasks using Spring’s `TaskScheduler` and then delegate the actual job execution to the `TaskExecutor`. Here is an example of a scheduler that schedules a job to execute every five minutes in a background thread: [source,java] ---- -@Component -class MyBackgroundJobScheduler { // <1> +@Service +class MyBackgroundJobScheduler { private static final Logger log = LoggerFactory.getLogger(MyBackgroundJobScheduler.class); private final MyBackgroundJob job; @@ -112,22 +109,19 @@ class MyBackgroundJobScheduler { // <1> this.job = job; } - @Scheduled(fixedRate = 5, timeUnit = TimeUnit.MINUTES) // <2> - @Async // <3> + @Scheduled(fixedRate = 5, timeUnit = TimeUnit.MINUTES) // <1> + @Async // <2> public void executeJob() { try { - job.executeJob(); // <4> + job.executeJob(); } catch (Exception ex) { - log.error("Error executing scheduled job", ex); // <5> + log.error("Error executing scheduled job", ex); } } } ---- -<1> Scheduler is package private. -<2> Spring calls the trigger every 5 minutes. -<3> Spring executes the method using its task executor thread pool. -<4> The scheduler delegates to the job. -<5> The scheduler logs any exceptions that may occur. +<1> Spring calls the trigger every 5 minutes. +<2> Spring executes the method using its task executor thread pool. This example uses the `@Scheduled` and `@Async` annotations, but you can also execute the job using the task scheduler and task executor <<../background-jobs#task-scheduling,programmatically>>. @@ -137,38 +131,32 @@ Programmatic schedulers are more verbose, but they are easier to debug. Therefor For startup jobs, you should create a startup trigger that executes the job when the application starts. -Since the trigger is not intended to be called by other objects, you should make it package private. - -If you want the initialization of the application to block until the job is finished, you should start the job inside the constructor of your trigger. Furthermore, you should execute the job in the calling thread, which in this case is Spring's main thread. If an error occurs during a job like this, you probably want the application to exit. Therefore, you can leave any exceptions unhandled. +If you need to block the application initialization until the job is completed, you can execute it in the main thread. For non-blocking execution, consider using a listener for the `ApplicationReadyEvent` to trigger the job once the application is fully initialized. Here is an example of a trigger that blocks initialization until the job is finished: [source,java] ---- -@Component -class MyStartupTrigger { // <1> +@Service +class MyStartupTrigger { MyStartupTrigger(MyBackgroundJob job) { - job.executeJob(); // <2> + job.executeJob(); } } ---- -<1> Trigger is package private. -<2> The trigger delegates to the job, and executes in the calling thread. [IMPORTANT] Whenever you implement a startup trigger like this, you have to remember that the application is still being initialized. That means that not all services may be available for your job to use. -If you want to trigger a job after the application has started, you should start the job in response to the `ApplicationReadyEvent` event. This event is published by Spring Boot when the application has started up and is ready to serve requests. Here is an example of a trigger that executes a job in a background thread after the application has started up: - -// TODO Is CommandLineRunner a simpler approach? +Here is an example of a trigger that executes a job in a background thread after the application has started up: [source,java] ---- import org.springframework.boot.context.event.ApplicationReadyEvent; -@Component -class MyStartupTrigger { // <1> +@Service +class MyStartupTrigger { private static final Logger log = LoggerFactory.getLogger(MyStartupTrigger.class); private final MyBackgroundJob job; @@ -177,22 +165,19 @@ class MyStartupTrigger { // <1> this.job = job; } - @EventListener // <2> - @Async // <3> + @EventListener // <1> + @Async // <2> public void onApplicationReady(ApplicationReadyEvent event) { try { - job.executeJob(); // <4> - } catch (Exception ex) { // <5> + job.executeJob(); + } catch (Exception ex) { log.error("Error executing job on startup", ex); } } } ---- -<1> Trigger is package private. -<2> Spring calls the trigger when the `ApplicationReadyEvent` is published. -<3> Spring executes the method using its task executor thread pool. -<4> The trigger delegates to the job. -<5> The trigger logs any exceptions that may occur. +<1> Spring calls the trigger when the `ApplicationReadyEvent` is published. +<2> Spring executes the method using its task executor thread pool. This example uses the `@Async` annotation, but you can also execute the job <<../background-jobs#task-execution,programmatically>>. From 8a629b8db01fc06fd6bb9ef38007553250718d54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petter=20Holmstr=C3=B6m?= Date: Mon, 14 Oct 2024 18:14:03 +0300 Subject: [PATCH 20/30] More edits based on peer review feedback --- .../background-jobs/index.adoc | 16 ++-- .../background-jobs/interaction/index.adoc | 4 +- .../background-jobs/jobs.adoc | 12 +-- .../server-push/reactive.adoc | 41 ++++++++-- .../server-push/threads.adoc | 75 ++++++++++--------- .../server-push/updates.adoc | 30 +++++--- 6 files changed, 111 insertions(+), 67 deletions(-) diff --git a/articles/building-apps/application-layer/background-jobs/index.adoc b/articles/building-apps/application-layer/background-jobs/index.adoc index e4843c33e6..8d1c9d251c 100644 --- a/articles/building-apps/application-layer/background-jobs/index.adoc +++ b/articles/building-apps/application-layer/background-jobs/index.adoc @@ -41,7 +41,7 @@ Here is an example of a class that uses the `TaskExecutor`: ---- import org.springframework.core.task.TaskExecutor; -@Component +@Service public class MyWorker { private final TaskExecutor taskExecutor; @@ -87,7 +87,7 @@ Here is a version of the earlier `MyWorker` example that uses `@Async` instead o ---- import org.springframework.scheduling.annotation.Async; -@Component +@Service public class MyWorker { @Async @@ -111,7 +111,7 @@ In the following example, `performTask()` is executed in a background thread, an [source,java] ---- -@Component +@Service public class MyWorker { @Async @@ -129,7 +129,7 @@ If you interact with `TaskExecutor` directly, you avoid this problem. In the fol [source,java] ---- -@Component +@Service public class MyWorker { private final TaskExecutor taskExecutor; @@ -180,7 +180,7 @@ import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.context.ApplicationListener; import org.springframework.scheduling.TaskScheduler; -@Component +@Service class MyScheduler implements ApplicationListener { private final TaskScheduler taskScheduler; @@ -206,7 +206,7 @@ You can achieve the same using the `@Scheduled` annotation, like this: ---- import org.springframework.scheduling.annotation.Scheduled; -@Component +@Service class MyScheduler { @Scheduled(fixedRate = 5, timeUnit = TimeUnit.MINUTES) @@ -226,7 +226,7 @@ To avoid problems, you should use the scheduling thread pool to schedule jobs, a [source,java] ---- -@Component +@Service class MyScheduler { @Scheduled(fixedRate = 5, timeUnit = TimeUnit.MINUTES) @@ -241,7 +241,7 @@ You can also interact with the `TaskScheduler` and `TaskExecutor` directly, like [source,java] ---- -@Component +@Service class MyScheduler implements ApplicationListener { private final TaskScheduler taskScheduler; diff --git a/articles/building-apps/application-layer/background-jobs/interaction/index.adoc b/articles/building-apps/application-layer/background-jobs/interaction/index.adoc index e65104b6a9..3cbacd340f 100644 --- a/articles/building-apps/application-layer/background-jobs/interaction/index.adoc +++ b/articles/building-apps/application-layer/background-jobs/interaction/index.adoc @@ -6,9 +6,9 @@ order: 25 = User Interface Interaction -Some background jobs execute business processes in the background. The end user may see the result of the background job, but does not have to interact with the directly. Scheduled jobs and event triggered jobs typically fall in this category. +Some background jobs execute business processes in the background. The end user may see the result of the background job, but does not have to interact with the job directly. Scheduled jobs and event triggered jobs typically fall in this category. -Then there are jobs that need to interact with the user interface. For instance, the job may want to update a progress indicator while running, and notify the user when the job has completed, or an error has occurred. Furthermore, the user may want to be able to cancel a running job before it has completed. +Then there are jobs that need to interact with the user interface. For instance, the job may want to update a progress indicator while running, and notify the user when the job has completed, or an error has occurred. Furthermore, the user may want to be able to cancel a running job before it has completed. This page explains different options for allowing a user to interact with a background job, and vice versa. == Options diff --git a/articles/building-apps/application-layer/background-jobs/jobs.adoc b/articles/building-apps/application-layer/background-jobs/jobs.adoc index a57ac85f89..a0d913701d 100644 --- a/articles/building-apps/application-layer/background-jobs/jobs.adoc +++ b/articles/building-apps/application-layer/background-jobs/jobs.adoc @@ -14,15 +14,15 @@ Here is a visual example of a job with three different triggers: image::images/job-and-triggers.png[A job with three triggers] -In code, a job is a Spring bean, annotated with the `@Component` or `@Service` annotation. It contains one or more methods, that when called, execute the job in the calling thread. +In code, a job is a Spring bean, annotated with the `@Service` annotation. It contains one or more methods, that when called, execute the job in the calling thread. Here is an example of a Spring bean that implements a single background job: [source,java] ---- -import org.springframework.stereotype.Component; +import org.springframework.stereotype.Service; -@Component +@Service public class MyBackgroundJob { public void performBackgroundJob() { @@ -41,11 +41,11 @@ Here is an example of a background job that uses declarative transaction managem [source,java] ---- -import org.springframework.stereotype.Component; +import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; -@Component +@Service public class MyBackgroundJob { @Transactional(propagation = Propagation.REQUIRES_NEW) @@ -69,7 +69,7 @@ For example, a batch job that generates invoices for shipped orders could look l [source,java] ---- -@Component +@Service public class InvoiceCreationJob { @Transactional(propagation = Propagation.REQUIRES_NEW) diff --git a/articles/building-apps/presentation-layer/server-push/reactive.adoc b/articles/building-apps/presentation-layer/server-push/reactive.adoc index 0c9b641705..172b3a24db 100644 --- a/articles/building-apps/presentation-layer/server-push/reactive.adoc +++ b/articles/building-apps/presentation-layer/server-push/reactive.adoc @@ -32,7 +32,8 @@ private void onJobCompleted(String result) { private void startJob() { var ui = UI.getCurrent(); - service.startBackgroundJob().subscribe(ui.accessLater(this::onJobCompleted, null)); + service.startBackgroundJob() + .subscribe(ui.accessLater(this::onJobCompleted, null)); } ---- @@ -50,7 +51,10 @@ private void onMessageReceived(ChatMessage message) { protected void onAttach(AttachEvent attachEvent) { var subscription = service.messages() .subscribe(attachEvent.getUI().accessLater(this::onMessageReceived, null)); - addDetachListener(detachEvent -> subscription.dispose()); + addDetachListener(detachEvent -> { + detachEvent.unregisterListener(); + subscription.dispose(); + }); } ---- @@ -58,7 +62,29 @@ In this example, you are dealing with a hot stream. Therefore, you subscribe to === Hilla -In Hilla, you can only use a `Flux`, even if you are only emitting a single value. You subscribe to it by calling the generated TypeScript endpoint method. You then use the returned `Subscription` object to register a function that gets called whenever the `Flux` emits a value. +In Hilla, you can only use a `Flux`, even if you are only emitting a single value. However, you can easily convert a `Mono` to a `Flux` by calling the `asFlux()` method. + +This is an example of a reactive endpoint that delegates to a worker to start a background job. The worker returns a `Mono`, which the endpoint converts to a `Flux`: + +[source,java] +---- +@BrowserCallable +public class MyBackgroundJobEndpoint { + + private final MyWorker worker; + + MyBackgroundJobEndpoint(MyWorker worker) { + this.worker = worker; + } + + @AnonymousAllowed + public Flux startBackgroundJob() { + return worker.startBackgroundJob().asFlux(); + } +} +---- + +You subscribe to a `Flux` by calling the generated TypeScript endpoint method. You then use the returned `Subscription` object to register a function that gets called whenever the `Flux` emits a value. For example, if you use a `Flux` to receive the single output of a background job, you could do this: @@ -85,7 +111,7 @@ const onMessageReceived = (message: ChatMessage) => { useEffect(() => { const subscription = MyEndpoint.messages().onNext(onMessageReceived) - return subscription.cancel + return () => subscription.cancel() }, []) ---- @@ -172,7 +198,10 @@ protected void onAttach(AttachEvent attachEvent) { .subscribe(attachEvent.getUI().accessLater((eventList) -> { // Update your UI here }, null)); - addDetachListener(detachEvent -> subscription.dispose()); + addDetachListener(detachEvent -> { + detachEvent.unregisterListener(); + subscription.dispose(); + }); } ---- @@ -218,7 +247,7 @@ useEffect(() => { const subscription = MyEndpoint.messages() .onNext(onMessageReceived) .onSubscriptionLost(() => ActionOnLostSubscription.RESUBSCRIBE) - return subscription.cancel + return () => subscription.cancel() }, []) ---- diff --git a/articles/building-apps/presentation-layer/server-push/threads.adoc b/articles/building-apps/presentation-layer/server-push/threads.adoc index a03c74cae8..382c95218e 100644 --- a/articles/building-apps/presentation-layer/server-push/threads.adoc +++ b/articles/building-apps/presentation-layer/server-push/threads.adoc @@ -11,79 +11,86 @@ You often use server push to update the user interface from background jobs. Thi If you have used Swing before, you might be tempted to use a `Timer`, or start a new `Thread` manually. In Flow, this is not a good idea. The reason is that Flow applications are multi-user applications, with potentially thousands of concurrent users. If each user creates its own `Timer`, or starts its own `Thread`, you may run out of threads. If that happens, the application crashes. -Instead, you should use a shared `ExecutorService` or `ScheduledExecutorService`, or virtual threads. +Instead, you should use Spring's `TaskExecutor`, `TaskScheduler`, or virtual threads. -== Executor Service +== Task Executor -Although Spring has a `TaskExecutor`, you should create your own executor service for your user interface. Create a separate `@Configuration` class, like this: +Setting up Spring's `TaskExecutor` and `TaskScheduler` is covered in the <<{articles}/building-apps/application-layer/background-jobs#,Background Jobs>> documentation page. You can use them to start tasks directly from the user interface as well. However, before you do, you should make sure your task is actually UI-related and not a background job. -[source,java] ----- -@Configuration -class UIThreadConfiguration { - - @Bean(destroyMethod = "shutdown") - public ScheduledExecutorService uiExecutor() { - return Executors.newSingleThreadScheduledExecutor(); - } -} ----- +To use the `TaskExecutor` or `TaskScheduler`, you inject them into your view, and then call them when needed. -This example uses a single thread. If you need more threads, you can use `Executors.newScheduledThreadPool()`. - -Next, you inject the executor into your view, like this: +Here is an example of a view that gets the `TaskExecutor` as a constructor parameter: [source,java] ---- @Route public class MyView extends VerticalLayout { - private final ScheduledExecutorService uiExecutor; + private final TaskExecutor taskExecutor; - public MyView(ScheduledExecutorService uiExecutor) { - this.uiExecutor = uiExecutor; + public MyView(TaskExecutor taskExecutor) { + this.taskExecutor = taskExecutor; ... } } ---- -Now, whenever you need to run a UI operation in a background thread, you can do this: +Here is an example of a button click listener that starts a UI operation in a background thread: [source,java] ---- -uiExecutor.submit(UI.getCurrent().accessLater(() -> { - // Perform the UI operation here. -}, null)); +button.addClickListener(clickEvent -> { + taskExecutor.execute(UI.getCurrent().accessLater(() -> { + // Perform the UI operation here. + }, null)); +}); ---- Because of the call to `UI.accessLater()`, the user interface is automatically updated through a server push when the task finishes. -You can also use the executor to schedule tasks. In this case, you have to schedule the task when the component is attached, and cancel it when it is detached, like this: +[CAUTION] +Do not use the `@Async` annotation in your Flow views. It turns them into proxies that do not work with Vaadin. + +== Task Scheduler + +You can use the `TaskScheduler` to schedule tasks. In this case, you have to schedule the task when the component is attached, and cancel it when it is detached. + +The following example schedules a task to be executed every second. The task sets the text of `currentTimeLabel` to the current date and time of the server. When the component is detached, the task is cancelled: [source,java] ---- @Override protected void onAttach(AttachEvent attachEvent) { - var task = uiExecutor.scheduleAtFixedRate( + var task = taskScheduler.scheduleAtFixedRate( attachEvent.getUI().accessLater(() -> { currentTimeLabel.setText(Instant.now().toString()); - }, null), 0, 1, TimeUnit.SECONDS + }, null), Duration.ofSeconds(1) ); - addDetachListener(detachEvent -> task.cancel(true)); + addDetachListener(detachEvent -> { + detachEvent.unregisterListener(); + task.cancel(true); + }); } ---- -This example schedules a task to be executed every second. The task sets the text of `currentTimeLabel` to the current date and time of the server. When the component is detached, the task is cancelled. +The tasks that you execute in the task scheduler should be fast. If you need to schedule long-running tasks, you should hand them over to `TaskExecutor` for execution. + +[CAUTION] +Do not use the `@Scheduled` annotation in your Flow views. It turns them into proxies that do not work with Vaadin. == Virtual Threads -If you use a Java version that supports virtual threads, you do not need to worry about setting up a thread pool. You can start up a new virtual thread whenever you need one, like this: +If you use a Java version that supports virtual threads, you do not need to worry about setting up a thread pool. You can start up a new virtual thread whenever you need one. + +Here is an example of a button click listener that starts a new virtual thread: [source,java] ---- -Thread.ofVirtual().start(UI.getCurrent().accessLater(() -> { - // Perform the UI operation here. -}, null)); +button.addClickListener(clickEvent -> { + Thread.ofVirtual().start(UI.getCurrent().accessLater(() -> { + // Perform the UI operation here. + }, null)); +}); ---- -For scheduled tasks, you should still use a `ScheduledExecutorService`. +For scheduled tasks, you should still use the `TaskScheduler`. For more information, see the <<{articles}/building-apps/application-layer/background-jobs#,Background Jobs>> documentation page. diff --git a/articles/building-apps/presentation-layer/server-push/updates.adoc b/articles/building-apps/presentation-layer/server-push/updates.adoc index f63c4ec67c..fcab61c137 100644 --- a/articles/building-apps/presentation-layer/server-push/updates.adoc +++ b/articles/building-apps/presentation-layer/server-push/updates.adoc @@ -16,7 +16,7 @@ ui.access(() -> { }); ---- -By default, Flow uses automatic pushing. This means that any pending changes are pushed to the browser automatically after `UI.access()` finishes. You can also configure Flow to use manual pushing. This gives you more control over when changes are pushed to the browser. For example, you can push multiple times inside a single call to `UI.access()`. +By default, Flow uses automatic pushing. This means that any pending changes are pushed to the browser automatically after the command passed to `UI.access()` finishes. You can also configure Flow to use manual pushing. This gives you more control over when changes are pushed to the browser. For example, you can push multiple times inside a single call to `UI.access()`. To enable manual pushing, you have to make an addition to the `@Push` annotation, like this: @@ -48,15 +48,19 @@ Before you can call `access()`, you need to get the `UI` instance. You typically `UI.getCurrent()` only returns a non-`null` value when the current thread owns the session lock. When called from a background thread, it returns `null`. Therefore, you cannot use it to call `access()`, either. -Whenever you are planning to use server push, you have to get a hold of the `UI` instance _while the user session is locked_. This typically happens right before you start your background thread, for example: +Whenever you are planning to use server push, you have to get a hold of the `UI` instance _while the user session is locked_. This typically happens right before you start your background thread. + +Here is an example of a button click listener that starts a background thread: [source,java] ---- -var ui = UI.getCurrent(); // <1> -taskExecutor.execute(() -> { - // Do your work here - ui.access(() -> { // <2> - // Update your UI here +button.addClickListener(clickEvent -> { + var ui = UI.getCurrent(); // <1> + taskExecutor.execute(() -> { // <2> + // Do your work here + ui.access(() -> { + // Update your UI here + }); }); }); ---- @@ -109,7 +113,7 @@ var subscription = myEventBus.subscribe(UI.getCurrent().accessLater((message) -> == Avoiding Memory Leaks -When you are using server push to update the user interface when an event has occurred, you typically subscribe to some broadcaster or event bus. When you do this, you have to remember to always unsubscribe when the UI is detached. Otherwise, you end up with a memory leak that prevents your UI from being garbage collected. +When you are using server push to update the user interface when an event has occurred, you typically subscribe a listener to some broadcaster or event bus. When you do this, you have to remember to always unsubscribe when the UI is detached. Otherwise, you end up with a memory leak that prevents your UI from being garbage collected. This is because the listener holds a reference to the `UI` instance. It is recommended to always subscribe when your view is attached to a UI, and unsubscribe when it is detached. You can do this by overriding the `Component.onAttach()` method, like this: @@ -120,16 +124,20 @@ protected void onAttach(AttachEvent attachEvent) { // <1> var subscription = myEventBus.subscribe(attachEvent.getUI().accessLater((message) -> { // <2> // Update your UI here when a message has arrived }, null)); - addDetachListener(detachEvent -> subscription.unsubscribe()); // <3> + addDetachListener(detachEvent -> { + detachEvent.unregisterListener(); // <3> + subscription.unsubscribe(); // <4> + }); } ---- <1> Subscribe when the view is attached to a UI. <2> Get the `UI` from the `AttachEvent`. -<3> Unsubscribe when the view is detached from the UI. +<3> Remove the detach listener itself, to prevent a memory leak in case the component is attached multiple times. +<4> Unsubscribe when the view is detached from the UI. == Avoiding Floods -Another risk you have to manage when updating the user interface in response to events is flooding the user interface with updates. As a rule of thumb, you should not push more than 2--4 times per second. Pushing more often than that can cause performance issues. Also, there is a limit to how many updates the human eye is able to detect per second. +Another risk you have to manage when updating the user interface in response to events is flooding the user interface with updates. As a rule of thumb, you should not push more than 2--4 times per second. Pushing more often than that can cause performance issues. Also, there is a limit to how many updates the human brain is able to register per second. If you know the events are coming in at a pace no faster than 2--4 events per second, you can push on every event. However, if they are more frequent, you have to start buffering events and update the user interface in batches. This is quite easy to do if you are using a `Flux` from https://projectreactor.io/[Reactor]. See the <> documentation page for more information about this. From 8534e0f559ffed03c192836be6a1d5f2d06c1a3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petter=20Holmstr=C3=B6m?= Date: Tue, 15 Oct 2024 14:24:58 +0300 Subject: [PATCH 21/30] More edits to server push pages --- .../server-push/callbacks.adoc | 20 ++-- .../server-push/futures.adoc | 25 ++--- .../{endpoints.adoc => hilla.adoc} | 16 ++-- .../presentation-layer/server-push/index.adoc | 3 + .../server-push/reactive.adoc | 94 ++++++++++--------- .../server-push/threads.adoc | 41 ++++---- .../server-push/updates.adoc | 26 +++-- 7 files changed, 128 insertions(+), 97 deletions(-) rename articles/building-apps/presentation-layer/server-push/{endpoints.adoc => hilla.adoc} (52%) diff --git a/articles/building-apps/presentation-layer/server-push/callbacks.adoc b/articles/building-apps/presentation-layer/server-push/callbacks.adoc index 4395c85035..45a480c2b8 100644 --- a/articles/building-apps/presentation-layer/server-push/callbacks.adoc +++ b/articles/building-apps/presentation-layer/server-push/callbacks.adoc @@ -7,11 +7,17 @@ section-nav: badge-flow = Callbacks [badge-flow]#Flow# -If you are building the user interface with Vaadin Flow, you can use <<{articles}/building-apps/application-layer/background-jobs/interaction/callbacks#,callbacks>> to allow a background thread to update the user interface. +[NOTE] +The examples on this page only work with push enabled. For information about how to do that, see the <<.#enabling-push-flow,Server Push>> documentation page. + +If you are building the user interface with Vaadin Flow, the easiest way of allowing an background job to update the user interface is through callbacks. This is explained in more detail in the + <<{articles}/building-apps/application-layer/background-jobs/interaction/callbacks#,UI Interaction - Callbacks>> documentation page. Whenever you implement a callback, you have remember that the callback is called by the background thread. This means that any updates to the user interface must happen inside a call to `UI.access()`. -For every callback, you should create a private method in your user interface. The method is going to be called inside `UI.access()` so you can safely update the user interface inside it. For example, a method for handling successful completion could look like this: +For every callback, you should create a private method in your user interface. The method is going to be called inside `UI.access()` so you can safely update the user interface inside it. + +For example, a method for handling successful completion could look like this: [source,java] ---- @@ -31,19 +37,17 @@ private void onJobFailed(Exception error) { For reporting progress, you can use a <<{articles}/components/progress-bar#,progress bar>>. If the background jobs reports the progress as a floating point value between 0.0 and 1.0, you can pass it directly to the `setValue` method of the progress bar. -With these methods in place, the method that starts the background job could look like this: +Here is an example of a button click listener that starts a background job, and uses the private methods, and the progress bar, to update the user interface: [source,java] ---- -private void startJob() { +button.addClickListener(clickEvent -> { var ui = UI.getCurrent(); service.startBackgroundJob( ui.accessLater(this::onJobCompleted, null), // <1> ui.accessLater(progressBar::setValue, null), ui.accessLater(this::onJobFailed, null) ); -} +}); ---- -<1> The `UI.accessLater()` method is explained on the <> documentation page. - -You would then call the `startJob()` method when a user clicks a button, for instance. +<1> The `UI.accessLater()` method is explained on the <> documentation page. \ No newline at end of file diff --git a/articles/building-apps/presentation-layer/server-push/futures.adoc b/articles/building-apps/presentation-layer/server-push/futures.adoc index 9afcace056..73f9b89840 100644 --- a/articles/building-apps/presentation-layer/server-push/futures.adoc +++ b/articles/building-apps/presentation-layer/server-push/futures.adoc @@ -7,9 +7,14 @@ section-nav: badge-flow = Futures [badge-flow]#Flow# -If you are building the user interface with Vaadin Flow, a background thread can use the standard Java <<{articles}/building-apps/application-layer/background-jobs/interaction/futures#,`CompletableFuture`>> to inform the user interface of results and errors. +[NOTE] +The examples on this page only work with push enabled. For information about how to do that, see the <<.#enabling-push-flow,Server Push>> documentation page. -In fact, you are using <> in this approach as well, but instead of calling them directly from your background thread, you are registering them with `CompletableFuture`. For example, a method for handling successful completion could look like this: +Some background jobs may use `CompletableFuture` to inform the user interface of results and errors. This is covered in the <<{articles}/building-apps/application-layer/background-jobs/interaction/futures#,UI Interaction - Futures>> documentation page. + +If you are building the user interface with Vaadin Flow, you can use <> and register them with the `CompletableFuture` to update your user interface. + +For example, a method for handling successful completion could look like this: [source,java] ---- @@ -33,20 +38,18 @@ Note, that the error handler must accept a `Throwable` and not an `Exception` wh If a `CompletableFuture` completes successfully, you can instruct it to perform a specific operation by calling the `thenAccept()` method on it. This method takes a `Consumer` as its input. When the `CompletableFuture` completes, it calls this consumer with the result. -You use it to update your user interface like this: +Here is an example of a button click listener that starts a background job, and updates the user interface when it has completed successfully: [source,java] ---- -private void startJob() { +button.addClickListener(clickEvent -> { var ui = UI.getCurrent(); service.startBackgroundJob() .thenAccept(ui.accessLater(this::onJobCompleted, null)); // <1> -} +}); ---- <1> The `UI.accessLater()` method is explained on the <> documentation page. -However, this version does not yet handle any exceptions. - == Exceptional Completion If a `CompletableFuture` is completed with an exception, you can instruct it to perform a specific operation by calling the `exceptionally()` method on it. However, this method works in a different way than `thenAccept()`. @@ -65,16 +68,14 @@ public static Function consumerToFunction(Consumer consumer) { } ---- -With this helper function in place, the code for starting the job with error handling now becomes: +Here is an example of a button click listener that starts a background job, and uses the helper function to update the user interface if an error occurs: [source,java] ---- -private void startJob() { +button.addClickListener(clickEvent -> { var ui = UI.getCurrent(); service.startBackgroundJob() .thenAccept(ui.accessLater(this::onJobCompleted, null)) .exceptionally(consumerToFunction(ui.accessLater(this::onJobFailed, null))) -} +}); ---- - -You would then call the `startJob()` method when a user clicks a button, for instance. \ No newline at end of file diff --git a/articles/building-apps/presentation-layer/server-push/endpoints.adoc b/articles/building-apps/presentation-layer/server-push/hilla.adoc similarity index 52% rename from articles/building-apps/presentation-layer/server-push/endpoints.adoc rename to articles/building-apps/presentation-layer/server-push/hilla.adoc index 6c1bb6eb9f..0aeeb083a1 100644 --- a/articles/building-apps/presentation-layer/server-push/endpoints.adoc +++ b/articles/building-apps/presentation-layer/server-push/hilla.adoc @@ -1,20 +1,22 @@ --- -title: Endpoints -description: How to create reactive endpoints for your Vaadin Hilla user interface. +title: Hilla Services +description: How to create reactive browser callable services for your Vaadin Hilla user interface. order: 39 section-nav: badge-hilla --- -= Reactive Endpoints [badge-hilla]#Hilla# += Reactive Browser Callable Services [badge-hilla]#Hilla# // TODO This text assumes that browser callable endpoints have already been explained earlier. -If you are building your user interface with Vaadin Hilla, you use reactive endpoints to push messages from the server to the browser. A reactive endpoint is an endpoint that returns a `Flux` from https://projectreactor.io/[Reactor]. For example, an endpoint that emits the current date and time every second could look like this: +If you are building your user interface with Vaadin Hilla, you use reactive browser callable services to push messages from the server to the browser. A reactive service is a service that returns a `Flux` from https://projectreactor.io/[Reactor]. + +Here is an example of a browser callable service that emits the current date and time every second: [source,java] ---- @BrowserCallable -public class TimeEndpoint { +public class TimeService { @AnonymousAllowed public Flux<@Nonnull String> getClock() { @@ -28,6 +30,6 @@ public class TimeEndpoint { <2> Drop any messages that for some reason cannot be sent to the client in time. <3> Output the current date and time as a string. -Hilla generates the necessary TypeScript types to subscribe to this endpoint from the browser. +Hilla generates the necessary TypeScript types to subscribe to this service from the browser. -For more information about updating the UI using a reactive endpoint, see the <> documentation page. \ No newline at end of file +For more information about updating the UI using a reactive service, see the <> documentation page. \ No newline at end of file diff --git a/articles/building-apps/presentation-layer/server-push/index.adoc b/articles/building-apps/presentation-layer/server-push/index.adoc index 4db49ebee4..7854e9763b 100644 --- a/articles/building-apps/presentation-layer/server-push/index.adoc +++ b/articles/building-apps/presentation-layer/server-push/index.adoc @@ -12,6 +12,9 @@ The server-client communication uses a WebSocket connection, if supported by the In Hilla views, push is always enabled when you subscribe to a _reactive endpoint_. For Flow views, you have to enable it explicitly. +[IMPORTANT] +Server push is not the same as Web Push, which is also supported by Vaadin Flow. For more information, see the <<{articles}/flow/configuration/setting-up-webpush#,Web Push Notifications>> documentation page. + == Enabling Push [badge-flow]#Flow# Before you can use server push in Flow, you have to enable it. You do this by adding the `@Push` annotation to the application shell class, like this: diff --git a/articles/building-apps/presentation-layer/server-push/reactive.adoc b/articles/building-apps/presentation-layer/server-push/reactive.adoc index 172b3a24db..0b5dba3714 100644 --- a/articles/building-apps/presentation-layer/server-push/reactive.adoc +++ b/articles/building-apps/presentation-layer/server-push/reactive.adoc @@ -6,7 +6,7 @@ order: 40 = Reactive Streams -If you are building the user interface with either Vaadin Flow or Hilla, you can use <<{articles}/building-apps/application-layer/background-jobs/interaction/reactive#,reactive streams>> to allow a background thread to update the user interface. +If you are building the user interface with either Vaadin Flow or Hilla, you can use reactive streams to allow a background job to update the user interface. This is covered in the <<{articles}/building-apps/application-layer/background-jobs/interaction/reactive#,UI Interaction - Reactive Streams>> documentation page. == Subscribing @@ -20,26 +20,29 @@ In your user interfaces, you typically do not need to worry about unsubscribing In Flow, you can use both `Flux` and `Mono`. You subscribe to both by calling the `subscribe()` method. The method takes a <> that is called for each emitted value. You should implement the callback as a private method, and then wrap it inside `UI.accessLater()` when you subscribe. -The `UI.accessLater()` method is explained on the <> documentation page. - -For example, if you use a `Mono` to receive the output of a background job, you could do this: +For example, a method for handling successful completion could look like this: [source,java] ---- private void onJobCompleted(String result) { Notification.show("Job completed: " + result); } +---- + +The `UI.accessLater()` method is explained in the <> documentation page. + +In the following example, a background job returns a `Mono`. The stream is cold, so you do not need to explicitly unsubscribe from it, as this happens automatically once the `Mono` has completed. The job is started by a button click listener. -private void startJob() { +[source,java] +---- +button.addClickListener(clickEvent -> { var ui = UI.getCurrent(); service.startBackgroundJob() .subscribe(ui.accessLater(this::onJobCompleted, null)); -} +}); ---- -In this example, you are dealing with a cold stream and so, you do not need to explicitly unsubscribe from it. - -If you use a `Flux` to receive chat messages, you could do this: +In the following example, a `Flux` is used to receive chat messages. The stream is hot, so you have to subscribe to it when the component is attached, and unbuscribe when it is detached: [source,java] ---- @@ -49,8 +52,9 @@ private void onMessageReceived(ChatMessage message) { @Override protected void onAttach(AttachEvent attachEvent) { - var subscription = service.messages() - .subscribe(attachEvent.getUI().accessLater(this::onMessageReceived, null)); + var ui = attachEvent.getUI(); + var subscription = chatService.messages() + .subscribe(ui.accessLater(this::onMessageReceived, null)); addDetachListener(detachEvent -> { detachEvent.unregisterListener(); subscription.dispose(); @@ -58,22 +62,20 @@ protected void onAttach(AttachEvent attachEvent) { } ---- -In this example, you are dealing with a hot stream. Therefore, you subscribe to it when your component is attached, and unsubscribe when it is detached. - === Hilla In Hilla, you can only use a `Flux`, even if you are only emitting a single value. However, you can easily convert a `Mono` to a `Flux` by calling the `asFlux()` method. -This is an example of a reactive endpoint that delegates to a worker to start a background job. The worker returns a `Mono`, which the endpoint converts to a `Flux`: +This is an example of a reactive service that delegates to a worker to start a background job. The worker returns a `Mono`, which the service converts to a `Flux`: [source,java] ---- @BrowserCallable -public class MyBackgroundJobEndpoint { +public class MyBackgroundJobService { private final MyWorker worker; - MyBackgroundJobEndpoint(MyWorker worker) { + MyBackgroundJobService(MyWorker worker) { this.worker = worker; } @@ -84,9 +86,9 @@ public class MyBackgroundJobEndpoint { } ---- -You subscribe to a `Flux` by calling the generated TypeScript endpoint method. You then use the returned `Subscription` object to register a function that gets called whenever the `Flux` emits a value. +You subscribe to a `Flux` by calling the generated TypeScript service method. You then use the returned `Subscription` object to register a function that gets called whenever the `Flux` emits a value. -For example, if you use a `Flux` to receive the single output of a background job, you could do this: +The following client-side example uses the `Flux` from the earlier example to receive a single output from a server-side background job. The stream is cold, so you do not need to explicitly unsubscribe from it: [source,typescript] ---- @@ -95,13 +97,11 @@ const onJobCompleted = (result: string) => { } const startJob = () => { - MyEndpoint.startBackgroundJob().onNext(onJobCompleted) + MyBackgroundJobService.startBackgroundJob().onNext(onJobCompleted) } ---- -In this example, you are dealing with a cold stream and so, you do not need to explicitly unsubscribe from it. - -If you use a `Flux` to receive chat messages, you could do this: +The following client-side examples uses a `Flux` to receive chat messages. The stream is hot, so you have to subscribe to it inside a React effect. In the cleanup function, you call the `cancel` method of the subscription object. This ensures that the subscription is cancelled whenever your component is removed from the DOM: [source,typescript] ---- @@ -110,39 +110,38 @@ const onMessageReceived = (message: ChatMessage) => { } useEffect(() => { - const subscription = MyEndpoint.messages().onNext(onMessageReceived) + const subscription = ChatService.messages().onNext(onMessageReceived) return () => subscription.cancel() }, []) ---- -In this example, you are dealing with a hot stream. Therefore, you subscribe to it inside a React effect. As a cleanup function, you return the `cancel` method of the subscription object. This ensures that the subscription is cancelled whenever your component is removed from the DOM. - == Handling Errors In a reactive stream, an error is a terminal event. This means that the subscription is cancelled and no more values are emitted. If you are dealing with a hot stream, you should therefore consider resubscribing to it as a part of error recovery. === Flow -In Flow, you can use the `doOnError()` method to attach a <> that gets called if an error occurs. +In Flow, you can use the `doOnError()` method to attach a <> that gets called if an error occurs. As for successful completion, you should declare a private method and wrap it inside `UI.accessLater()` . -If you add error handling to the earlier background job example, you end up with something like this: +For example, a method for handling errors could look like this: [source,java] ---- -private void onJobCompleted(String result) { - Notification.show("Job completed: " + result); -} - private void onJobFailed(Throwable error) { Notification.show("Job failed: " + error.getMessage()); } +---- + +In the following example, a button click listener starts a new background job, and uses the callback method to handle any errors that may occur: -private void startJob() { +[source,java] +---- +button.addClickListener(clickEvent -> { var ui = UI.getCurrent(); service.startBackgroundJob() - .doOnError(ui.accessLater(this::onJobFailed, null)) - .subscribe(ui.accessLater(this::onJobCompleted, null)); -} + .doOnError(ui.accessLater(this::onJobFailed, null)) + .subscribe(ui.accessLater(this::onJobCompleted, null)); +}); ---- === Hilla @@ -170,9 +169,9 @@ Note, that the error callback function does not get any information about the er == Buffering -You should not push updates to the browser more than 2--4 times per second. If your `Flux` is emitting events faster than that, you should buffer them and update the user interface in batches. +You should not push updates to the browser more than 2--4 times per second. If your `Flux` is emitting events faster than that, you should buffer them and update the user interface in batches. Buffering a `Flux` is easy, as it has built-in support for it through the `buffer()` method. -Buffering a `Flux` is easy, as it has built-in support for it: +In the following example, the buffered stream buffers events for 250 milliseconds before it emits them in batches. Because of this, the user interface is receiving a `List` instead of an `Event`: [source,java] ---- @@ -185,9 +184,10 @@ public Flux> bufferedEventStream() { } ---- -In this example, the buffered stream buffers events for 250 milliseconds before it emits them in batches. Because of this, the user interface is receiving a `List` instead of an `Event`. -If you are using Flow, you can do the buffering in your user interface, before you subscribe to the stream: +If you are using Flow, you can do the buffering in your user interface, before you subscribe to the stream. + +In the following example, the a user interface component subscribes to the buffered stream when it is attached, and unsubscribes when it is detached: [source,java] ---- @@ -205,12 +205,14 @@ protected void onAttach(AttachEvent attachEvent) { } ---- -If you are using Hilla, you have to do the buffering inside the reactive endpoint: +If you are using Hilla, you have to do the buffering inside the reactive service. + +The following example shows a browser callable service that buffers the stream before it is returned. Because of this, the generated TypeScript service method emits arrays of `Event` objects: [source,java] ---- @BrowserCallable -public class EventEndpoint { +public class EventService { private Flux eventStream() { ... @@ -223,11 +225,10 @@ public class EventEndpoint { } ---- -After this, the generated TypeScript endpoint method emits arrays of `Event` objects. == Lost Subscriptions [badge-hilla]#Hilla# -In Hilla, you have to be prepared to handle the case where a subscription is lost without being cancelled. For instance, the user may close their laptop lid, or get temporarily disconnected from the network. Hilla automatically re-establishes the connection, but the subscription may no longer be valid. When this happen, Hilla calls the `onSubscriptionLost` callback function if one has been registered with the `Subscription` object. +In Hilla, you have to be prepared to handle the case where a subscription is lost without being cancelled. For instance, the user may close their laptop lid, or get temporarily disconnected from the network. Hilla automatically re-establishes the connection, but the subscription may no longer be valid. When this happens, Hilla calls the `onSubscriptionLost` callback function if one has been registered with the `Subscription` object. This function can return two values: @@ -235,7 +236,9 @@ This function can return two values: `RESUBSCRIBE`:: Re-subscribe by calling the same server method again. -If you add automatic re-subscription to the earlier chat example, you end up with something like this: +If no callback has been specified, `REMOVE` is the default action. + +In the following example, a React component subscribes to a reactive service inside an effect. It automatically resubscribes to the same service if it loses the subscription: [source,typescript] ---- @@ -244,11 +247,10 @@ const onMessageReceived = (message: ChatMessage) => { } useEffect(() => { - const subscription = MyEndpoint.messages() + const subscription = ChatService.messages() .onNext(onMessageReceived) .onSubscriptionLost(() => ActionOnLostSubscription.RESUBSCRIBE) return () => subscription.cancel() }, []) ---- -If no callback has been specified, `REMOVE` is the default action. diff --git a/articles/building-apps/presentation-layer/server-push/threads.adoc b/articles/building-apps/presentation-layer/server-push/threads.adoc index 382c95218e..98788d1460 100644 --- a/articles/building-apps/presentation-layer/server-push/threads.adoc +++ b/articles/building-apps/presentation-layer/server-push/threads.adoc @@ -7,11 +7,33 @@ section-nav: badge-flow = User Interface Threads [badge-flow]#Flow# +[NOTE] +The examples on this page only work with push enabled. For information about how to do that, see the <<.#enabling-push-flow,Server Push>> documentation page. + You often use server push to update the user interface from background jobs. This is covered in the <<{articles}/building-apps/application-layer/background-jobs/interaction#,Background Jobs - UI Interaction>> documentation page. However, in Vaadin Flow, there are also cases where you want to start up a separate thread for use by the user interface itself. You might, for instance, want to show the server date and time in "real time". If you have used Swing before, you might be tempted to use a `Timer`, or start a new `Thread` manually. In Flow, this is not a good idea. The reason is that Flow applications are multi-user applications, with potentially thousands of concurrent users. If each user creates its own `Timer`, or starts its own `Thread`, you may run out of threads. If that happens, the application crashes. -Instead, you should use Spring's `TaskExecutor`, `TaskScheduler`, or virtual threads. +Instead, you should use virtual threads, or Spring's `TaskExecutor` and `TaskScheduler`. + +== Virtual Threads + +If you use a Java version that supports virtual threads, you can start up a new virtual thread whenever you need one. + +Here is an example of a button click listener that starts a new virtual thread: + +[source,java] +---- +button.addClickListener(clickEvent -> { + Thread.ofVirtual().start(UI.getCurrent().accessLater(() -> { + // Perform the UI operation here. + }, null)); +}); +---- + +This is the easiest way of starting a new user interface thread. If you are able to use virtual threads, they should be your first choice. If you run into problems, switch to the `TaskExecutor`. + +For scheduled tasks, you should still use the `TaskScheduler`. This is covered later in this documentation page. == Task Executor @@ -77,20 +99,3 @@ The tasks that you execute in the task scheduler should be fast. If you need to [CAUTION] Do not use the `@Scheduled` annotation in your Flow views. It turns them into proxies that do not work with Vaadin. - -== Virtual Threads - -If you use a Java version that supports virtual threads, you do not need to worry about setting up a thread pool. You can start up a new virtual thread whenever you need one. - -Here is an example of a button click listener that starts a new virtual thread: - -[source,java] ----- -button.addClickListener(clickEvent -> { - Thread.ofVirtual().start(UI.getCurrent().accessLater(() -> { - // Perform the UI operation here. - }, null)); -}); ----- - -For scheduled tasks, you should still use the `TaskScheduler`. For more information, see the <<{articles}/building-apps/application-layer/background-jobs#,Background Jobs>> documentation page. diff --git a/articles/building-apps/presentation-layer/server-push/updates.adoc b/articles/building-apps/presentation-layer/server-push/updates.adoc index fcab61c137..97cc18aa38 100644 --- a/articles/building-apps/presentation-layer/server-push/updates.adoc +++ b/articles/building-apps/presentation-layer/server-push/updates.adoc @@ -7,7 +7,12 @@ section-nav: badge-flow = Pushing UI Updates [badge-flow]#Flow# -Whenever you are using server push in Vaadin Flow, you are triggering it from another thread than the normal HTTP request thread. Making changes to a UI from another thread and pushing them to the browser requires locking the user session. Otherwise, the UI update done from another thread could conflict with a regular event-driven update and cause either data corruption, race conditions or deadlocks. Such errors are by nature hard to discover and fix, since they often occur randomly, under heavy load. Because of this, you may only access a UI using the `UI.access()` method, which locks the session to prevent race conditions. You use it like this: +[NOTE] +The examples on this page only work with push enabled. For information about how to do that, see the <<.#enabling-push-flow,Server Push>> documentation page. + +Whenever you are using server push in Vaadin Flow, you are triggering it from another thread than the normal HTTP request thread. Making changes to a UI from another thread and pushing them to the browser requires locking the user session. Otherwise, the UI update done from another thread could conflict with a regular event-driven update and cause either data corruption, race conditions or deadlocks. Such errors are by nature hard to discover and fix, since they often occur randomly, under heavy load. + +Because of this, you may only access a UI using the `UI.access()` method, which locks the session to prevent race conditions. You use it like this: [source,java] ---- @@ -69,7 +74,9 @@ button.addClickListener(clickEvent -> { == Access Later -You often use server push in various types of event listeners and <>. A background job might inform you that it has finished processing, like this: +You often use server push in various types of event listeners and <>. A background job might inform you that it has finished processing. + +In the following example, the user interface is updated in a callback after a background job has finished: [source,java] ---- @@ -79,7 +86,9 @@ myService.startBackgroundJob(() -> ui.access(() -> { })); ---- -Or an event bus might inform you that a new message has arrived, like this: +Another common use case is an event bus informing you that a new message has arrived. + +In the following example, the user interface subscribes to an even bus, and updates the user interface whenever a new message arrives: [source,java] ---- @@ -91,7 +100,9 @@ var subscription = myEventBus.subscribe((message) -> ui.access(() -> { In cases like these, you should consider using `UI.accessLater()` instead of `UI.access()`. -`UI.accessLater()` exists in two versions: one that wraps a `SerializableRunnable`, and another that wraps a `SerializableConsumer`. It stores the `UI` instance, and runs the wrapped delegate inside a call to `UI.access()`. It also takes a second parameter, which is a _detach handler_. The detach handler is a `Runnable` that runs if the `UI` has been detached when `UI.access()` is called. The detach handler can be `null` if no special actions are needed. +`UI.accessLater()` exists in two versions: one that wraps a `SerializableRunnable`, and another that wraps a `SerializableConsumer`. It stores the `UI` instance, and runs the wrapped delegate inside a call to `UI.access()`. + +It also takes a second parameter, which is a _detach handler_. The detach handler is a `Runnable` that runs if the `UI` has been detached when `UI.access()` is called. The detach handler can be `null` if no special actions are needed. Rewritten with `accessLater()`, the thread completion example becomes: @@ -121,7 +132,8 @@ It is recommended to always subscribe when your view is attached to a UI, and un ---- @Override protected void onAttach(AttachEvent attachEvent) { // <1> - var subscription = myEventBus.subscribe(attachEvent.getUI().accessLater((message) -> { // <2> + var ui = attachEvent.getUI(); // <2> + var subscription = myEventBus.subscribe(ui.accessLater((message) -> { // Update your UI here when a message has arrived }, null)); addDetachListener(detachEvent -> { @@ -145,7 +157,9 @@ The buffering duration depends on the size of the UI update, and the network lat == Avoiding Unnecessary Pushes -The `UI.access()` method updates the user interface asynchronously. The update operation is not executed right away, but added to a queue and executed at some point in the future. If this is combined with regular event-driven updates in the HTTP request thread, you may end up in a situation where the user interface is updated out-of-order. Look at this example: +The `UI.access()` method updates the user interface asynchronously. The update operation is not executed right away, but added to a queue and executed at some point in the future. If this is combined with regular event-driven updates in the HTTP request thread, you may end up in a situation where the user interface is updated out-of-order. + +Look at this example: [source,java] ---- From dca8805a00a8c3c9c72ca9d8f832b643aa24307a Mon Sep 17 00:00:00 2001 From: russelljtdyer <6652767+russelljtdyer@users.noreply.github.com> Date: Tue, 15 Oct 2024 13:50:06 +0200 Subject: [PATCH 22/30] Initial formatting and minor edits. --- .../application-layer/background-jobs/index.adoc | 6 ++++++ .../background-jobs/interaction/callbacks.adoc | 5 +++++ .../background-jobs/interaction/futures.adoc | 3 +++ .../background-jobs/interaction/index.adoc | 2 ++ .../background-jobs/interaction/reactive.adoc | 8 ++++++-- .../application-layer/background-jobs/jobs.adoc | 5 +++++ .../background-jobs/triggers.adoc | 5 +++++ articles/building-apps/index.adoc | 7 ++++--- .../presentation-layer/server-push/callbacks.adoc | 10 +++++----- .../presentation-layer/server-push/futures.adoc | 8 +++++--- .../presentation-layer/server-push/hilla.adoc | 2 +- .../presentation-layer/server-push/index.adoc | 3 +++ .../presentation-layer/server-push/reactive.adoc | 10 +++++++++- .../presentation-layer/server-push/threads.adoc | 14 +++++++++----- .../presentation-layer/server-push/updates.adoc | 12 +++++++++--- articles/flow/advanced/long-running-tasks.adoc | 2 +- articles/flow/advanced/server-push.adoc | 2 +- articles/hilla/lit/guides/reactive-endpoints.adoc | 2 +- 18 files changed, 80 insertions(+), 26 deletions(-) diff --git a/articles/building-apps/application-layer/background-jobs/index.adoc b/articles/building-apps/application-layer/background-jobs/index.adoc index 8d1c9d251c..e3ae993645 100644 --- a/articles/building-apps/application-layer/background-jobs/index.adoc +++ b/articles/building-apps/application-layer/background-jobs/index.adoc @@ -4,12 +4,14 @@ description: How to handle background jobs. order: 11 --- + = Background Jobs Many business applications need to perform in background threads. These tasks might be long-running operations triggered by the user, or scheduled jobs that run automatically at specific times or intervals. Working with more than one thread increases the risk of bugs. Furthermore, there are many different ways of implementing background jobs. To reduce the risk, you should learn one way, and then apply it consistently in all your Vaadin applications. + == Threads Whenever you work with background threads in a Vaadin application, you should never create new `Thread` objects directly. First, new threads are expensive to start. Second, the number of concurrent threads in a Java application is limited. While the exact limit depends on various factors, Java applications typically support thousands of threads. @@ -23,6 +25,7 @@ Virtual threads were added in Java 21. Whereas ordinary threads are managed by t For more information on virtual threads, see the https://docs.oracle.com/en/java/javase/21/core/virtual-threads.html[Java Documentation]. + == Task Execution The background jobs themselves should not need to manage their own thread pools, or virtual threads. Instead, they should use _executors_. An executor is an object that takes a `Runnable`, and executes it at some point in the future. Spring provides a `TaskExecutor`, that you should use in your background jobs. @@ -99,6 +102,7 @@ public class MyWorker { For more information about task execution, see the https://docs.spring.io/spring-framework/reference/integration/scheduling.html[Spring Documentation]. + === Caveats Using annotations makes the code more concise. However, they come with some caveats you need to be aware of. @@ -150,6 +154,7 @@ public class MyWorker { } ---- + == Task Scheduling Spring also has built in support for scheduling tasks through a `TaskScheduler`. You can interact with it either directly, or through annotations. In both cases, you have to enable it by adding the `@EnableScheduling` annotation to your main application class, or any other `@Configuration` class. @@ -265,6 +270,7 @@ class MyScheduler implements ApplicationListener { } ---- + == Building // TODO Come up with a better heading, and maybe a short intro to this section. diff --git a/articles/building-apps/application-layer/background-jobs/interaction/callbacks.adoc b/articles/building-apps/application-layer/background-jobs/interaction/callbacks.adoc index 76b543422d..3560725c35 100644 --- a/articles/building-apps/application-layer/background-jobs/interaction/callbacks.adoc +++ b/articles/building-apps/application-layer/background-jobs/interaction/callbacks.adoc @@ -5,6 +5,7 @@ order: 10 section-nav: badge-flow --- + = Callbacks [badge-flow]#Flow# If you are using a Flow user interface, the simplest way of allowing your background jobs to interact with it is through callbacks. @@ -32,6 +33,7 @@ You can use `Consumer`, `Runnable`, and `Supplier` as callback interfaces, depen |=== + == Returning a Result For example, a background job that returns a string or an exception could be implemented like this: @@ -50,6 +52,7 @@ public void startBackgroundJob(Consumer onComplete, } ---- + == Reporting Progress If the background job is also reporting its progress, for instance as a percentage number, it could look like this: @@ -82,6 +85,7 @@ public void startBackgroundJob(Consumer onComplete, } ---- + == Cancelling Furthermore, if the job can also be cancelled, it could look like this: @@ -129,6 +133,7 @@ public void startBackgroundJob(Consumer onComplete, All the callbacks have to be thread-safe, as they are called from the background thread, but owned and created by the user interface. For more information about how to implement these callbacks, see the <<{articles}/building-apps/presentation-layer/server-push/callbacks#,Server Push - Callbacks>> documentation page. + === Improving Cancel API If you want to make the cancelling API nicer, you can replace the callback with a handle. First, create a handle interface that the user interface can use to cancel the job: diff --git a/articles/building-apps/application-layer/background-jobs/interaction/futures.adoc b/articles/building-apps/application-layer/background-jobs/interaction/futures.adoc index 2d6d582006..d436dc5280 100644 --- a/articles/building-apps/application-layer/background-jobs/interaction/futures.adoc +++ b/articles/building-apps/application-layer/background-jobs/interaction/futures.adoc @@ -5,10 +5,12 @@ order: 20 section-nav: badge-flow --- + = Futures [badge-flow]#Flow# If you are using a Flow user interface, you can use a standard Java `CompletableFuture` to report results and errors to the user interface. You can also use it to cancel the job from the user interface. For reporting progress, however, you still need to use a callback. + == Returning a Result The advantage of working with `CompletableFuture` is that Spring has built-in support for them when using the `@Async` annotation. For example, a background job that completes with either a string or an exception could be implemented like this: @@ -25,6 +27,7 @@ If the `doSomethingThatTakesALongTime()` method throws an exception, Spring auto To update the user interface, you have to add special completion stages that execute after the `CompletableFuture` completes. For more information about how to add these, see the <<{articles}/building-apps/presentation-layer/server-push/futures#,Server Push - Futures>> documentation page. + == Cancelling You can cancel a Java `Future` by calling its `cancel()` method. The method has a `boolean` parameter that indicates whether the thread should be interrupted or not. However, `CompletableFuture`, which implements `Future`, does not use this parameter. It therefore does not make any difference whether you pass in `true` or `false`. diff --git a/articles/building-apps/application-layer/background-jobs/interaction/index.adoc b/articles/building-apps/application-layer/background-jobs/interaction/index.adoc index 3cbacd340f..c97744181e 100644 --- a/articles/building-apps/application-layer/background-jobs/interaction/index.adoc +++ b/articles/building-apps/application-layer/background-jobs/interaction/index.adoc @@ -4,12 +4,14 @@ description: How to interact with jobs from the user interface. order: 25 --- + = User Interface Interaction Some background jobs execute business processes in the background. The end user may see the result of the background job, but does not have to interact with the job directly. Scheduled jobs and event triggered jobs typically fall in this category. Then there are jobs that need to interact with the user interface. For instance, the job may want to update a progress indicator while running, and notify the user when the job has completed, or an error has occurred. Furthermore, the user may want to be able to cancel a running job before it has completed. This page explains different options for allowing a user to interact with a background job, and vice versa. + == Options section_outline::[] diff --git a/articles/building-apps/application-layer/background-jobs/interaction/reactive.adoc b/articles/building-apps/application-layer/background-jobs/interaction/reactive.adoc index eb08919e2e..da4fce1dc8 100644 --- a/articles/building-apps/application-layer/background-jobs/interaction/reactive.adoc +++ b/articles/building-apps/application-layer/background-jobs/interaction/reactive.adoc @@ -6,13 +6,15 @@ order: 30 // TODO This page is about returning results from background threads. You can also use reactive streams for broadcasting, but that is a different use case. This should be covered in another documentation page, and linked to from here. + = Reactive Streams -If you are using Flow or Hilla to build your user interface, you can use `Flux` or `Mono` from https://projectreactor.io/[Reactor] to allow your background jobs to interact with them. Reactor has an extensive API, which means you can do a lot of things with it. This also means that it has a steeper learning curve than using callbacks or `CompletableFuture`. If you are new to reactive programming, you should read Reactor's https://projectreactor.io/docs/core/release/reference/#intro-reactive[Introduction to Reactive Programming] before continuing. +When using Flow or Hilla to build your user interface, you can use `Flux` or `Mono` from https://projectreactor.io/[Reactor] to allow your background jobs to interact with them. Reactor has an extensive API, which means you can do a lot of things with it. This also means that it has a steeper learning curve than using callbacks or `CompletableFuture`. If you are new to reactive programming, you should read Reactor's https://projectreactor.io/docs/core/release/reference/#intro-reactive[Introduction to Reactive Programming] before continuing. + == Returning a Result -When you are using Reactor, you cannot use the `@Async` annotation. Instead, you have to manually instruct your `Mono` or `Flux` to execute using the Spring `TaskExecutor`. Otherwise, your job executes in the thread that subscribes to the `Mono` or `Flux`. +When you're using Reactor, you cannot use the `@Async` annotation. Instead, you have to manually instruct your `Mono` or `Flux` to execute using the Spring `TaskExecutor`. Otherwise, your job executes in the thread that subscribes to the `Mono` or `Flux`. For example, a background job that returns a string or an exception could be implemented like this: @@ -31,6 +33,7 @@ To update the user interface, you have to subscribe to the `Mono` or `Flux`. For [IMPORTANT] Hilla only supports `Flux`, so if your job is returning a `Mono`, you have to convert it to a `Flux` inside your `@BrowserCallable` endpoint. You can do this by calling the `Mono.flux()` method. + == Reporting Progress If your background job only needs to report its progress without actually returning a result, you can return a `Flux`. Your job should then emit progress updates, and complete the stream when done. However, you often also want to return a result. Since Hilla only supports returning a single `Flux`, you have to use the same stream for emitting both progress updates and the end result. The code may be a bit messy, but it works. @@ -92,6 +95,7 @@ public Flux startBackgroundJob() { When your user interface subscribes to this `Flux`, it needs to check the state of the returned `BackgroundJobOutput` objects. If `progressUpdate` contains a value, it should update the progress indicator. If `result` contains a value, the operation is finished. + == Cancelling You can cancel a subscription to a `Flux` or `Mono` at any time. However, as with `CompletableFuture`, cancelling the subscription does not stop the background job itself. To fix this, you need to tell the background job when it has been cancelled, so that it can stop itself. Continuing on the earlier example, adding support for cancelling could look like this: diff --git a/articles/building-apps/application-layer/background-jobs/jobs.adoc b/articles/building-apps/application-layer/background-jobs/jobs.adoc index a0d913701d..9079f85d6f 100644 --- a/articles/building-apps/application-layer/background-jobs/jobs.adoc +++ b/articles/building-apps/application-layer/background-jobs/jobs.adoc @@ -4,6 +4,7 @@ description: How to implement backgorund jobs. order: 10 --- + = Implementing Jobs When implementing a background job, it's important to decouple its logic from how and where it is triggered. This ensures flexibility in triggering the job from different sources. @@ -33,6 +34,7 @@ public class MyBackgroundJob { If the job is <> from within the same package, the class can be package-private. If triggered externally, it must be public. + == Transactions If the job works on the database, it should manage its own transactions. Because a job is a Spring bean, you can use either declarative, or programmatic transaction management. @@ -55,12 +57,14 @@ public class MyBackgroundJob { } ---- + == Security Unlike <<../application-services#,application services>>, background jobs should _not_ rely on method-level security. The reason is that Spring Security uses the `SecurityContext` to access information about the current user. This context is typically thread local, which means it is not available in a background thread. Therefore, whenever the job is executed by a background thread, Spring would deny access. If the background job needs information about the current user, this information should be passed to it by the <>, as an immutable method parameter. + == Batch Jobs Consider implementing two versions of your batch job: one for processing all applicable inputs and another for handling a specific set of inputs. This approach provides flexibility when you need to process individual cases or recover from errors. @@ -88,6 +92,7 @@ In this example, the first method creates invoices for the orders whose ID:s hav Implementing batch jobs like this does not require much effort if done from the start, but allows for flexibility that may be useful. Continuing on the invoice generation example, you may discover a bug in production. This bug has caused some orders to have bad data in the database. As a result, the batch job has not been able to generate invoices for them. Fixing the bug is easy, but your users do not want to wait for the next batch run to occur. Instead, as a part of the fix, you can add a button to the user interface that allows a user to trigger invoice generation for an individual order. + == Idempotent Jobs Whenever you build a background job that updates, or generates data, you should consider making the job _idempotent_. An idempotent job leaves the database in the same state regardless of how many times it has been executed on the same input. diff --git a/articles/building-apps/application-layer/background-jobs/triggers.adoc b/articles/building-apps/application-layer/background-jobs/triggers.adoc index 3483dd000f..30755efba8 100644 --- a/articles/building-apps/application-layer/background-jobs/triggers.adoc +++ b/articles/building-apps/application-layer/background-jobs/triggers.adoc @@ -4,6 +4,7 @@ description: How to trigger backgorund jobs. order: 20 --- + = Triggering Jobs A trigger is an object responsible for starting a job, determining the thread in which to execute it, and handling any exceptions that occur during execution. @@ -21,6 +22,7 @@ Some triggers, like event listeners and schedulers, are not intended to be invok [NOTE] On this page, all the trigger examples are delegating to a separate <>. However, if your job is simple, and you know it only needs one trigger, you can implement the job itself inside the trigger. + == User Triggered Jobs For user triggered jobs, an <<../application-services#,application service>> acts as the trigger. You can create a dedicated service class for this, or add a method to a suitable, existing application service. Like all other application service methods, it should be protected using method-level security to ensure only authorized users can trigger the job. @@ -56,6 +58,7 @@ This example uses the `@Async` annotation, but you can also execute the job <<.. If the job needs to provide real-time updates to the user interface (e.g., showing a progress bar or error messages), you have to use server push. For more details, see the <> documentation page. + == Event Triggered Jobs For event triggered jobs, you should create an event listener that receives events from Spring's event publisher. By default, the event publisher calls each listener in the same thread that published the event. You should therefore hand over the job to the `TaskExecutor`. @@ -89,6 +92,7 @@ class PerformBackgroundJobOnMyEventTrigger { This example uses the `@Async` annotation, but you can also execute the job <<../background-jobs#task-execution,programmatically>>. + == Scheduled Jobs For scheduled jobs, you should create a scheduler that uses Spring's scheduling mechanism to trigger the job. @@ -127,6 +131,7 @@ This example uses the `@Scheduled` and `@Async` annotations, but you can also ex Programmatic schedulers are more verbose, but they are easier to debug. Therefore, you should start with annotations when you implement schedulers. If you later need more control over the scheduling, or run into problems that are difficult to debug, you should switch to a programmatic approach. + == Startup Jobs For startup jobs, you should create a startup trigger that executes the job when the application starts. diff --git a/articles/building-apps/index.adoc b/articles/building-apps/index.adoc index 2a57673a26..edba4ec98e 100644 --- a/articles/building-apps/index.adoc +++ b/articles/building-apps/index.adoc @@ -7,11 +7,11 @@ section-nav: flat expanded // TODO Change order once there is more material -= Building Apps the Vaadin Way += Building Applications the Vaadin Way -.Work in progress +.Work in Progress [IMPORTANT] -This section of the documentation is still being written. Its content is already useful, but several parts are missing. +This section of the documentation is still being written. Its content is useful, but several parts are missing. Vaadin is a powerful tool for building real-world business applications. However, like all versatile tools, it can be used in many different ways and some of these ways lead to better outcomes than others. @@ -19,6 +19,7 @@ Like all big things, big business applications have small beginnings. You can fi That said, the Vaadin Way is not the only way to build Vaadin applications. Experienced developers and maintainers of existing applications should be able to continue using Vaadin in their preferred way, or cherry pick the pieces of the Vaadin Way that works for them. + == Topics section_outline::[] diff --git a/articles/building-apps/presentation-layer/server-push/callbacks.adoc b/articles/building-apps/presentation-layer/server-push/callbacks.adoc index 45a480c2b8..84e39d3d84 100644 --- a/articles/building-apps/presentation-layer/server-push/callbacks.adoc +++ b/articles/building-apps/presentation-layer/server-push/callbacks.adoc @@ -5,16 +5,16 @@ order: 20 section-nav: badge-flow --- -= Callbacks [badge-flow]#Flow# -[NOTE] -The examples on this page only work with push enabled. For information about how to do that, see the <<.#enabling-push-flow,Server Push>> documentation page. += Callbacks [badge-flow]#Flow# -If you are building the user interface with Vaadin Flow, the easiest way of allowing an background job to update the user interface is through callbacks. This is explained in more detail in the - <<{articles}/building-apps/application-layer/background-jobs/interaction/callbacks#,UI Interaction - Callbacks>> documentation page. +When building the user interface with Vaadin Flow, the easiest way of allowing a background job to update the user interface is through callbacks. This is explained in more detail in the <<{articles}/building-apps/application-layer/background-jobs/interaction/callbacks#,UI Interaction - Callbacks>> documentation page. Whenever you implement a callback, you have remember that the callback is called by the background thread. This means that any updates to the user interface must happen inside a call to `UI.access()`. +[NOTE] +The examples on this page only work with push enabled. For information about how to do that, see the <<.#enabling-push-flow,Server Push>> documentation page. + For every callback, you should create a private method in your user interface. The method is going to be called inside `UI.access()` so you can safely update the user interface inside it. For example, a method for handling successful completion could look like this: diff --git a/articles/building-apps/presentation-layer/server-push/futures.adoc b/articles/building-apps/presentation-layer/server-push/futures.adoc index 73f9b89840..1b69aebbaf 100644 --- a/articles/building-apps/presentation-layer/server-push/futures.adoc +++ b/articles/building-apps/presentation-layer/server-push/futures.adoc @@ -7,12 +7,12 @@ section-nav: badge-flow = Futures [badge-flow]#Flow# +Some background jobs may use `CompletableFuture` to inform the user interface of results and errors. This is covered in the <<{articles}/building-apps/application-layer/background-jobs/interaction/futures#,UI Interaction - Futures>> documentation page. + [NOTE] The examples on this page only work with push enabled. For information about how to do that, see the <<.#enabling-push-flow,Server Push>> documentation page. -Some background jobs may use `CompletableFuture` to inform the user interface of results and errors. This is covered in the <<{articles}/building-apps/application-layer/background-jobs/interaction/futures#,UI Interaction - Futures>> documentation page. - -If you are building the user interface with Vaadin Flow, you can use <> and register them with the `CompletableFuture` to update your user interface. +When building the user interface with Vaadin Flow, you can use <> and register them with the `CompletableFuture` to update your user interface. For example, a method for handling successful completion could look like this: @@ -34,6 +34,7 @@ private void onJobFailed(Throwable error) { Note, that the error handler must accept a `Throwable` and not an `Exception` when you are working with `CompletableFuture`. + == Successful Completion If a `CompletableFuture` completes successfully, you can instruct it to perform a specific operation by calling the `thenAccept()` method on it. This method takes a `Consumer` as its input. When the `CompletableFuture` completes, it calls this consumer with the result. @@ -50,6 +51,7 @@ button.addClickListener(clickEvent -> { ---- <1> The `UI.accessLater()` method is explained on the <> documentation page. + == Exceptional Completion If a `CompletableFuture` is completed with an exception, you can instruct it to perform a specific operation by calling the `exceptionally()` method on it. However, this method works in a different way than `thenAccept()`. diff --git a/articles/building-apps/presentation-layer/server-push/hilla.adoc b/articles/building-apps/presentation-layer/server-push/hilla.adoc index 0aeeb083a1..9f323363b0 100644 --- a/articles/building-apps/presentation-layer/server-push/hilla.adoc +++ b/articles/building-apps/presentation-layer/server-push/hilla.adoc @@ -9,7 +9,7 @@ section-nav: badge-hilla // TODO This text assumes that browser callable endpoints have already been explained earlier. -If you are building your user interface with Vaadin Hilla, you use reactive browser callable services to push messages from the server to the browser. A reactive service is a service that returns a `Flux` from https://projectreactor.io/[Reactor]. +When building a user interface with Vaadin Hilla, you would use reactive browser callable services to push messages from the server to the browser. A reactive service is a service that returns a `Flux` from https://projectreactor.io/[Reactor]. Here is an example of a browser callable service that emits the current date and time every second: diff --git a/articles/building-apps/presentation-layer/server-push/index.adoc b/articles/building-apps/presentation-layer/server-push/index.adoc index 7854e9763b..5f3a8bbeb1 100644 --- a/articles/building-apps/presentation-layer/server-push/index.adoc +++ b/articles/building-apps/presentation-layer/server-push/index.adoc @@ -4,6 +4,7 @@ description: How to use server push in your user interfaces. order: 50 --- + = Server Push Server push is based on a client-server connection established by the client. The server can then use the connection to send updates to the client. For example, it could send a new chat message to all participants without delay. @@ -15,6 +16,7 @@ In Hilla views, push is always enabled when you subscribe to a _reactive endpoin [IMPORTANT] Server push is not the same as Web Push, which is also supported by Vaadin Flow. For more information, see the <<{articles}/flow/configuration/setting-up-webpush#,Web Push Notifications>> documentation page. + == Enabling Push [badge-flow]#Flow# Before you can use server push in Flow, you have to enable it. You do this by adding the `@Push` annotation to the application shell class, like this: @@ -40,6 +42,7 @@ public class Application implements AppShellConfigurator { // TODO Transport modes? Or is that something for the reference material. + == Topics section_outline::[] diff --git a/articles/building-apps/presentation-layer/server-push/reactive.adoc b/articles/building-apps/presentation-layer/server-push/reactive.adoc index 0b5dba3714..ac951b885c 100644 --- a/articles/building-apps/presentation-layer/server-push/reactive.adoc +++ b/articles/building-apps/presentation-layer/server-push/reactive.adoc @@ -4,9 +4,11 @@ description: How to use server push with reactive streams. order: 40 --- + = Reactive Streams -If you are building the user interface with either Vaadin Flow or Hilla, you can use reactive streams to allow a background job to update the user interface. This is covered in the <<{articles}/building-apps/application-layer/background-jobs/interaction/reactive#,UI Interaction - Reactive Streams>> documentation page. +When building the user interface with either Vaadin Flow or Hilla, you can use reactive streams to allow a background job to update the user interface. This is covered in the <<{articles}/building-apps/application-layer/background-jobs/interaction/reactive#,UI Interaction - Reactive Streams>> documentation page. + == Subscribing @@ -16,6 +18,7 @@ Broadcasts typically use hot streams for output. A hot stream emits values regar In your user interfaces, you typically do not need to worry about unsubscribing from cold streams, as they are often short lived. However, if you subscribe to a hot stream, it is important that you remember to unsubscribe when no longer needed. + === Flow In Flow, you can use both `Flux` and `Mono`. You subscribe to both by calling the `subscribe()` method. The method takes a <> that is called for each emitted value. You should implement the callback as a private method, and then wrap it inside `UI.accessLater()` when you subscribe. @@ -62,6 +65,7 @@ protected void onAttach(AttachEvent attachEvent) { } ---- + === Hilla In Hilla, you can only use a `Flux`, even if you are only emitting a single value. However, you can easily convert a `Mono` to a `Flux` by calling the `asFlux()` method. @@ -115,10 +119,12 @@ useEffect(() => { }, []) ---- + == Handling Errors In a reactive stream, an error is a terminal event. This means that the subscription is cancelled and no more values are emitted. If you are dealing with a hot stream, you should therefore consider resubscribing to it as a part of error recovery. + === Flow In Flow, you can use the `doOnError()` method to attach a <> that gets called if an error occurs. As for successful completion, you should declare a private method and wrap it inside `UI.accessLater()` . @@ -144,6 +150,7 @@ button.addClickListener(clickEvent -> { }); ---- + === Hilla In Hilla, you can use the `onError()` method of the `Subscription` object to register a function that gets called if an error occurs. @@ -167,6 +174,7 @@ const startJob = () => { Note, that the error callback function does not get any information about the error itself. + == Buffering You should not push updates to the browser more than 2--4 times per second. If your `Flux` is emitting events faster than that, you should buffer them and update the user interface in batches. Buffering a `Flux` is easy, as it has built-in support for it through the `buffer()` method. diff --git a/articles/building-apps/presentation-layer/server-push/threads.adoc b/articles/building-apps/presentation-layer/server-push/threads.adoc index 98788d1460..27285ce4ce 100644 --- a/articles/building-apps/presentation-layer/server-push/threads.adoc +++ b/articles/building-apps/presentation-layer/server-push/threads.adoc @@ -5,17 +5,19 @@ order: 10 section-nav: badge-flow --- -= User Interface Threads [badge-flow]#Flow# -[NOTE] -The examples on this page only work with push enabled. For information about how to do that, see the <<.#enabling-push-flow,Server Push>> documentation page. += User Interface Threads [badge-flow]#Flow# -You often use server push to update the user interface from background jobs. This is covered in the <<{articles}/building-apps/application-layer/background-jobs/interaction#,Background Jobs - UI Interaction>> documentation page. However, in Vaadin Flow, there are also cases where you want to start up a separate thread for use by the user interface itself. You might, for instance, want to show the server date and time in "real time". +Developers often use server push to update the user interface from background jobs (see <<{articles}/building-apps/application-layer/background-jobs/interaction#,Background Jobs - UI Interaction>>). However, in Vaadin Flow, there are also cases where you want to start a separate thread for use by the user interface itself. You might, for instance, want to show the server date and time in "real time". -If you have used Swing before, you might be tempted to use a `Timer`, or start a new `Thread` manually. In Flow, this is not a good idea. The reason is that Flow applications are multi-user applications, with potentially thousands of concurrent users. If each user creates its own `Timer`, or starts its own `Thread`, you may run out of threads. If that happens, the application crashes. +If you've used Swing before, you might be tempted to use a `Timer`, or to start a new `Thread` manually. In Flow, this is not a good idea. The reason is that Flow applications are multi-user applications, with potentially thousands of concurrent users. If each user creates its own `Timer`, or starts its own `Thread`, you may run out of threads. If that happens, the application crashes. Instead, you should use virtual threads, or Spring's `TaskExecutor` and `TaskScheduler`. +[NOTE] +The examples on this page only work with push enabled. For information about how to do that, see the <<.#enabling-push-flow,Server Push>> documentation page. + + == Virtual Threads If you use a Java version that supports virtual threads, you can start up a new virtual thread whenever you need one. @@ -35,6 +37,7 @@ This is the easiest way of starting a new user interface thread. If you are able For scheduled tasks, you should still use the `TaskScheduler`. This is covered later in this documentation page. + == Task Executor Setting up Spring's `TaskExecutor` and `TaskScheduler` is covered in the <<{articles}/building-apps/application-layer/background-jobs#,Background Jobs>> documentation page. You can use them to start tasks directly from the user interface as well. However, before you do, you should make sure your task is actually UI-related and not a background job. @@ -73,6 +76,7 @@ Because of the call to `UI.accessLater()`, the user interface is automatically u [CAUTION] Do not use the `@Async` annotation in your Flow views. It turns them into proxies that do not work with Vaadin. + == Task Scheduler You can use the `TaskScheduler` to schedule tasks. In this case, you have to schedule the task when the component is attached, and cancel it when it is detached. diff --git a/articles/building-apps/presentation-layer/server-push/updates.adoc b/articles/building-apps/presentation-layer/server-push/updates.adoc index 97cc18aa38..2b375a34aa 100644 --- a/articles/building-apps/presentation-layer/server-push/updates.adoc +++ b/articles/building-apps/presentation-layer/server-push/updates.adoc @@ -5,10 +5,8 @@ order: 1 section-nav: badge-flow --- -= Pushing UI Updates [badge-flow]#Flow# -[NOTE] -The examples on this page only work with push enabled. For information about how to do that, see the <<.#enabling-push-flow,Server Push>> documentation page. += Pushing UI Updates [badge-flow]#Flow# Whenever you are using server push in Vaadin Flow, you are triggering it from another thread than the normal HTTP request thread. Making changes to a UI from another thread and pushing them to the browser requires locking the user session. Otherwise, the UI update done from another thread could conflict with a regular event-driven update and cause either data corruption, race conditions or deadlocks. Such errors are by nature hard to discover and fix, since they often occur randomly, under heavy load. @@ -21,6 +19,9 @@ ui.access(() -> { }); ---- +[NOTE] +The examples on this page only work with push enabled. For information about how to do that, see the <<.#enabling-push-flow,Server Push>> documentation page. + By default, Flow uses automatic pushing. This means that any pending changes are pushed to the browser automatically after the command passed to `UI.access()` finishes. You can also configure Flow to use manual pushing. This gives you more control over when changes are pushed to the browser. For example, you can push multiple times inside a single call to `UI.access()`. To enable manual pushing, you have to make an addition to the `@Push` annotation, like this: @@ -43,6 +44,7 @@ ui.access(() -> { }); ---- + == Getting the UI Instance // This assumes that the UI has been explained earlier, and what attach and detach means. @@ -72,6 +74,7 @@ button.addClickListener(clickEvent -> { <1> This is executed in an HTTP request thread. The user session is locked and `UI.getCurrent()` returns the current `UI`-instance. <2> This is executed in the background thread. `UI.getCurrent()` returns `null`, but the `UI` instance is stored in a local variable. + == Access Later You often use server push in various types of event listeners and <>. A background job might inform you that it has finished processing. @@ -122,6 +125,7 @@ var subscription = myEventBus.subscribe(UI.getCurrent().accessLater((message) -> }, null)); ---- + == Avoiding Memory Leaks When you are using server push to update the user interface when an event has occurred, you typically subscribe a listener to some broadcaster or event bus. When you do this, you have to remember to always unsubscribe when the UI is detached. Otherwise, you end up with a memory leak that prevents your UI from being garbage collected. This is because the listener holds a reference to the `UI` instance. @@ -147,6 +151,7 @@ protected void onAttach(AttachEvent attachEvent) { // <1> <3> Remove the detach listener itself, to prevent a memory leak in case the component is attached multiple times. <4> Unsubscribe when the view is detached from the UI. + == Avoiding Floods Another risk you have to manage when updating the user interface in response to events is flooding the user interface with updates. As a rule of thumb, you should not push more than 2--4 times per second. Pushing more often than that can cause performance issues. Also, there is a limit to how many updates the human brain is able to register per second. @@ -155,6 +160,7 @@ If you know the events are coming in at a pace no faster than 2--4 events per se The buffering duration depends on the size of the UI update, and the network latency. In some applications, you may need to use a longer buffer duration, in others, a shorter one might work. You should try various durations and see what works best for your application. + == Avoiding Unnecessary Pushes The `UI.access()` method updates the user interface asynchronously. The update operation is not executed right away, but added to a queue and executed at some point in the future. If this is combined with regular event-driven updates in the HTTP request thread, you may end up in a situation where the user interface is updated out-of-order. diff --git a/articles/flow/advanced/long-running-tasks.adoc b/articles/flow/advanced/long-running-tasks.adoc index fd156e5d23..3fe8d3a247 100644 --- a/articles/flow/advanced/long-running-tasks.adoc +++ b/articles/flow/advanced/long-running-tasks.adoc @@ -8,7 +8,7 @@ order: 175 = Handling Long-Running Tasks [IMPORTANT] -This page is being migrated to the new <<{articles}/building-apps#,Building Apps>> section. See the <<{articles}/building-apps/application-layer/background-jobs#,Background Jobs>> and <<{articles}/building-apps/presentation-layer/server-push#,Server Push>> documentation pages. +This page is being migrated to the new <<{articles}/building-apps#,Building Applications>> section. See the <<{articles}/building-apps/application-layer/background-jobs#,Background Jobs>> and <<{articles}/building-apps/presentation-layer/server-push#,Server Push>> documentation pages. Often, server-side tasks can take a long time to complete. For example, a task that requires fetching a large amount of data from the database can take a long time to finish. In such cases, a poorly designed application can freeze the UI and prevent the user from interacting with the application. diff --git a/articles/flow/advanced/server-push.adoc b/articles/flow/advanced/server-push.adoc index bd800d4782..5bab893158 100644 --- a/articles/flow/advanced/server-push.adoc +++ b/articles/flow/advanced/server-push.adoc @@ -9,7 +9,7 @@ order: 620 = Server Push Configuration [IMPORTANT] -This page is being migrated to the new <<{articles}/building-apps#,Building Apps>> section. See the <<{articles}/building-apps/application-layer/background-jobs#,Background Jobs>> and <<{articles}/building-apps/presentation-layer/server-push#,Server Push>> documentation pages. +This page is being migrated to the new <<{articles}/building-apps#,Building Applications>> section. See the <<{articles}/building-apps/application-layer/background-jobs#,Background Jobs>> and <<{articles}/building-apps/presentation-layer/server-push#,Server Push>> documentation pages. Server push is based on a client-server connection established by the client. The server can then use the connection to send updates to the client. For example, it could send a new chat message to all participants without delay. diff --git a/articles/hilla/lit/guides/reactive-endpoints.adoc b/articles/hilla/lit/guides/reactive-endpoints.adoc index 5ef140447c..06d576a961 100644 --- a/articles/hilla/lit/guides/reactive-endpoints.adoc +++ b/articles/hilla/lit/guides/reactive-endpoints.adoc @@ -11,7 +11,7 @@ order: 35 = [since:dev.hilla:hilla@v1.2]#Reactive Endpoints# [IMPORTANT] -This page is being migrated to the new <<{articles}/building-apps#,Building Apps>> section. See the <<{articles}/building-apps/application-layer/background-jobs#,Background Jobs>> and <<{articles}/building-apps/presentation-layer/server-push#,Server Push>> documentation pages. +This page is being migrated to the new <<{articles}/building-apps#,Building Applications>> section. See the <<{articles}/building-apps/application-layer/background-jobs#,Background Jobs>> and <<{articles}/building-apps/presentation-layer/server-push#,Server Push>> documentation pages. Although traditional server calls work fine in most cases, sometimes you need different tools. https://projectreactor.io/[Reactor] is one of these, and can help you stream data to clients -- and it fits well into a non-blocking application. Whenever the simple request-response pattern doesn't suit your needs, you might consider Reactor. Multi-user applications, infinite data streaming, and retries are all good examples of what you can do with it. From 81e8f6745e24c247f1a675bcceff8b9a31d6675e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petter=20Holmstr=C3=B6m?= Date: Tue, 15 Oct 2024 15:00:21 +0300 Subject: [PATCH 23/30] More edits to background job pages. --- .../application-layer/background-jobs/index.adoc | 6 +++--- .../background-jobs/interaction/callbacks.adoc | 4 ++-- .../background-jobs/interaction/futures.adoc | 14 ++++++++------ .../background-jobs/interaction/reactive.adoc | 10 ++++++---- .../background-jobs/triggers.adoc | 2 ++ .../presentation-layer/server-push/callbacks.adoc | 4 ++-- .../presentation-layer/server-push/futures.adoc | 4 ++-- .../presentation-layer/server-push/reactive.adoc | 6 +++--- 8 files changed, 28 insertions(+), 22 deletions(-) diff --git a/articles/building-apps/application-layer/background-jobs/index.adoc b/articles/building-apps/application-layer/background-jobs/index.adoc index e3ae993645..f77f78b96e 100644 --- a/articles/building-apps/application-layer/background-jobs/index.adoc +++ b/articles/building-apps/application-layer/background-jobs/index.adoc @@ -21,7 +21,7 @@ Instead of creating threads manually, you should use either a thread pool, or vi A thread pool consists of a queue, and a pool of running threads. The threads pick tasks from the queue and execute them. When the thread pool receives a new job, it adds it to the queue. The queue has an upper size limit. If the queue is full, the thread pool rejects the job, and throws an exception. -Virtual threads were added in Java 21. Whereas ordinary threads are managed by the operating system, virtual threads are managed by the Java virtual machine. They are cheaper to start and run, which means you can have a much higher number of concurrent virtual threads than ordinary threads. +Virtual threads were added in Java 21. Whereas ordinary threads are managed by the operating system, virtual threads are managed by the Java virtual machine. They are cheaper to start and run, which means you can have a much higher number of concurrent virtual threads than ordinary threads. If your virtual machine supports virtual threads, you should use them. For more information on virtual threads, see the https://docs.oracle.com/en/java/javase/21/core/virtual-threads.html[Java Documentation]. @@ -103,7 +103,7 @@ public class MyWorker { For more information about task execution, see the https://docs.spring.io/spring-framework/reference/integration/scheduling.html[Spring Documentation]. -=== Caveats +=== Task Execution Annotation Caveats Using annotations makes the code more concise. However, they come with some caveats you need to be aware of. @@ -223,7 +223,7 @@ class MyScheduler { For more information about task scheduling, see the https://docs.spring.io/spring-framework/reference/integration/scheduling.html[Spring Documentation]. -=== Caveats +=== Task Scheduling Caveats Spring uses a separate thread pool for task scheduling. The tasks themselves are also executed in this thread pool. If you have a small number of short tasks, this is not a problem. However, if you have many tasks, or long-running tasks, you may run into problems. For instance, your scheduled jobs may stop running because the thread pool has become exhausted. diff --git a/articles/building-apps/application-layer/background-jobs/interaction/callbacks.adoc b/articles/building-apps/application-layer/background-jobs/interaction/callbacks.adoc index 3560725c35..8af325f454 100644 --- a/articles/building-apps/application-layer/background-jobs/interaction/callbacks.adoc +++ b/articles/building-apps/application-layer/background-jobs/interaction/callbacks.adoc @@ -8,7 +8,7 @@ section-nav: badge-flow = Callbacks [badge-flow]#Flow# -If you are using a Flow user interface, the simplest way of allowing your background jobs to interact with it is through callbacks. +If you are using a Flow user interface, the simplest way of allowing your background jobs to interact with it is through callbacks. If you are unsure which option to pick, you should start with this one. You can use `Consumer`, `Runnable`, and `Supplier` as callback interfaces, depending on how you want to interact with the background job. @@ -131,7 +131,7 @@ public void startBackgroundJob(Consumer onComplete, } ---- -All the callbacks have to be thread-safe, as they are called from the background thread, but owned and created by the user interface. For more information about how to implement these callbacks, see the <<{articles}/building-apps/presentation-layer/server-push/callbacks#,Server Push - Callbacks>> documentation page. +All the callbacks have to be thread-safe, as they are called from the background thread, but owned and created by the user interface. For more information about how to implement these callbacks, see the <<{articles}/building-apps/presentation-layer/server-push/callbacks#,Implementing Callbacks>> documentation page. === Improving Cancel API diff --git a/articles/building-apps/application-layer/background-jobs/interaction/futures.adoc b/articles/building-apps/application-layer/background-jobs/interaction/futures.adoc index d436dc5280..7d71ea4657 100644 --- a/articles/building-apps/application-layer/background-jobs/interaction/futures.adoc +++ b/articles/building-apps/application-layer/background-jobs/interaction/futures.adoc @@ -6,14 +6,18 @@ section-nav: badge-flow --- -= Futures [badge-flow]#Flow# += Returning Futures [badge-flow]#Flow# -If you are using a Flow user interface, you can use a standard Java `CompletableFuture` to report results and errors to the user interface. You can also use it to cancel the job from the user interface. For reporting progress, however, you still need to use a callback. +If you are using a Flow user interface, you can use a standard Java `CompletableFuture` to report results and errors to it, and to cancel the job. For reporting progress, however, you still need to use a callback. + +Compared to <>, this approach is arguably easier to implement in the application layer, and more difficult to implement in the presentation layer. You should only use it if you have used `CompletableFuture` before and need other features that it offers, like chaining completion stages together. == Returning a Result -The advantage of working with `CompletableFuture` is that Spring has built-in support for them when using the `@Async` annotation. For example, a background job that completes with either a string or an exception could be implemented like this: +Spring has built-in support for `CompletableFuture` when using the `@Async` annotation. + +The following example shows a background job that completes with either a string or an exception. If the method throws an exception, Spring automatically returns a `CompletableFuture` with the exception in question: [source,java] ---- @@ -23,9 +27,7 @@ public CompletableFuture startBackgroundJob() { } ---- -If the `doSomethingThatTakesALongTime()` method throws an exception, Spring automatically returns a `CompletableFuture` with the exception in question. - -To update the user interface, you have to add special completion stages that execute after the `CompletableFuture` completes. For more information about how to add these, see the <<{articles}/building-apps/presentation-layer/server-push/futures#,Server Push - Futures>> documentation page. +To update the user interface, you have to add special completion stages that execute after the `CompletableFuture` completes. For more information about how to add these, see the <<{articles}/building-apps/presentation-layer/server-push/futures#,Consuming Futures>> documentation page. == Cancelling diff --git a/articles/building-apps/application-layer/background-jobs/interaction/reactive.adoc b/articles/building-apps/application-layer/background-jobs/interaction/reactive.adoc index da4fce1dc8..2c0dfae399 100644 --- a/articles/building-apps/application-layer/background-jobs/interaction/reactive.adoc +++ b/articles/building-apps/application-layer/background-jobs/interaction/reactive.adoc @@ -7,9 +7,11 @@ order: 30 // TODO This page is about returning results from background threads. You can also use reactive streams for broadcasting, but that is a different use case. This should be covered in another documentation page, and linked to from here. -= Reactive Streams += Producing Reactive Streams -When using Flow or Hilla to build your user interface, you can use `Flux` or `Mono` from https://projectreactor.io/[Reactor] to allow your background jobs to interact with them. Reactor has an extensive API, which means you can do a lot of things with it. This also means that it has a steeper learning curve than using callbacks or `CompletableFuture`. If you are new to reactive programming, you should read Reactor's https://projectreactor.io/docs/core/release/reference/#intro-reactive[Introduction to Reactive Programming] before continuing. +When using Flow or Hilla to build your user interface, you can use `Flux` or `Mono` from https://projectreactor.io/[Reactor] to allow your background jobs to interact with them. Reactor has an extensive API, which means you can do a lot of things with it. This also means that it has a steeper learning curve than using callbacks or `CompletableFuture`. + +If you are new to reactive programming, you should read Reactor's https://projectreactor.io/docs/core/release/reference/#intro-reactive[Introduction to Reactive Programming] before continuing. == Returning a Result @@ -28,10 +30,10 @@ public Mono startBackgroundJob() { If the `doSomethingThatTakesALongTime()` method throws an exception, the `Mono` terminates with an error. -To update the user interface, you have to subscribe to the `Mono` or `Flux`. For more information about how to do this, see the <<{articles}/building-apps/presentation-layer/server-push/reactive#,Server Push - Reactive Streams>> documentation page. +To update the user interface, you have to subscribe to the `Mono` or `Flux`. For more information about how to do this, see the <<{articles}/building-apps/presentation-layer/server-push/reactive#,Consuming Reactive Streams>> documentation page. [IMPORTANT] -Hilla only supports `Flux`, so if your job is returning a `Mono`, you have to convert it to a `Flux` inside your `@BrowserCallable` endpoint. You can do this by calling the `Mono.flux()` method. +Hilla only supports `Flux`, so if your job is returning a `Mono`, you have to convert it to a `Flux` inside your `@BrowserCallable` endpoint. You can do this by calling the `Mono.asFlux()` method. == Reporting Progress diff --git a/articles/building-apps/application-layer/background-jobs/triggers.adoc b/articles/building-apps/application-layer/background-jobs/triggers.adoc index 30755efba8..0d3809fab8 100644 --- a/articles/building-apps/application-layer/background-jobs/triggers.adoc +++ b/articles/building-apps/application-layer/background-jobs/triggers.adoc @@ -27,6 +27,8 @@ On this page, all the trigger examples are delegating to a separate <> acts as the trigger. You can create a dedicated service class for this, or add a method to a suitable, existing application service. Like all other application service methods, it should be protected using method-level security to ensure only authorized users can trigger the job. +// TODO Add link to security page once it has been written + The following example demonstrates how to trigger a background job from an application service using `@Async` and security annotations: [source,java] diff --git a/articles/building-apps/presentation-layer/server-push/callbacks.adoc b/articles/building-apps/presentation-layer/server-push/callbacks.adoc index 84e39d3d84..7d4492a878 100644 --- a/articles/building-apps/presentation-layer/server-push/callbacks.adoc +++ b/articles/building-apps/presentation-layer/server-push/callbacks.adoc @@ -6,9 +6,9 @@ section-nav: badge-flow --- -= Callbacks [badge-flow]#Flow# += Implementing Callbacks [badge-flow]#Flow# -When building the user interface with Vaadin Flow, the easiest way of allowing a background job to update the user interface is through callbacks. This is explained in more detail in the <<{articles}/building-apps/application-layer/background-jobs/interaction/callbacks#,UI Interaction - Callbacks>> documentation page. +When building the user interface with Vaadin Flow, the easiest way of allowing a background job to update the user interface is through callbacks. This is explained in more detail in the <<{articles}/building-apps/application-layer/background-jobs/interaction/callbacks#,Callbacks>> documentation page. Whenever you implement a callback, you have remember that the callback is called by the background thread. This means that any updates to the user interface must happen inside a call to `UI.access()`. diff --git a/articles/building-apps/presentation-layer/server-push/futures.adoc b/articles/building-apps/presentation-layer/server-push/futures.adoc index 1b69aebbaf..55970bbc7a 100644 --- a/articles/building-apps/presentation-layer/server-push/futures.adoc +++ b/articles/building-apps/presentation-layer/server-push/futures.adoc @@ -5,9 +5,9 @@ order: 30 section-nav: badge-flow --- -= Futures [badge-flow]#Flow# += Consuming Futures [badge-flow]#Flow# -Some background jobs may use `CompletableFuture` to inform the user interface of results and errors. This is covered in the <<{articles}/building-apps/application-layer/background-jobs/interaction/futures#,UI Interaction - Futures>> documentation page. +Some background jobs may use `CompletableFuture` to inform the user interface of results and errors. This is covered in the <<{articles}/building-apps/application-layer/background-jobs/interaction/futures#,Returning Futures>> documentation page. [NOTE] The examples on this page only work with push enabled. For information about how to do that, see the <<.#enabling-push-flow,Server Push>> documentation page. diff --git a/articles/building-apps/presentation-layer/server-push/reactive.adoc b/articles/building-apps/presentation-layer/server-push/reactive.adoc index ac951b885c..cddaedc25a 100644 --- a/articles/building-apps/presentation-layer/server-push/reactive.adoc +++ b/articles/building-apps/presentation-layer/server-push/reactive.adoc @@ -5,9 +5,9 @@ order: 40 --- -= Reactive Streams += Consuming Reactive Streams -When building the user interface with either Vaadin Flow or Hilla, you can use reactive streams to allow a background job to update the user interface. This is covered in the <<{articles}/building-apps/application-layer/background-jobs/interaction/reactive#,UI Interaction - Reactive Streams>> documentation page. +When building the user interface with either Vaadin Flow or Hilla, you can use reactive streams to allow a background job to update the user interface. This is covered in the <<{articles}/building-apps/application-layer/background-jobs/interaction/reactive#,Producing Reactive Streams>> documentation page. == Subscribing @@ -45,7 +45,7 @@ button.addClickListener(clickEvent -> { }); ---- -In the following example, a `Flux` is used to receive chat messages. The stream is hot, so you have to subscribe to it when the component is attached, and unbuscribe when it is detached: +In the following example, a `Flux` is used to receive chat messages. The stream is hot, so you have to subscribe to it when the component is attached, and unsubscribe when it is detached: [source,java] ---- From 7c186b4835f1b6c3672e736c0d4432aa571e4203 Mon Sep 17 00:00:00 2001 From: russelljtdyer <6652767+russelljtdyer@users.noreply.github.com> Date: Tue, 15 Oct 2024 17:17:55 +0200 Subject: [PATCH 24/30] Full editing of new files and touched ones: callbacks - indexes. --- .../background-jobs/index.adoc | 36 +++++++++---------- .../interaction/callbacks.adoc | 14 ++++---- .../background-jobs/interaction/futures.adoc | 14 ++++---- .../background-jobs/interaction/index.adoc | 4 +-- articles/building-apps/index.adoc | 12 ++++--- .../server-push/callbacks.adoc | 6 ++-- .../server-push/futures.adoc | 9 ++--- .../presentation-layer/server-push/hilla.adoc | 4 +-- .../presentation-layer/server-push/index.adoc | 4 +-- 9 files changed, 52 insertions(+), 51 deletions(-) diff --git a/articles/building-apps/application-layer/background-jobs/index.adoc b/articles/building-apps/application-layer/background-jobs/index.adoc index f77f78b96e..fdf96577e4 100644 --- a/articles/building-apps/application-layer/background-jobs/index.adoc +++ b/articles/building-apps/application-layer/background-jobs/index.adoc @@ -9,34 +9,33 @@ order: 11 Many business applications need to perform in background threads. These tasks might be long-running operations triggered by the user, or scheduled jobs that run automatically at specific times or intervals. -Working with more than one thread increases the risk of bugs. Furthermore, there are many different ways of implementing background jobs. To reduce the risk, you should learn one way, and then apply it consistently in all your Vaadin applications. +Working with more than one thread increases the risk of bugs. Furthermore, there are many different ways of implementing background jobs. To reduce the risk, you should learn one way, and then apply it consistently in all of your Vaadin applications. == Threads -Whenever you work with background threads in a Vaadin application, you should never create new `Thread` objects directly. First, new threads are expensive to start. Second, the number of concurrent threads in a Java application is limited. While the exact limit depends on various factors, Java applications typically support thousands of threads. +Whenever you work with background threads in a Vaadin application, you should never create new `Thread` objects, directly. First, new threads are expensive to start. Second, the number of concurrent threads in a Java application is limited. While the exact limit depends on various factors, Java applications typically support thousands of threads. Instead of creating threads manually, you should use either a thread pool, or virtual threads. -A thread pool consists of a queue, and a pool of running threads. The threads pick tasks from the queue and execute them. When the thread pool receives a new job, it adds it to the queue. -The queue has an upper size limit. If the queue is full, the thread pool rejects the job, and throws an exception. +A thread pool consists of a queue, and a pool of running threads. The threads pick tasks from the queue and execute them. When the thread pool receives a new job, it adds it to the queue. The queue has an upper size limit. If the queue is full, the thread pool rejects the job, and throws an exception. -Virtual threads were added in Java 21. Whereas ordinary threads are managed by the operating system, virtual threads are managed by the Java virtual machine. They are cheaper to start and run, which means you can have a much higher number of concurrent virtual threads than ordinary threads. If your virtual machine supports virtual threads, you should use them. +Virtual threads were added in Java 21. Whereas ordinary threads are managed by the operating system, virtual threads are managed by the Java virtual machine. They're cheaper to start and run, which means you can have a much higher number of concurrent virtual threads than ordinary threads. If your virtual machine supports virtual threads, you should use them. For more information on virtual threads, see the https://docs.oracle.com/en/java/javase/21/core/virtual-threads.html[Java Documentation]. == Task Execution -The background jobs themselves should not need to manage their own thread pools, or virtual threads. Instead, they should use _executors_. An executor is an object that takes a `Runnable`, and executes it at some point in the future. Spring provides a `TaskExecutor`, that you should use in your background jobs. +The background jobs themselves shouldn't need to manage their own thread pools, or virtual threads. Instead, they should use _executors_. An executor is an object that takes a `Runnable`, and executes it at some point in the future. Spring provides a `TaskExecutor`, that you should use in your background jobs. By default, Spring Boot sets up a `ThreadPoolTaskExecutor` in your application context. You can tweak the parameters of this executor through the `spring.task.executor.*` configuration properties. -If you want to use virtual threads, you can enable them by setting the `spring.threads.virtual.enabled` configuration property to `true`. In this case, Spring Boot sets up a `SimpleAsyncTaskExecutor`, and creates a new virtual thread for every task. +To use virtual threads, you can enable them by setting the `spring.threads.virtual.enabled` configuration property to `true`. In this case, Spring Boot sets up a `SimpleAsyncTaskExecutor`, and creates a new virtual thread for every task. You can interact with the `TaskExecutor` either directly, or declaratively through annotations. -When interacting with it directly, you inject an instance of `TaskExecutor` into your code, and submit work to it. +When interacting with it directly, you would inject an instance of `TaskExecutor` into your code, and submit work to it. Here is an example of a class that uses the `TaskExecutor`: @@ -62,7 +61,7 @@ public class MyWorker { ---- [IMPORTANT] -When you inject the `TaskExecutor`, you have to name the parameter `taskExecutor`. The application context may contain more than one bean that implements the `TaskExecutor` interface. If the parameter name does not match the name of the bean, Spring does not know which instance to inject. +When you inject the `TaskExecutor`, you have to name the parameter `taskExecutor`. The application context may contain more than one bean that implements the `TaskExecutor` interface. If the parameter name doesn't match the name of the bean, Spring doesn't know which instance to inject. If you want to use annotations, you have to enable them before you can use them. Do this by adding the `@EnableAsync` annotation to your main application class, or any other `@Configuration` class. @@ -105,11 +104,9 @@ For more information about task execution, see the https://docs.spring.io/spring === Task Execution Annotation Caveats -Using annotations makes the code more concise. However, they come with some caveats you need to be aware of. +Using annotations makes the code more concise. However, they come with some caveats. -It is important to remember that if you forget to add `@EnableAsync` to your application, your `@Async` methods run synchronously in the calling thread instead of in a background thread. - -Also, you cannot call an `@Async` method from within the bean itself. This is because Spring by default uses proxies to process `@Async` annotations, and local method calls bypass the proxy. +It's important to remember that if you forget to add `@EnableAsync` to your application, your `@Async` methods run synchronously in the calling thread instead of in a background thread. Also, you can't call an `@Async` method from within the bean itself. This is because Spring by default uses proxies to process `@Async` annotations, and local method calls bypass the proxy. In the following example, `performTask()` is executed in a background thread, and `performAnotherTask()` in the calling thread: @@ -129,7 +126,7 @@ public class MyWorker { } ---- -If you interact with `TaskExecutor` directly, you avoid this problem. In the following example, both `performTask()` and `performAnotherTask()` execute in a background thread: +If you interact directly with `TaskExecutor`, you'll avoid this problem. In the following example, both `performTask()` and `performAnotherTask()` execute in a background thread: [source,java] ---- @@ -157,7 +154,7 @@ public class MyWorker { == Task Scheduling -Spring also has built in support for scheduling tasks through a `TaskScheduler`. You can interact with it either directly, or through annotations. In both cases, you have to enable it by adding the `@EnableScheduling` annotation to your main application class, or any other `@Configuration` class. +Spring also has built in support for scheduling tasks through a `TaskScheduler`. You can interact with it either directly, or through annotations. With both, you have to enable it by adding the `@EnableScheduling` annotation to your main application class, or any other `@Configuration` class. Here is an example that adds the annotation to the main application class: @@ -175,7 +172,7 @@ public class Application{ } ---- -When interacting with the `TaskScheduler` directly, you inject it into your code, and schedule work with it. +When interacting directly with the `TaskScheduler`, you inject it into your code, and schedule work with it. Here is an example that uses the `TaskScheduler` to execute the `performTask()` method every five minutes: @@ -223,11 +220,12 @@ class MyScheduler { For more information about task scheduling, see the https://docs.spring.io/spring-framework/reference/integration/scheduling.html[Spring Documentation]. + === Task Scheduling Caveats -Spring uses a separate thread pool for task scheduling. The tasks themselves are also executed in this thread pool. If you have a small number of short tasks, this is not a problem. However, if you have many tasks, or long-running tasks, you may run into problems. For instance, your scheduled jobs may stop running because the thread pool has become exhausted. +Spring uses a separate thread pool for task scheduling. The tasks themselves are also executed in this thread pool. It's fine if you have a small number of short tasks. However, if you have many tasks, or long-running tasks, you then may have problems. For instance, your scheduled jobs may stop running because the thread pool has become exhausted. -To avoid problems, you should use the scheduling thread pool to schedule jobs, and then hand them over to the task execution thread pool for execution. You can combine the `@Async` and `@Scheduled` annotations, like this: +To avoid trouble, you should use the scheduling thread pool to schedule jobs. Then give them to the task execution thread pool to execute. You can combine the `@Async` and `@Scheduled` annotations, like this: [source,java] ---- @@ -242,7 +240,7 @@ class MyScheduler { } ---- -You can also interact with the `TaskScheduler` and `TaskExecutor` directly, like this: +You can also interact directly with the `TaskScheduler` and `TaskExecutor`, like this: [source,java] ---- diff --git a/articles/building-apps/application-layer/background-jobs/interaction/callbacks.adoc b/articles/building-apps/application-layer/background-jobs/interaction/callbacks.adoc index 8af325f454..63811ae115 100644 --- a/articles/building-apps/application-layer/background-jobs/interaction/callbacks.adoc +++ b/articles/building-apps/application-layer/background-jobs/interaction/callbacks.adoc @@ -8,7 +8,7 @@ section-nav: badge-flow = Callbacks [badge-flow]#Flow# -If you are using a Flow user interface, the simplest way of allowing your background jobs to interact with it is through callbacks. If you are unsure which option to pick, you should start with this one. +When using a Flow user interface, the simplest way of allowing background jobs to interact with it is through callbacks. If you're unsure which option to pick, start with this one. You can use `Consumer`, `Runnable`, and `Supplier` as callback interfaces, depending on how you want to interact with the background job. @@ -36,7 +36,7 @@ You can use `Consumer`, `Runnable`, and `Supplier` as callback interfaces, depen == Returning a Result -For example, a background job that returns a string or an exception could be implemented like this: +A background job that returns a string or an exception could be implemented like this: [source,java] ---- @@ -88,7 +88,7 @@ public void startBackgroundJob(Consumer onComplete, == Cancelling -Furthermore, if the job can also be cancelled, it could look like this: +A job can be cancelled. To do that, it would look like this: [source,java] ---- @@ -131,12 +131,12 @@ public void startBackgroundJob(Consumer onComplete, } ---- -All the callbacks have to be thread-safe, as they are called from the background thread, but owned and created by the user interface. For more information about how to implement these callbacks, see the <<{articles}/building-apps/presentation-layer/server-push/callbacks#,Implementing Callbacks>> documentation page. +All callbacks have to be thread-safe since they're called from the background thread, but owned and created by the user interface. For more information about how to implement these callbacks, see the <<{articles}/building-apps/presentation-layer/server-push/callbacks#,Implementing Callbacks>> documentation page. === Improving Cancel API -If you want to make the cancelling API nicer, you can replace the callback with a handle. First, create a handle interface that the user interface can use to cancel the job: +To make the cancelling API nicer, you can replace the callback with a handle. First, create a handle interface that the user interface can use to cancel the job: [source,java] ---- @@ -191,6 +191,6 @@ public CancellableJob startBackgroundJob(Consumer onComplete, } ---- -The user interface would have to store the handle while the job is running, and call the `cancel()` method to cancel it. Note, that you cannot use the `@Async` annotation in this case. This is because `@Async` methods can only return `void` or future-like types. In this case, you want to return neither. +The user interface would have to store the handle while the job is running, and call the `cancel()` method to cancel it. However, you can't use the `@Async` annotation in this case. It's because `@Async` methods can only return `void` or future-like types. In this case, you may want to return neither. -The handle itself is thread safe because you are using an `AtomicBoolean`. You do not need to take any special precautions to call it from the user interface. +The handle itself is thread safe because you're using an `AtomicBoolean`. You don't need to take any special precautions to call it from the user interface. diff --git a/articles/building-apps/application-layer/background-jobs/interaction/futures.adoc b/articles/building-apps/application-layer/background-jobs/interaction/futures.adoc index 7d71ea4657..e5549e3121 100644 --- a/articles/building-apps/application-layer/background-jobs/interaction/futures.adoc +++ b/articles/building-apps/application-layer/background-jobs/interaction/futures.adoc @@ -8,16 +8,16 @@ section-nav: badge-flow = Returning Futures [badge-flow]#Flow# -If you are using a Flow user interface, you can use a standard Java `CompletableFuture` to report results and errors to it, and to cancel the job. For reporting progress, however, you still need to use a callback. +When using a Flow user interface, you can use a standard Java `CompletableFuture` to report results and errors to it, and to cancel the job. For reporting progress, however, you still need to use a callback. -Compared to <>, this approach is arguably easier to implement in the application layer, and more difficult to implement in the presentation layer. You should only use it if you have used `CompletableFuture` before and need other features that it offers, like chaining completion stages together. +Compared to <>, this approach is easier to implement in the application layer, but more difficult to implement in the presentation layer. You should only use it if you've used `CompletableFuture` previously, and you need other features that it offers, like chaining completion stages together. == Returning a Result Spring has built-in support for `CompletableFuture` when using the `@Async` annotation. -The following example shows a background job that completes with either a string or an exception. If the method throws an exception, Spring automatically returns a `CompletableFuture` with the exception in question: +The following example shows a background job that completes with either a string or an exception. If the method throws an exception, Spring returns a `CompletableFuture` with the exception in question: [source,java] ---- @@ -32,11 +32,11 @@ To update the user interface, you have to add special completion stages that exe == Cancelling -You can cancel a Java `Future` by calling its `cancel()` method. The method has a `boolean` parameter that indicates whether the thread should be interrupted or not. However, `CompletableFuture`, which implements `Future`, does not use this parameter. It therefore does not make any difference whether you pass in `true` or `false`. +You can cancel a Java `Future` by calling its `cancel()` method. The method has a `boolean` parameter that indicates whether the thread should be interrupted or not. However, `CompletableFuture`, which implements `Future`, doesn't use this parameter. It therefore doesn't make any difference whether you pass in `true` or `false`. -When you cancel a `CompletableFuture`, it completes with a `CompletionException` caused by a `CancellationException`. However, the job continues to silently run in the background until it has finished. If you want to notify the job itself that it has been cancelled, and should stop running at the next suitable moment, you have to make some changes. +When you cancel a `CompletableFuture`, it completes with a `CompletionException` caused by a `CancellationException`. However, the job continues to run silently in the background until it's finished. If you want to notify the job itself that it has been cancelled, and should stop running at the next suitable moment, you have to make some changes. -`CompletableFuture` has an `isCancelled()` method that you can use to query whether the job has been cancelled or not. However, do to this, you cannot use the `@Async` annotation anymore. Instead, you have to manually execute the job using the `TaskExecutor`, and manage the state of the returned `CompletableFuture`. The principle is the same as when you are using callbacks or handles. +`CompletableFuture` has an `isCancelled()` method that you can use to query whether the job has been cancelled or not. Therefore, you can no longer use the `@Async` annotation. Instead, you have to execute manually the job using the `TaskExecutor`, and manage the state of the returned `CompletableFuture`. The principle is the same as when you're using callbacks or handles. The earlier example would look like this when implemented using a `CompletableFuture`: @@ -71,4 +71,4 @@ public CompletableFuture startBackgroundJob() { } ---- -You do not need to do anything with the `future` after it has been cancelled, as it has already been completed. Returning is enough. \ No newline at end of file +You don't need to do anything with the `future` after it has been cancelled, as it has already been completed. Returning is enough. diff --git a/articles/building-apps/application-layer/background-jobs/interaction/index.adoc b/articles/building-apps/application-layer/background-jobs/interaction/index.adoc index c97744181e..84465e1fc4 100644 --- a/articles/building-apps/application-layer/background-jobs/interaction/index.adoc +++ b/articles/building-apps/application-layer/background-jobs/interaction/index.adoc @@ -7,9 +7,9 @@ order: 25 = User Interface Interaction -Some background jobs execute business processes in the background. The end user may see the result of the background job, but does not have to interact with the job directly. Scheduled jobs and event triggered jobs typically fall in this category. +Some background jobs execute business processes in the background. The end user may see the result of the job, but doesn't have to interact directly with it. Scheduled and event triggered jobs are typically in this category. -Then there are jobs that need to interact with the user interface. For instance, the job may want to update a progress indicator while running, and notify the user when the job has completed, or an error has occurred. Furthermore, the user may want to be able to cancel a running job before it has completed. This page explains different options for allowing a user to interact with a background job, and vice versa. +Then there are jobs that need to interact with the user interface. For instance, a job may want to update a progress indicator while running, and notify the user when it's finished, or an error has occurred. Furthermore, the user may want to be able to cancel a running job before it has completed. This page explains different options for allowing a user to interact with a background job, and vice versa. == Options diff --git a/articles/building-apps/index.adoc b/articles/building-apps/index.adoc index edba4ec98e..56ecbf81db 100644 --- a/articles/building-apps/index.adoc +++ b/articles/building-apps/index.adoc @@ -1,6 +1,6 @@ --- title: Building Apps -description: Learn how to build real-world business applications with Vaadin. +description: How to build real-world business applications with Vaadin. order: 800 section-nav: flat expanded --- @@ -11,13 +11,15 @@ section-nav: flat expanded .Work in Progress [IMPORTANT] -This section of the documentation is still being written. Its content is useful, but several parts are missing. +This section of the documentation is still being written. Its content is useful, but several parts have not yet been added. -Vaadin is a powerful tool for building real-world business applications. However, like all versatile tools, it can be used in many different ways and some of these ways lead to better outcomes than others. +Vaadin is a powerful tool for building real-world business applications. However, as with many versatile tools, it can be used in several different ways and some of these lead to better outcomes than others. -Like all big things, big business applications have small beginnings. You can figure things out as you go along, but wouldn't it be easier if somebody showed you how to use your new tools from the start? In this section of the documentation, you'll learn the Vaadin Way of building business applications. The Vaadin Way is not a tutorial. Rather, it is an opinionated and holistic look at building modern business applications with Vaadin. It is not limited to the user interface, but also covers all the other parts, like business logic, persistence, integration with external systems, and even deployment. It scales progressively from a demo, to a hobby project, to serving even the most demanding enterprise needs. +Big business applications have often have small beginnings. You can determine things as you go along, but it might be easier if someone showed you how to use your new tools from the start. In this section of the documentation, you'll learn the Vaadin Way of building business applications. -That said, the Vaadin Way is not the only way to build Vaadin applications. Experienced developers and maintainers of existing applications should be able to continue using Vaadin in their preferred way, or cherry pick the pieces of the Vaadin Way that works for them. +The Vaadin Way is not a tutorial. Rather, it is an opinionated and holistic look at building modern business applications with Vaadin. It's not limited to the user interface: it also covers other aspects, like business logic, persistence, integration with external systems, and even deployment. It scales progressively from a demo to a hobby project, to serving even the most demanding enterprise needs. + +The Vaadin Way is not the only way, though, to build Vaadin applications. Experienced developers and maintainers of existing applications can continue using Vaadin in their preferred way, or choose the parts of the Vaadin Way that works for them. == Topics diff --git a/articles/building-apps/presentation-layer/server-push/callbacks.adoc b/articles/building-apps/presentation-layer/server-push/callbacks.adoc index 7d4492a878..56757b1ae9 100644 --- a/articles/building-apps/presentation-layer/server-push/callbacks.adoc +++ b/articles/building-apps/presentation-layer/server-push/callbacks.adoc @@ -15,7 +15,7 @@ Whenever you implement a callback, you have remember that the callback is called [NOTE] The examples on this page only work with push enabled. For information about how to do that, see the <<.#enabling-push-flow,Server Push>> documentation page. -For every callback, you should create a private method in your user interface. The method is going to be called inside `UI.access()` so you can safely update the user interface inside it. +For every callback, you should create a private method in your user interface. The method is called inside `UI.access()`, so you can safely update the user interface inside it. For example, a method for handling successful completion could look like this: @@ -26,7 +26,7 @@ private void onJobCompleted(String result) { } ---- -Likewise, a method for handling errors could look like this: +Likewise, a method for handling errors might look like this: [source,java] ---- @@ -50,4 +50,4 @@ button.addClickListener(clickEvent -> { ); }); ---- -<1> The `UI.accessLater()` method is explained on the <> documentation page. \ No newline at end of file +<1> The `UI.accessLater()` method is explained on the <> documentation page. diff --git a/articles/building-apps/presentation-layer/server-push/futures.adoc b/articles/building-apps/presentation-layer/server-push/futures.adoc index 55970bbc7a..d840ffcaac 100644 --- a/articles/building-apps/presentation-layer/server-push/futures.adoc +++ b/articles/building-apps/presentation-layer/server-push/futures.adoc @@ -5,6 +5,7 @@ order: 30 section-nav: badge-flow --- + = Consuming Futures [badge-flow]#Flow# Some background jobs may use `CompletableFuture` to inform the user interface of results and errors. This is covered in the <<{articles}/building-apps/application-layer/background-jobs/interaction/futures#,Returning Futures>> documentation page. @@ -23,7 +24,7 @@ private void onJobCompleted(String result) { } ---- -Likewise, a method for handling errors could look like this: +Likewise, a method for handling errors might look like this: [source,java] ---- @@ -32,7 +33,7 @@ private void onJobFailed(Throwable error) { } ---- -Note, that the error handler must accept a `Throwable` and not an `Exception` when you are working with `CompletableFuture`. +Note, that the error handler must accept a `Throwable`, and not an `Exception` when you're working with `CompletableFuture`. == Successful Completion @@ -56,9 +57,9 @@ button.addClickListener(clickEvent -> { If a `CompletableFuture` is completed with an exception, you can instruct it to perform a specific operation by calling the `exceptionally()` method on it. However, this method works in a different way than `thenAccept()`. -The `exceptionally()` method takes a `Function` instead of a `Consumer` as input. The exception is passed to the function as input. The function output is used as the result of the `CompletableFuture` that is returned by `exceptionally()`. +The `exceptionally()` method takes a `Function`, instead of a `Consumer` as input. The exception is passed to the function as input. The function output is used as the result of the `CompletableFuture` that is returned by `exceptionally()`. -Flow has no version of `UI.accessLater()` that works with `Function`. However, since you are not interested in returning a result, you can create a helper function that adapts a `Consumer` to a `Function`, like this: +Flow has no version of `UI.accessLater()` that works with `Function`. However, since you're not interested in returning a result, you can create a helper function that adapts a `Consumer` to a `Function`, like this: [source,java] ---- diff --git a/articles/building-apps/presentation-layer/server-push/hilla.adoc b/articles/building-apps/presentation-layer/server-push/hilla.adoc index 9f323363b0..fb22e1cbad 100644 --- a/articles/building-apps/presentation-layer/server-push/hilla.adoc +++ b/articles/building-apps/presentation-layer/server-push/hilla.adoc @@ -1,6 +1,6 @@ --- title: Hilla Services -description: How to create reactive browser callable services for your Vaadin Hilla user interface. +description: How to create reactive browser callable services for a Vaadin Hilla user interface. order: 39 section-nav: badge-hilla --- @@ -32,4 +32,4 @@ public class TimeService { Hilla generates the necessary TypeScript types to subscribe to this service from the browser. -For more information about updating the UI using a reactive service, see the <> documentation page. \ No newline at end of file +For more information about updating the UI using a reactive service, see the <> documentation page. diff --git a/articles/building-apps/presentation-layer/server-push/index.adoc b/articles/building-apps/presentation-layer/server-push/index.adoc index 5f3a8bbeb1..d51d474c7e 100644 --- a/articles/building-apps/presentation-layer/server-push/index.adoc +++ b/articles/building-apps/presentation-layer/server-push/index.adoc @@ -9,9 +9,9 @@ order: 50 Server push is based on a client-server connection established by the client. The server can then use the connection to send updates to the client. For example, it could send a new chat message to all participants without delay. -The server-client communication uses a WebSocket connection, if supported by the browser and the server. If not, the connection resorts to whatever method is supported by the browser. Vaadin uses the link:https://github.com/Atmosphere/atmosphere[Atmosphere framework], internally. +The server-client communication uses a WebSocket connection, if supported by the browser and the server. If not, the connection resorts to whatever method is supported. Vaadin uses the link:https://github.com/Atmosphere/atmosphere[Atmosphere framework], internally. -In Hilla views, push is always enabled when you subscribe to a _reactive endpoint_. For Flow views, you have to enable it explicitly. +In Hilla views, push is always enabled when you subscribe to a _reactive endpoint_. For Flow views, you have to enable it, explicitly. [IMPORTANT] Server push is not the same as Web Push, which is also supported by Vaadin Flow. For more information, see the <<{articles}/flow/configuration/setting-up-webpush#,Web Push Notifications>> documentation page. From 0ab911400cd35d229b69b76bba14c8dec0fe184f Mon Sep 17 00:00:00 2001 From: russelljtdyer <6652767+russelljtdyer@users.noreply.github.com> Date: Wed, 16 Oct 2024 09:44:25 +0200 Subject: [PATCH 25/30] Second editing of new and touched file: jobs - updates. --- .../background-jobs/interaction/reactive.adoc | 20 +++---- .../background-jobs/jobs.adoc | 26 +++++---- .../background-jobs/triggers.adoc | 26 ++++----- .../server-push/reactive.adoc | 58 +++++++++---------- .../server-push/threads.adoc | 24 ++++---- .../server-push/updates.adoc | 42 +++++++------- 6 files changed, 99 insertions(+), 97 deletions(-) diff --git a/articles/building-apps/application-layer/background-jobs/interaction/reactive.adoc b/articles/building-apps/application-layer/background-jobs/interaction/reactive.adoc index 2c0dfae399..c91a741bd3 100644 --- a/articles/building-apps/application-layer/background-jobs/interaction/reactive.adoc +++ b/articles/building-apps/application-layer/background-jobs/interaction/reactive.adoc @@ -1,5 +1,5 @@ --- -title: Reactive Streams +title: Producing Reactive Streams description: How to use reactive streams to interact with the user interface. order: 30 --- @@ -9,14 +9,14 @@ order: 30 = Producing Reactive Streams -When using Flow or Hilla to build your user interface, you can use `Flux` or `Mono` from https://projectreactor.io/[Reactor] to allow your background jobs to interact with them. Reactor has an extensive API, which means you can do a lot of things with it. This also means that it has a steeper learning curve than using callbacks or `CompletableFuture`. +When using Flow or Hilla to build your user interface, you can use `Flux` or `Mono` from https://projectreactor.io/[Reactor] to allow your background jobs to interact with them. Reactor has an extensive API, which means you can do many things with it. This also means that it can be more difficult to learn than using callbacks or `CompletableFuture`. -If you are new to reactive programming, you should read Reactor's https://projectreactor.io/docs/core/release/reference/#intro-reactive[Introduction to Reactive Programming] before continuing. +If you're new to reactive programming, you should read Reactor's https://projectreactor.io/docs/core/release/reference/#intro-reactive[Introduction to Reactive Programming] before continuing. == Returning a Result -When you're using Reactor, you cannot use the `@Async` annotation. Instead, you have to manually instruct your `Mono` or `Flux` to execute using the Spring `TaskExecutor`. Otherwise, your job executes in the thread that subscribes to the `Mono` or `Flux`. +When you're using Reactor, you can't use the `@Async` annotation. Instead, you have to instruct your `Mono` or `Flux` to execute using the Spring `TaskExecutor`. Otherwise, your job executes in the thread that subscribes to the `Mono` or `Flux`. For example, a background job that returns a string or an exception could be implemented like this: @@ -38,7 +38,7 @@ Hilla only supports `Flux`, so if your job is returning a `Mono`, you have to co == Reporting Progress -If your background job only needs to report its progress without actually returning a result, you can return a `Flux`. Your job should then emit progress updates, and complete the stream when done. However, you often also want to return a result. Since Hilla only supports returning a single `Flux`, you have to use the same stream for emitting both progress updates and the end result. The code may be a bit messy, but it works. +If your background job only needs to report its progress without actually returning a result, you can return a `Flux`. Your job should then emit progress updates, and complete the stream when done. However, you may often want also to return a result. Since Hilla only supports returning a single `Flux`, you have to use the same stream for emitting both progress updates and the end result. The code may be a bit messy, but it works. You first need to create a data type that can contain both progress updates and the result. For a job that returns a string, it could look like this: @@ -60,10 +60,10 @@ public record BackgroundJobOutput( } ---- -The two factory methods `progressUpdate()` and `finished()` make the code look better when it is time to create instances of `BackgroundJobOutput`. +The two built-in methods, `progressUpdate()` and `finished()` make the code look better when it's time to create instances of `BackgroundJobOutput`. [NOTE] -If you have worked with sealed classes, you may be tempted to create a sealed interface called `BackgroundJobOutput`, and then create two records that implement that interface: one for progress updates, and another for the result. However, Hilla does not support this at the moment. +If you've worked with sealed classes, you may be tempted to create a sealed interface called `BackgroundJobOutput`, and then create two records that implement that interface: one for progress updates; and another for the result. However, Hilla doesn't support this at the moment. Next, you have to implement the background job like this: @@ -91,16 +91,16 @@ public Flux startBackgroundJob() { ); } ---- -<1> Create a sink that you can emit progress updates to. +<1> Create a sink to which you can emit progress updates. <2> Create a `Mono` that emits the result of the background job. <3> Map both streams to `BackgroundJobOutput` and merge them. -When your user interface subscribes to this `Flux`, it needs to check the state of the returned `BackgroundJobOutput` objects. If `progressUpdate` contains a value, it should update the progress indicator. If `result` contains a value, the operation is finished. +When your user interface subscribes to this `Flux`, it needs to check the state of the returned `BackgroundJobOutput` objects. If `progressUpdate` contains a value, it should update the progress indicator. If `result` contains a value, though, the operation is finished. == Cancelling -You can cancel a subscription to a `Flux` or `Mono` at any time. However, as with `CompletableFuture`, cancelling the subscription does not stop the background job itself. To fix this, you need to tell the background job when it has been cancelled, so that it can stop itself. Continuing on the earlier example, adding support for cancelling could look like this: +You can cancel a subscription to a `Flux` or `Mono` at any time. However, as with `CompletableFuture`, cancelling the subscription doesn't stop the background job itself. To fix this, you need to tell the background job when it has been cancelled, so that it can stop. Continuing on the earlier example, adding support for cancelling could look like this: [source,java] ---- diff --git a/articles/building-apps/application-layer/background-jobs/jobs.adoc b/articles/building-apps/application-layer/background-jobs/jobs.adoc index 9079f85d6f..a7f5ec32c2 100644 --- a/articles/building-apps/application-layer/background-jobs/jobs.adoc +++ b/articles/building-apps/application-layer/background-jobs/jobs.adoc @@ -9,11 +9,11 @@ order: 10 When implementing a background job, it's important to decouple its logic from how and where it is triggered. This ensures flexibility in triggering the job from different sources. -For instance, you may want to run the job every time the application starts up. In this case, you may want to run it in the main thread, blocking the initialization of the rest of the application until the job is finished. You may also want to run the job in a background thread every day at midnight, or whenever a certain application event is published. +For instance, you may want to run the job every time the application starts. In this case, you may want to run it in the main thread, blocking the initialization of the rest of the application until the job is finished. You may also want to run the job in a background thread every day perhaps at midnight, or whenever a certain application event is published. Here is a visual example of a job with three different triggers: -image::images/job-and-triggers.png[A job with three triggers] +image::images/job-and-triggers.png[Job with Three Triggers] In code, a job is a Spring bean, annotated with the `@Service` annotation. It contains one or more methods, that when called, execute the job in the calling thread. @@ -32,12 +32,12 @@ public class MyBackgroundJob { } ---- -If the job is <> from within the same package, the class can be package-private. If triggered externally, it must be public. +For a job <> from within the same package, the class can be package-private. A job triggered externally, must be public. == Transactions -If the job works on the database, it should manage its own transactions. Because a job is a Spring bean, you can use either declarative, or programmatic transaction management. +A job that works on the database, should manage its own transactions. Because a job is a Spring bean, you can use either declarative, or programmatic transaction management. Here is an example of a background job that uses declarative transaction management to ensure the job runs inside a new transaction: @@ -60,14 +60,14 @@ public class MyBackgroundJob { == Security -Unlike <<../application-services#,application services>>, background jobs should _not_ rely on method-level security. The reason is that Spring Security uses the `SecurityContext` to access information about the current user. This context is typically thread local, which means it is not available in a background thread. Therefore, whenever the job is executed by a background thread, Spring would deny access. +Unlike <<../application-services#,application services>>, background jobs should not rely on method-level security. The reason is that Spring Security uses the `SecurityContext` to access information about the current user. This context is typically thread local, which means it's not available in a background thread. Therefore, whenever the job is executed by a background thread, Spring would deny access. -If the background job needs information about the current user, this information should be passed to it by the <>, as an immutable method parameter. +When the background job needs information about the current user, this information should be passed to it by the <>, as an immutable method parameter. == Batch Jobs -Consider implementing two versions of your batch job: one for processing all applicable inputs and another for handling a specific set of inputs. This approach provides flexibility when you need to process individual cases or recover from errors. +Consider implementing two versions of your batch job: one for processing all applicable inputs; and another for handling a specific set of inputs. This approach provides flexibility when you need to process individual cases or recover from errors. For example, a batch job that generates invoices for shipped orders could look like this: @@ -88,15 +88,17 @@ public class InvoiceCreationJob { } ---- -In this example, the first method creates invoices for the orders whose ID:s have been passed as parameters. The second method creates invoices for all orders that have been shipped and not yet invoiced. +In this example, the first method creates invoices for the orders whose IDs have been passed as parameters. The second generates invoices for all orders that have been shipped, but not yet invoiced. -Implementing batch jobs like this does not require much effort if done from the start, but allows for flexibility that may be useful. Continuing on the invoice generation example, you may discover a bug in production. This bug has caused some orders to have bad data in the database. As a result, the batch job has not been able to generate invoices for them. Fixing the bug is easy, but your users do not want to wait for the next batch run to occur. Instead, as a part of the fix, you can add a button to the user interface that allows a user to trigger invoice generation for an individual order. +Implementing batch jobs like this doesn't require much effort if done from the start, but allows for flexibility that may be useful. Continuing on the invoice generation example, you may discover a bug in production. This bug causes some orders to have bad data in the database. As a result, the batch job won't be able to generate invoices for them. + +Fixing the bug is easy, but your users don't want to wait for the next batch run to occur. Instead, you can add a button to the user interface that allows a user to trigger invoice generation for an individual order. == Idempotent Jobs -Whenever you build a background job that updates, or generates data, you should consider making the job _idempotent_. An idempotent job leaves the database in the same state regardless of how many times it has been executed on the same input. +Whenever you build a background job that updates or generates data, you should consider making the job _idempotent_. An idempotent job leaves the database in the same state regardless of how many times it's been executed on the same input. -For example, a job that generates invoices for shipped orders should always check that no invoice already exists before it generates a new one. Otherwise, some customers may end up getting multiple invoices because of an error somewhere. +For example, a job that generates invoices for shipped orders should always check that no invoice already exists before it generates a new one. Otherwise, some customers may receive multiple invoices because of an error somewhere. -How to make a job idempotent depends on the job itself. It is therefore outside the scope of this documentation page. +How to make a job idempotent depends on the job itself. This is outside the scope of this documentation page. diff --git a/articles/building-apps/application-layer/background-jobs/triggers.adoc b/articles/building-apps/application-layer/background-jobs/triggers.adoc index 0d3809fab8..bc3b58726b 100644 --- a/articles/building-apps/application-layer/background-jobs/triggers.adoc +++ b/articles/building-apps/application-layer/background-jobs/triggers.adoc @@ -13,9 +13,9 @@ Jobs can be triggered in various ways, such as on application startup, at regula Below is a visual example of a job with three different triggers: -image::images/job-and-triggers.png[A job with three triggers] +image::images/job-and-triggers.png[Job with Three Triggers] -Spring has support for plugging in an `AsyncUncaughtExceptionHandler` that gets called whenever an `@Async` method throws an exception. However, this moves the error handling outside of the method where the error occurred. To increase code readability, you should handle the error explicitly in every trigger. +Spring has support for plugging in an `AsyncUncaughtExceptionHandler` that's called whenever an `@Async` method throws an exception. However, this moves the error handling outside of the method where the error occurred. To increase code readability, you should handle the error explicitly in every trigger. Some triggers, like event listeners and schedulers, are not intended to be invoked by other objects. You should make them package-private to limit their visibility. @@ -56,16 +56,16 @@ public class MyApplicationService { <1> Spring ensures the current user has permission to start the job. <2> Spring executes the method using its task executor thread pool. -This example uses the `@Async` annotation, but you can also execute the job <<../background-jobs#task-execution,programmatically>>. +This example uses the `@Async` annotation, but you can also execute the job, <<../background-jobs#task-execution,programmatically>>. If the job needs to provide real-time updates to the user interface (e.g., showing a progress bar or error messages), you have to use server push. For more details, see the <> documentation page. == Event Triggered Jobs -For event triggered jobs, you should create an event listener that receives events from Spring's event publisher. By default, the event publisher calls each listener in the same thread that published the event. You should therefore hand over the job to the `TaskExecutor`. +For event triggered jobs, you should create an event listener that receives events from Spring's event publisher. By default, the event publisher calls each listener in the same thread that published the event. You should therefore give the job to the `TaskExecutor`. -Here is an example of a listener that listens for `MyEvent` to be published. When the event occurs, it triggers the job in a background thread: +Below is an example of a listener that listens for `MyEvent` to be published. When the event occurs, it triggers the job in a background thread: [source,java] ---- @@ -92,16 +92,16 @@ class PerformBackgroundJobOnMyEventTrigger { <1> Spring calls the trigger when the `MyEvent` is published. <2> Spring executes the method using its task executor thread pool. -This example uses the `@Async` annotation, but you can also execute the job <<../background-jobs#task-execution,programmatically>>. +This example uses the `@Async` annotation, but you can also execute the job, <<../background-jobs#task-execution,programmatically>>. == Scheduled Jobs For scheduled jobs, you should create a scheduler that uses Spring's scheduling mechanism to trigger the job. -Spring uses a separate thread pool for scheduled tasks. It is important not to use the scheduling thread pool for executing jobs directly. Instead, schedule tasks using Spring’s `TaskScheduler` and then delegate the actual job execution to the `TaskExecutor`. +Spring uses a separate thread pool for scheduled tasks. It's important not to use the scheduling thread pool for executing jobs, directly. Instead, schedule tasks using Spring’s `TaskScheduler` and then delegate the actual job execution to the `TaskExecutor`. -Here is an example of a scheduler that schedules a job to execute every five minutes in a background thread: +This is an example of a scheduler that schedules a job to execute every five minutes in a background thread: [source,java] ---- @@ -129,9 +129,9 @@ class MyBackgroundJobScheduler { <1> Spring calls the trigger every 5 minutes. <2> Spring executes the method using its task executor thread pool. -This example uses the `@Scheduled` and `@Async` annotations, but you can also execute the job using the task scheduler and task executor <<../background-jobs#task-scheduling,programmatically>>. +This example uses the `@Scheduled` and `@Async` annotations, but you can also execute the job using the task scheduler and task executor, <<../background-jobs#task-scheduling,programmatically>>. -Programmatic schedulers are more verbose, but they are easier to debug. Therefore, you should start with annotations when you implement schedulers. If you later need more control over the scheduling, or run into problems that are difficult to debug, you should switch to a programmatic approach. +Programmatic schedulers are more verbose, but they're easier to debug. Therefore, you should start with annotations when you implement schedulers. If you later need more control over the scheduling, or run into problems that are difficult to debug, you should switch to a programmatic approach. == Startup Jobs @@ -154,9 +154,9 @@ class MyStartupTrigger { ---- [IMPORTANT] -Whenever you implement a startup trigger like this, you have to remember that the application is still being initialized. That means that not all services may be available for your job to use. +Whenever you implement a startup trigger as in the example, you have to remember that the application is still being initialized. That means that not all services may be available for your job to use. -Here is an example of a trigger that executes a job in a background thread after the application has started up: +Below is an example of a trigger that executes a job in a background thread after the application has started: [source,java] ---- @@ -186,6 +186,6 @@ class MyStartupTrigger { <1> Spring calls the trigger when the `ApplicationReadyEvent` is published. <2> Spring executes the method using its task executor thread pool. -This example uses the `@Async` annotation, but you can also execute the job <<../background-jobs#task-execution,programmatically>>. +This example uses the `@Async` annotation, but you can also execute the job, <<../background-jobs#task-execution,programmatically>>. // TODO How to trigger jobs using Control Center? diff --git a/articles/building-apps/presentation-layer/server-push/reactive.adoc b/articles/building-apps/presentation-layer/server-push/reactive.adoc index cddaedc25a..767b007e7a 100644 --- a/articles/building-apps/presentation-layer/server-push/reactive.adoc +++ b/articles/building-apps/presentation-layer/server-push/reactive.adoc @@ -1,5 +1,5 @@ --- -title: Rective Streams +title: Consuming Rective Streams description: How to use server push with reactive streams. order: 40 --- @@ -9,19 +9,21 @@ order: 40 When building the user interface with either Vaadin Flow or Hilla, you can use reactive streams to allow a background job to update the user interface. This is covered in the <<{articles}/building-apps/application-layer/background-jobs/interaction/reactive#,Producing Reactive Streams>> documentation page. +//RUSSELL: This opening paragraph gives the feeling that the reader shouldn't read this page since it immediately sends them elsewhere. You need a sentence or two that says the point of continuing, maybe something about subscribing, handling errors, etc. -== Subscribing + +== Types of Subscriptions Background threads typically use cold streams for output. A cold stream starts emitting values when the client subscribes to it, and then completes. Broadcasts typically use hot streams for output. A hot stream emits values regardless of whether a client is subscribed or not. A subscriber only receives the values that were emitted while it was subscribed. -In your user interfaces, you typically do not need to worry about unsubscribing from cold streams, as they are often short lived. However, if you subscribe to a hot stream, it is important that you remember to unsubscribe when no longer needed. +In your user interfaces, you typically don't need to worry about unsubscribing from cold streams, as they're often short lived. However, if you subscribe to a hot stream, it's important that you remember to unsubscribe when no longer needed. -=== Flow +=== Subscribing with Flow -In Flow, you can use both `Flux` and `Mono`. You subscribe to both by calling the `subscribe()` method. The method takes a <> that is called for each emitted value. You should implement the callback as a private method, and then wrap it inside `UI.accessLater()` when you subscribe. +In Flow, you can use both `Flux` and `Mono`. You subscribe to both by calling the `subscribe()` method. The method takes a <> which is called for each emitted value. You should implement the callback as a private method, and then wrap it inside `UI.accessLater()` when you subscribe. For example, a method for handling successful completion could look like this: @@ -34,7 +36,7 @@ private void onJobCompleted(String result) { The `UI.accessLater()` method is explained in the <> documentation page. -In the following example, a background job returns a `Mono`. The stream is cold, so you do not need to explicitly unsubscribe from it, as this happens automatically once the `Mono` has completed. The job is started by a button click listener. +In the following example, a background job returns a `Mono`. The stream is cold, so you don't need to unsubscribe explicitly from it, as this happens once the `Mono` has completed. The job is started by a button click listener. [source,java] ---- @@ -45,7 +47,7 @@ button.addClickListener(clickEvent -> { }); ---- -In the following example, a `Flux` is used to receive chat messages. The stream is hot, so you have to subscribe to it when the component is attached, and unsubscribe when it is detached: +In the following example, a `Flux` is used to receive chat messages. The stream is hot, so you have to subscribe to it when the component is attached, and unsubscribe when it's detached: [source,java] ---- @@ -66,9 +68,9 @@ protected void onAttach(AttachEvent attachEvent) { ---- -=== Hilla +=== Subscribing with Hilla -In Hilla, you can only use a `Flux`, even if you are only emitting a single value. However, you can easily convert a `Mono` to a `Flux` by calling the `asFlux()` method. +In Hilla, you can only use a `Flux` -- even if you're only emitting a single value. However, you can easily convert a `Mono` to a `Flux` by calling the `asFlux()` method. This is an example of a reactive service that delegates to a worker to start a background job. The worker returns a `Mono`, which the service converts to a `Flux`: @@ -92,7 +94,7 @@ public class MyBackgroundJobService { You subscribe to a `Flux` by calling the generated TypeScript service method. You then use the returned `Subscription` object to register a function that gets called whenever the `Flux` emits a value. -The following client-side example uses the `Flux` from the earlier example to receive a single output from a server-side background job. The stream is cold, so you do not need to explicitly unsubscribe from it: +The following client-side uses the `Flux` from the earlier example to receive a single output from a server-side background job. The stream is cold, so you don't need to unsubscribe from it: [source,typescript] ---- @@ -105,7 +107,7 @@ const startJob = () => { } ---- -The following client-side examples uses a `Flux` to receive chat messages. The stream is hot, so you have to subscribe to it inside a React effect. In the cleanup function, you call the `cancel` method of the subscription object. This ensures that the subscription is cancelled whenever your component is removed from the DOM: +The following client-side example uses a `Flux` to receive chat messages. The stream is hot, so you have to subscribe to it inside a React effect. In the cleanup function, it calls the `cancel` method of the subscription object. This ensures the subscription is cancelled whenever your component is removed from the DOM: [source,typescript] ---- @@ -122,12 +124,12 @@ useEffect(() => { == Handling Errors -In a reactive stream, an error is a terminal event. This means that the subscription is cancelled and no more values are emitted. If you are dealing with a hot stream, you should therefore consider resubscribing to it as a part of error recovery. +In a reactive stream, an error is a terminal event. This means that the subscription is cancelled and no more values are emitted. If you're dealing with a hot stream, you should therefore consider resubscribing to it as a part of error recovery. -=== Flow +=== Errors with Flow -In Flow, you can use the `doOnError()` method to attach a <> that gets called if an error occurs. As for successful completion, you should declare a private method and wrap it inside `UI.accessLater()` . +In Flow, you can use the `doOnError()` method to attach a <> that's called if an error occurs. As for successful completion, you should declare a private method and wrap it inside `UI.accessLater()` . For example, a method for handling errors could look like this: @@ -151,11 +153,11 @@ button.addClickListener(clickEvent -> { ---- -=== Hilla +=== Errors with Hilla -In Hilla, you can use the `onError()` method of the `Subscription` object to register a function that gets called if an error occurs. +With Hilla, you can use the `onError()` method of the `Subscription` object to register a function that's called if an error occurs. -If you add error handling to the earlier background job example, you end up with something like this: +If you add error handling to the earlier background job example, you get something like this: [source,typescript] ---- @@ -172,12 +174,12 @@ const startJob = () => { } ---- -Note, that the error callback function does not get any information about the error itself. +Note, that the error callback function doesn't receive any information about the error itself. == Buffering -You should not push updates to the browser more than 2--4 times per second. If your `Flux` is emitting events faster than that, you should buffer them and update the user interface in batches. Buffering a `Flux` is easy, as it has built-in support for it through the `buffer()` method. +You shouldn't push updates to the browser more than 2 to 4 times per second. If your `Flux` is emitting events faster than that, you should buffer them and update the user interface in batches. Buffering a `Flux` is easy, as it has built-in support for it through the `buffer()` method. In the following example, the buffered stream buffers events for 250 milliseconds before it emits them in batches. Because of this, the user interface is receiving a `List` instead of an `Event`: @@ -193,9 +195,9 @@ public Flux> bufferedEventStream() { ---- -If you are using Flow, you can do the buffering in your user interface, before you subscribe to the stream. +If you're using Flow, you can do the buffering in your user interface, before you subscribe to the stream. -In the following example, the a user interface component subscribes to the buffered stream when it is attached, and unsubscribes when it is detached: +In the following example, the user interface component subscribes to the buffered stream when it's attached, and unsubscribes when it's detached: [source,java] ---- @@ -213,9 +215,9 @@ protected void onAttach(AttachEvent attachEvent) { } ---- -If you are using Hilla, you have to do the buffering inside the reactive service. +If you're using Hilla, you have to do the buffering inside the reactive service. -The following example shows a browser callable service that buffers the stream before it is returned. Because of this, the generated TypeScript service method emits arrays of `Event` objects: +The following example shows a browser callable service that buffers the stream before it's returned. Because of this, the generated TypeScript service method emits arrays of `Event` objects: [source,java] ---- @@ -236,17 +238,15 @@ public class EventService { == Lost Subscriptions [badge-hilla]#Hilla# -In Hilla, you have to be prepared to handle the case where a subscription is lost without being cancelled. For instance, the user may close their laptop lid, or get temporarily disconnected from the network. Hilla automatically re-establishes the connection, but the subscription may no longer be valid. When this happens, Hilla calls the `onSubscriptionLost` callback function if one has been registered with the `Subscription` object. +In Hilla, you have to be prepared to handle situations in which a subscription is lost without being cancelled. For instance, the user may close their laptop lid, or be temporarily disconnected from the network. Hilla automatically re-establishes the connection, but the subscription may no longer be valid. When this happens, Hilla calls the `onSubscriptionLost` callback function if one has been registered with the `Subscription` object. This function can return two values: -`REMOVE`:: Remove the subscription. No more values are received until the client has explicitly resubscribed. - -`RESUBSCRIBE`:: Re-subscribe by calling the same server method again. +`REMOVE`:: Remove the subscription. No more values are received until the client has explicitly resubscribed. This is the default action if no callback has been specified. -If no callback has been specified, `REMOVE` is the default action. +`RESUBSCRIBE`:: Re-subscribe by calling the same server method. -In the following example, a React component subscribes to a reactive service inside an effect. It automatically resubscribes to the same service if it loses the subscription: +In the following example, a React component subscribes to a reactive service inside an effect. It resubscribes to the same service if it loses the subscription: [source,typescript] ---- diff --git a/articles/building-apps/presentation-layer/server-push/threads.adoc b/articles/building-apps/presentation-layer/server-push/threads.adoc index 27285ce4ce..6a8c9f23f9 100644 --- a/articles/building-apps/presentation-layer/server-push/threads.adoc +++ b/articles/building-apps/presentation-layer/server-push/threads.adoc @@ -1,6 +1,6 @@ --- title: Threads -description: How to use threads in your Vaadin Flow user interface. +description: How to use threads in a Vaadin Flow user interface. order: 10 section-nav: badge-flow --- @@ -8,9 +8,9 @@ section-nav: badge-flow = User Interface Threads [badge-flow]#Flow# -Developers often use server push to update the user interface from background jobs (see <<{articles}/building-apps/application-layer/background-jobs/interaction#,Background Jobs - UI Interaction>>). However, in Vaadin Flow, there are also cases where you want to start a separate thread for use by the user interface itself. You might, for instance, want to show the server date and time in "real time". +Developers often use server push to update the user interface from background jobs (see <<{articles}/building-apps/application-layer/background-jobs/interaction#,Background Jobs - UI Interaction>>). However, in Vaadin Flow, there are also cases where you may want to start a separate thread for use by the user interface itself. You might, for instance, want to show the server date and time in "real time". -If you've used Swing before, you might be tempted to use a `Timer`, or to start a new `Thread` manually. In Flow, this is not a good idea. The reason is that Flow applications are multi-user applications, with potentially thousands of concurrent users. If each user creates its own `Timer`, or starts its own `Thread`, you may run out of threads. If that happens, the application crashes. +If you have experience with Swing, you might be tempted to use a `Timer`, or to start a new `Thread`, manually. In Flow, this isn't a good idea. The reason is that Flow applications are multi-user applications, with potentially thousands of concurrent users. If each user creates their own `Timer`, or starts their own `Thread`, you may run out of threads. If that happens, the application crashes. Instead, you should use virtual threads, or Spring's `TaskExecutor` and `TaskScheduler`. @@ -20,7 +20,7 @@ The examples on this page only work with push enabled. For information about how == Virtual Threads -If you use a Java version that supports virtual threads, you can start up a new virtual thread whenever you need one. +If you use a Java version that supports virtual threads, you can start a new virtual thread whenever you need one. Here is an example of a button click listener that starts a new virtual thread: @@ -33,16 +33,16 @@ button.addClickListener(clickEvent -> { }); ---- -This is the easiest way of starting a new user interface thread. If you are able to use virtual threads, they should be your first choice. If you run into problems, switch to the `TaskExecutor`. +This is the easiest way of starting a new user interface thread. If you're able to use virtual threads, they should be your first choice. If you run into problems, though, switch to the `TaskExecutor`. For scheduled tasks, you should still use the `TaskScheduler`. This is covered later in this documentation page. == Task Executor -Setting up Spring's `TaskExecutor` and `TaskScheduler` is covered in the <<{articles}/building-apps/application-layer/background-jobs#,Background Jobs>> documentation page. You can use them to start tasks directly from the user interface as well. However, before you do, you should make sure your task is actually UI-related and not a background job. +You can use Spring's `TaskExecutor` and `TaskScheduler` to start tasks directly from the user interface, as well. Configuring them is covered in the <<{articles}/building-apps/application-layer/background-jobs#,Background Jobs>> documentation page. -To use the `TaskExecutor` or `TaskScheduler`, you inject them into your view, and then call them when needed. +Before starting a task with `TaskExecutor` and `TaskScheduler`, you should make sure it's actually UI-related and not a background job. To use them properly, you would inject them into your view, and then call them when needed. Here is an example of a view that gets the `TaskExecutor` as a constructor parameter: @@ -60,7 +60,7 @@ public class MyView extends VerticalLayout { } ---- -Here is an example of a button click listener that starts a UI operation in a background thread: +This example uses a button click listener that starts a UI operation in a background thread: [source,java] ---- @@ -74,12 +74,12 @@ button.addClickListener(clickEvent -> { Because of the call to `UI.accessLater()`, the user interface is automatically updated through a server push when the task finishes. [CAUTION] -Do not use the `@Async` annotation in your Flow views. It turns them into proxies that do not work with Vaadin. +Do not use the `@Async` annotation in your Flow views. It turns them into proxies that don't work with Vaadin. == Task Scheduler -You can use the `TaskScheduler` to schedule tasks. In this case, you have to schedule the task when the component is attached, and cancel it when it is detached. +You can use the `TaskScheduler` to schedule tasks. With it, you have to schedule the task when the component is attached, and cancel it when it is detached. The following example schedules a task to be executed every second. The task sets the text of `currentTimeLabel` to the current date and time of the server. When the component is detached, the task is cancelled: @@ -99,7 +99,7 @@ protected void onAttach(AttachEvent attachEvent) { } ---- -The tasks that you execute in the task scheduler should be fast. If you need to schedule long-running tasks, you should hand them over to `TaskExecutor` for execution. +The tasks that you execute in the task scheduler should be fast. If you need to schedule long-running tasks, you should give them to `TaskExecutor` for execution. [CAUTION] -Do not use the `@Scheduled` annotation in your Flow views. It turns them into proxies that do not work with Vaadin. +Do not use the `@Scheduled` annotation in your Flow views. It turns them into proxies that don't work with Vaadin. diff --git a/articles/building-apps/presentation-layer/server-push/updates.adoc b/articles/building-apps/presentation-layer/server-push/updates.adoc index 2b375a34aa..52a752d799 100644 --- a/articles/building-apps/presentation-layer/server-push/updates.adoc +++ b/articles/building-apps/presentation-layer/server-push/updates.adoc @@ -1,6 +1,6 @@ --- title: Pushing -description: How to push updates to your Vaadin Flow user interface. +description: How to push updates to a Vaadin Flow user interface. order: 1 section-nav: badge-flow --- @@ -8,9 +8,9 @@ section-nav: badge-flow = Pushing UI Updates [badge-flow]#Flow# -Whenever you are using server push in Vaadin Flow, you are triggering it from another thread than the normal HTTP request thread. Making changes to a UI from another thread and pushing them to the browser requires locking the user session. Otherwise, the UI update done from another thread could conflict with a regular event-driven update and cause either data corruption, race conditions or deadlocks. Such errors are by nature hard to discover and fix, since they often occur randomly, under heavy load. +Whenever you're using server push in Vaadin Flow, you're triggering it from another thread than the normal HTTP request thread. Making changes to a UI from another thread and pushing them to the browser requires locking the user session. Otherwise, the UI update done from another thread could conflict with a regular event-driven update and cause either data corruption, race conditions or deadlocks. -Because of this, you may only access a UI using the `UI.access()` method, which locks the session to prevent race conditions. You use it like this: +Such errors are by nature hard to discover and fix, since they often occur randomly and under a heavy load. Because of this, you may only access a UI using the `UI.access()` method, which locks the session to prevent race conditions. You would use it like this: [source,java] ---- @@ -22,7 +22,7 @@ ui.access(() -> { [NOTE] The examples on this page only work with push enabled. For information about how to do that, see the <<.#enabling-push-flow,Server Push>> documentation page. -By default, Flow uses automatic pushing. This means that any pending changes are pushed to the browser automatically after the command passed to `UI.access()` finishes. You can also configure Flow to use manual pushing. This gives you more control over when changes are pushed to the browser. For example, you can push multiple times inside a single call to `UI.access()`. +By default, Flow uses automatic pushing. This means that any pending changes are pushed to the browser after the command passed to `UI.access()` finishes. You can also configure Flow to use manual pushing. This gives you more control over when changes are pushed to the browser. For example, you can push multiple times inside a single call to `UI.access()`. To enable manual pushing, you have to make an addition to the `@Push` annotation, like this: @@ -34,7 +34,7 @@ public class Application implements AppShellConfigurator { } ---- -After this, you have to call the `UI.push()` method whenever you want to push your changes to the browser, like this: +Afterwards, you'll have to call the `UI.push()` method whenever you want to push your changes to the browser, like this: [source,java] ---- @@ -51,11 +51,11 @@ ui.access(() -> { Before you can call `access()`, you need to get the `UI` instance. You typically use `Component.getUI()` or `UI.getCurrent()` for this. However, both are problematic when it comes to server push. -`Component.getUI()` is not thread-safe, which means you should only call it while the user session is locked. Therefore, you cannot use it to call `access()`. +`Component.getUI()` is not thread-safe, which means you should only call it while the user session is locked. Therefore, you can't use it to call `access()`. -`UI.getCurrent()` only returns a non-`null` value when the current thread owns the session lock. When called from a background thread, it returns `null`. Therefore, you cannot use it to call `access()`, either. +`UI.getCurrent()` only returns a non-`null` value when the current thread owns the session lock. When called from a background thread, it returns `null`. Therefore, you can't use it to call `access()`, either. -Whenever you are planning to use server push, you have to get a hold of the `UI` instance _while the user session is locked_. This typically happens right before you start your background thread. +Whenever you're planning to use server push, you have to get a hold of the `UI` instance while the user session is locked. This typically happens right before you start your background thread. Here is an example of a button click listener that starts a background thread: @@ -77,7 +77,7 @@ button.addClickListener(clickEvent -> { == Access Later -You often use server push in various types of event listeners and <>. A background job might inform you that it has finished processing. +You probably use often server push in various types of event listeners and <>. A background job might inform you that it has finished processing. In the following example, the user interface is updated in a callback after a background job has finished: @@ -91,7 +91,7 @@ myService.startBackgroundJob(() -> ui.access(() -> { Another common use case is an event bus informing you that a new message has arrived. -In the following example, the user interface subscribes to an even bus, and updates the user interface whenever a new message arrives: +In the following example, the user interface subscribes to an event bus, and updates the user interface whenever a new message arrives: [source,java] ---- @@ -101,13 +101,13 @@ var subscription = myEventBus.subscribe((message) -> ui.access(() -> { })); ---- -In cases like these, you should consider using `UI.accessLater()` instead of `UI.access()`. +In cases like these, you should consider using `UI.accessLater()`, instead of `UI.access()`. -`UI.accessLater()` exists in two versions: one that wraps a `SerializableRunnable`, and another that wraps a `SerializableConsumer`. It stores the `UI` instance, and runs the wrapped delegate inside a call to `UI.access()`. +`UI.accessLater()` exists in two versions: one that wraps a `SerializableRunnable`; and another that wraps a `SerializableConsumer`. It stores the `UI` instance, and runs the wrapped delegate inside a call to `UI.access()`. It also takes a second parameter, which is a _detach handler_. The detach handler is a `Runnable` that runs if the `UI` has been detached when `UI.access()` is called. The detach handler can be `null` if no special actions are needed. -Rewritten with `accessLater()`, the thread completion example becomes: +Rewritten with `accessLater()`, the thread completion example becomes this: [source,java] ---- @@ -116,7 +116,7 @@ myService.startBackgroundJob(UI.getCurrent().accessLater(() -> { }, null)); ---- -Likewise, the event listener becomes: +Likewise, the event listener becomes this: [source,java] ---- @@ -128,9 +128,9 @@ var subscription = myEventBus.subscribe(UI.getCurrent().accessLater((message) -> == Avoiding Memory Leaks -When you are using server push to update the user interface when an event has occurred, you typically subscribe a listener to some broadcaster or event bus. When you do this, you have to remember to always unsubscribe when the UI is detached. Otherwise, you end up with a memory leak that prevents your UI from being garbage collected. This is because the listener holds a reference to the `UI` instance. +When you're using server push to update the user interface when an event has occurred, you typically subscribe a listener to some broadcaster or event bus. When you do this, be sure to unsubscribe when the UI is detached. Otherwise, you'll have a memory leak that prevents your UI from being garbage collected. This is because the listener holds a reference to the `UI` instance. -It is recommended to always subscribe when your view is attached to a UI, and unsubscribe when it is detached. You can do this by overriding the `Component.onAttach()` method, like this: +Always subscribe when your view is attached to a UI, and unsubscribe when it's detached. You can do this by overriding the `Component.onAttach()` method, like so: [source,java] ---- @@ -156,16 +156,16 @@ protected void onAttach(AttachEvent attachEvent) { // <1> Another risk you have to manage when updating the user interface in response to events is flooding the user interface with updates. As a rule of thumb, you should not push more than 2--4 times per second. Pushing more often than that can cause performance issues. Also, there is a limit to how many updates the human brain is able to register per second. -If you know the events are coming in at a pace no faster than 2--4 events per second, you can push on every event. However, if they are more frequent, you have to start buffering events and update the user interface in batches. This is quite easy to do if you are using a `Flux` from https://projectreactor.io/[Reactor]. See the <> documentation page for more information about this. +When you know events are coming no faster than 2 to 4 events per second, you can push on every event. However, if they're more frequent, you have to start buffering events and update the user interface in batches. This is quite easy to do if you're using a `Flux` from https://projectreactor.io/[Reactor]. See the <> documentation page for more information about this. -The buffering duration depends on the size of the UI update, and the network latency. In some applications, you may need to use a longer buffer duration, in others, a shorter one might work. You should try various durations and see what works best for your application. +The buffering duration depends on the size of the UI update, and the network latency. In some applications, you may need to use a longer buffer duration. In others, a shorter one might work. You should try various durations to see what works best for your application. == Avoiding Unnecessary Pushes -The `UI.access()` method updates the user interface asynchronously. The update operation is not executed right away, but added to a queue and executed at some point in the future. If this is combined with regular event-driven updates in the HTTP request thread, you may end up in a situation where the user interface is updated out-of-order. +The `UI.access()` method updates the user interface, asynchronously. The update operation is not executed immediately, but added to a queue and executed at some time later. If this is combined with regular event-driven updates in the HTTP request thread, you may have a situation in which the user interface is updated out-of-order. -Look at this example: +To understand better, look at this example: [source,java] ---- @@ -186,7 +186,7 @@ This
is added from an event listener This
is added from within a call to UI.access() ---- -In this particular case, the call to `UI.access()` would not have been needed. Sometimes, you can deduce this by looking at the code. However, there are situations where this is not clear. You may have code that sometimes is executed by the HTTP request thread, and sometimes by another thread. In this case, you can check whether the current thread has locked the user session or not, like this: +In this particular case, the call to `UI.access()` would not have been needed. Sometimes, you can deduce this by looking at the code. However, there are situations where this is not clear. You may have code that is executed sometimes by the HTTP request thread, and sometimes by another thread. For this situation, you can check whether the current thread has locked the user session, like this: [source,java] ---- From 9eb26cec54d067b2ca420fc8668aebc7097c4eb9 Mon Sep 17 00:00:00 2001 From: russelljtdyer <6652767+russelljtdyer@users.noreply.github.com> Date: Thu, 17 Oct 2024 10:45:15 +0200 Subject: [PATCH 26/30] Third editing of all new and touched files. --- .../background-jobs/index.adoc | 20 +++++++------- .../interaction/callbacks.adoc | 16 +++++------- .../background-jobs/interaction/futures.adoc | 10 +++---- .../background-jobs/interaction/index.adoc | 2 +- .../background-jobs/jobs.adoc | 12 ++++----- .../background-jobs/triggers.adoc | 16 ++++++------ articles/building-apps/index.adoc | 6 ++--- .../server-push/callbacks.adoc | 2 +- .../server-push/futures.adoc | 14 +++++----- ...> reactive-browser-callable-services.adoc} | 2 +- .../server-push/threads.adoc | 14 +++++----- .../server-push/updates.adoc | 26 +++++++++---------- 12 files changed, 69 insertions(+), 71 deletions(-) rename articles/building-apps/presentation-layer/server-push/{hilla.adoc => reactive-browser-callable-services.adoc} (94%) diff --git a/articles/building-apps/application-layer/background-jobs/index.adoc b/articles/building-apps/application-layer/background-jobs/index.adoc index fdf96577e4..ca8316534c 100644 --- a/articles/building-apps/application-layer/background-jobs/index.adoc +++ b/articles/building-apps/application-layer/background-jobs/index.adoc @@ -7,7 +7,7 @@ order: 11 = Background Jobs -Many business applications need to perform in background threads. These tasks might be long-running operations triggered by the user, or scheduled jobs that run automatically at specific times or intervals. +Many business applications need to perform in background threads. These tasks might be long-running operations triggered by the user, or scheduled jobs that run at specific times or intervals. Working with more than one thread increases the risk of bugs. Furthermore, there are many different ways of implementing background jobs. To reduce the risk, you should learn one way, and then apply it consistently in all of your Vaadin applications. @@ -63,9 +63,9 @@ public class MyWorker { [IMPORTANT] When you inject the `TaskExecutor`, you have to name the parameter `taskExecutor`. The application context may contain more than one bean that implements the `TaskExecutor` interface. If the parameter name doesn't match the name of the bean, Spring doesn't know which instance to inject. -If you want to use annotations, you have to enable them before you can use them. Do this by adding the `@EnableAsync` annotation to your main application class, or any other `@Configuration` class. +If you want to use annotations, you have to enable them first. Do this by adding the `@EnableAsync` annotation to your main application class, or any other `@Configuration` class. -Here is an example that adds the annotation to the main application class: +Here's an example that adds the annotation to the main application class: [source,java] ---- @@ -81,9 +81,9 @@ public class Application{ } ---- -You can now use the `@Async` annotation to tell Spring to execute your code in a background thread. +You can now use the `@Async` annotation to tell Spring to execute your code in a background thread. -Here is a version of the earlier `MyWorker` example that uses `@Async` instead of the `TaskExecutor`: +Here is a version of the earlier `MyWorker` example, but using `@Async` instead of the `TaskExecutor`: [source,java] ---- @@ -106,7 +106,7 @@ For more information about task execution, see the https://docs.spring.io/spring Using annotations makes the code more concise. However, they come with some caveats. -It's important to remember that if you forget to add `@EnableAsync` to your application, your `@Async` methods run synchronously in the calling thread instead of in a background thread. Also, you can't call an `@Async` method from within the bean itself. This is because Spring by default uses proxies to process `@Async` annotations, and local method calls bypass the proxy. +Should you forget to add `@EnableAsync` to your application, your `@Async` methods run synchronously in the calling thread instead of in a background thread. Also, you can't call an `@Async` method from within the bean itself. This is because Spring by default uses proxies to process `@Async` annotations, and local method calls bypass the proxy. In the following example, `performTask()` is executed in a background thread, and `performAnotherTask()` in the calling thread: @@ -156,7 +156,7 @@ public class MyWorker { Spring also has built in support for scheduling tasks through a `TaskScheduler`. You can interact with it either directly, or through annotations. With both, you have to enable it by adding the `@EnableScheduling` annotation to your main application class, or any other `@Configuration` class. -Here is an example that adds the annotation to the main application class: +Below is an example that adds the annotation to the main application class: [source,java] ---- @@ -172,9 +172,9 @@ public class Application{ } ---- -When interacting directly with the `TaskScheduler`, you inject it into your code, and schedule work with it. +When interacting directly with the `TaskScheduler`, you'd inject it into your code, and schedule work with it. -Here is an example that uses the `TaskScheduler` to execute the `performTask()` method every five minutes: +This is an example that uses the `TaskScheduler` to execute the `performTask()` method every five minutes: [source,java] ---- @@ -223,7 +223,7 @@ For more information about task scheduling, see the https://docs.spring.io/sprin === Task Scheduling Caveats -Spring uses a separate thread pool for task scheduling. The tasks themselves are also executed in this thread pool. It's fine if you have a small number of short tasks. However, if you have many tasks, or long-running tasks, you then may have problems. For instance, your scheduled jobs may stop running because the thread pool has become exhausted. +Spring uses a separate thread pool for task scheduling. The tasks themselves are also executed in this thread pool. That's fine if you have a small number of short tasks. However, if you have many tasks, or long-running tasks, you may have problems. For instance, your scheduled jobs might stop running because the thread pool has become exhausted. To avoid trouble, you should use the scheduling thread pool to schedule jobs. Then give them to the task execution thread pool to execute. You can combine the `@Async` and `@Scheduled` annotations, like this: diff --git a/articles/building-apps/application-layer/background-jobs/interaction/callbacks.adoc b/articles/building-apps/application-layer/background-jobs/interaction/callbacks.adoc index 63811ae115..cb93f8b4a3 100644 --- a/articles/building-apps/application-layer/background-jobs/interaction/callbacks.adoc +++ b/articles/building-apps/application-layer/background-jobs/interaction/callbacks.adoc @@ -8,27 +8,25 @@ section-nav: badge-flow = Callbacks [badge-flow]#Flow# -When using a Flow user interface, the simplest way of allowing background jobs to interact with it is through callbacks. If you're unsure which option to pick, start with this one. - -You can use `Consumer`, `Runnable`, and `Supplier` as callback interfaces, depending on how you want to interact with the background job. +When using a Flow user interface, the simplest way of allowing background jobs to interact with it is through callbacks. You can use `Consumer`, `Runnable`, and `Supplier` as callback interfaces, depending on how you want to interact with the background job. [cols="1,1"] |=== |Event |Callback -|Completed without a result +|Completed without a result. |`Runnable` -|Completed with a result of type `T` +|Completed with a result of type `T`. |`Consumer` -|Completed with an exception +|Completed with an exception. |`Consumer` -|Reported percentage done +|Reported percentage done. |`Consumer` -|Cancelled by user +|Cancelled by user. |`Supplier` |=== @@ -55,7 +53,7 @@ public void startBackgroundJob(Consumer onComplete, == Reporting Progress -If the background job is also reporting its progress, for instance as a percentage number, it could look like this: +When the background job is also reporting its progress, for instance as a percentage number, it could look like this: [source,java] ---- diff --git a/articles/building-apps/application-layer/background-jobs/interaction/futures.adoc b/articles/building-apps/application-layer/background-jobs/interaction/futures.adoc index e5549e3121..e9abc9b918 100644 --- a/articles/building-apps/application-layer/background-jobs/interaction/futures.adoc +++ b/articles/building-apps/application-layer/background-jobs/interaction/futures.adoc @@ -1,6 +1,6 @@ --- title: Futures -description: How to use `CompletableFuture` to iteract with the user interface. +description: How to use CompletableFuture to iteract with the user interface. order: 20 section-nav: badge-flow --- @@ -10,7 +10,7 @@ section-nav: badge-flow When using a Flow user interface, you can use a standard Java `CompletableFuture` to report results and errors to it, and to cancel the job. For reporting progress, however, you still need to use a callback. -Compared to <>, this approach is easier to implement in the application layer, but more difficult to implement in the presentation layer. You should only use it if you've used `CompletableFuture` previously, and you need other features that it offers, like chaining completion stages together. +Compared to <>, this approach is easier to implement in the application layer, but more difficult to implement in the presentation layer. You should only use it if you've previously used `CompletableFuture`, and you need other features that it offers, like chaining completion stages together. == Returning a Result @@ -32,11 +32,11 @@ To update the user interface, you have to add special completion stages that exe == Cancelling -You can cancel a Java `Future` by calling its `cancel()` method. The method has a `boolean` parameter that indicates whether the thread should be interrupted or not. However, `CompletableFuture`, which implements `Future`, doesn't use this parameter. It therefore doesn't make any difference whether you pass in `true` or `false`. +You can cancel a Java `Future` by calling its `cancel()` method. The method has a `boolean` parameter that indicates whether the thread should be interrupted or not. However, `CompletableFuture`, which implements `Future`, doesn't use this parameter. It doesn't therefore make a difference whether you pass `true` or `false`. -When you cancel a `CompletableFuture`, it completes with a `CompletionException` caused by a `CancellationException`. However, the job continues to run silently in the background until it's finished. If you want to notify the job itself that it has been cancelled, and should stop running at the next suitable moment, you have to make some changes. +When you cancel a `CompletableFuture`, it completes with a `CompletionException` caused by a `CancellationException`. However, the job continues to run silently in the background until it's finished. If you want to notify the job itself that it has been cancelled, and it should stop running at the next suitable moment, you'll have to make some changes. -`CompletableFuture` has an `isCancelled()` method that you can use to query whether the job has been cancelled or not. Therefore, you can no longer use the `@Async` annotation. Instead, you have to execute manually the job using the `TaskExecutor`, and manage the state of the returned `CompletableFuture`. The principle is the same as when you're using callbacks or handles. +`CompletableFuture` has an `isCancelled()` method that you can use to query whether the job has been cancelled or not. Therefore, you can no longer use the `@Async` annotation. Instead, you have to execute manually the job with the `TaskExecutor`, and manage the state of the returned `CompletableFuture`. The principle is the same as when using callbacks or handles. The earlier example would look like this when implemented using a `CompletableFuture`: diff --git a/articles/building-apps/application-layer/background-jobs/interaction/index.adoc b/articles/building-apps/application-layer/background-jobs/interaction/index.adoc index 84465e1fc4..06d7dbbf0d 100644 --- a/articles/building-apps/application-layer/background-jobs/interaction/index.adoc +++ b/articles/building-apps/application-layer/background-jobs/interaction/index.adoc @@ -9,7 +9,7 @@ order: 25 Some background jobs execute business processes in the background. The end user may see the result of the job, but doesn't have to interact directly with it. Scheduled and event triggered jobs are typically in this category. -Then there are jobs that need to interact with the user interface. For instance, a job may want to update a progress indicator while running, and notify the user when it's finished, or an error has occurred. Furthermore, the user may want to be able to cancel a running job before it has completed. This page explains different options for allowing a user to interact with a background job, and vice versa. +Then there are jobs that need to interact with the user interface. For instance, a job may want to update a progress indicator while running, and notify the user when it's finished or an error has occurred. Furthermore, the user may want to cancel a running job before it has completed. This page explains different options for allowing a user to interact with a background job, and vice versa. == Options diff --git a/articles/building-apps/application-layer/background-jobs/jobs.adoc b/articles/building-apps/application-layer/background-jobs/jobs.adoc index a7f5ec32c2..418255b6ae 100644 --- a/articles/building-apps/application-layer/background-jobs/jobs.adoc +++ b/articles/building-apps/application-layer/background-jobs/jobs.adoc @@ -9,7 +9,7 @@ order: 10 When implementing a background job, it's important to decouple its logic from how and where it is triggered. This ensures flexibility in triggering the job from different sources. -For instance, you may want to run the job every time the application starts. In this case, you may want to run it in the main thread, blocking the initialization of the rest of the application until the job is finished. You may also want to run the job in a background thread every day perhaps at midnight, or whenever a certain application event is published. +For instance, you may want to run the job every time the application starts. In this case, you may want to run it in the main thread, blocking the initialization of the rest of the application until the job is finished. You may also want to run the job in a background thread once a day -- perhaps at midnight, or whenever a certain application event is published. Here is a visual example of a job with three different triggers: @@ -17,7 +17,7 @@ image::images/job-and-triggers.png[Job with Three Triggers] In code, a job is a Spring bean, annotated with the `@Service` annotation. It contains one or more methods, that when called, execute the job in the calling thread. -Here is an example of a Spring bean that implements a single background job: +Below is an example of a Spring bean that implements a single background job: [source,java] ---- @@ -37,9 +37,9 @@ For a job <> from within the same package, the class can be == Transactions -A job that works on the database, should manage its own transactions. Because a job is a Spring bean, you can use either declarative, or programmatic transaction management. +A job that works on the database, should manage its own transactions. Because a job is a Spring bean, you can use either declarative or programmatic transaction management. -Here is an example of a background job that uses declarative transaction management to ensure the job runs inside a new transaction: +This is an example of a background job that uses declarative transaction management to ensure the job runs inside a new transaction: [source,java] ---- @@ -67,7 +67,7 @@ When the background job needs information about the current user, this informati == Batch Jobs -Consider implementing two versions of your batch job: one for processing all applicable inputs; and another for handling a specific set of inputs. This approach provides flexibility when you need to process individual cases or recover from errors. +Consider implementing two versions of a batch job: one for processing all applicable inputs; and another for handling a specific set of inputs. This approach provides flexibility when you need to process individual cases or recover from errors. For example, a batch job that generates invoices for shipped orders could look like this: @@ -92,7 +92,7 @@ In this example, the first method creates invoices for the orders whose IDs have Implementing batch jobs like this doesn't require much effort if done from the start, but allows for flexibility that may be useful. Continuing on the invoice generation example, you may discover a bug in production. This bug causes some orders to have bad data in the database. As a result, the batch job won't be able to generate invoices for them. -Fixing the bug is easy, but your users don't want to wait for the next batch run to occur. Instead, you can add a button to the user interface that allows a user to trigger invoice generation for an individual order. +Fixing the bug is easy, but your users won't want to wait for the next batch run to occur. Instead, you can add a button to the user interface that allows them to trigger invoice generation for an individual order. == Idempotent Jobs diff --git a/articles/building-apps/application-layer/background-jobs/triggers.adoc b/articles/building-apps/application-layer/background-jobs/triggers.adoc index bc3b58726b..4a45a2457f 100644 --- a/articles/building-apps/application-layer/background-jobs/triggers.adoc +++ b/articles/building-apps/application-layer/background-jobs/triggers.adoc @@ -15,12 +15,12 @@ Below is a visual example of a job with three different triggers: image::images/job-and-triggers.png[Job with Three Triggers] -Spring has support for plugging in an `AsyncUncaughtExceptionHandler` that's called whenever an `@Async` method throws an exception. However, this moves the error handling outside of the method where the error occurred. To increase code readability, you should handle the error explicitly in every trigger. +Spring has support for plugging in an `AsyncUncaughtExceptionHandler` that's called whenever an `@Async` method throws an exception. However, this moves the error handling outside of the method where the error occurred. To increase code readability, you should handle the error explicitly in each trigger. Some triggers, like event listeners and schedulers, are not intended to be invoked by other objects. You should make them package-private to limit their visibility. [NOTE] -On this page, all the trigger examples are delegating to a separate <>. However, if your job is simple, and you know it only needs one trigger, you can implement the job itself inside the trigger. +On this page, all of the trigger examples are delegating to a separate <>. However, if your job is simple, and you know it only needs one trigger, you can implement the job inside the trigger. == User Triggered Jobs @@ -56,7 +56,7 @@ public class MyApplicationService { <1> Spring ensures the current user has permission to start the job. <2> Spring executes the method using its task executor thread pool. -This example uses the `@Async` annotation, but you can also execute the job, <<../background-jobs#task-execution,programmatically>>. +The example above uses the `@Async` annotation, but you can also execute the job, <<../background-jobs#task-execution,programmatically>>. If the job needs to provide real-time updates to the user interface (e.g., showing a progress bar or error messages), you have to use server push. For more details, see the <> documentation page. @@ -126,12 +126,12 @@ class MyBackgroundJobScheduler { } } ---- -<1> Spring calls the trigger every 5 minutes. +<1> Spring calls the trigger every five minutes. <2> Spring executes the method using its task executor thread pool. -This example uses the `@Scheduled` and `@Async` annotations, but you can also execute the job using the task scheduler and task executor, <<../background-jobs#task-scheduling,programmatically>>. +The example here uses the `@Scheduled` and `@Async` annotations, but you can also execute the job using the task scheduler and task executor, <<../background-jobs#task-scheduling,programmatically>>. -Programmatic schedulers are more verbose, but they're easier to debug. Therefore, you should start with annotations when you implement schedulers. If you later need more control over the scheduling, or run into problems that are difficult to debug, you should switch to a programmatic approach. +Programmatic schedulers are more verbose, but they're easier to debug. Therefore, you should start with annotations when you implement schedulers. If you later need more control over scheduling, or run into problems that are difficult to debug, you should switch to a programmatic approach. == Startup Jobs @@ -140,7 +140,7 @@ For startup jobs, you should create a startup trigger that executes the job when If you need to block the application initialization until the job is completed, you can execute it in the main thread. For non-blocking execution, consider using a listener for the `ApplicationReadyEvent` to trigger the job once the application is fully initialized. -Here is an example of a trigger that blocks initialization until the job is finished: +Here's an example of a trigger that blocks initialization until the job is finished: [source,java] ---- @@ -154,7 +154,7 @@ class MyStartupTrigger { ---- [IMPORTANT] -Whenever you implement a startup trigger as in the example, you have to remember that the application is still being initialized. That means that not all services may be available for your job to use. +Whenever you implement a startup trigger, be aware that the application is still being initialized. That means that not all services may be available for your job to use. Below is an example of a trigger that executes a job in a background thread after the application has started: diff --git a/articles/building-apps/index.adoc b/articles/building-apps/index.adoc index 56ecbf81db..9c0a4fc78e 100644 --- a/articles/building-apps/index.adoc +++ b/articles/building-apps/index.adoc @@ -15,11 +15,11 @@ This section of the documentation is still being written. Its content is useful, Vaadin is a powerful tool for building real-world business applications. However, as with many versatile tools, it can be used in several different ways and some of these lead to better outcomes than others. -Big business applications have often have small beginnings. You can determine things as you go along, but it might be easier if someone showed you how to use your new tools from the start. In this section of the documentation, you'll learn the Vaadin Way of building business applications. +Big business applications often have small beginnings. You can determine things as you go along, but it might be easier if someone showed you how to use new tools from the start. In this section of the documentation, you'll learn the Vaadin Way of building business applications. -The Vaadin Way is not a tutorial. Rather, it is an opinionated and holistic look at building modern business applications with Vaadin. It's not limited to the user interface: it also covers other aspects, like business logic, persistence, integration with external systems, and even deployment. It scales progressively from a demo to a hobby project, to serving even the most demanding enterprise needs. +The Vaadin Way is not a tutorial. Rather, it is an opinionated and holistic look at building modern business applications with Vaadin. It's not limited to the user interface: it also covers other aspects, like business logic, persistence, integration with external systems, and even deployment. It scales progressively from a demo to a personal project, to serving even the most demanding enterprise needs. -The Vaadin Way is not the only way, though, to build Vaadin applications. Experienced developers and maintainers of existing applications can continue using Vaadin in their preferred way, or choose the parts of the Vaadin Way that works for them. +The Vaadin Way isn't the only way to build Vaadin applications. Experienced developers and maintainers of existing applications can continue using Vaadin in their preferred way, or choose the parts of the Vaadin Way that works for them. == Topics diff --git a/articles/building-apps/presentation-layer/server-push/callbacks.adoc b/articles/building-apps/presentation-layer/server-push/callbacks.adoc index 56757b1ae9..ca7a4ea1ae 100644 --- a/articles/building-apps/presentation-layer/server-push/callbacks.adoc +++ b/articles/building-apps/presentation-layer/server-push/callbacks.adoc @@ -26,7 +26,7 @@ private void onJobCompleted(String result) { } ---- -Likewise, a method for handling errors might look like this: +Likewise, a method for handling errors might be done like this: [source,java] ---- diff --git a/articles/building-apps/presentation-layer/server-push/futures.adoc b/articles/building-apps/presentation-layer/server-push/futures.adoc index d840ffcaac..26ab1c9d8e 100644 --- a/articles/building-apps/presentation-layer/server-push/futures.adoc +++ b/articles/building-apps/presentation-layer/server-push/futures.adoc @@ -1,6 +1,6 @@ --- title: Futures -description: How to use server push with `CompletableFuture`. +description: How to use server push with CompletableFuture. order: 30 section-nav: badge-flow --- @@ -10,9 +10,6 @@ section-nav: badge-flow Some background jobs may use `CompletableFuture` to inform the user interface of results and errors. This is covered in the <<{articles}/building-apps/application-layer/background-jobs/interaction/futures#,Returning Futures>> documentation page. -[NOTE] -The examples on this page only work with push enabled. For information about how to do that, see the <<.#enabling-push-flow,Server Push>> documentation page. - When building the user interface with Vaadin Flow, you can use <> and register them with the `CompletableFuture` to update your user interface. For example, a method for handling successful completion could look like this: @@ -24,7 +21,10 @@ private void onJobCompleted(String result) { } ---- -Likewise, a method for handling errors might look like this: +[NOTE] +The examples on this page only work with push enabled (see <<.#enabling-push-flow,Server Push>>). + +A method for handling errors might look like this: [source,java] ---- @@ -55,11 +55,11 @@ button.addClickListener(clickEvent -> { == Exceptional Completion -If a `CompletableFuture` is completed with an exception, you can instruct it to perform a specific operation by calling the `exceptionally()` method on it. However, this method works in a different way than `thenAccept()`. +If a `CompletableFuture` is completed with an exception, you can instruct it to perform a specific operation by calling the `exceptionally()` method on it. However, this method works in a different way than `thenAccept()`. The `exceptionally()` method takes a `Function`, instead of a `Consumer` as input. The exception is passed to the function as input. The function output is used as the result of the `CompletableFuture` that is returned by `exceptionally()`. -Flow has no version of `UI.accessLater()` that works with `Function`. However, since you're not interested in returning a result, you can create a helper function that adapts a `Consumer` to a `Function`, like this: +Flow has no version of `UI.accessLater()` that works with `Function`. However, since you're probably not interested in returning a result, you can create a helper function that adapts a `Consumer` to a `Function`, like this: [source,java] ---- diff --git a/articles/building-apps/presentation-layer/server-push/hilla.adoc b/articles/building-apps/presentation-layer/server-push/reactive-browser-callable-services.adoc similarity index 94% rename from articles/building-apps/presentation-layer/server-push/hilla.adoc rename to articles/building-apps/presentation-layer/server-push/reactive-browser-callable-services.adoc index fb22e1cbad..081b34c9d9 100644 --- a/articles/building-apps/presentation-layer/server-push/hilla.adoc +++ b/articles/building-apps/presentation-layer/server-push/reactive-browser-callable-services.adoc @@ -27,7 +27,7 @@ public class TimeService { } ---- <1> Emit a new message every second. -<2> Drop any messages that for some reason cannot be sent to the client in time. +<2> Drop any messages that for some reason can't be sent to the client in time. <3> Output the current date and time as a string. Hilla generates the necessary TypeScript types to subscribe to this service from the browser. diff --git a/articles/building-apps/presentation-layer/server-push/threads.adoc b/articles/building-apps/presentation-layer/server-push/threads.adoc index 6a8c9f23f9..c321a09fba 100644 --- a/articles/building-apps/presentation-layer/server-push/threads.adoc +++ b/articles/building-apps/presentation-layer/server-push/threads.adoc @@ -10,9 +10,9 @@ section-nav: badge-flow Developers often use server push to update the user interface from background jobs (see <<{articles}/building-apps/application-layer/background-jobs/interaction#,Background Jobs - UI Interaction>>). However, in Vaadin Flow, there are also cases where you may want to start a separate thread for use by the user interface itself. You might, for instance, want to show the server date and time in "real time". -If you have experience with Swing, you might be tempted to use a `Timer`, or to start a new `Thread`, manually. In Flow, this isn't a good idea. The reason is that Flow applications are multi-user applications, with potentially thousands of concurrent users. If each user creates their own `Timer`, or starts their own `Thread`, you may run out of threads. If that happens, the application crashes. +If you have experience with Swing, you might be tempted to use a `Timer`, or to start a new `Thread`, manually. In Flow, this isn't a good idea. Flow applications are multi-user applications, with potentially thousands of concurrent users. If each user creates their own `Timer`, or starts their own `Thread`, you may run out of threads. If that happens, the application crashes. -Instead, you should use virtual threads, or Spring's `TaskExecutor` and `TaskScheduler`. +As a better strategy, use virtual threads, or Spring's `TaskExecutor` and `TaskScheduler`. These are explained in the following sections, with some examples. [NOTE] The examples on this page only work with push enabled. For information about how to do that, see the <<.#enabling-push-flow,Server Push>> documentation page. @@ -35,7 +35,7 @@ button.addClickListener(clickEvent -> { This is the easiest way of starting a new user interface thread. If you're able to use virtual threads, they should be your first choice. If you run into problems, though, switch to the `TaskExecutor`. -For scheduled tasks, you should still use the `TaskScheduler`. This is covered later in this documentation page. +For scheduled tasks, you should still use the `TaskScheduler`. This is covered later on this page. == Task Executor @@ -71,15 +71,15 @@ button.addClickListener(clickEvent -> { }); ---- -Because of the call to `UI.accessLater()`, the user interface is automatically updated through a server push when the task finishes. +Because of the call to `UI.accessLater()`, the user interface is updated through a server push when the task finishes. [CAUTION] -Do not use the `@Async` annotation in your Flow views. It turns them into proxies that don't work with Vaadin. +Don't use the `@Async` annotation in Flow views. It turns them into proxies that don't work with Vaadin. == Task Scheduler -You can use the `TaskScheduler` to schedule tasks. With it, you have to schedule the task when the component is attached, and cancel it when it is detached. +You can use the `TaskScheduler` to schedule tasks. With it, you have to schedule the task when the component is attached, and cancel it when it's detached. The following example schedules a task to be executed every second. The task sets the text of `currentTimeLabel` to the current date and time of the server. When the component is detached, the task is cancelled: @@ -102,4 +102,4 @@ protected void onAttach(AttachEvent attachEvent) { The tasks that you execute in the task scheduler should be fast. If you need to schedule long-running tasks, you should give them to `TaskExecutor` for execution. [CAUTION] -Do not use the `@Scheduled` annotation in your Flow views. It turns them into proxies that don't work with Vaadin. +Do not use the `@Scheduled` annotation in Flow views. It turns them into proxies that don't work with Vaadin. diff --git a/articles/building-apps/presentation-layer/server-push/updates.adoc b/articles/building-apps/presentation-layer/server-push/updates.adoc index 52a752d799..ce2c741e54 100644 --- a/articles/building-apps/presentation-layer/server-push/updates.adoc +++ b/articles/building-apps/presentation-layer/server-push/updates.adoc @@ -8,7 +8,7 @@ section-nav: badge-flow = Pushing UI Updates [badge-flow]#Flow# -Whenever you're using server push in Vaadin Flow, you're triggering it from another thread than the normal HTTP request thread. Making changes to a UI from another thread and pushing them to the browser requires locking the user session. Otherwise, the UI update done from another thread could conflict with a regular event-driven update and cause either data corruption, race conditions or deadlocks. +Whenever you're using server push in Vaadin Flow, you're triggering it from a thread other than the normal HTTP request thread. Making changes to a UI from another thread and pushing them to the browser requires locking the user session. Otherwise, the UI update performed from another thread could conflict with a regular event-driven update and cause either data corruption, race conditions or deadlocks. Such errors are by nature hard to discover and fix, since they often occur randomly and under a heavy load. Because of this, you may only access a UI using the `UI.access()` method, which locks the session to prevent race conditions. You would use it like this: @@ -22,7 +22,7 @@ ui.access(() -> { [NOTE] The examples on this page only work with push enabled. For information about how to do that, see the <<.#enabling-push-flow,Server Push>> documentation page. -By default, Flow uses automatic pushing. This means that any pending changes are pushed to the browser after the command passed to `UI.access()` finishes. You can also configure Flow to use manual pushing. This gives you more control over when changes are pushed to the browser. For example, you can push multiple times inside a single call to `UI.access()`. +By default, Flow uses automatic pushing. This means that any pending changes are pushed to the browser after the command passed to `UI.access()` finishes. You can also configure Flow to use manual pushing. This would give you more control over when changes are pushed to the browser. For example, you can push multiple times inside a single call to `UI.access()`. To enable manual pushing, you have to make an addition to the `@Push` annotation, like this: @@ -49,15 +49,15 @@ ui.access(() -> { // This assumes that the UI has been explained earlier, and what attach and detach means. -Before you can call `access()`, you need to get the `UI` instance. You typically use `Component.getUI()` or `UI.getCurrent()` for this. However, both are problematic when it comes to server push. +Before you can call `access()`, you need to get the `UI` instance. You'd typically use `Component.getUI()` or `UI.getCurrent()` for this. However, both are problematic when it comes to server push. `Component.getUI()` is not thread-safe, which means you should only call it while the user session is locked. Therefore, you can't use it to call `access()`. -`UI.getCurrent()` only returns a non-`null` value when the current thread owns the session lock. When called from a background thread, it returns `null`. Therefore, you can't use it to call `access()`, either. +`UI.getCurrent()` only returns a non-`null` value when the current thread owns the session lock. When called from a background thread, it returns `null`. Therefore, you can't use it either to call `access()`. Whenever you're planning to use server push, you have to get a hold of the `UI` instance while the user session is locked. This typically happens right before you start your background thread. -Here is an example of a button click listener that starts a background thread: +Below is an example of a button click listener that starts a background thread: [source,java] ---- @@ -77,7 +77,7 @@ button.addClickListener(clickEvent -> { == Access Later -You probably use often server push in various types of event listeners and <>. A background job might inform you that it has finished processing. +You probably often use server push in various types of event listeners and <>. A background job might inform you that it has finished processing. In the following example, the user interface is updated in a callback after a background job has finished: @@ -89,7 +89,7 @@ myService.startBackgroundJob(() -> ui.access(() -> { })); ---- -Another common use case is an event bus informing you that a new message has arrived. +Another common use case is an event bus informing you of a new message. In the following example, the user interface subscribes to an event bus, and updates the user interface whenever a new message arrives: @@ -128,7 +128,7 @@ var subscription = myEventBus.subscribe(UI.getCurrent().accessLater((message) -> == Avoiding Memory Leaks -When you're using server push to update the user interface when an event has occurred, you typically subscribe a listener to some broadcaster or event bus. When you do this, be sure to unsubscribe when the UI is detached. Otherwise, you'll have a memory leak that prevents your UI from being garbage collected. This is because the listener holds a reference to the `UI` instance. +When you're using server push to update the user interface when an event has occurred, you would typically subscribe a listener to some broadcaster or event bus. When you do this, be sure to unsubscribe when the UI is detached. Otherwise, you'll have a memory leak that prevents your UI from being garbage collected. This is because the listener holds a reference to the `UI` instance. Always subscribe when your view is attached to a UI, and unsubscribe when it's detached. You can do this by overriding the `Component.onAttach()` method, like so: @@ -154,11 +154,11 @@ protected void onAttach(AttachEvent attachEvent) { // <1> == Avoiding Floods -Another risk you have to manage when updating the user interface in response to events is flooding the user interface with updates. As a rule of thumb, you should not push more than 2--4 times per second. Pushing more often than that can cause performance issues. Also, there is a limit to how many updates the human brain is able to register per second. +Another risk you have to manage when updating the user interface in response to events is flooding the user interface with updates. As a rule of thumb, you should not push more than two to four times per second. Pushing more often than that can cause performance issues. Plus, there is a limit to how many updates the human brain is able to register per second. -When you know events are coming no faster than 2 to 4 events per second, you can push on every event. However, if they're more frequent, you have to start buffering events and update the user interface in batches. This is quite easy to do if you're using a `Flux` from https://projectreactor.io/[Reactor]. See the <> documentation page for more information about this. +When you know events are coming no faster than two to four events per second, you can push on every event. However, if they're more frequent, you have to buffer events and update the user interface in batches. This is quite easy to do if you're using a `Flux` from https://projectreactor.io/[Reactor]. See the <> documentation page for more information about this. -The buffering duration depends on the size of the UI update, and the network latency. In some applications, you may need to use a longer buffer duration. In others, a shorter one might work. You should try various durations to see what works best for your application. +The buffering duration depends on the size of the UI update, and the network latency. In some applications, you may need to use a longer buffer duration. In others, a shorter one might work. You should try various durations to see what's best for your application. == Avoiding Unnecessary Pushes @@ -178,7 +178,7 @@ var button = new Button("Test Me", event -> { add(button); ---- -If you click the button, the user interface looks like this: +If you were to click the button, the user interface would look like this: [source] ---- @@ -186,7 +186,7 @@ This
is added from an event listener This
is added from within a call to UI.access() ---- -In this particular case, the call to `UI.access()` would not have been needed. Sometimes, you can deduce this by looking at the code. However, there are situations where this is not clear. You may have code that is executed sometimes by the HTTP request thread, and sometimes by another thread. For this situation, you can check whether the current thread has locked the user session, like this: +In this particular case, the call to `UI.access()` would not have been needed. Sometimes, you can deduce this by looking at the code. However, there are situations in which this isn't obvious. You may have code that's executed sometimes by the HTTP request thread, and other times by another thread. For this situation, you can check whether the current thread has locked the user session, like this: [source,java] ---- From 2e667f16b3fd5ce8ee9f0351989f5cbafc6f0e41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petter=20Holmstr=C3=B6m?= Date: Thu, 17 Oct 2024 13:02:34 +0300 Subject: [PATCH 27/30] Update vale.yml Try installing AsciiDoctor in separate action --- .github/workflows/vale.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/vale.yml b/.github/workflows/vale.yml index bb1e930a3b..655c1354c0 100644 --- a/.github/workflows/vale.yml +++ b/.github/workflows/vale.yml @@ -5,6 +5,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 + - uses: reitzig/actions-asciidoctor@v2.0.2 - uses: errata-ai/vale-action@reviewdog with: files: articles From a613e57486a8e4c9c9e3f934bd610b115ced1c9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petter=20Holmstr=C3=B6m?= Date: Thu, 17 Oct 2024 13:03:57 +0300 Subject: [PATCH 28/30] Update vale.yml Try with explicit Ruby setup as well --- .github/workflows/vale.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/vale.yml b/.github/workflows/vale.yml index 655c1354c0..5c032903c4 100644 --- a/.github/workflows/vale.yml +++ b/.github/workflows/vale.yml @@ -5,6 +5,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 + - uses: ruby/setup-ruby@v1 - uses: reitzig/actions-asciidoctor@v2.0.2 - uses: errata-ai/vale-action@reviewdog with: From f02cf62f2a5bad2a6e8aa3d5433643c0b14e931e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petter=20Holmstr=C3=B6m?= Date: Thu, 17 Oct 2024 13:05:01 +0300 Subject: [PATCH 29/30] Update vale.yml Add versions of Ruby and AsciiDoctor --- .github/workflows/vale.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/vale.yml b/.github/workflows/vale.yml index 5c032903c4..8d7af55eed 100644 --- a/.github/workflows/vale.yml +++ b/.github/workflows/vale.yml @@ -6,7 +6,11 @@ jobs: steps: - uses: actions/checkout@v2 - uses: ruby/setup-ruby@v1 + with: + ruby-version: 2.7 - uses: reitzig/actions-asciidoctor@v2.0.2 + with: + version: 2.0.18 - uses: errata-ai/vale-action@reviewdog with: files: articles From db93eb2862c0c23e80c30024121a0e94695e20fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petter=20Holmstr=C3=B6m?= Date: Thu, 17 Oct 2024 13:07:29 +0300 Subject: [PATCH 30/30] Update vale.yml Use latest version of AsciiDoctor --- .github/workflows/vale.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/vale.yml b/.github/workflows/vale.yml index 8d7af55eed..a1cd6fe439 100644 --- a/.github/workflows/vale.yml +++ b/.github/workflows/vale.yml @@ -9,8 +9,6 @@ jobs: with: ruby-version: 2.7 - uses: reitzig/actions-asciidoctor@v2.0.2 - with: - version: 2.0.18 - uses: errata-ai/vale-action@reviewdog with: files: articles