Scheduling Spring Boot Tasks

Introduction

Scheduling tasks to be performed at a later date, or repeated in a fixed interval, is a very useful feature. For example, newsletter systems or tasks which process information at a set timeframe rely on being scheduled to run at certain time points.

Since Spring Boot offers several options, we're going to cover and implement all of them.

Project Setup

As usual, when it comes to building Spring Boot applications, making a skeleton project is easiest with the help of Spring Initializr. You don't need any additional dependencies to enable scheduling.

To enable scheduling, all we have to do is annotate our main class:

@SpringBootApplication
@EnableScheduling
public class SampleScheduleApplication {
    public static void main(String[] args) {
        SpringApplication.run(SampleScheduleApplication.class, args);
    }
}

The @EnableScheduling annotation allows the Spring container to notice any @Scheduled annotations in Spring-managed beans.

Conditional Scheduling

There's another way to enable scheduling - by using the @ConditionalOnProperty annotation. It allows us to "switch on" and "switch off" our configuration classes by setting a property in the application.properties class.

To do this, let's make a new class and annotate it with both the @EnableScheduling, @Configuration and @ConditionalOnProperty annotations:

@EnableScheduling
@Configuration
@ConditionalOnProperty(name = "spring.enable.scheduling")
public class ScheduleEnabling {}

Now, in the application.properties file, let's add our new property and set it to true:

spring.enable.scheduling = true

Just by shifting this variable, we can turn the functionality on and off.

To execute a method on a schedule, you need to annotate it with the @Scheduled annotation. Changing the parameters of the annotation will define if it's at a fixed rate, with a fixed delay, custom intervals, etc.

Scheduling with fixedRate and fixedRateString

To schedule a method to execute on a fixed rate, we'll add the adequate parameter to our annotation - @Scheduled(fixedRate). This parameter accepts integers, expressed in milliseconds. So if you want a rate of 1 second, the rate should be entered as 1000 since the value is milliseconds.

Alternatively, you can use the @Scheduled(fixedRateString) parameter to externalize the amount by using an environment string variable.

To avoid confusion, we'll go with the fixedRateString, which is essentially a string parameter that specifies the rate, instead of the integer value. It can get a bit tricky trying to make a method recur monthly in milliseconds.

Let's set a variable in the application.properties file:

sample.schedule.string = PT2S

The prefix PT is the ISO-8601 standard for notating durations or periods, and with this, we can call the sample.schedule.string to schedule a method call in 2 seconds.

This also allows us to specify different delays for different profiles in use:

@Scheduled(fixedRateString = "${sample.schedule.string}")
public void scheduleTaskWithFixedRate() throws InterruptedException {
    task1();
    task2();
}

public void task1() throws InterruptedException {
    logger.info("Task 1 starts" + Thread.currentThread());
    Thread.sleep(1000);
    logger.info("Task 1 ends" + Thread.currentThread());
}

public void task2() throws InterruptedException {
    logger.info("Task 2 starts" + Thread.currentThread());
    Thread.sleep(1000);
    logger.info("Task 2 ends" + Thread.currentThread());
}

Let's run this piece of code:

Scheduling with Fixed Rate

However, we can see a problem here:

Task 1 starts at 00:01:44 and ends at 00:01:45, as expected.
Task 2 starts at 00:01:45 and ends at 00:01:46, as expected.
Task 1 starts at 00:01:46 and ends at 00:01:47.

Since both task1() and task2() have been executed, you'd expect the method to wait for an additional 2 seconds to run again.

Fixed rate tasks don't wait for the completion of the previous execution, it simply invokes the method at a specific rate. Since it takes 2 seconds to finish methods task1() and task2(), the method is invoked again at the same time these two finish.

This can become a problem in an environment with multiple scheduled tasks in place.

Consider this - task1() takes 1 minute to complete, and task2() takes 5 seconds to complete. Since both of them are running on a single thread, there might be a situation that task1() starts processing and will lock the thread. This won't allow task2() to process even if there is a fixedRate of 5 seconds.

In this scenario, we need to increase the number of threads that are available in our thread pool for scheduling. Spring provides a property that we can manipulate to specify the size: spring.task.scheduling.pool.size - the default value is 1.

We may use fixed rate when a particular task needs to be done repeatedly but each task is independent of the other. Also, be careful not to have heavy tasks without a proper rate, since incompletion may lead to a nasty OutOfMemoryError.

Scheduling with fixedDelay and fixedDelayString

A fixedDelay works very similar to a fixedRate. But the difference here is that fixed delay waits until the completion of the previous execution to start the next one. Imagine a scenario where your function takes 1 second to complete execution and you've given a fixed delay of 2 seconds.

This, in turn, will result in a total of 3 seconds.

In the logs below, you can clearly see that the difference between the two subsequent tasks is 3 seconds. This includes the fixed delay time of 1 second as well as the 2 seconds that we've given as sleep:

@Scheduled(fixedDelayString = "${sample.schedule.string}")
public void scheduleTaskWithFixedDelay() throws InterruptedException {
    task1();
}

public void task1() throws InterruptedException {
    logger.info("Task 1 starts" + Thread.currentThread());
    Thread.sleep(1000);
    logger.info("Task 1 ends" + Thread.currentThread());
}

Let's run this piece of code:

Scheduling with Fixed Delay

There's an additional parameter that can be added to scheduled tasks, and that is the initialDelay.

This one doesn't require much explanation as it is used in conjunction with the above two. Initial delay, as the name rightfully suggests, provides the initial delay for the first execution.

If you have a function with an initial delay of 2 seconds and a fixed rate of 1 second, then the first execution will be delayed by 2 seconds and the function will run every 1 second afterward:

@Scheduled(initialDelay = 1000, fixedRateString = "${sample.schedule.string}")
public void scheduleTaskWithInitialDelay() throws InterruptedException {
    task1();
}

We can also opt to use an initialDelayString that allows us to externalize the delay value.

Custom Time Intervals

Fixed rate and fixed delay are the most commonly used parameters for scheduling and the delay strings allow us to externalize the values and make them configurable.

But till now we've seen only very generic examples for the rates. There might be a situation where we need very specific time intervals. Behold, custom cron expressions.

Cron Expressions

Most developers have probably heard about the cron utility in Linux. It is a daemon process which runs without the need for user intervention and executes tasks.

The syntax for cron expressions in the cron utility and the syntax for cron expressions for scheduling are mostly similar.

Cron expressions are basically strings that describe the details of the schedule. It provides much more control than the previous 2 methods:

Name Required Allowed Values Allowed Special Characters
Seconds Yes 0-59 , - * /
Minutes Yes 0-59 , - * /
Hours Yes 0-23 , - * /
Day of Month Yes 1-31 , - * / L W C
Month Yes 0-11 or JAN-DEC , - * /
Day of Week Yes 1-7 or SUN-SAT , - * / L C #
Year No empty or 1970-2099 , - * /

The table above specifies the required values, allowed values, and special characters for a cron expression.

Cron expressions can be very simple, but also very complex. A clear understanding of the values will make it easier to play around with them.

Except for the year field, all the other fields are mandatory:

<second> <minute> <hour> <day-of-month> <month> <day-of-week> <year>

Example: 0 0 12 * * ? 2019 – This cron expression fires at 12 PM, every day of the month, for every month, for the year 2019.

For some common values, you can also use the predefined annotations:

  • @reboot: Schedule the method for every reboot of the application
  • @yearly/@anually: Schedule the method to run once a year
  • @monthly: Schedule the method to run once a month
  • @weekly: Schedule the method to run once a week
  • @daily/@midnight: Schedule the method to run once a day
  • @hourly: Schedule the method to run once every hour

Let's write a code example for this:

@Scheduled(cron="0 0 12 * * ? 2019")
public void doSomething() {
    // Something
}

An important thing to note when scheduling are time zones - the bane of every developer who works with time.

You'll likely wish to set the zone flag to a specific region. For an example, we'll run this method at 12PM, every day in 2019, based on Paris' time zone:

@Scheduled(cron="0 0 12 * * ? 2019", zone="Europe/Paris")
public void doSomething() {
    // Something
}

You can find all of the time zones at the official Oracle docs.

Of course, you can also externalize cron expressions via the application.properties file:

cron.expression= 0 0 12 * * ? 2019

And then invoke it through:

@Scheduled(cron="${cron.expression}", zone="Europe/Paris")
public void doSomething() {
    // Something
}

You can also use a site like FreeFormatter to generate a cron expression by setting the input parameters. This is very helpful for those new to creating cron expressions.

Conclusion

In this article, we've seen how we can schedule tasks with Spring Boot. The biggest advantage of using Spring Boot is the ease with which we can implement scheduling. Not only that, it also offers several options so that we can pick what suits our requirements.

Schedulers are essential components of most applications because they send out time-critical as well as user-specific information as and when needed. Now you know how!