Skip to content

Unable to configure custom scheduler for @Scheduled annotation #34058

@ahrytsiuk

Description

@ahrytsiuk

Since Spring 6.2.0 functionality to specify custom scheduler for @Scheduled annotation is not working anymore.

Minimal reproducible example:

import static org.assertj.core.api.Assertions.assertThat;

import java.util.HashSet;
import java.util.Set;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.scheduling.concurrent.SimpleAsyncTaskScheduler;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.scheduling.config.ScheduledTaskHolder;
import org.springframework.scheduling.config.TaskSchedulerRouter;

class SchedulerConfigTest {

    @Test
    void withQualifiedScheduler() throws Exception {
        var ctx = new AnnotationConfigApplicationContext(QualifiedExplicitSchedulerConfig.class);
        assertThat(ctx.getBean(ScheduledTaskHolder.class).getScheduledTasks()).hasSize(2);

        Thread.sleep(110);

        assertThat(ctx.getBean("defaultSchedulerThreads", Set.class))
            .hasSizeGreaterThanOrEqualTo(1).allMatch(e -> ((String) e).startsWith("taskScheduler-"));

        assertThat(ctx.getBean("explicitSchedulerThreads", Set.class))
            .hasSizeGreaterThanOrEqualTo(1).allMatch(e -> ((String) e).startsWith("customScheduler-"));
    }

    @TestConfiguration
    @EnableScheduling
    static class QualifiedExplicitSchedulerConfig {

        public static final String DEFAULT_TASK_SCHEDULER_BEAN_NAME = TaskSchedulerRouter.DEFAULT_TASK_SCHEDULER_BEAN_NAME;
        public static final String CUSTOM_TASK_SCHEDULER_BEAN_NAME = "customTaskScheduler";

        @Bean
        public Set<String> defaultSchedulerThreads() {
            return new HashSet<>();
        }

        @Bean
        public Set<String> explicitSchedulerThreads() {
            return new HashSet<>();
        }

        @Bean(name = DEFAULT_TASK_SCHEDULER_BEAN_NAME)
        public ThreadPoolTaskScheduler taskScheduler() {
            ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
            taskScheduler.setThreadNamePrefix("taskScheduler-");
            return taskScheduler;
        }

        @Bean(name = CUSTOM_TASK_SCHEDULER_BEAN_NAME)
        public SimpleAsyncTaskScheduler customTaskScheduler() {
            SimpleAsyncTaskScheduler taskScheduler = new SimpleAsyncTaskScheduler();
            taskScheduler.setThreadNamePrefix("customScheduler-");
            return taskScheduler;
        }

        @Scheduled(fixedRate = 10)
        public void task() throws Exception {
            defaultSchedulerThreads().add(Thread.currentThread().getName());
        }

        @Scheduled(fixedRate = 10, scheduler = CUSTOM_TASK_SCHEDULER_BEAN_NAME)
        public void taskWithExplicitScheduler() throws Exception {
            explicitSchedulerThreads().add(Thread.currentThread().getName());
        }

    }

}

I believe this happened in this commit dc2c8d60. Now Runnable is wrapped into OutcomeTrackingRunnable. As result TaskSchedulerRouter fails to determine qualifier:

protected String determineQualifier(Runnable task) {
     return (task instanceof SchedulingAwareRunnable sar ? sar.getQualifier() : null);
}

since OutcomeTrackingRunnable is not implementing SchedulingAwareRunnable.

@bclozel, do you think it makes sense for OutcomeTrackingRunnable to implement SchedulingAwareRunnable interface instead of Runnable? Or to have two flavours of OutcomeTrackingRunnable? And Task will wrap underlying runnable depends on underlying task?

Smth like:

public Task(Runnable runnable) {
    Assert.notNull(runnable, "Runnable must not be null");
    if (runnable instanceof SchedulingAwareRunnable sar) {
        this.runnable = new OutcomeTrackingSchedulingAwareRunnable(runnable);
    } else {
        this.runnable = new OutcomeTrackingRunnable(runnable);
    }
    this.lastExecutionOutcome = TaskExecutionOutcome.create();
}

Metadata

Metadata

Assignees

Labels

in: coreIssues in core modules (aop, beans, core, context, expression)type: regressionA bug that is also a regression

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions