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] 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.