Skip to content

ThreadPoolTaskScheduler silently terminates fixed-rate tasks on exception due to inconsistent default ErrorHandler #36107

@QiuYucheng2003

Description

@QiuYucheng2003

Bug Reports

Description
ThreadPoolTaskScheduler behaves inconsistently regarding exception handling between Trigger-based scheduling and Fixed-Rate/Fixed-Delay scheduling.

  1. Trigger-based scheduling (Safe):
    In schedule(Runnable task, Trigger trigger), the implementation explicitly provides a default ErrorHandler if 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();
  2. 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

Metadata

Metadata

Assignees

Labels

in: coreIssues in core modules (aop, beans, core, context, expression)status: waiting-for-feedbackWe need additional information before we can continuestatus: waiting-for-triageAn issue we've not yet triaged or decided on

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions