fix(scheduling): cancel unstarted delayed jobs during shutdown

This commit is contained in:
mateaix 2026-09-13 21:39:40 +08:00
parent 3843dc04b7
commit 511b689387
2 changed files with 78 additions and 0 deletions

View File

@ -41,6 +41,9 @@ public class SchedulingConfig implements SchedulingConfigurer {
scheduler.setPoolSize(POOL_SIZE);
scheduler.setThreadNamePrefix("sched-");
scheduler.setRemoveOnCancelPolicy(true);
// Pending future ticks belong to the next application lifetime. Keeping
// them queued would consume the grace period and outlive bean teardown.
scheduler.setExecuteExistingDelayedTasksAfterShutdownPolicy(false);
scheduler.setAwaitTerminationSeconds(30);
scheduler.setWaitForTasksToCompleteOnShutdown(true);
return scheduler;

View File

@ -0,0 +1,75 @@
package vip.mate.config;
import org.junit.jupiter.api.Test;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import java.time.Instant;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import static org.junit.jupiter.api.Assertions.*;
class SchedulingConfigShutdownTest {
private ThreadPoolTaskScheduler scheduler() {
var scheduler = (ThreadPoolTaskScheduler) new SchedulingConfig().taskScheduler();
scheduler.initialize();
return scheduler;
}
@Test
void futureTaskDoesNotHoldGracefulShutdownOpen() throws Exception {
var scheduler = scheduler();
var ran = new AtomicBoolean();
var pending = scheduler.schedule(() -> ran.set(true), Instant.now().plusSeconds(3600));
try (var closer = Executors.newSingleThreadExecutor()) {
try {
var closed = closer.submit(scheduler::destroy);
assertDoesNotThrow(() -> closed.get(2, TimeUnit.SECONDS),
"unstarted delayed tasks must not consume the graceful shutdown window");
assertTrue(pending.isCancelled());
assertFalse(ran.get());
} finally {
scheduler.getScheduledExecutor().shutdownNow();
}
}
}
@Test
void alreadyRunningTaskStillFinishesWithoutInterruption() throws Exception {
var scheduler = scheduler();
var started = new CountDownLatch(1);
var release = new CountDownLatch(1);
var interrupted = new AtomicBoolean();
var completed = new AtomicBoolean();
scheduler.schedule(() -> {
started.countDown();
try {
if (release.await(5, TimeUnit.SECONDS)) completed.set(true);
} catch (InterruptedException exception) {
interrupted.set(true);
Thread.currentThread().interrupt();
}
}, Instant.now());
try (var closer = Executors.newSingleThreadExecutor()) {
try {
assertTrue(started.await(2, TimeUnit.SECONDS));
var closed = closer.submit(scheduler::destroy);
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(2);
while (!scheduler.getScheduledExecutor().isShutdown() && System.nanoTime() < deadline) {
Thread.sleep(5);
}
assertTrue(scheduler.getScheduledExecutor().isShutdown());
assertFalse(closed.isDone(), "shutdown must wait for the running task");
release.countDown();
closed.get(2, TimeUnit.SECONDS);
assertTrue(completed.get());
assertFalse(interrupted.get());
} finally {
release.countDown();
scheduler.getScheduledExecutor().shutdownNow();
}
}
}
}