Skip to content

Commit ef61b4e

Browse files
committed
SimpleAsyncTaskScheduler runs fixed-delay tasks on scheduler thread
Closes gh-31334
1 parent 86b764d commit ef61b4e

File tree

2 files changed

+106
-19
lines changed

2 files changed

+106
-19
lines changed

spring-context/src/main/java/org/springframework/scheduling/concurrent/SimpleAsyncTaskScheduler.java

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,11 @@
4646
* separate thread. This is an attractive choice with virtual threads on JDK 21,
4747
* expecting common usage with {@link #setVirtualThreads setVirtualThreads(true)}.
4848
*
49+
* <p><b>NOTE: Scheduling with a fixed delay enforces execution on the single
50+
* scheduler thread, in order to provide traditional fixed-delay semantics!</b>
51+
* Prefer the use of fixed rates or cron triggers instead which are a better fit
52+
* with this thread-per-task scheduler variant.
53+
*
4954
* <p>Supports a graceful shutdown through {@link #setTaskTerminationTimeout},
5055
* at the expense of task tracking overhead per execution thread at runtime.
5156
* Supports limiting concurrent threads through {@link #setConcurrencyLimit}.
@@ -234,7 +239,8 @@ public ScheduledFuture<?> scheduleAtFixedRate(Runnable task, Duration period) {
234239
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable task, Instant startTime, Duration delay) {
235240
Duration initialDelay = Duration.between(this.clock.instant(), startTime);
236241
try {
237-
return this.scheduledExecutor.scheduleWithFixedDelay(scheduledTask(task),
242+
// Blocking task on scheduler thread for fixed delay semantics
243+
return this.scheduledExecutor.scheduleWithFixedDelay(task,
238244
NANO.convert(initialDelay), NANO.convert(delay), NANO);
239245
}
240246
catch (RejectedExecutionException ex) {
@@ -245,7 +251,8 @@ public ScheduledFuture<?> scheduleWithFixedDelay(Runnable task, Instant startTim
245251
@Override
246252
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable task, Duration delay) {
247253
try {
248-
return this.scheduledExecutor.scheduleWithFixedDelay(scheduledTask(task),
254+
// Blocking task on scheduler thread for fixed delay semantics
255+
return this.scheduledExecutor.scheduleWithFixedDelay(task,
249256
0, NANO.convert(delay), NANO);
250257
}
251258
catch (RejectedExecutionException ex) {

spring-context/src/test/java/org/springframework/scheduling/annotation/EnableSchedulingTests.java

Lines changed: 97 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,20 @@ public void withQualifiedSchedulerAndPlaceholder() throws InterruptedException {
175175

176176
Thread.sleep(110);
177177
assertThat(ctx.getBean(AtomicInteger.class).get()).isGreaterThanOrEqualTo(10);
178-
assertThat(ctx.getBean(QualifiedExplicitSchedulerConfigWithPlaceholder.class).threadName).startsWith("explicitScheduler1");
178+
assertThat(ctx.getBean(QualifiedExplicitSchedulerConfigWithPlaceholder.class).threadName)
179+
.startsWith("explicitScheduler1").isNotEqualTo("explicitScheduler1-1");
180+
}
181+
182+
@Test
183+
@EnabledForTestGroups(LONG_RUNNING)
184+
public void withQualifiedSchedulerWithFixedDelayTask() throws InterruptedException {
185+
ctx = new AnnotationConfigApplicationContext(QualifiedExplicitSchedulerConfigWithFixedDelayTask.class);
186+
assertThat(ctx.getBean(ScheduledTaskHolder.class).getScheduledTasks()).hasSize(1);
187+
188+
Thread.sleep(110);
189+
assertThat(ctx.getBean(AtomicInteger.class).get()).isBetween(4, 5);
190+
assertThat(ctx.getBean(QualifiedExplicitSchedulerConfigWithFixedDelayTask.class).threadName)
191+
.isEqualTo("explicitScheduler1-1");
179192
}
180193

181194
@Test
@@ -228,7 +241,20 @@ public void withInitiallyDelayedFixedRateTask() throws InterruptedException {
228241

229242
// The @Scheduled method should have been called several times
230243
// but not more times than the delay allows.
231-
assertThat(counter.get()).isBetween(2, 10);
244+
assertThat(counter.get()).isBetween(6, 10);
245+
}
246+
247+
@Test
248+
@EnabledForTestGroups(LONG_RUNNING)
249+
public void withInitiallyDelayedFixedDelayTask() throws InterruptedException {
250+
ctx = new AnnotationConfigApplicationContext(FixedDelayTaskConfig_withInitialDelay.class);
251+
252+
Thread.sleep(1950);
253+
AtomicInteger counter = ctx.getBean(AtomicInteger.class);
254+
255+
// The @Scheduled method should have been called several times
256+
// but not more times than the delay allows.
257+
assertThat(counter.get()).isBetween(1, 5);
232258
}
233259

234260
@Test
@@ -333,14 +359,14 @@ static class AmbiguousExplicitSchedulerConfig {
333359
@Bean
334360
public TaskScheduler taskScheduler1() {
335361
SimpleAsyncTaskScheduler scheduler = new SimpleAsyncTaskScheduler();
336-
scheduler.setThreadNamePrefix("explicitScheduler1");
362+
scheduler.setThreadNamePrefix("explicitScheduler1-");
337363
return scheduler;
338364
}
339365

340366
@Bean
341367
public TaskScheduler taskScheduler2() {
342368
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
343-
scheduler.setThreadNamePrefix("explicitScheduler2");
369+
scheduler.setThreadNamePrefix("explicitScheduler2-");
344370
return scheduler;
345371
}
346372

@@ -359,14 +385,14 @@ static class ExplicitScheduledTaskRegistrarConfig implements SchedulingConfigure
359385
@Bean
360386
public TaskScheduler taskScheduler1() {
361387
SimpleAsyncTaskScheduler scheduler = new SimpleAsyncTaskScheduler();
362-
scheduler.setThreadNamePrefix("explicitScheduler1");
388+
scheduler.setThreadNamePrefix("explicitScheduler1-");
363389
return scheduler;
364390
}
365391

366392
@Bean
367393
public TaskScheduler taskScheduler2() {
368394
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
369-
scheduler.setThreadNamePrefix("explicitScheduler2");
395+
scheduler.setThreadNamePrefix("explicitScheduler2-");
370396
return scheduler;
371397
}
372398

@@ -397,14 +423,14 @@ static class QualifiedExplicitSchedulerConfig {
397423
@Bean @Qualifier("myScheduler")
398424
public TaskScheduler taskScheduler1() {
399425
SimpleAsyncTaskScheduler scheduler = new SimpleAsyncTaskScheduler();
400-
scheduler.setThreadNamePrefix("explicitScheduler1");
426+
scheduler.setThreadNamePrefix("explicitScheduler1-");
401427
return scheduler;
402428
}
403429

404430
@Bean
405431
public TaskScheduler taskScheduler2() {
406432
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
407-
scheduler.setThreadNamePrefix("explicitScheduler2");
433+
scheduler.setThreadNamePrefix("explicitScheduler2-");
408434
return scheduler;
409435
}
410436

@@ -414,9 +440,10 @@ public AtomicInteger counter() {
414440
}
415441

416442
@Scheduled(fixedRate = 10, scheduler = "myScheduler")
417-
public void task() {
443+
public void task() throws InterruptedException {
418444
threadName = Thread.currentThread().getName();
419445
counter().incrementAndGet();
446+
Thread.sleep(10);
420447
}
421448
}
422449

@@ -430,14 +457,14 @@ static class QualifiedExplicitSchedulerConfigWithPlaceholder {
430457
@Bean @Qualifier("myScheduler")
431458
public TaskScheduler taskScheduler1() {
432459
SimpleAsyncTaskScheduler scheduler = new SimpleAsyncTaskScheduler();
433-
scheduler.setThreadNamePrefix("explicitScheduler1");
460+
scheduler.setThreadNamePrefix("explicitScheduler1-");
434461
return scheduler;
435462
}
436463

437464
@Bean
438465
public TaskScheduler taskScheduler2() {
439466
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
440-
scheduler.setThreadNamePrefix("explicitScheduler2");
467+
scheduler.setThreadNamePrefix("explicitScheduler2-");
441468
return scheduler;
442469
}
443470

@@ -447,9 +474,10 @@ public AtomicInteger counter() {
447474
}
448475

449476
@Scheduled(fixedRate = 10, scheduler = "${scheduler}")
450-
public void task() {
477+
public void task() throws InterruptedException {
451478
threadName = Thread.currentThread().getName();
452479
counter().incrementAndGet();
480+
Thread.sleep(10);
453481
}
454482

455483
@Bean
@@ -463,21 +491,55 @@ public static PropertySourcesPlaceholderConfigurer placeholderConfigurer() {
463491
}
464492

465493

494+
@Configuration
495+
@EnableScheduling
496+
static class QualifiedExplicitSchedulerConfigWithFixedDelayTask {
497+
498+
String threadName;
499+
500+
@Bean @Qualifier("myScheduler")
501+
public TaskScheduler taskScheduler1() {
502+
SimpleAsyncTaskScheduler scheduler = new SimpleAsyncTaskScheduler();
503+
scheduler.setThreadNamePrefix("explicitScheduler1-");
504+
return scheduler;
505+
}
506+
507+
@Bean
508+
public TaskScheduler taskScheduler2() {
509+
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
510+
scheduler.setThreadNamePrefix("explicitScheduler2-");
511+
return scheduler;
512+
}
513+
514+
@Bean
515+
public AtomicInteger counter() {
516+
return new AtomicInteger();
517+
}
518+
519+
@Scheduled(fixedDelay = 10, scheduler = "myScheduler")
520+
public void task() throws InterruptedException {
521+
threadName = Thread.currentThread().getName();
522+
counter().incrementAndGet();
523+
Thread.sleep(10);
524+
}
525+
}
526+
527+
466528
@Configuration
467529
@EnableScheduling
468530
static class SchedulingEnabled_withAmbiguousTaskSchedulers_butNoActualTasks {
469531

470532
@Bean
471533
public TaskScheduler taskScheduler1() {
472534
SimpleAsyncTaskScheduler scheduler = new SimpleAsyncTaskScheduler();
473-
scheduler.setThreadNamePrefix("explicitScheduler1");
535+
scheduler.setThreadNamePrefix("explicitScheduler1-");
474536
return scheduler;
475537
}
476538

477539
@Bean
478540
public TaskScheduler taskScheduler2() {
479541
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
480-
scheduler.setThreadNamePrefix("explicitScheduler2");
542+
scheduler.setThreadNamePrefix("explicitScheduler2-");
481543
return scheduler;
482544
}
483545
}
@@ -494,15 +556,15 @@ public void task() {
494556
@Bean
495557
public TaskScheduler taskScheduler1() {
496558
SimpleAsyncTaskScheduler scheduler = new SimpleAsyncTaskScheduler();
497-
scheduler.setThreadNamePrefix("explicitScheduler1");
559+
scheduler.setThreadNamePrefix("explicitScheduler1-");
498560
scheduler.setConcurrencyLimit(1);
499561
return scheduler;
500562
}
501563

502564
@Bean
503565
public TaskScheduler taskScheduler2() {
504566
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
505-
scheduler.setThreadNamePrefix("explicitScheduler2");
567+
scheduler.setThreadNamePrefix("explicitScheduler2-");
506568
return scheduler;
507569
}
508570
}
@@ -620,8 +682,26 @@ public AtomicInteger counter() {
620682
}
621683

622684
@Scheduled(initialDelay = 1000, fixedRate = 100)
623-
public void task() {
685+
public void task() throws InterruptedException {
686+
counter().incrementAndGet();
687+
Thread.sleep(100);
688+
}
689+
}
690+
691+
692+
@Configuration
693+
@EnableScheduling
694+
static class FixedDelayTaskConfig_withInitialDelay {
695+
696+
@Bean
697+
public AtomicInteger counter() {
698+
return new AtomicInteger();
699+
}
700+
701+
@Scheduled(initialDelay = 1000, fixedDelay = 100)
702+
public void task() throws InterruptedException {
624703
counter().incrementAndGet();
704+
Thread.sleep(100);
625705
}
626706
}
627707

0 commit comments

Comments
 (0)