5 min read

The @Scheduled annotation may be deceiving you..!

September 10, 2020

Is your @Scheduled method with fixedRate is not firing exactly at the time interval configured? Here I try to explain this behaviour and how you can fix it.

Let’s take a look at this example. From the documentation, The value of fixedRate denotes the number of milliseconds between subsequent method invocation. So one might hope that the following method someTask should be called every 5 seconds.

@Scheduled(fixedRate = 5000)
public void someTask() throws InterruptedException {
    logger.info("Start @:{}", LocalTime.now());
    TimeUnit.SECONDS.sleep(10);
    logger.info("End   @:{}", LocalTime.now());
}

But for some reasons, the output says otherwise. Each method invocation is between 10 seconds.

2020-09-13 18:17:53.432  INFO 21748 --- [   scheduling-1] c.s.e.s.s.SchedulingApplication          : Start @:18:17:53.432
2020-09-13 18:18:03.435  INFO 21748 --- [   scheduling-1] c.s.e.s.s.SchedulingApplication          : End   @:18:18:03.435
2020-09-13 18:18:03.435  INFO 21748 --- [   scheduling-1] c.s.e.s.s.SchedulingApplication          : Start @:18:18:03.435
2020-09-13 18:18:13.436  INFO 21748 --- [   scheduling-1] c.s.e.s.s.SchedulingApplication          : End   @:18:18:13.436
2020-09-13 18:18:13.436  INFO 21748 --- [   scheduling-1] c.s.e.s.s.SchedulingApplication          : Start @:18:18:13.436
2020-09-13 18:18:23.436  INFO 21748 --- [   scheduling-1] c.s.e.s.s.SchedulingApplication          : End   @:18:18:23.436

So what happened?. Take a look at this image. This illustrates how the method call will be initiated for each cycle. In the fixedRate cycles, notice that the second run took more than what was configured and hence the next call waited till run 2 was complete.

@Scheduled annotation with fixed delay and fixed rate execution comparison

It all has to do with the default ThreadPoolTaskScheduler that is provided by Spring Boot’s auto-configuration. This Executor’s thread pool size is 1 by default. Every method that uses @Scheduled will have to use this single thread.

Spring has deliberately done this so that there won’t be two runs of the same method happening in parallel. To achieve parallelism, we need to do three things.

  • Enable Async Support by annotating the Spring Boot main class with @EnableAsync.
  • Mark The @Scheduled method with @Async.
  • Increase the default number of threads available using spring.task.scheduling.pool.size property.

I have made my thread count to 3. And enabled all these annotations. Also, I have added a counter to debug which execution is starting and completing.

2020-09-13 18:38:06.358  INFO 21888 --- [         task-1] c.s.e.s.s.SchedulingApplication          : Start0 @:18:38:06.358
2020-09-13 18:38:11.342  INFO 21888 --- [         task-2] c.s.e.s.s.SchedulingApplication          : Start1 @:18:38:11.342
2020-09-13 18:38:16.342  INFO 21888 --- [         task-3] c.s.e.s.s.SchedulingApplication          : Start2 @:18:38:16.342
2020-09-13 18:38:16.360  INFO 21888 --- [         task-1] c.s.e.s.s.SchedulingApplication          : End  0 @:18:38:16.360
2020-09-13 18:38:21.343  INFO 21888 --- [         task-2] c.s.e.s.s.SchedulingApplication          : End  1 @:18:38:21.343
2020-09-13 18:38:21.343  INFO 21888 --- [         task-4] c.s.e.s.s.SchedulingApplication          : Start3 @:18:38:21.343
2020-09-13 18:38:26.342  INFO 21888 --- [         task-5] c.s.e.s.s.SchedulingApplication          : Start4 @:18:38:26.342
2020-09-13 18:38:26.343  INFO 21888 --- [         task-3] c.s.e.s.s.SchedulingApplication          : End  2 @:18:38:26.343
2020-09-13 18:38:31.343  INFO 21888 --- [         task-6] c.s.e.s.s.SchedulingApplication          : Start5 @:18:38:31.343

If you look at the output, The second and third calls have started even before the first call has completed. And it happened exactly at the fixedRate. This is due to one thread executing Start0 while the second thread is available to execute the next invocation asynchronously. This behaviour is shown in the illustration below.

@Scheduled annotation with fixed delay and fixed rate execution comparison with multi threading enabled

Now you may wonder what happens to methods annotated with fixedDelay. Fixed delays are programmed to work in a single thread. Regardless of @Async and how many threads you configure to the ThreadPoolTaskScheduler, These methods will always run one after another.

Let’s take a look at this scenario. Here I made the number of threads to 1. And added another scheduled task to see how the system behaves.

    private static Integer runCount1 = 0;
    private static Integer runCount2 = 0;

    @Scheduled(fixedDelay = 5000)
    public void someTask1() throws InterruptedException {
        int count = ++runCount1;
        logger.info("Start1 {} @:{}", count, LocalTime.now());
        TimeUnit.SECONDS.sleep(10);
        logger.info("End1  {} @:{}", count, LocalTime.now());
    }

    @Scheduled(fixedDelay = 5000)
    public void someTask2() throws InterruptedException {
        int count = ++runCount2;
        logger.info("Start2 {} @:{}", count, LocalTime.now());
        TimeUnit.SECONDS.sleep(10);
        logger.info("End2  {} @:{}", count, LocalTime.now());
    }

Here both of the methods are annotated with fixed delay five seconds. As we know, fixedDelay methods will be triggered only after the old call is complete. But it looks like second method waits for first method to complete. Also the delay between each start1 is 30 seconds instead of 15 seconds. This is totally unexpected and unacceptable.

2020-09-13 19:35:15.660  INFO 7644 --- [   scheduling-1] c.s.e.s.s.SchedulingApplication          : Start1 1 @:19:35:15.660
2020-09-13 19:35:25.661  INFO 7644 --- [   scheduling-1] c.s.e.s.s.SchedulingApplication          : End1  1 @:19:35:25.661
2020-09-13 19:35:25.661  INFO 7644 --- [   scheduling-1] c.s.e.s.s.SchedulingApplication          : Start2 1 @:19:35:25.661
2020-09-13 19:35:35.662  INFO 7644 --- [   scheduling-1] c.s.e.s.s.SchedulingApplication          : End2  1 @:19:35:35.662
2020-09-13 19:35:35.662  INFO 7644 --- [   scheduling-1] c.s.e.s.s.SchedulingApplication          : Start1 2 @:19:35:35.662
2020-09-13 19:35:45.663  INFO 7644 --- [   scheduling-1] c.s.e.s.s.SchedulingApplication          : End1  2 @:19:35:45.663

But this is also because the thread pool is being shared across two jobs. This is why in an ideal scenario you should increase the number of scheduler threads based on how many jobs may co-exist. This way if one of the jobs is hung or long-running, then the other tasks may still be able to execute.

Hope you learned something new. What is your experience with @Scheduled Tasks? Comment below.

Raja Anbazhagan

About the author

Raja is a Software Engineer with over 7 years of experience in working with Enterprise Java applications. Lately, He is focused in cloud-based Java applications and serverless technologies. He spends his spare time in stackoverflow.

Browse Categories