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
相关产品推荐
相关产品推荐

