Setting Up the Basic Environment
To demonstrate common issues with scheduled tasks in distributed systems, we start by configuring a basic Spring Boot application with scheudling capabilities.
Dependencies
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.2</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
Application and Scheduler Configuration
@SpringBootApplication
public class TaskSchedulerApplication {
public static void main(String[] args) {
SpringApplication.run(TaskSchedulerApplication.class, args);
}
}
@Component
@EnableScheduling
public class ScheduledTaskComponent {
private static final Logger logger = LoggerFactory.getLogger(ScheduledTaskComponent.class);
@Scheduled(cron = "0/5 * * * * ?")
public void executePeriodicTask() {
logger.info("Current executing thread ID: {}", Thread.currentThread().getId());
}
}
Identifying Execution Delays and Single-Thread Limitations
When the scheduled task executes quick, timing appears normal. However, when task execution takes longer than the interval, significant delays occur due to single-thread execution.
@Scheduled(cron = "0/5 * * * * ?")
public void executeLongRunningTask() {
try {
Thread.sleep(10000);
logger.info("Current thread ID: {}", Thread.currentThread().getId());
} catch (InterruptedException ex) {
logger.error("Task interrupted", ex);
}
}
Execution output shows 15-second intervals instead of the expected 5-second intervals, demonstrating both timing drift and single-thread blocking.
Root Cause Analysis
The @EnableScheduling annotation triggers auto-configuration via TaskSchedulingAutoConfiguration, which creates a ThreadPoolTaskScheduler with default pool size of 1. This single-thread execution causes blocking when tasks exceed their scheduled interval.
Solutions for Improved Scheduling
Configuration-Based Approach
spring:
task:
scheduling:
thread-name-prefix: custom-scheduler-
pool:
size: 5
This increases the thread pool size but may not consistently resolve timing issues in all environments.
Asynchronous Execution with Custom Thread Pool
@Configuration
public class ThreadPoolConfiguration {
@Bean
public Executor customTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(8);
executor.setMaxPoolSize(16);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("custom-executor-");
executor.initialize();
return executor;
}
}
@Component
@EnableScheduling
public class AsyncScheduledTask {
@Autowired
private Executor customTaskExecutor;
@Scheduled(cron = "0/5 * * * * ?")
public void executeAsyncTask() {
CompletableFuture.runAsync(() -> {
try {
Thread.sleep(10000);
logger.info("Async task thread ID: {}", Thread.currentThread().getId());
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
}, customTaskExecutor);
}
}
Annotation-Based Asynchronous Scheduling
@Configuration
@EnableAsync
public class AsyncConfiguration {
@Bean(name = "asyncExecutor")
public Executor asyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(20);
executor.setQueueCapacity(50);
executor.setThreadNamePrefix("async-scheduler-");
executor.initialize();
return executor;
}
}
@Component
@EnableScheduling
public class AnnotatedAsyncTask {
@Async("asyncExecutor")
@Scheduled(cron = "0/5 * * * * ?")
public void scheduledAsyncOperation() {
try {
Thread.sleep(10000);
logger.info("Async scheduled thread ID: {}", Thread.currentThread().getId());
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
}
}
Distributed Environment Considerations
In distributed deployments, multiple instances may execute the same scheduled task concurrently, leading to duplicate processing and data corruption.
Distributed Lock Implementation
@Configuration
public class RedisLockConfig {
@Bean
public RedissonClient redissonClient() {
Config config = new Config();
config.useSingleServer()
.setAddress("redis://localhost:6379");
return Redisson.create(config);
}
}
@Component
@EnableScheduling
public class DistributedScheduledTask {
@Autowired
private RedissonClient redissonClient;
private static final String LOCK_KEY = "scheduled:task:lock";
@Scheduled(cron = "0/5 * * * * ?")
public void executeWithDistributedLock() {
RLock distributedLock = redissonClient.getLock(LOCK_KEY);
try {
if (distributedLock.tryLock(5, 10, TimeUnit.SECONDS)) {
try {
Thread.sleep(10000);
logger.info("Distributed task execution by thread: {}",
Thread.currentThread().getId());
} finally {
distributedLock.unlock();
}
}
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
}
}
This approach ensures only one instance executes the task across the distributed system, preventing concurrent execution issues.