diff --git a/mq-starter-core/src/main/java/com/njcn/mq/container/MqListenerRegistry.java b/mq-starter-core/src/main/java/com/njcn/mq/container/MqListenerRegistry.java index bae3ad4..206db4e 100644 --- a/mq-starter-core/src/main/java/com/njcn/mq/container/MqListenerRegistry.java +++ b/mq-starter-core/src/main/java/com/njcn/mq/container/MqListenerRegistry.java @@ -150,6 +150,16 @@ public class MqListenerRegistry implements SmartLifecycle { if (flushScheduler != null) { // 温和关闭(不打断正在执行的最后一次刷出;后续执行取消,残留批由下方 forceFlush 接住)。 flushScheduler.shutdown(); + // 必须等在途刷批跑完:超时快照已把批移出 buffer(下方 forceFlush 看不见它), + // 不等就返回会让 JVM 退出杀掉 daemon 线程,已逐条 ACK 的整批静默丢失。 + try { + if (!flushScheduler.awaitTermination(30, TimeUnit.SECONDS)) { + log.warn("[mq] 等待在途批量刷出超过 30s 仍未完成,放弃等待继续停机(该批可能丢失)"); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + log.warn("[mq] 等待在途批量刷出被中断,继续停机(该批可能丢失)"); + } flushScheduler = null; } for (MqConsumeDispatcher d : dispatchers) { diff --git a/mq-starter-core/src/test/java/com/njcn/mq/container/MqListenerRegistryTest.java b/mq-starter-core/src/test/java/com/njcn/mq/container/MqListenerRegistryTest.java index 66172d2..0b12de8 100644 --- a/mq-starter-core/src/test/java/com/njcn/mq/container/MqListenerRegistryTest.java +++ b/mq-starter-core/src/test/java/com/njcn/mq/container/MqListenerRegistryTest.java @@ -14,6 +14,9 @@ import java.util.List; import java.util.Map; import java.util.function.Function; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import static org.junit.jupiter.api.Assertions.*; @@ -219,4 +222,28 @@ class MqListenerRegistryTest { "停机应先停 driver 再强刷残留批(2 条未满批不丢)"); assertNull(reg.getFlushScheduler(), "停机后定时器应已关闭置空"); } + + @Test + void stop_waitsForInFlightTimedFlushBeforeReturning() throws Exception { + BatchBiz biz = new BatchBiz(); + MqListenerAnnotationBeanPostProcessor bpp = new MqListenerAnnotationBeanPostProcessor(); + bpp.postProcessAfterInitialization(biz, "biz"); + MqListenerRegistry reg = new MqListenerRegistry(new CaptureDriver(), bpp.getEndpoints()); + reg.start(); + + // 模拟停机瞬间定时线程正在 doFlush 跑业务方法(超时快照已清空 buffer,批只在该线程手上): + // 直接往真定时线程塞一个在途任务。stop 若不等它跑完,JVM 退出即静默丢批。 + CountDownLatch entered = new CountDownLatch(1); + AtomicBoolean completed = new AtomicBoolean(false); + reg.getFlushScheduler().execute(() -> { + entered.countDown(); + try { Thread.sleep(300); } catch (InterruptedException ignored) { } + completed.set(true); + }); + assertTrue(entered.await(2, TimeUnit.SECONDS), "在途刷批任务应已开始执行"); + + reg.stop(); + + assertTrue(completed.get(), "stop 返回前应等待定时线程在途的刷批任务完成"); + } }