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();
}
Comment From: jhoeller
Looks sensible to me for OutcomeTrackingRunnable
to implement SchedulingAwareRunnable
and pass the isLongLived()
/getQualifier()
calls through to the underlying Runnable
when possible or return the default values otherwise (which is technically equivalent to not implementing SchedulingAwareRunnable
at all). Let's revise this for 6.2.1.