You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring Boot优雅关闭未等待定时任务(含Thread.sleep)问题求助

问题描述

我有一个Spring Boot 3应用,使用@Scheduled任务从SQS轮询消息,需要配置优雅关闭(通过kill pid发送SIGTERM信号时),确保当前消息处理完成后再销毁POD。

已在application.properties中配置以下属性:

server.shutdown=graceful
spring.lifecycle.timeout-per-shutdown-phase=60s
spring.task.execution.shutdown.await-termination=true
spring.task.execution.shutdown.await-termination-period=60s
spring.task.scheduling.shutdown.await-termination=true
spring.task.scheduling.shutdown.await-termination-period=60s

定时任务代码如下:

@Scheduled(fixedDelay = 1L, timeUnit = TimeUnit.SECONDS, initialDelay = 3L)
public void pollMessages() {
    final var messages = sqsClient.receiveMessage(ReceiveMessageRequest.builder()
                .queueUrl(queueUrl)
                .waitTimeSeconds(maxWaitTime)
                .maxNumberOfMessages(numberOfMessages)
                .messageAttributeNames(".*")
                .build());
    // 处理消息....
}

但应用关闭时,若任务正在轮询消息,AWS SDK会抛出错误,关闭流程中断了等待SQS响应的线程:

Unexpected error occurred in scheduled task
s.a.a.c.e.SdkInterruptedException: null
    at s.a.a.c.i.h.InterruptMonitor.checkInterrupted(InterruptMonitor.java:55)
    at s.a.a.c.i.h.p.s.AfterTransmissionExecutionInterceptorsStage.execute(AfterTransmissionExecutionInterceptorsStage.java:34)
    at s.a.a.c.i.h.p.s.AfterTransmissionExecutionInterceptorsStage.execute(AfterTransmissionExecutionInterceptorsStage.java:28)
    at s.a.a.c.i.h.p.RequestPipelineBuilder$ComposingRequestPipelineStage.execute(RequestPipelineBuilder.java:206)
    at s.a.a.c.i.h.p.RequestPipelineBuilder$ComposingRequestPipelineStage.execute(RequestPipelineBuilder.java:206)
    at s.a.a.c.i.h.p.RequestPipelineBuilder$ComposingRequestPipelineStage.execute(RequestPipelineBuilder.java:206)
    at s.a.a.c.i.h.p.s.ApiCallAttemptTimeoutTrackingStage.execute(ApiCallAttemptTimeoutTrackingStage.java:72)
    ... 54 common frames omitted
Wrapped by: s.a.a.c.e.AbortedException: Thread was interrupted
    at s.a.a.c.e.AbortedException$BuilderImpl.build(AbortedException.java:93)
    at s.a.a.c.e.AbortedException.create(AbortedException.java:38)
    at s.a.a.c.i.h.p.s.ApiCallAttemptTimeoutTrackingStage.handleInterruptedException(ApiCallAttemptTimeoutTrackingStage.java:141)
    at s.a.a.c.i.h.p.s.ApiCallAttemptTimeoutTrackingStage.translatePipelineException(ApiCallAttemptTimeoutTrackingStage.java:105)
    at s.a.a.c.i.h.p.s.ApiCallAttemptTimeoutTrackingStage.execute(ApiCallAttemptTimeoutTrackingStage.java:89)
    at s.a.a.c.i.h.p.s.ApiCallAttemptTimeoutTrackingStage.execute(ApiCallAttemptTimeoutTrackingStage.java:42)
    at s.a.a.c.i.h.p.s.TimeoutExceptionHandlingStage.execute(TimeoutExceptionHandlingStage.java:78)
    at s.a.a.c.i.h.p.s.TimeoutExceptionHandlingStage.execute(TimeoutExceptionHandlingStage.java:40)
    at s.a.a.c.i.h.p.s.ApiCallAttemptMetricCollectionStage.execute(ApiCallAttemptMetricCollectionStage.java:52)
    at s.a.a.c.i.h.p.s.ApiCallAttemptMetricCollectionStage.execute(ApiCallAttemptMetricCollectionStage.java:37)
    at s.a.a.c.i.h.p.s.RetryableStage.execute(RetryableStage.java:81)
    at s.a.a.c.i.h.p.s.RetryableStag...

类似问题也出现在含Thread.sleep()的定时任务中,优雅关闭会中断睡眠线程:

@Scheduled(fixedDelay = 1L, timeUnit = TimeUnit.SECONDS, initialDelay = 3L)
public void pollMessages() throws Exception {
    Thread.sleep(10000); // 优雅关闭会中断此睡眠线程
    // 省略额外处理步骤
}

需要解决:优雅关闭时不中断定时任务中的睡眠/等待SQS响应的线程,确保当前任务处理完成后再关闭。

解决方案

1. 自定义调度线程池,禁用线程中断

Spring默认调度线程池在关闭时会中断正在运行的任务,可自定义线程池,配置等待终止时间并关闭线程中断行为:

@Configuration
public class SchedulerConfig {

    @Bean
    public TaskScheduler taskScheduler() {
        ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
        scheduler.setPoolSize(1);
        scheduler.setThreadNamePrefix("sqs-poll-scheduler-");
        scheduler.setAwaitTerminationSeconds(60);
        // 核心配置:等待任务完成后再关闭,不中断线程
        scheduler.setWaitForTasksToCompleteOnShutdown(true);
        scheduler.setInterruptOnShutdown(false);
        return scheduler;
    }
}

2. 捕获中断异常并恢复线程状态

在定时任务中捕获AWS SDK的AbortedException和通用InterruptedException,恢复线程中断状态,避免任务被强制终止:

@Scheduled(fixedDelay = 1L, timeUnit = TimeUnit.SECONDS, initialDelay = 3L)
public void pollMessages() {
    try {
        final var messages = sqsClient.receiveMessage(ReceiveMessageRequest.builder()
                    .queueUrl(queueUrl)
                    .waitTimeSeconds(maxWaitTime)
                    .maxNumberOfMessages(numberOfMessages)
                    .messageAttributeNames(".*")
                    .build());
        // 处理消息逻辑
    } catch (AbortedException e) {
        if (Thread.currentThread().isInterrupted()) {
            Thread.currentThread().interrupt();
            // 记录日志,不终止当前任务
            log.warn("任务线程被中断,恢复中断状态并继续处理");
        }
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
        log.warn("任务线程被中断,恢复中断状态");
    }
}

3. 优化SQS轮询逻辑

将长轮询拆分为多个短轮询,结合中断检查,平衡响应性与任务完整性:

@Scheduled(fixedDelay = 1L, timeUnit = TimeUnit.SECONDS, initialDelay = 3L)
public void pollMessages() {
    int remainingWaitTime = maxWaitTime;
    while (remainingWaitTime > 0 && !Thread.currentThread().isInterrupted()) {
        try {
            ReceiveMessageRequest request = ReceiveMessageRequest.builder()
                    .queueUrl(queueUrl)
                    .waitTimeSeconds(Math.min(5, remainingWaitTime)) // 每次短等待5秒
                    .maxNumberOfMessages(numberOfMessages)
                    .messageAttributeNames(".*")
                    .build();
            final var messages = sqsClient.receiveMessage(request);
            if (!messages.messages().isEmpty()) {
                processMessages(messages); // 处理消息
                remainingWaitTime = maxWaitTime; // 重置等待时间
            } else {
                remainingWaitTime -= 5;
            }
        } catch (AbortedException e) {
            if (Thread.currentThread().isInterrupted()) {
                Thread.currentThread().interrupt();
                log.info("调度线程已中断,终止轮询");
                break;
            }
        }
    }
}

4. 验证配置有效性

确认spring.task.scheduling.shutdown.await-termination=true和await-termination-period配置生效,自定义线程池的interruptOnShutdown=false会覆盖默认行为,确保任务在关闭时不被中断,直到自然完成或超时。


内容的提问来源于stack exchange,提问作者João Pedro Schmitt

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.13 20:14:51