-
Notifications
You must be signed in to change notification settings - Fork 38.9k
Description
Bug Reports
Description
ThreadPoolTaskScheduler behaves inconsistently regarding exception handling between Trigger-based scheduling and Fixed-Rate/Fixed-Delay scheduling.
- Trigger-based scheduling (Safe):
Inschedule(Runnable task, Trigger trigger), the implementation explicitly provides a defaultErrorHandlerif one is not configured. This ensures exceptions are logged and do not kill the scheduler.// ThreadPoolTaskScheduler.java (schedule with Trigger) ErrorHandler errorHandler = this.errorHandler; if (errorHandler == null) { errorHandler = TaskUtils.getDefaultErrorHandler(true); } return new ReschedulingRunnable(task, trigger, ..., errorHandler).schedule();
- Fixed-Rate/Fixed-Delay scheduling (Unsafe/ENC): In methods like scheduleAtFixedRate and scheduleWithFixedDelay, the code delegates to errorHandlingTask, which passes this.errorHandler directly without checking for null or providing a fallback.
// ThreadPoolTaskScheduler.java (errorHandlingTask)
return TaskUtils.decorateTaskWithErrorHandler(task, this.errorHandler, isRepeatingTask);
Consequence: If a user does not explicitly set an setErrorHandler(...) (which is the default state), TaskUtils does not wrap the Runnable in a safety block for periodic tasks. Consequently, if a user's task throws a RuntimeException, it propagates directly to the underlying JDK ScheduledThreadPoolExecutor.
According to the JDK contract, ScheduledThreadPoolExecutor suppresses all future executions of a task if it throws an exception. This results in a "Silent Failure" (Exception Not Caught) where the periodic task stops running forever without any error logs.
Sample Application
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import java.time.Duration;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
public class SpringSchedulerBug {
public static void main(String[] args) throws InterruptedException {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.initialize();
// Note: We did NOT call scheduler.setErrorHandler(...)
CountDownLatch latch = new CountDownLatch(5);
System.out.println("Starting periodic task...");
scheduler.scheduleAtFixedRate(() -> {
System.out.println("Executing task logic...");
latch.countDown();
// Simulate a transient error
throw new RuntimeException("Oops, something went wrong!");
}, Duration.ofMillis(200));
// Wait to see if it continues running
boolean continued = latch.await(2, TimeUnit.SECONDS);
if (!continued) {
System.err.println("FAILURE: Task stopped silently after the first exception.");
} else {
System.out.println("SUCCESS: Task continued running.");
}
scheduler.shutdown();
}}
Expected Behavior ThreadPoolTaskScheduler should apply the same safety mechanism to scheduleAtFixedRate/scheduleWithFixedDelay as it does to schedule(Trigger).
If this.errorHandler is null, it should default to TaskUtils.getDefaultErrorHandler(true) (or similar logic) to prevent the underlying JDK executor from silencing the periodic task upon exception.
Affected Component
org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler