From e9c149f3bbc498cd7db5f6ab2382a0bb8eb9ef6e Mon Sep 17 00:00:00 2001 From: hongawen <83944980@qq.com> Date: Wed, 15 Jul 2026 03:00:33 +0100 Subject: [PATCH] =?UTF-8?q?fix(mq-starter-core):=20=E6=89=93=E7=83=8A?= =?UTF-8?q?=E4=BA=8C=E6=AC=A1=E6=A3=80=E6=9F=A5=E6=8B=92=E6=94=B6=E6=97=B6?= =?UTF-8?q?=E5=A4=8D=E4=BD=8D=20processing=20=E6=A0=87=E8=AE=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit review 发现 #6(终审残余观察①的 follow-up):批量分支锁内的 closed 二次 检查发生在 tryAcquire 之后,拒收抛 InProgress 时去重标记已置 processing 却无人复位。重投每次都撞 stale 标记再抛 InProgress,白烧重试计数直到 TTL 过期;redis-stream 端若 deliveryCount 先超 maxRetry 且未配 errorHandler, 业务一次没跑就被判耗尽、静默丢弃。 修复:锁内只判定拒收,锁外 markFail 复位标记后再抛(markFail 是 Redis 往返, 不进 buffer 锁)。dispatch 入口的打烊快速检查在 tryAcquire 之前,不受影响。 Co-Authored-By: Claude Fable 5 --- .../mq/container/MqConsumeDispatcher.java | 27 +++++++++++----- .../mq/container/MqConsumeDispatcherTest.java | 31 +++++++++++++++++++ 2 files changed, 50 insertions(+), 8 deletions(-) diff --git a/mq-starter-core/src/main/java/com/njcn/mq/container/MqConsumeDispatcher.java b/mq-starter-core/src/main/java/com/njcn/mq/container/MqConsumeDispatcher.java index c03fd27..cd6dcf3 100644 --- a/mq-starter-core/src/main/java/com/njcn/mq/container/MqConsumeDispatcher.java +++ b/mq-starter-core/src/main/java/com/njcn/mq/container/MqConsumeDispatcher.java @@ -83,18 +83,29 @@ public class MqConsumeDispatcher { } // 攒批:满 batchSize 才 flush。锁内只攒/取快照,消费放锁外,避免持锁做 IO。 List flush = null; + boolean rejected = false; synchronized (buffer) { if (closed) { - throw new MqMessageInProgressException(msg.getKey()); + rejected = true; + } else { + if (buffer.isEmpty()) { + batchStartAtMs = clock.getAsLong(); + } + buffer.add(msg); + if (buffer.size() >= batchSize) { + flush = new ArrayList<>(buffer); + buffer.clear(); + } } - if (buffer.isEmpty()) { - batchStartAtMs = clock.getAsLong(); - } - buffer.add(msg); - if (buffer.size() >= batchSize) { - flush = new ArrayList<>(buffer); - buffer.clear(); + } + if (rejected) { + // 打烊二次检查发生在 tryAcquire 之后:拒收前必须把 processing 标记复位(markFail 是 + // Redis 往返,放 buffer 锁外做),否则重投每次都撞 stale 标记再抛 InProgress,白烧重试 + // 计数——redis-stream 端可能业务一次没跑就被判耗尽。 + if (idempotent && store != null) { + store.markFail(msg.getKey()); } + throw new MqMessageInProgressException(msg.getKey()); } if (flush != null) { doFlush(flush); diff --git a/mq-starter-core/src/test/java/com/njcn/mq/container/MqConsumeDispatcherTest.java b/mq-starter-core/src/test/java/com/njcn/mq/container/MqConsumeDispatcherTest.java index 3d5d088..a5ab0ea 100644 --- a/mq-starter-core/src/test/java/com/njcn/mq/container/MqConsumeDispatcherTest.java +++ b/mq-starter-core/src/test/java/com/njcn/mq/container/MqConsumeDispatcherTest.java @@ -416,6 +416,37 @@ class MqConsumeDispatcherTest { assertEquals(1, consumed.size(), "拒收的消息不得进入缓冲(再次强刷无动作)"); } + @Test + void closedDuringBatchDispatch_afterAcquire_resetsProcessingMark() { + // 竞态窗口:dispatch 已过打烊快速检查、tryAcquire 已把标记置 processing, + // 此刻停机 forceFlush 打烊 → 批量分支锁内二次检查拒收抛 InProgress。 + // 拒收前必须 markFail 复位标记:否则重投每次都撞 stale processing 再抛 InProgress, + // 白烧重试计数(redis-stream 端 deliveryCount 超 maxRetry 且未配 errorHandler 时, + // 业务一次没跑就被判耗尽、静默丢弃)。 + List failed = new ArrayList<>(); + MqConsumeDispatcher[] ref = new MqConsumeDispatcher[1]; + MqIdempotentStore store = new MqIdempotentStore() { + @Override public boolean tryAcquire(String key) { + ref[0].forceFlush(); // 模拟 tryAcquire 成功瞬间停机打烊 + return true; + } + @Override public void markSuccess(String key) { } + @Override public void markFail(String key) { failed.add(key); } + }; + MqConsumeDispatcher d = MqConsumeDispatcher.builder() + .batchSize(3).batchTimeoutMs(60_000) + .idempotent(true).store(store) + .consumer(batch -> { }) + .build(); + ref[0] = d; + + assertThrows(MqMessageInProgressException.class, + () -> d.dispatch(MqMessage.of("k1", "", "a")), + "打烊后批量路径锁内二次检查应拒收"); + assertEquals(Arrays.asList("k1"), failed, + "已 tryAcquire 的拒收必须 markFail 复位,否则重投一直撞 processing 标记"); + } + @Test void closed_rejectsSingleModeToo_beforeTouchingDedupStore() { List acquired = new ArrayList<>();