feat(mq-starter): redis-stream 重试可放量(PEL reclaim + 超限落库)
- MqSubscription 增加 retryExhaustedHandler;registry 从 errorHandler 接线 RETRY_EXHAUSTED 回调 - redis driver 每轮 reclaim PEL:connection 层 xClaim 认领超 idle 消息并使 delivery+1 - delivery <= maxRetry:重投消费,成功 ACK / 失败留 PEL - delivery > maxRetry:回调 retryExhaustedHandler 落库 + ACK 终止,不再卡队列 - 可配 reclaimMinIdleMs(默认 5000ms,对齐 legacy 5s 延迟);4 参构造 + 3 参重载 - 集成测试连真实 redis 验证 重试耗尽→落库→ACK;全仓 28 测试绿 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,7 @@ import com.njcn.mq.annotation.MqListener;
|
||||
import com.njcn.mq.core.MqContext;
|
||||
import com.njcn.mq.core.MqMessage;
|
||||
import com.njcn.mq.spi.MqConsumeErrorHandler;
|
||||
import com.njcn.mq.spi.MqErrorIdentity;
|
||||
import com.njcn.mq.spi.MqDriver;
|
||||
import com.njcn.mq.spi.MqIdempotentStore;
|
||||
import com.njcn.mq.spi.MqMessageListener;
|
||||
@@ -33,29 +34,39 @@ public class MqListenerRegistry implements SmartLifecycle {
|
||||
if (running) return;
|
||||
for (MqListenerEndpoint ep : endpoints) {
|
||||
MqListener meta = ep.getMeta();
|
||||
MqConsumeErrorHandler eh = resolveErrorHandler(meta);
|
||||
|
||||
MqConsumeDispatcher.Builder db = MqConsumeDispatcher.builder()
|
||||
.batchSize(meta.batchSize())
|
||||
.consumer(batch -> consume(ep, batch));
|
||||
if (meta.idempotent()) {
|
||||
db.idempotent(true).store(store);
|
||||
}
|
||||
if (errorHandlerResolver != null && !meta.errorHandler().isEmpty()) {
|
||||
MqConsumeErrorHandler eh = errorHandlerResolver.apply(meta.errorHandler());
|
||||
if (eh != null) {
|
||||
db.errorHandler(eh);
|
||||
}
|
||||
if (eh != null) {
|
||||
db.errorHandler(eh);
|
||||
}
|
||||
MqConsumeDispatcher dispatcher = db.build();
|
||||
|
||||
java.util.function.Consumer<MqMessage> retryExhausted = eh == null ? null
|
||||
: msg -> eh.onError(msg, MqErrorIdentity.RETRY_EXHAUSTED, null);
|
||||
|
||||
MqSubscription sub = MqSubscription.builder()
|
||||
.topic(meta.topic()).group(meta.group()).tag(meta.tag())
|
||||
.payloadType(ep.getPayloadType())
|
||||
.concurrency(meta.concurrency()).batchSize(meta.batchSize()).maxRetry(meta.maxRetry())
|
||||
.listener(dispatcher::dispatch).build();
|
||||
.listener(dispatcher::dispatch)
|
||||
.retryExhaustedHandler(retryExhausted)
|
||||
.build();
|
||||
driver.subscribe(sub);
|
||||
}
|
||||
running = true;
|
||||
}
|
||||
|
||||
private MqConsumeErrorHandler resolveErrorHandler(MqListener meta) {
|
||||
if (errorHandlerResolver == null || meta.errorHandler().isEmpty()) return null;
|
||||
return errorHandlerResolver.apply(meta.errorHandler());
|
||||
}
|
||||
|
||||
/** 业务调用适配:批量 endpoint 调 (List<payload>[,ctx]);单条 endpoint 逐条调 (payload[,ctx])。 */
|
||||
private void consume(MqListenerEndpoint ep, List<MqMessage> batch) throws Exception {
|
||||
if (ep.isBatch()) {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package com.njcn.mq.spi;
|
||||
import com.njcn.mq.core.MqMessage;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@@ -13,4 +15,6 @@ public class MqSubscription {
|
||||
private int batchSize;
|
||||
private int maxRetry;
|
||||
private MqMessageListener listener;
|
||||
/** 重试耗尽(超过 maxRetry)时由 driver 回调,用于落库 RETRY_EXHAUSTED;可空。 */
|
||||
private Consumer<MqMessage> retryExhaustedHandler;
|
||||
}
|
||||
|
||||
@@ -61,6 +61,13 @@ class MqListenerRegistryTest {
|
||||
catch (Exception e) { throw new RuntimeException(e); }
|
||||
}
|
||||
}
|
||||
/** fake driver:捕获 subscription,不喂消息。 */
|
||||
static class CaptureDriver implements MqDriver {
|
||||
MqSubscription captured;
|
||||
@Override public void send(String topic, MqMessage m) {}
|
||||
@Override public void stop() {}
|
||||
@Override public void subscribe(MqSubscription s) { this.captured = s; }
|
||||
}
|
||||
/** fake driver:subscribe 时按序喂多条消息。 */
|
||||
static class FeedDriver implements MqDriver {
|
||||
private final MqMessage[] msgs;
|
||||
@@ -123,4 +130,23 @@ class MqListenerRegistryTest {
|
||||
assertEquals(1, errors.size(), "按名解析的 errorHandler 应被调用");
|
||||
assertEquals(MqErrorIdentity.SINGLE, errors.get(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
void retryExhaustedHandler_wiredFromErrorHandler() {
|
||||
ErrBiz biz = new ErrBiz();
|
||||
MqListenerAnnotationBeanPostProcessor bpp = new MqListenerAnnotationBeanPostProcessor();
|
||||
bpp.postProcessAfterInitialization(biz, "biz");
|
||||
List<MqErrorIdentity> errors = new ArrayList<>();
|
||||
MqConsumeErrorHandler eh = (m, id, ex) -> errors.add(id);
|
||||
Function<String, MqConsumeErrorHandler> resolver = name -> "myEH".equals(name) ? eh : null;
|
||||
CaptureDriver driver = new CaptureDriver();
|
||||
MqListenerRegistry reg = new MqListenerRegistry(driver, bpp.getEndpoints(), null, resolver);
|
||||
|
||||
reg.start();
|
||||
|
||||
assertNotNull(driver.captured.getRetryExhaustedHandler(), "应从 errorHandler 接线重试耗尽回调");
|
||||
driver.captured.getRetryExhaustedHandler().accept(MqMessage.of("k", "", "p"));
|
||||
assertEquals(1, errors.size());
|
||||
assertEquals(MqErrorIdentity.RETRY_EXHAUSTED, errors.get(0), "重试耗尽应落库 RETRY_EXHAUSTED");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user