Skip to content

Commit 8cfd6ae

Browse files
committed
Separate internal ScheduledExecutorService for fixed-delay tasks
Closes gh-33408
1 parent 81a66a0 commit 8cfd6ae

File tree

1 file changed

+50
-24
lines changed

1 file changed

+50
-24
lines changed

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

Lines changed: 50 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@
5252
* separate thread. This is an attractive choice with virtual threads on JDK 21,
5353
* expecting common usage with {@link #setVirtualThreads setVirtualThreads(true)}.
5454
*
55-
* <p><b>NOTE: Scheduling with a fixed delay enforces execution on the single
55+
* <p><b>NOTE: Scheduling with a fixed delay enforces execution on a single
5656
* scheduler thread, in order to provide traditional fixed-delay semantics!</b>
5757
* Prefer the use of fixed rates or cron triggers instead which are a better fit
5858
* with this thread-per-task scheduler variant.
@@ -113,9 +113,13 @@ public class SimpleAsyncTaskScheduler extends SimpleAsyncTaskExecutor implements
113113
private static final TimeUnit NANO = TimeUnit.NANOSECONDS;
114114

115115

116-
private final ScheduledExecutorService scheduledExecutor = createScheduledExecutor();
116+
private final ScheduledExecutorService triggerExecutor = createScheduledExecutor();
117117

118-
private final ExecutorLifecycleDelegate lifecycleDelegate = new ExecutorLifecycleDelegate(this.scheduledExecutor);
118+
private final ExecutorLifecycleDelegate triggerLifecycle = new ExecutorLifecycleDelegate(this.triggerExecutor);
119+
120+
private final ScheduledExecutorService fixedDelayExecutor = createFixedDelayExecutor();
121+
122+
private final ExecutorLifecycleDelegate fixedDelayLifecycle = new ExecutorLifecycleDelegate(this.fixedDelayExecutor);
119123

120124
@Nullable
121125
private ErrorHandler errorHandler;
@@ -195,11 +199,24 @@ private ScheduledExecutorService createScheduledExecutor() {
195199
return new ScheduledThreadPoolExecutor(1, this::newThread) {
196200
@Override
197201
protected void beforeExecute(Thread thread, Runnable task) {
198-
lifecycleDelegate.beforeExecute(thread);
202+
triggerLifecycle.beforeExecute(thread);
203+
}
204+
@Override
205+
protected void afterExecute(Runnable task, Throwable ex) {
206+
triggerLifecycle.afterExecute();
207+
}
208+
};
209+
}
210+
211+
private ScheduledExecutorService createFixedDelayExecutor() {
212+
return new ScheduledThreadPoolExecutor(1, this::newThread) {
213+
@Override
214+
protected void beforeExecute(Thread thread, Runnable task) {
215+
fixedDelayLifecycle.beforeExecute(thread);
199216
}
200217
@Override
201218
protected void afterExecute(Runnable task, Throwable ex) {
202-
lifecycleDelegate.afterExecute();
219+
fixedDelayLifecycle.afterExecute();
203220
}
204221
};
205222
}
@@ -227,7 +244,7 @@ private void shutdownAwareErrorHandler(Throwable ex) {
227244
if (this.errorHandler != null) {
228245
this.errorHandler.handleError(ex);
229246
}
230-
else if (this.scheduledExecutor.isShutdown()) {
247+
else if (this.triggerExecutor.isShutdown()) {
231248
LogFactory.getLog(getClass()).debug("Ignoring scheduled task exception after shutdown", ex);
232249
}
233250
else {
@@ -271,44 +288,44 @@ public ScheduledFuture<?> schedule(Runnable task, Trigger trigger) {
271288
ErrorHandler errorHandler =
272289
(this.errorHandler != null ? this.errorHandler : TaskUtils.getDefaultErrorHandler(true));
273290
return new ReschedulingRunnable(
274-
delegate, trigger, this.clock, this.scheduledExecutor, errorHandler).schedule();
291+
delegate, trigger, this.clock, this.triggerExecutor, errorHandler).schedule();
275292
}
276293
catch (RejectedExecutionException ex) {
277-
throw new TaskRejectedException(this.scheduledExecutor, task, ex);
294+
throw new TaskRejectedException(this.triggerExecutor, task, ex);
278295
}
279296
}
280297

281298
@Override
282299
public ScheduledFuture<?> schedule(Runnable task, Instant startTime) {
283300
Duration delay = Duration.between(this.clock.instant(), startTime);
284301
try {
285-
return this.scheduledExecutor.schedule(scheduledTask(task), NANO.convert(delay), NANO);
302+
return this.triggerExecutor.schedule(scheduledTask(task), NANO.convert(delay), NANO);
286303
}
287304
catch (RejectedExecutionException ex) {
288-
throw new TaskRejectedException(this.scheduledExecutor, task, ex);
305+
throw new TaskRejectedException(this.triggerExecutor, task, ex);
289306
}
290307
}
291308

292309
@Override
293310
public ScheduledFuture<?> scheduleAtFixedRate(Runnable task, Instant startTime, Duration period) {
294311
Duration initialDelay = Duration.between(this.clock.instant(), startTime);
295312
try {
296-
return this.scheduledExecutor.scheduleAtFixedRate(scheduledTask(task),
313+
return this.triggerExecutor.scheduleAtFixedRate(scheduledTask(task),
297314
NANO.convert(initialDelay), NANO.convert(period), NANO);
298315
}
299316
catch (RejectedExecutionException ex) {
300-
throw new TaskRejectedException(this.scheduledExecutor, task, ex);
317+
throw new TaskRejectedException(this.triggerExecutor, task, ex);
301318
}
302319
}
303320

304321
@Override
305322
public ScheduledFuture<?> scheduleAtFixedRate(Runnable task, Duration period) {
306323
try {
307-
return this.scheduledExecutor.scheduleAtFixedRate(scheduledTask(task),
324+
return this.triggerExecutor.scheduleAtFixedRate(scheduledTask(task),
308325
0, NANO.convert(period), NANO);
309326
}
310327
catch (RejectedExecutionException ex) {
311-
throw new TaskRejectedException(this.scheduledExecutor, task, ex);
328+
throw new TaskRejectedException(this.triggerExecutor, task, ex);
312329
}
313330
}
314331

@@ -317,57 +334,66 @@ public ScheduledFuture<?> scheduleWithFixedDelay(Runnable task, Instant startTim
317334
Duration initialDelay = Duration.between(this.clock.instant(), startTime);
318335
try {
319336
// Blocking task on scheduler thread for fixed delay semantics
320-
return this.scheduledExecutor.scheduleWithFixedDelay(taskOnSchedulerThread(task),
337+
return this.fixedDelayExecutor.scheduleWithFixedDelay(taskOnSchedulerThread(task),
321338
NANO.convert(initialDelay), NANO.convert(delay), NANO);
322339
}
323340
catch (RejectedExecutionException ex) {
324-
throw new TaskRejectedException(this.scheduledExecutor, task, ex);
341+
throw new TaskRejectedException(this.fixedDelayExecutor, task, ex);
325342
}
326343
}
327344

328345
@Override
329346
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable task, Duration delay) {
330347
try {
331348
// Blocking task on scheduler thread for fixed delay semantics
332-
return this.scheduledExecutor.scheduleWithFixedDelay(taskOnSchedulerThread(task),
349+
return this.fixedDelayExecutor.scheduleWithFixedDelay(taskOnSchedulerThread(task),
333350
0, NANO.convert(delay), NANO);
334351
}
335352
catch (RejectedExecutionException ex) {
336-
throw new TaskRejectedException(this.scheduledExecutor, task, ex);
353+
throw new TaskRejectedException(this.fixedDelayExecutor, task, ex);
337354
}
338355
}
339356

340357

341358
@Override
342359
public void start() {
343-
this.lifecycleDelegate.start();
360+
this.triggerLifecycle.start();
361+
this.fixedDelayLifecycle.start();
344362
}
345363

346364
@Override
347365
public void stop() {
348-
this.lifecycleDelegate.stop();
366+
this.triggerLifecycle.stop();
367+
this.fixedDelayLifecycle.stop();
349368
}
350369

351370
@Override
352371
public void stop(Runnable callback) {
353-
this.lifecycleDelegate.stop(callback);
372+
this.triggerLifecycle.stop(); // no callback necessary since it's just triggers with hand-offs
373+
this.fixedDelayLifecycle.stop(callback); // callback for currently executing fixed-delay tasks
354374
}
355375

356376
@Override
357377
public boolean isRunning() {
358-
return this.lifecycleDelegate.isRunning();
378+
return this.triggerLifecycle.isRunning();
359379
}
360380

361381
@Override
362382
public void onApplicationEvent(ContextClosedEvent event) {
363383
if (event.getApplicationContext() == this.applicationContext) {
364-
this.scheduledExecutor.shutdown();
384+
this.triggerExecutor.shutdown();
385+
this.fixedDelayExecutor.shutdown();
365386
}
366387
}
367388

368389
@Override
369390
public void close() {
370-
for (Runnable remainingTask : this.scheduledExecutor.shutdownNow()) {
391+
for (Runnable remainingTask : this.triggerExecutor.shutdownNow()) {
392+
if (remainingTask instanceof Future<?> future) {
393+
future.cancel(true);
394+
}
395+
}
396+
for (Runnable remainingTask : this.fixedDelayExecutor.shutdownNow()) {
371397
if (remainingTask instanceof Future<?> future) {
372398
future.cancel(true);
373399
}

0 commit comments

Comments
 (0)