Using Async Schedulers in Spring Boot

Srikanth Dannarapu
Javarevisited
Published in
5 min readAug 7, 2023

Problem Statement: Efficiently Managing Multiple Asynchronous Schedulers in a Spring Boot Application

Usecase: In a spring boot application I have 2 schedulers, one scheduler runs every 3 mins to get data from DB table and 2nd scheduler runs every one minute which consume a REST API.

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;

@SpringBootApplication
public class YourSpringBootApplication {

public static void main(String[] args) {
SpringApplication.run(YourSpringBootApplication.class, args);
}
}
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

@Component
public class FirstScheduler {

@Scheduled(fixedRate = 3 * 60 * 1000) // Runs every 3 minutes
public void getDataFromDB() {
// Your code to get data from the DB

}
}
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

@Component
public class SecondScheduler {

@Scheduled(fixedRate = 1 * 60 * 1000) // Runs every 1 minute
public void consumeRestApi() {
// Your code to consume the REST API
}
}

Problem: When 1st scheduler takes more time say 10mins then 2nd scheduler is not running until first scheduler completes its job.

above code has the problem that when one scheduler runs 2nd scheduler is not running

How to handle this?

Solution:

To avoid the second scheduler from being blocked by the first scheduler, you can use Spring’s asynchronous execution support. By doing this, each scheduler will run in its own separate thread, allowing them to work independently of each other. Here’s how you can achieve this:

When using Spring’s asynchronous execution support, it’s a good practice to configure a custom thread pool to control the number of threads used for executing asynchronous tasks. By default, Spring uses a SimpleAsyncTaskExecutor, which may not be suitable for production scenarios, as it does not offer much control over the thread pool.

To handle the above problem, it is recommended to create a custom thread pool bean in your application to handle the asynchronous tasks effectively and efficiently. Here’s how you can do it:

Step 1: Define a configuration class for creating the custom thread pool bean.

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

@Configuration
@EnableAsync
public class AsyncConfiguration {

@Bean(name = "asyncTaskExecutor")
public ThreadPoolTaskExecutor asyncTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5); // Set the initial number of threads in the pool
executor.setMaxPoolSize(10); // Set the maximum number of threads in the pool
executor.setQueueCapacity(25); // Set the queue capacity for holding pending tasks
executor.setThreadNamePrefix("AsyncTask-"); // Set a prefix for thread names
executor.initialize();
return executor;
}
}

Step 2: Modify your first scheduler to use this custom thread pool by specifying the taskExecutor attribute in the @Async annotation.

import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

@Component
public class FirstScheduler {

@Async("asyncTaskExecutor") // Specify the custom thread pool bean name
@Scheduled(fixedRate = 3 * 60 * 1000) // Runs every 3 minutes
public void getDataFromDB() {
// Your code to get data from the DB
// This method will run asynchronously in the custom thread pool
}
}

With this configuration, the first scheduler method (getDataFromDB) will run asynchronously using the custom thread pool, while the second scheduler method (consumeRestApi) will run in the default scheduler's thread.

Adjust the corePoolSize, maxPoolSize, and queueCapacity values in the asyncTaskExecutor() method based on the requirements of your application and available system resources. The thread pool configuration can significantly impact the performance of your application, so it's essential to tune these values appropriately.

To make the second scheduler also use the custom thread pool for asynchronous execution, you need to add the @Async annotation along with the taskExecutor attribute to the consumeRestApi method in the SecondScheduler class. This will ensure that both schedulers run asynchronously using the same custom thread pool. Here's the updated code:

import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

@Component
public class SecondScheduler {

@Async("asyncTaskExecutor") // Specify the custom thread pool bean name
@Scheduled(fixedRate = 1 * 60 * 1000) // Runs every 1 minute
public void consumeRestApi() {
// Your code to consume the REST API
// This method will run asynchronously in the custom thread pool
}
}

With this setup, both the first scheduler (getDataFromDB) and the second scheduler (consumeRestApi) will run asynchronously using the same custom thread pool. This will allow them to work independently of each other, even if one of the tasks takes more time to complete.

Using Custom Thread Pool

Log Error message when the required thread pool size > Configured thread pool size:

To log an error message when the required thread pool size exceeds the configured thread pool size, you can make use of Spring’s ThreadPoolTaskExecutor’s RejectedExecutionHandler. This handler will be invoked when the thread pool's task queue is full, and the thread pool cannot accept more tasks. You can use this callback to log an error message indicating that the thread pool's capacity has been exceeded.

Below updated configuration class with the RejectedExecutionHandler:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.concurrent.ThreadPoolExecutor;

@Configuration
@EnableAsync
@Sl4J
public class AsyncConfiguration {

@Bean(name = "asyncTaskExecutor")
public ThreadPoolTaskExecutor asyncTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5); // Set the initial number of threads in the pool
executor.setMaxPoolSize(10); // Set the maximum number of threads in the pool
executor.setQueueCapacity(25); // Set the queue capacity for holding pending tasks
executor.setThreadNamePrefix("AsyncTask-"); // Set a prefix for thread names

// Set the RejectedExecutionHandler to log an error message
executor.setRejectedExecutionHandler((Runnable r, ThreadPoolExecutor e) -> {
// Log the error message indicating the task has been rejected due to the full queue
// You can customize this message based on your requirements
log.error("Task Rejected: Thread pool is full. Increase the thread pool size.");
});

executor.initialize();
return executor;
}
}

With this configuration, the RejectedExecutionHandler will be triggered when the thread pool's queue is full, and the system tries to submit additional tasks beyond the queue capacity. You can customize the error message or take any other necessary actions based on your application's requirements.

Keep in mind that setting the right thread pool size and queue capacity is crucial for your application’s performance and resource utilization. If tasks are consistently being rejected due to a full queue, you may need to increase the thread pool size or adjust the queue capacity accordingly.

Conclusion:

By configuring a shared custom thread pool, utilizing the @Async annotation, and potentially incorporating a RejectedExecutionHandler, your Spring Boot application can effectively manage and execute multiple schedulers concurrently. This ensures that the schedulers run independently and that the system responds gracefully when thread pool limits are reached.

Thanks, before you go:

  • 👏 Please clap for the story and follow the author 👉
  • Please share your questions or insights in the comments section below. Let’s help each other and become better Java developers.
  • Let’s connect on LinkedIn

--

--