From 7d2c4a3bb7ce72d877a37305e71fabb65388732b Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Tue, 14 Oct 2025 19:29:46 +0800 Subject: [PATCH 001/157] docs: add comments to class GrpcMessagingApplication --- .../grpc/v2/GrpcMessagingApplication.java | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/GrpcMessagingApplication.java b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/GrpcMessagingApplication.java index 9ee3f4fddd4..7446dc5378d 100644 --- a/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/GrpcMessagingApplication.java +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/GrpcMessagingApplication.java @@ -71,6 +71,15 @@ import org.apache.rocketmq.proxy.grpc.v2.common.ResponseWriter; import org.apache.rocketmq.proxy.processor.MessagingProcessor; +/** + * RocketMQ gRPC protocol implementation + * + * + */ public class GrpcMessagingApplication extends MessagingServiceGrpc.MessagingServiceImplBase implements StartAndShutdown { private final static Logger log = LoggerFactory.getLogger(LoggerName.PROXY_LOGGER_NAME); @@ -167,6 +176,16 @@ protected Status convertExceptionToStatus(Throwable t) { return ResponseBuilder.getInstance().buildStatus(t); } + /** + * submit grpc task to related thread pool. + * + * @param executor thread pool + * @param context context + * @param request grpc request + * @param runnable process task + * @param responseObserver grpc response observer + * @param statusResponseCreator error response creator + */ protected void addExecutor(ExecutorService executor, ProxyContext context, V request, Runnable runnable, StreamObserver responseObserver, Function statusResponseCreator) { if (request instanceof GeneratedMessageV3) { @@ -200,6 +219,12 @@ protected void validateContext(ProxyContext context) { } } + /** + * route query api, producer/consumer will call this api while starting. + * + * @param request request + * @param responseObserver gRPC response observer + */ @Override public void queryRoute(QueryRouteRequest request, StreamObserver responseObserver) { Function statusResponseCreator = status -> QueryRouteResponse.newBuilder().setStatus(status).build(); @@ -217,6 +242,12 @@ public void queryRoute(QueryRouteRequest request, StreamObserver responseObserver) { Function statusResponseCreator = status -> HeartbeatResponse.newBuilder().setStatus(status).build(); @@ -251,6 +282,12 @@ public void sendMessage(SendMessageRequest request, StreamObserver responseObserver) { @@ -399,6 +436,15 @@ public void recallMessage(RecallMessageRequest request, StreamObserver + *
  • register producer/consumer
  • + *
  • process trace
  • + *
  • verify message result
  • + * + */ @Override public StreamObserver telemetry(StreamObserver responseObserver) { Function statusResponseCreator = status -> TelemetryCommand.newBuilder().setStatus(status).build(); From 512bc80c1d2847997fc8010c8cd1cb7a88f68986 Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Fri, 14 Nov 2025 08:20:00 +0800 Subject: [PATCH 002/157] comment: add comments to ProxyStartup --- .../java/org/apache/rocketmq/proxy/ProxyStartup.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/ProxyStartup.java b/proxy/src/main/java/org/apache/rocketmq/proxy/ProxyStartup.java index 131faffa38e..c34c3f4e5d3 100644 --- a/proxy/src/main/java/org/apache/rocketmq/proxy/ProxyStartup.java +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/ProxyStartup.java @@ -55,6 +55,14 @@ public class ProxyStartup { private static final Logger log = LoggerFactory.getLogger(LoggerName.PROXY_LOGGER_NAME); + /** + * proxy components container, manager components with method start/shutdown/... + * - gRPC thread pool executor + * - message processor (wrap broker controller) + * - grpc server + * - remoting protocol server + * - ... + */ private static final ProxyStartAndShutdown PROXY_START_AND_SHUTDOWN = new ProxyStartAndShutdown(); private static class ProxyStartAndShutdown extends AbstractStartAndShutdown { @@ -73,8 +81,10 @@ public static void main(String[] args) { // init thread pool monitor for proxy. initThreadPoolMonitor(); + // init business thread pool for grpc server ThreadPoolExecutor executor = createServerExecutor(); + // create message processor, wrap broker controller in local mode MessagingProcessor messagingProcessor = createMessagingProcessor(); // tls cert update From a0cac8589251fb0bd3fdf5b327c50e40119251aa Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Sun, 16 Nov 2025 21:31:01 +0800 Subject: [PATCH 003/157] comment: add comments to remoting related thread pool config of ProxyConfig --- .../rocketmq/proxy/config/ProxyConfig.java | 31 ++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/config/ProxyConfig.java b/proxy/src/main/java/org/apache/rocketmq/proxy/config/ProxyConfig.java index a99b0afc352..db480ee850b 100644 --- a/proxy/src/main/java/org/apache/rocketmq/proxy/config/ProxyConfig.java +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/config/ProxyConfig.java @@ -243,7 +243,7 @@ public class ProxyConfig implements ConfigFile { private String remotingAccessAddr = ""; private int remotingListenPort = 8080; - // related to proxy's send strategy in cluster mode. + // related to proxy's sending strategy in cluster mode. private boolean sendLatencyEnable = false; private boolean startDetectorEnable = false; private int detectTimeout = 200; @@ -251,9 +251,38 @@ public class ProxyConfig implements ConfigFile { private int remotingHeartbeatThreadPoolNums = 2 * PROCESSOR_NUMBER; private int remotingTopicRouteThreadPoolNums = 2 * PROCESSOR_NUMBER; + /** + * thread pool number for + * 1. send message(and send message v2) + * 2. send batch message + * 3. consume send message back + * 4. end transaction + * 5. recall message + */ private int remotingSendMessageThreadPoolNums = 4 * PROCESSOR_NUMBER; + /** + * thread pool number for + * 1. pull message + * 2. lite pull message + * 3. pop message + */ private int remotingPullMessageThreadPoolNums = 4 * PROCESSOR_NUMBER; + /** + * thread pool number for + * 1. update consumer offset + * 2. ack message + * 3. change message invisible time + * 4. get consumer connection list + */ private int remotingUpdateOffsetThreadPoolNums = 4 * PROCESSOR_NUMBER; + /** + * thread pool number for + * 1. unregister client + * 2. check client config + * 3. get consumer list by group + * 4. get min/max offset, query consume offset, search offset by timestamp + * 5. lock/unlock batch mq + */ private int remotingDefaultThreadPoolNums = 4 * PROCESSOR_NUMBER; private int remotingHeartbeatThreadPoolQueueCapacity = 50000; From 156ec2281ac56965a83a1a0f84d9d3277d4c981a Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Sun, 16 Nov 2025 21:48:33 +0800 Subject: [PATCH 004/157] comment: add comments to method sendMessage of SendMessageActivity of grpc --- .../grpc/v2/producer/SendMessageActivity.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/producer/SendMessageActivity.java b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/producer/SendMessageActivity.java index f7b8014bb99..4ec2042404e 100644 --- a/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/producer/SendMessageActivity.java +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/producer/SendMessageActivity.java @@ -63,6 +63,20 @@ public SendMessageActivity(MessagingProcessor messagingProcessor, super(messagingProcessor, grpcClientSettingsManager, grpcChannelManager); } + /** + * send message, execute in producer thread pool + * request flow: + * producer -> grpcRequest -> GrpcMessagingApplication -> ProducerThreadPoolForGrpc(...) + * functionality: + * 1. validate topic + * 2. create queue selector + * 3. build and validate message + * 4. convert response + * + * @param ctx proxy context + * @param request send message request + * @return send message response future + */ public CompletableFuture sendMessage(ProxyContext ctx, SendMessageRequest request) { CompletableFuture future = new CompletableFuture<>(); From 7517165efc8eb3b0550f7b79b54c4cda9ebe0420 Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Sun, 16 Nov 2025 22:20:01 +0800 Subject: [PATCH 005/157] comment: add comments to method sendMessage of ProducerProcessor --- .../rocketmq/proxy/processor/ProducerProcessor.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/processor/ProducerProcessor.java b/proxy/src/main/java/org/apache/rocketmq/proxy/processor/ProducerProcessor.java index 5aeb553f216..26553568e58 100644 --- a/proxy/src/main/java/org/apache/rocketmq/proxy/processor/ProducerProcessor.java +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/processor/ProducerProcessor.java @@ -66,6 +66,14 @@ public ProducerProcessor(MessagingProcessor messagingProcessor, this.topicMessageTypeValidator = new DefaultTopicMessageTypeValidator(); } + /** + * send message + * 1. validate message type + * 2. select queue + * 3. set message id if not set + * 4. call message service + * 5. fill transaction data if send succeed and is transaction message + */ public CompletableFuture> sendMessage(ProxyContext ctx, QueueSelector queueSelector, String producerGroup, int sysFlag, List messageList, long timeoutMillis) { CompletableFuture> future = new CompletableFuture<>(); From d567caf16f34e71ed559993215766f5275b7d70e Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Sun, 16 Nov 2025 22:27:04 +0800 Subject: [PATCH 006/157] comment: add comments to method sendMessage of MessageService --- .../org/apache/rocketmq/proxy/processor/ProducerProcessor.java | 1 + .../apache/rocketmq/proxy/service/message/MessageService.java | 3 +++ 2 files changed, 4 insertions(+) diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/processor/ProducerProcessor.java b/proxy/src/main/java/org/apache/rocketmq/proxy/processor/ProducerProcessor.java index 26553568e58..d92fbf66e85 100644 --- a/proxy/src/main/java/org/apache/rocketmq/proxy/processor/ProducerProcessor.java +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/processor/ProducerProcessor.java @@ -104,6 +104,7 @@ public CompletableFuture> sendMessage(ProxyContext ctx, QueueSe SendMessageRequestHeader requestHeader = buildSendMessageRequestHeader(messageList, producerGroup, sysFlag, messageQueue.getQueueId()); AddressableMessageQueue finalMessageQueue = messageQueue; + // call SendMessageProcessor of broker future = this.serviceManager.getMessageService().sendMessage( ctx, messageQueue, diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/service/message/MessageService.java b/proxy/src/main/java/org/apache/rocketmq/proxy/service/message/MessageService.java index 80f5ae7217c..6ad3e3ed884 100644 --- a/proxy/src/main/java/org/apache/rocketmq/proxy/service/message/MessageService.java +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/service/message/MessageService.java @@ -46,6 +46,9 @@ public interface MessageService { + /** + * call SendMessageProcessor of broker + */ CompletableFuture> sendMessage( ProxyContext ctx, AddressableMessageQueue messageQueue, From 30aa8b545b3bed040688725f208427e6d170f1a8 Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Wed, 6 May 2026 19:00:12 +0800 Subject: [PATCH 007/157] comment: add comments of ack mode to BrokerConfig.popConsumerKVServiceEnable --- .../java/org/apache/rocketmq/common/BrokerConfig.java | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/common/src/main/java/org/apache/rocketmq/common/BrokerConfig.java b/common/src/main/java/org/apache/rocketmq/common/BrokerConfig.java index 08e27a20ee3..a9892253bf0 100644 --- a/common/src/main/java/org/apache/rocketmq/common/BrokerConfig.java +++ b/common/src/main/java/org/apache/rocketmq/common/BrokerConfig.java @@ -243,6 +243,11 @@ public class BrokerConfig extends BrokerIdentity { private int popFromRetryProbabilityForPriority = 0; // 0 as the lowest priority if true private boolean priorityOrderAsc = true; + /** + * There are two types of ack mode: + * 1. ack by file system service, which is the default mode. + * 2. ack by key-value service, when popConsumerKVServiceEnable and popConsumerKVServiceInit are both true. + */ private boolean popConsumerFSServiceInit = true; private boolean popConsumerKVServiceLog = false; private boolean popConsumerKVServiceInit = false; @@ -463,7 +468,7 @@ public class BrokerConfig extends BrokerIdentity { private boolean usePIDColdCtrStrategy = true; private long cgColdReadThreshold = 3 * 1024 * 1024; private long globalColdReadThreshold = 100 * 1024 * 1024; - + /** * The interval to fetch namesrv addr, default value is 10 second */ @@ -2104,11 +2109,11 @@ public boolean isUseStaticSubscription() { public void setUseStaticSubscription(boolean useStaticSubscription) { this.useStaticSubscription = useStaticSubscription; } - + public long getFetchNamesrvAddrInterval() { return fetchNamesrvAddrInterval; } - + public void setFetchNamesrvAddrInterval(final long fetchNamesrvAddrInterval) { this.fetchNamesrvAddrInterval = fetchNamesrvAddrInterval; } From a75c7bae5b739e4eeec127c2b1b74d033c27ec4e Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Thu, 14 May 2026 14:17:02 +0800 Subject: [PATCH 008/157] comment: add class comments to SelectMappedBufferResult --- .../org/apache/rocketmq/store/SelectMappedBufferResult.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/store/src/main/java/org/apache/rocketmq/store/SelectMappedBufferResult.java b/store/src/main/java/org/apache/rocketmq/store/SelectMappedBufferResult.java index 5c38cfe92a9..b96dfd98882 100644 --- a/store/src/main/java/org/apache/rocketmq/store/SelectMappedBufferResult.java +++ b/store/src/main/java/org/apache/rocketmq/store/SelectMappedBufferResult.java @@ -19,6 +19,12 @@ import java.nio.ByteBuffer; import org.apache.rocketmq.store.logfile.MappedFile; +/** + * result while select mapped file + * - mapped file + * - offset and size + * - whether it is in memory + */ public class SelectMappedBufferResult { private final long startOffset; From 477cec21e160bfffdc72cdc4bc8330737ba5779c Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Thu, 14 May 2026 14:24:23 +0800 Subject: [PATCH 009/157] comment: add class comments to GetMessageResult --- .../org/apache/rocketmq/store/GetMessageResult.java | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/store/src/main/java/org/apache/rocketmq/store/GetMessageResult.java b/store/src/main/java/org/apache/rocketmq/store/GetMessageResult.java index 6f322a19e19..980f9a7bc89 100644 --- a/store/src/main/java/org/apache/rocketmq/store/GetMessageResult.java +++ b/store/src/main/java/org/apache/rocketmq/store/GetMessageResult.java @@ -21,15 +21,23 @@ import java.util.Collections; import java.util.List; +/** + * result of get message + */ public class GetMessageResult { - + // mappedFile info list private final List messageMapedList; + // message info list in form of ByteBuffer, used by zero copy in version 4.* private final List messageBufferList; + // consume queue offset list private final List messageQueueOffset; private GetMessageStatus status; + // next offset of queue(Consume Queue) private long nextBeginOffset; + // min offset of queue(Consume Queue) private long minOffset; + // max offset of queue(Consume Queue) private long maxOffset; private int bufferTotalSize = 0; From 3e81bb12e1bf5e643308a3405fef3a79a7b425b8 Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Sat, 16 May 2026 11:49:45 +0800 Subject: [PATCH 010/157] comment: add comments to method getMessage --- .../rocketmq/store/DefaultMessageStore.java | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/store/src/main/java/org/apache/rocketmq/store/DefaultMessageStore.java b/store/src/main/java/org/apache/rocketmq/store/DefaultMessageStore.java index aee767dae2f..cb8389111a0 100644 --- a/store/src/main/java/org/apache/rocketmq/store/DefaultMessageStore.java +++ b/store/src/main/java/org/apache/rocketmq/store/DefaultMessageStore.java @@ -900,19 +900,19 @@ public GetMessageResult getMessage(final String group, final String topic, final minOffset = consumeQueue.getMinOffsetInQueue(); maxOffset = consumeQueue.getMaxOffsetInQueue(); - if (maxOffset == 0) { + if (maxOffset == 0) { // empty queue status = GetMessageStatus.NO_MESSAGE_IN_QUEUE; nextBeginOffset = nextOffsetCorrection(offset, 0); - } else if (offset < minOffset) { + } else if (offset < minOffset) { // offset too small status = GetMessageStatus.OFFSET_TOO_SMALL; nextBeginOffset = nextOffsetCorrection(offset, minOffset); - } else if (offset == maxOffset) { + } else if (offset == maxOffset) { // offset overflow one status = GetMessageStatus.OFFSET_OVERFLOW_ONE; nextBeginOffset = nextOffsetCorrection(offset, offset); - } else if (offset > maxOffset) { + } else if (offset > maxOffset) { // offset too big status = GetMessageStatus.OFFSET_OVERFLOW_BADLY; nextBeginOffset = nextOffsetCorrection(offset, maxOffset); - } else { + } else { // offset is ok final int maxFilterMessageSize = Math.max(this.messageStoreConfig.getMaxFilterMessageSize(), maxMsgNums * consumeQueue.getUnitSize()); final boolean diskFallRecorded = this.messageStoreConfig.isDiskFallRecorded(); @@ -925,6 +925,14 @@ public GetMessageResult getMessage(final String group, final String topic, final long maxPhyOffsetPulling = 0; int cqFileNum = 0; + /* + * bufferTotalSize is the total message size + * bufferTotalSize less than 0 means + * the while loop will break after getting more than one messages + * + * travelCqFileNumWhenGetMessage limits the max file nums to travel when get message + * default is 1 + */ while (getResult.getBufferTotalSize() <= 0 && nextBeginOffset < maxOffset && cqFileNum++ < this.messageStoreConfig.getTravelCqFileNumWhenGetMessage()) { From c869be86f415165a129933c0ad3250ea137f8e84 Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Sat, 16 May 2026 16:12:09 +0800 Subject: [PATCH 011/157] comment: add comments to method receiveMessage of ReceiveMessageActivity --- .../proxy/grpc/v2/consumer/ReceiveMessageActivity.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/consumer/ReceiveMessageActivity.java b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/consumer/ReceiveMessageActivity.java index f5e1c7b76f3..771c05b7573 100644 --- a/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/consumer/ReceiveMessageActivity.java +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/consumer/ReceiveMessageActivity.java @@ -59,6 +59,16 @@ public ReceiveMessageActivity(MessagingProcessor messagingProcessor, super(messagingProcessor, grpcClientSettingsManager, grpcChannelManager); } + /** + * + * @param ctx ctx + * @param request + * request.invisible_duration => + * Required if client type is simple consumer. + * useless for PushConsumer + * + * @param responseObserver responseObserver + */ public void receiveMessage(ProxyContext ctx, ReceiveMessageRequest request, StreamObserver responseObserver) { ReceiveMessageResponseStreamWriter writer = createWriter(ctx, responseObserver); From ae075b33cf57283f15749bad42e965e76e826aef Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Sun, 17 May 2026 14:15:22 +0800 Subject: [PATCH 012/157] comment: add comments to the fields startOffsetInfo, msgOffsetInfo, and orderCountInfo --- .../apache/rocketmq/broker/processor/PopMessageProcessor.java | 3 +++ .../proxy/grpc/v2/consumer/ReceiveMessageActivity.java | 1 + 2 files changed, 4 insertions(+) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopMessageProcessor.java b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopMessageProcessor.java index c32e1b5ae23..4b4708d4d01 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopMessageProcessor.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopMessageProcessor.java @@ -503,8 +503,11 @@ public RemotingCommand processRequest(final ChannelHandlerContext ctx, RemotingC this.brokerController.getBrokerConfig().getReviveQueueNum()); } + // properties of rocketmq 4.x, useless in 5.x StringBuilder startOffsetInfo = new StringBuilder(64); + // properties of rocketmq 4.x, useless in 5.x StringBuilder msgOffsetInfo = new StringBuilder(64); + // properties of rocketmq 4.x, useless in 5.x StringBuilder orderCountInfo = requestHeader.isOrder() ? new StringBuilder(64) : null; // Due to the design of the fields startOffsetInfo, msgOffsetInfo, and orderCountInfo, diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/consumer/ReceiveMessageActivity.java b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/consumer/ReceiveMessageActivity.java index 771c05b7573..ed32e3d5e61 100644 --- a/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/consumer/ReceiveMessageActivity.java +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/consumer/ReceiveMessageActivity.java @@ -74,6 +74,7 @@ public void receiveMessage(ProxyContext ctx, ReceiveMessageRequest request, ReceiveMessageResponseStreamWriter writer = createWriter(ctx, responseObserver); try { + // Settings were registered when client connected Settings settings = this.grpcClientSettingsManager.getClientSettings(ctx); final boolean isLite = ClientType.LITE_PUSH_CONSUMER.equals(settings.getClientType()); From 031348e91400767b957094238f43c0c14b9a8684 Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Sun, 17 May 2026 14:22:17 +0800 Subject: [PATCH 013/157] comment: add comments of ack mode to method processRequest of PopMessageProcessor --- .../rocketmq/broker/processor/PopMessageProcessor.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopMessageProcessor.java b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopMessageProcessor.java index 4b4708d4d01..b049cfb7367 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopMessageProcessor.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopMessageProcessor.java @@ -382,6 +382,9 @@ public RemotingCommand processRequest(final ChannelHandlerContext ctx, RemotingC ExpressionMessageFilter finalMessageFilter = messageFilter; SubscriptionData finalSubscriptionData = subscriptionData; + // There are two type of ack mode: + // 1. ack by KV service + // 2. ack by file merge service if (brokerConfig.isPopConsumerKVServiceEnable()) { CompletableFuture popAsyncFuture = brokerController.getPopConsumerService().popAsync( @@ -492,8 +495,9 @@ public RemotingCommand processRequest(final ChannelHandlerContext ctx, RemotingC return response; }).thenAccept(result -> NettyRemotingAbstract.writeResponse(channel, request, result, null, brokerController.getBrokerMetricsManager().getRemotingMetricsManager())); return null; - } + } // end of ack by kv service + // start of ack by file merge service int randomQ = random.nextInt(100); int reviveQid; if (requestHeader.isOrder()) { From 7e490e9b438b3df62e6c7a771914d4656f6eb2cf Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Mon, 18 May 2026 15:13:59 +0800 Subject: [PATCH 014/157] comment: add process flow comments to method processRequest of PopMessageProcessor --- .../broker/processor/PopMessageProcessor.java | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopMessageProcessor.java b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopMessageProcessor.java index b049cfb7367..8049b4dc381 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopMessageProcessor.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopMessageProcessor.java @@ -226,9 +226,11 @@ public void notifyMessageArriving(final String topic, final int queueId, final S public RemotingCommand processRequest(final ChannelHandlerContext ctx, RemotingCommand request) throws RemotingCommandException { + // init request and response final long beginTimeMills = this.brokerController.getMessageStore().now(); Channel channel = ctx.channel(); + RemotingCommand response = RemotingCommand.createResponseCommand(PopMessageResponseHeader.class); response.setOpaque(request.getOpaque()); @@ -240,6 +242,7 @@ public RemotingCommand processRequest(final ChannelHandlerContext ctx, RemotingC } final PopMessageResponseHeader responseHeader = (PopMessageResponseHeader) response.readCustomHeader(); + // validation // Pop mode only supports consumption in cluster load balancing mode brokerController.getConsumerManager().compensateBasicConsumerInfo( requestHeader.getConsumerGroup(), ConsumeType.CONSUME_POP, MessageModel.CLUSTERING); @@ -319,6 +322,7 @@ public RemotingCommand processRequest(final ChannelHandlerContext ctx, RemotingC return response; } + // init filter BrokerConfig brokerConfig = brokerController.getBrokerConfig(); SubscriptionData subscriptionData = null; ExpressionMessageFilter messageFilter = null; @@ -497,7 +501,8 @@ public RemotingCommand processRequest(final ChannelHandlerContext ctx, RemotingC return null; } // end of ack by kv service - // start of ack by file merge service + // start of ack by file merge service mode + // init pop parameters int randomQ = random.nextInt(100); int reviveQid; if (requestHeader.isOrder()) { @@ -530,6 +535,8 @@ public RemotingCommand processRequest(final ChannelHandlerContext ctx, RemotingC } randomQ = usePriorityMode ? 0 : randomQ; // reset randomQ long popTime = System.currentTimeMillis(); + + // pop message CompletableFuture getMessageFuture = CompletableFuture.completedFuture(0L); if (needRetry && !requestHeader.isOrder()) { if (needRetryV1) { @@ -542,6 +549,7 @@ public RemotingCommand processRequest(final ChannelHandlerContext ctx, RemotingC popTime, finalMessageFilter, startOffsetInfo, msgOffsetInfo, orderCountInfo, randomQ, getMessageFuture); } } + if (requestHeader.getQueueId() < 0) { // read all queue getMessageFuture = popMsgFromTopic(topicConfig, false, getMessageResult, requestHeader, reviveQid, channel, @@ -553,6 +561,7 @@ public RemotingCommand processRequest(final ChannelHandlerContext ctx, RemotingC getMessageResult, requestHeader, queueId, restNum, reviveQid, channel, popTime, finalMessageFilter, startOffsetInfo, msgOffsetInfo, orderCountInfo)); } + // if not full , fetch retry again if (!needRetry && getMessageResult.getMessageMapedList().size() < requestHeader.getMaxMsgNums() && !requestHeader.isOrder()) { if (needRetryV1) { @@ -622,7 +631,7 @@ public RemotingCommand processRequest(final ChannelHandlerContext ctx, RemotingC requestHeader.getTopic(), requestHeader.getQueueId(), (int) (this.brokerController.getMessageStore().now() - beginTimeMills)); finalResponse.setBody(r); - } else { + } else { // zero copy final GetMessageResult tmpGetMessageResult = getMessageResult; try { FileRegion fileRegion = From f491213b6c01055a7358c874476d0d97038fee53 Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Mon, 18 May 2026 15:33:57 +0800 Subject: [PATCH 015/157] comment: add class comments to PopLongPollingService --- .../longpolling/PopLongPollingService.java | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/longpolling/PopLongPollingService.java b/broker/src/main/java/org/apache/rocketmq/broker/longpolling/PopLongPollingService.java index c595178d193..134887f9ad4 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/longpolling/PopLongPollingService.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/longpolling/PopLongPollingService.java @@ -52,6 +52,24 @@ import static org.apache.rocketmq.broker.longpolling.PollingResult.POLLING_SUC; import static org.apache.rocketmq.broker.longpolling.PollingResult.POLLING_TIMEOUT; +/** + * Pop-mode long polling service that suspends Pop requests and wakes them up when new messages arrive. + *

    + * Core responsibilities: + *

      + *
    • Suspend Pop requests — when the broker has no messages to return immediately, registers requests + * into the {@code pollingMap} keyed by {@code topic@cid@queueId} and waits
    • + *
    • Wake up on new message arrival — {@link #notifyMessageArriving} is triggered by the message arriving + * listener; it fetches matching Pop requests from the pollingMap, applies Tag filtering, and re-submits + * them to the PopMessageProcessor to return results to the client
    • + *
    • Timeout scanning — the background thread periodically scans the waiting queues and wakes up + * timed-out requests with an empty result
    • + *
    • Retry topic bridging — {@link #notifyMessageArrivingFromRetry} translates a new message on the retry + * topic into a wake-up notification on the original topic
    • + *
    • Resource cleanup — periodically removes stale polling entries for deleted topics or + * offline consumer groups
    • + *
    + */ public class PopLongPollingService extends ServiceThread { private static final Logger POP_LOGGER = From 80f3e62e25e3318ab2f21dacbcf44a3231d92197 Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Mon, 18 May 2026 17:16:32 +0800 Subject: [PATCH 016/157] comment: add version related comments to method processRequest of PopMessageProcessor --- .../rocketmq/broker/processor/PopMessageProcessor.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopMessageProcessor.java b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopMessageProcessor.java index 8049b4dc381..809343ddae6 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopMessageProcessor.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopMessageProcessor.java @@ -586,6 +586,7 @@ public RemotingCommand processRequest(final ChannelHandlerContext ctx, RemotingC POP_LOGGER.error("PopProcessor execute callback error", t); } + // long polling used in version 4.*, useless in 5.* if (!getMessageResult.getMessageBufferList().isEmpty()) { finalResponse.setCode(ResponseCode.SUCCESS); getMessageResult.setStatus(GetMessageStatus.FOUND); @@ -612,6 +613,7 @@ public RemotingCommand processRequest(final ChannelHandlerContext ctx, RemotingC } getMessageResult.setStatus(GetMessageStatus.NO_MESSAGE_IN_QUEUE); } + responseHeader.setInvisibleTime(requestHeader.getInvisibleTime()); responseHeader.setPopTime(popTime); responseHeader.setReviveQid(reviveQid); @@ -622,6 +624,9 @@ public RemotingCommand processRequest(final ChannelHandlerContext ctx, RemotingC responseHeader.setOrderCountInfo(orderCountInfo.toString()); } finalResponse.setRemark(getMessageResult.getStatus().name()); + + // transfer msg by heap or zero copy, + // zero copy used in 4.*, useless in 5.* switch (finalResponse.getCode()) { case ResponseCode.SUCCESS: if (this.brokerController.getBrokerConfig().isTransferMsgByHeap()) { From 92bffeaecf2a181559bd3e32953bb9dcc65128f0 Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Mon, 18 May 2026 19:24:53 +0800 Subject: [PATCH 017/157] comment: add method comments to method popMsgFromTopic of PopMessageProcessor --- .../broker/processor/PopMessageProcessor.java | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopMessageProcessor.java b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopMessageProcessor.java index 809343ddae6..21f28812bb1 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopMessageProcessor.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopMessageProcessor.java @@ -575,6 +575,7 @@ public RemotingCommand processRequest(final ChannelHandlerContext ctx, RemotingC } } + // async result handle final RemotingCommand finalResponse = response; getMessageFuture.thenApply(restNum -> { try { @@ -673,6 +674,37 @@ public RemotingCommand processRequest(final ChannelHandlerContext ctx, RemotingC return null; } + /** + * Pop messages from every read queue of the given topic. + * + *

    Queues are visited sequentially (respecting {@code priorityOrderAsc}). + * For each queue a {@link #popMsgFromQueue} call is chained via + * {@code CompletableFuture#thenCompose}. The chained future carries the + * remaining number of messages still needed ({@code restNum}). + * + *

    Early termination can occur inside {@link #popMsgFromQueue} when: + *

      + *
    • the queue lock cannot be acquired
    • + *
    • too many in-flight (un-acked) messages exist
    • + *
    • an order queue is blocked
    • + *
    • the accumulated message count already reaches {@code maxMsgNums}
    • + *
    + * + * @param topicConfig topic configuration; {@code null} skips all queues + * @param isRetry whether the topic is a retry topic + * @param getMessageResult accumulator for the messages popped so far + * @param requestHeader pop request parameters + * @param reviveQid revive queue id + * @param channel netty channel of the requesting client + * @param popTime pop timestamp + * @param messageFilter expression filter applied to each message + * @param startOffsetInfo buffer for offset tracing info + * @param msgOffsetInfo buffer for per-message offset tracing info + * @param orderCountInfo buffer for order-consume count info + * @param randomQ random queue offset for round-robin load balancing + * @param getMessageFuture future that carries the remaining message count + * @return a future completing with the remaining number of messages needed + */ private CompletableFuture popMsgFromTopic(TopicConfig topicConfig, boolean isRetry, GetMessageResult getMessageResult, PopMessageRequestHeader requestHeader, int reviveQid, Channel channel, long popTime, ExpressionMessageFilter messageFilter, StringBuilder startOffsetInfo, From c43e83e89a1b50b87da4ed7b57f9f2b15796f4fd Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Mon, 18 May 2026 20:00:44 +0800 Subject: [PATCH 018/157] comment: add process flow comments to method popMsgFromQueue of PopMessageProcessor --- .../broker/processor/PopMessageProcessor.java | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopMessageProcessor.java b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopMessageProcessor.java index 21f28812bb1..171a1bfd66b 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopMessageProcessor.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopMessageProcessor.java @@ -738,6 +738,7 @@ private CompletableFuture popMsgFromQueue(String topic, String attemptId, Channel channel, long popTime, ExpressionMessageFilter messageFilter, StringBuilder startOffsetInfo, StringBuilder msgOffsetInfo, StringBuilder orderCountInfo) { + // get pop offset String lockKey = topic + PopAckConstants.SPLIT + requestHeader.getConsumerGroup() + PopAckConstants.SPLIT + queueId; boolean isOrder = requestHeader.isOrder(); @@ -751,6 +752,7 @@ private CompletableFuture popMsgFromQueue(String topic, String attemptId, return failure; } + // try lock CompletableFuture future = new CompletableFuture<>(); if (!queueLockManager.tryLock(lockKey)) { try { @@ -763,8 +765,9 @@ private CompletableFuture popMsgFromQueue(String topic, String attemptId, } return future; } - future.whenComplete((result, throwable) -> queueLockManager.unLock(lockKey)); + + // check inflight message number if (isPopShouldStop(topic, requestHeader.getConsumerGroup(), queueId)) { POP_LOGGER.warn("Too much msgs unacked, then stop popping. topic={}, group={}, queueId={}", topic, requestHeader.getConsumerGroup(), queueId); @@ -777,6 +780,7 @@ private CompletableFuture popMsgFromQueue(String topic, String attemptId, return future; } + // check orderly lock and max message number try { offset = getPopOffset(topic, requestHeader.getConsumerGroup(), queueId, requestHeader.getInitMode(), true, lockKey, true); @@ -817,6 +821,7 @@ private CompletableFuture popMsgFromQueue(String topic, String attemptId, return this.brokerController.getMessageStore() .getMessageAsync(requestHeader.getConsumerGroup(), topic, queueId, offset, requestHeader.getMaxMsgNums() - getMessageResult.getMessageMapedList().size(), messageFilter) + // result check and retry if offset is not correct .thenCompose(result -> { if (result == null) { return CompletableFuture.completedFuture(null); @@ -837,7 +842,9 @@ private CompletableFuture popMsgFromQueue(String topic, String attemptId, requestHeader.getMaxMsgNums() - getMessageResult.getMessageMapedList().size(), messageFilter); } return CompletableFuture.completedFuture(result); - }).thenApply(result -> { + }) + // update order info or append checkpoint then format result + .thenApply(result -> { if (result == null) { try { atomicRestNum.set(brokerController.getMessageStore().getMaxOffsetInQueue(topic, queueId) - atomicOffset.get() + atomicRestNum.get()); @@ -929,7 +936,9 @@ private CompletableFuture popMsgFromQueue(String topic, String attemptId, result.getMessageCount() ); return atomicRestNum.get(); - }).whenComplete((result, throwable) -> { + }) + // unlock queueLock + .whenComplete((result, throwable) -> { if (throwable != null) { POP_LOGGER.error("Pop message error, {}", lockKey, throwable); } From af61749246c87f391570301408f8fdb3b41d14b2 Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Mon, 18 May 2026 20:09:41 +0800 Subject: [PATCH 019/157] comment: add method comments to method popMsgFromQueue of PopMessageProcessor --- .../broker/processor/PopMessageProcessor.java | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopMessageProcessor.java b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopMessageProcessor.java index 171a1bfd66b..269914580bb 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopMessageProcessor.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopMessageProcessor.java @@ -732,6 +732,45 @@ private CompletableFuture popMsgFromTopic(String topic, boolean isRetry, G messageFilter, startOffsetInfo, msgOffsetInfo, orderCountInfo, randomQ, getMessageFuture); } + /** + * Pop messages from a specific queue of a topic. + * + *

    This method is called as a step in a {@link CompletableFuture} chain + * (see {@link #popMsgFromTopic}). The {@code restNum} argument is the + * number of messages still needed — when it drops to {@code 0} or below, + * subsequent calls in the chain may short-circuit early. + * + *

    The method has several early-termination paths (all return + * immediately with the current {@code restNum}): + *

      + *
    • Queue lock cannot be acquired — skips this queue
    • + *
    • Too many in-flight (un-acked) messages for this + * {@code topic@group@queueId}
    • + *
    • Order queue is blocked by a previous un-acked message
    • + *
    • Already accumulated {@code >= maxMsgNums} messages
    • + *
    + * + *

    Otherwise, it asynchronously fetches messages from the store, handles + * offset correction, updates order-consume tracking / checkpoint data, and + * merges the results into {@code getMessageResult}. + * + * @param topic topic name + * @param attemptId attempt id for idempotent consumption + * @param isRetry whether this is a retry topic + * @param getMessageResult accumulator for messages popped so far + * @param requestHeader pop request parameters + * @param queueId target queue id + * @param restNum number of messages still needed before the batch + * size is satisfied + * @param reviveQid revive queue id for checkpoint + * @param channel netty channel of the requesting client + * @param popTime pop invocation timestamp + * @param messageFilter expression filter applied to each message + * @param startOffsetInfo buffer for offset tracing info + * @param msgOffsetInfo buffer for per-message offset tracing info + * @param orderCountInfo buffer for order-consume count info + * @return a future completing with the remaining number of messages needed + */ private CompletableFuture popMsgFromQueue(String topic, String attemptId, boolean isRetry, GetMessageResult getMessageResult, PopMessageRequestHeader requestHeader, int queueId, long restNum, int reviveQid, From 314879f5bb407a6b93e24fc63508a48719a597e4 Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Mon, 18 May 2026 20:15:09 +0800 Subject: [PATCH 020/157] comment: add comments to method getPopOffset of PopMessageProcessor --- .../broker/processor/PopMessageProcessor.java | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopMessageProcessor.java b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopMessageProcessor.java index 269914580bb..1d760f23782 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopMessageProcessor.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopMessageProcessor.java @@ -990,6 +990,25 @@ private boolean isPopShouldStop(String topic, String group, int queueId) { brokerController.getPopInflightMessageCounter().getGroupPopInFlightMessageNum(topic, group, queueId) > brokerController.getBrokerConfig().getPopInflightMessageThreshold(); } + /** + * get consume offset for pop mode + * called by: + * - this.popMsgFromQueue() + * functionality: + * - return resetOffset if exists + * - get offset if exists + * - init offset if not exists + * - get offset from popBufferMergeService + * + * @param topic topic + * @param group group + * @param queueId queueId + * @param initMode initMode ConsumeInitMode.MAX for pop mode + * @param init flag of whether commit offset the first time pop message + * @param lockKey lockKey + * @param checkResetOffset flag of whether resetPopOffset + * @return offset + */ private long getPopOffset(String topic, String group, int queueId, int initMode, boolean init, String lockKey, boolean checkResetOffset) throws ConsumeQueueException { From 174e0ff40e57d905e03a344fe642c4aa0856553e Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Mon, 18 May 2026 20:31:28 +0800 Subject: [PATCH 021/157] comment: add comments to method getInitOffset of PopMessageProcessor --- .../rocketmq/broker/processor/PopMessageProcessor.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopMessageProcessor.java b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopMessageProcessor.java index 1d760f23782..7bdde5339f7 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopMessageProcessor.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopMessageProcessor.java @@ -1032,6 +1032,14 @@ private long getPopOffset(String topic, String group, int queueId, int initMode, } } + /** + * get offset from consume queue + * If consume from min offset: + * - return min offset. + * If consume from max offset: + * - get max offset + * - commit max offset if init is true. + */ public long getInitOffset(String topic, String group, int queueId, int initMode, boolean init) throws ConsumeQueueException { long offset; From c8cb465ad5974ebde253544c916e8219409f8900 Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Mon, 18 May 2026 20:34:45 +0800 Subject: [PATCH 022/157] comment: add process comments to method getInitOffset of PopMessageProcessor --- .../apache/rocketmq/broker/processor/PopMessageProcessor.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopMessageProcessor.java b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopMessageProcessor.java index 7bdde5339f7..d260951ea59 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopMessageProcessor.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopMessageProcessor.java @@ -1014,9 +1014,13 @@ private long getPopOffset(String topic, String group, int queueId, int initMode, long offset = this.brokerController.getConsumerOffsetManager().queryOffset(group, topic, queueId); if (offset < 0) { + //the first time consume, init offset by initMode offset = this.getInitOffset(topic, group, queueId, initMode, init); } + // before lock checkResetOffset is false + // after lock checkResetOffset is true + // This is an admin related feature if (checkResetOffset) { Long resetOffset = resetPopOffset(topic, group, queueId); if (resetOffset != null) { From 0cab192e38378e8412fc9482c8aff30c75ebea49 Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Tue, 19 May 2026 21:00:38 +0800 Subject: [PATCH 023/157] comment: add class comments to PopBufferMergeService --- .../processor/PopBufferMergeService.java | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java index 5373eaea333..07128d0dfa3 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java @@ -44,8 +44,37 @@ import java.util.concurrent.LinkedBlockingDeque; import java.util.concurrent.atomic.AtomicInteger; +/** + * File based Ack buffer merge service. + * + *

    buffer checkpoint in memory then enqueue them into system revive queue then wait to be acked. + * + *

    Two in-memory data structures drive the merge logic: + *

      + *
    • {@link #buffer} — maps {@code mergeKey} to {@link PopCheckPointWrapper}, + * tracking which sub-messages within a CK batch have been acked + * (via {@code bits} bitmask) and which have been persisted + * (via {@code toStoreBits} bitmask)
    • + *
    • {@link #commitOffsets} — maps {@code topic@cid@queueId} to an ordered + * queue of {@link PopCheckPointWrapper}s for sequential offset committing
    • + *
    + * + *

    The background {@link #scan()} thread periodically evaluates each buffered CK: + *

      + *
    • All acks received — removes the CK from the buffer without writing + * anything to storage (clean completion)
    • + *
    • About to expire ({@code reviveTime - now < popCkStayBufferTimeOut}) + * or stayed too long — writes the CK and all un-persisted acks + * (or batch acks) to the revive topic
    • + *
    + * + *

    This service is enabled by {@code enablePopBufferMerge} and only runs on + * a master or a slave acting as master. When {@code enablePopBatchAck} is set, + * multiple ack offsets are packed into a single {@link BatchAckMsg}. + */ public class PopBufferMergeService extends ServiceThread { private static final Logger POP_LOGGER = LoggerFactory.getLogger(LoggerName.ROCKETMQ_POP_LOGGER_NAME); + // mergeKey: topic + group + queueId + startOffset + popTime + brokerName ConcurrentHashMap buffer = new ConcurrentHashMap<>(1024 * 16); ConcurrentHashMap> commitOffsets = From 4f612a9801b33e46be0e3112ca93cd31d0e174bf Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Thu, 21 May 2026 16:15:33 +0800 Subject: [PATCH 024/157] comment: add class comments and attribute comments to PopCheckPoint --- .../rocketmq/store/pop/PopCheckPoint.java | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/store/src/main/java/org/apache/rocketmq/store/pop/PopCheckPoint.java b/store/src/main/java/org/apache/rocketmq/store/pop/PopCheckPoint.java index 803ebc68957..bbd030650f1 100644 --- a/store/src/main/java/org/apache/rocketmq/store/pop/PopCheckPoint.java +++ b/store/src/main/java/org/apache/rocketmq/store/pop/PopCheckPoint.java @@ -21,22 +21,46 @@ import java.util.ArrayList; import java.util.List; +/** + * state check info for multi-messages pop from consume queue + */ public class PopCheckPoint implements Comparable { @JSONField(name = "so") private long startOffset; + /** + * pop time, which is the time when message is popped + * reviveTime = popTime + invisibleTime + */ @JSONField(name = "pt") private long popTime; + /** + * the invisible time of messages + * default is 60s, it can be changed by MQ client + */ @JSONField(name = "it") private long invisibleTime; + /** + * store ack states of messages + * one byte for each message + */ @JSONField(name = "bm") private int bitMap; + /** + * total number of messages + */ @JSONField(name = "n") private byte num; @JSONField(name = "q") private int queueId; @JSONField(name = "t") private String topic; + /** + * consumer group + */ private String cid; + /** + * revive offset, which is the consume queue offset of messageExt + */ @JSONField(name = "ro") private long reviveOffset; @JSONField(name = "d") From baef8762a9478829d7d16de4f890a5418e886fc1 Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Sat, 23 May 2026 15:19:16 +0800 Subject: [PATCH 025/157] comment: add attribute comments to PopCheckPointWrapper --- .../broker/processor/PopBufferMergeService.java | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java index 07128d0dfa3..06340999f8d 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java @@ -466,10 +466,10 @@ private boolean checkQueueOk(PopCheckPointWrapper pointWrapper) { /** * put to store && add to buffer. * - * @param point - * @param reviveQueueId - * @param reviveQueueOffset - * @param nextBeginOffset + * @param point check point + * @param reviveQueueId revive queueId + * @param reviveQueueOffset revive queueOffset + * @param nextBeginOffset next offset * @return */ public boolean addCkJustOffset(PopCheckPoint point, int reviveQueueId, long reviveQueueOffset, @@ -874,14 +874,18 @@ public class PopCheckPointWrapper { // -1: not stored, >=0: stored, Long.MAX: storing. private volatile long reviveQueueOffset; private final PopCheckPoint ck; - // bit for concurrent + // bits for concurrent private final AtomicInteger bits; // bit for stored buffer ak private final AtomicInteger toStoreBits; + // nextOffset of original topic private final long nextBeginOffset; + // topic@group@queueId private final String lockKey; + // topic + group + queueId + startOffset + popTime + brokerName private final String mergeKey; private final boolean justOffset; + // whether check point has stored in revive queue private volatile boolean ckStored = false; public PopCheckPointWrapper(int reviveQueueId, long reviveQueueOffset, PopCheckPoint point, From dac6f4eb2351e40b7cd6bd5e621e50e84b31fd09 Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Sat, 23 May 2026 15:26:26 +0800 Subject: [PATCH 026/157] comment: add method comments to addCkMock of PopBufferMergeService --- .../rocketmq/broker/processor/PopBufferMergeService.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java index 06340999f8d..f0f70e7d999 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java @@ -494,6 +494,14 @@ public boolean addCkJustOffset(PopCheckPoint point, int reviveQueueId, long revi return true; } + /** + * mock checkpoint then add to buffer. + * this method is called when popped message is: + * - NO_MATCHED_MESSAGE + * - OFFSET_FOUND_NULL + * - MESSAGE_WAS_REMOVING + * - NO_MATCHED_LOGIC_QUEUE + */ public void addCkMock(String group, String topic, int queueId, long startOffset, long invisibleTime, long popTime, int reviveQueueId, long nextBeginOffset, String brokerName) { final PopCheckPoint ck = new PopCheckPoint(); From ffea2f42447f357e85d838378f50015ce32e3988 Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Sat, 23 May 2026 15:29:40 +0800 Subject: [PATCH 027/157] comment: add method comments to addCk of PopBufferMergeService --- .../rocketmq/broker/processor/PopBufferMergeService.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java index f0f70e7d999..876e7ccb29e 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java @@ -465,12 +465,13 @@ private boolean checkQueueOk(PopCheckPointWrapper pointWrapper) { /** * put to store && add to buffer. + * addAndStoreCheckpoint maybe a better name. * * @param point check point * @param reviveQueueId revive queueId * @param reviveQueueOffset revive queueOffset * @param nextBeginOffset next offset - * @return + * @return true if success */ public boolean addCkJustOffset(PopCheckPoint point, int reviveQueueId, long reviveQueueOffset, long nextBeginOffset) { @@ -524,6 +525,9 @@ public void addCkMock(String group, String topic, int queueId, long startOffset, } } + /** + * add checkpoint to buffer. + */ public boolean addCk(PopCheckPoint point, int reviveQueueId, long reviveQueueOffset, long nextBeginOffset) { // key: point.getT() + point.getC() + point.getQ() + point.getSo() + point.getPt() if (!brokerController.getBrokerConfig().isEnablePopBufferMerge()) { From 31a456dcc764ce93c9c8084e174c3ba6dd13f04c Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Sat, 23 May 2026 15:40:00 +0800 Subject: [PATCH 028/157] comment: add method comments to putCkToStore of PopBufferMergeService --- .../rocketmq/broker/processor/PopBufferMergeService.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java index 876e7ccb29e..5aca7d7aae2 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java @@ -649,6 +649,12 @@ public void clearOffsetQueue(String lockKey) { this.commitOffsets.remove(lockKey); } + /** + * write message(checkpoint) to revive topic, then update pointWrapper related info. + * + * @param pointWrapper checkpoint + * @param runInCurrent async or sync + */ private void putCkToStore(final PopCheckPointWrapper pointWrapper, final boolean runInCurrent) { if (pointWrapper.getReviveQueueOffset() >= 0) { return; @@ -658,6 +664,7 @@ private void putCkToStore(final PopCheckPointWrapper pointWrapper, final boolean // Indicates that ck message is storing pointWrapper.setReviveQueueOffset(Long.MAX_VALUE); + // default value of isAppendCkAsync is false if (brokerController.getBrokerConfig().isAppendCkAsync() && runInCurrent) { brokerController.getEscapeBridge().asyncPutMessageToSpecificQueue(msgInner).thenAccept(putMessageResult -> { handleCkMessagePutResult(putMessageResult, pointWrapper); From 81a3a140351896828dcbbff09e42acc35c767256 Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Sat, 23 May 2026 20:10:19 +0800 Subject: [PATCH 029/157] comment: add attribute comments to buffer and commitOffsets of PopBufferMergeService --- .../processor/PopBufferMergeService.java | 29 +++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java index 5aca7d7aae2..30df279410c 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java @@ -74,9 +74,26 @@ */ public class PopBufferMergeService extends ServiceThread { private static final Logger POP_LOGGER = LoggerFactory.getLogger(LoggerName.ROCKETMQ_POP_LOGGER_NAME); - // mergeKey: topic + group + queueId + startOffset + popTime + brokerName + /** + * In-memory cache of check points. + * Key: topic + group + queueId + startOffset + popTime + brokerName + * Value: check point wrapper + * use cases: + * - scan: iterate buffer + * - addAckMsg: get check point from buffer and mark ack state of Check Point + */ ConcurrentHashMap buffer = new ConcurrentHashMap<>(1024 * 16); + /** + * manager check point for given consumer and given queue + * Key: topic@cid@queueId + * Value: check point queue of specific consumer and queue + * use cases: + * - getLatestOffset: get consumer next start offset of given queue + * - scanGarbage + * - getOffsetTotalSize: get total popping num + * - isQueueFull + */ ConcurrentHashMap> commitOffsets = new ConcurrentHashMap<>(); private volatile boolean serving = true; @@ -444,6 +461,8 @@ private boolean commitOffset(final PopCheckPointWrapper wrapper) { private boolean putOffsetQueue(PopCheckPointWrapper pointWrapper) { QueueWithTime queue = this.commitOffsets.get(pointWrapper.getLockKey()); + + // init with empty queue if (queue == null) { queue = new QueueWithTime<>(); QueueWithTime old = this.commitOffsets.putIfAbsent(pointWrapper.getLockKey(), queue); @@ -451,6 +470,7 @@ private boolean putOffsetQueue(PopCheckPointWrapper pointWrapper) { queue = old; } } + queue.setTime(pointWrapper.getCk().getPopTime()); return queue.get().offer(pointWrapper); } @@ -484,6 +504,8 @@ public boolean addCkJustOffset(PopCheckPoint point, int reviveQueueId, long revi return false; } + // called before buffer operation + // because store operation will update attributes of pointWrapper this.putCkToStore(pointWrapper, checkQueueOk(pointWrapper)); putOffsetQueue(pointWrapper); @@ -496,7 +518,7 @@ public boolean addCkJustOffset(PopCheckPoint point, int reviveQueueId, long revi } /** - * mock checkpoint then add to buffer. + * mock checkpoint then add it to offset queue. * this method is called when popped message is: * - NO_MATCHED_MESSAGE * - OFFSET_FOUND_NULL @@ -505,6 +527,7 @@ public boolean addCkJustOffset(PopCheckPoint point, int reviveQueueId, long revi */ public void addCkMock(String group, String topic, int queueId, long startOffset, long invisibleTime, long popTime, int reviveQueueId, long nextBeginOffset, String brokerName) { + // create checkpoint final PopCheckPoint ck = new PopCheckPoint(); ck.setBitMap(0); ck.setNum((byte) 0); @@ -520,6 +543,7 @@ public void addCkMock(String group, String topic, int queueId, long startOffset, pointWrapper.setCkStored(true); putOffsetQueue(pointWrapper); + if (brokerController.getBrokerConfig().isEnablePopLog()) { POP_LOGGER.info("[PopBuffer]add ck just offset, mocked, {}", pointWrapper); } @@ -529,6 +553,7 @@ public void addCkMock(String group, String topic, int queueId, long startOffset, * add checkpoint to buffer. */ public boolean addCk(PopCheckPoint point, int reviveQueueId, long reviveQueueOffset, long nextBeginOffset) { + // validate env and checkpoint // key: point.getT() + point.getC() + point.getQ() + point.getSo() + point.getPt() if (!brokerController.getBrokerConfig().isEnablePopBufferMerge()) { return false; From e6a279f918cf61daa2ab9ee5e051eb746dd5d535 Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Sat, 23 May 2026 20:16:21 +0800 Subject: [PATCH 030/157] comment: add method comments to putOffsetQueue of PopBufferMergeService --- .../processor/PopBufferMergeService.java | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java index 30df279410c..72a462ab253 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java @@ -75,7 +75,7 @@ public class PopBufferMergeService extends ServiceThread { private static final Logger POP_LOGGER = LoggerFactory.getLogger(LoggerName.ROCKETMQ_POP_LOGGER_NAME); /** - * In-memory cache of check points. + * In-memory map of check points. * Key: topic + group + queueId + startOffset + popTime + brokerName * Value: check point wrapper * use cases: @@ -85,7 +85,7 @@ public class PopBufferMergeService extends ServiceThread { ConcurrentHashMap buffer = new ConcurrentHashMap<>(1024 * 16); /** - * manager check point for given consumer and given queue + * manage check point of given consumer and given queue * Key: topic@cid@queueId * Value: check point queue of specific consumer and queue * use cases: @@ -459,6 +459,21 @@ private boolean commitOffset(final PopCheckPointWrapper wrapper) { return true; } + /** + * Enqueue the checkpoint wrapper into the per-{@code topic@cid@queueId} offset queue + * for sequential offset committing. + * + *

    The queue is maintained in FIFO order. The {@link #scanCommitOffset()} method + * drains the queue from the head, ensuring that offsets are committed in the same + * order as the checkpoints were created, which prevents consumer offset regression. + * + *

    The {@link QueueWithTime#time} is also updated to the CK's pop time so that + * {@link #scanGarbage()} can identify and remove stale entries after 5 minutes of + * inactivity. + * + * @param pointWrapper the checkpoint wrapper to enqueue + * @return true if the element was added to the queue successfully + */ private boolean putOffsetQueue(PopCheckPointWrapper pointWrapper) { QueueWithTime queue = this.commitOffsets.get(pointWrapper.getLockKey()); From 6a247b522a753fd6d93724069675553755d3f925 Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Sat, 23 May 2026 21:35:53 +0800 Subject: [PATCH 031/157] comment: add process comments to method run of PopBufferMergeService --- .../rocketmq/broker/processor/PopBufferMergeService.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java index 72a462ab253..5beb8d5b3a0 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java @@ -138,6 +138,7 @@ public void run() { // scan while (!this.isStopped()) { try { + // env check if (!isShouldRunning()) { // slave this.waitForRunning(interval * 200 * 5); @@ -153,8 +154,8 @@ public void run() { scanGarbage(); } + // waiting this.waitForRunning(interval); - if (!this.serving && this.buffer.size() == 0 && getOffsetTotalSize() == 0) { this.serving = true; } @@ -164,6 +165,7 @@ public void run() { } } + // scan until buffer is empty this.serving = false; try { Thread.sleep(2000); From f5a0fc6c6af0ba192ff8f150ab5f3dbbdb23d41f Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Sat, 23 May 2026 21:42:38 +0800 Subject: [PATCH 032/157] comment: add method comments to scanGarbage of PopBufferMergeService --- .../broker/processor/PopBufferMergeService.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java index 5beb8d5b3a0..3a6b617b128 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java @@ -233,9 +233,20 @@ public long getLatestOffset(String topic, String group, int queueId) { return getLatestOffset(KeyBuilder.buildPollingKey(topic, group, queueId)); } + /** + * Remove stale entries from {@link #commitOffsets}. + * + *

    Three types of entries are removed: + *

      + *
    • Topic no longer exists (deleted)
    • + *
    • Consumer group no longer exists (unsubscribed)
    • + *
    • No activity for more than 5 minutes (idle)
    • + *
    + */ private void scanGarbage() { Iterator>> iterator = commitOffsets.entrySet().iterator(); while (iterator.hasNext()) { + // validate checkpoint Map.Entry> entry = iterator.next(); if (entry.getKey() == null) { continue; @@ -256,6 +267,7 @@ private void scanGarbage() { iterator.remove(); continue; } + if (System.currentTimeMillis() - entry.getValue().getTime() > minute5) { POP_LOGGER.info("[PopBuffer]remove long time not used sub {} of topic {} in buffer!", cid, topic); iterator.remove(); From a3ea07bcf1a6504fccad2cbc8547fbc104e9706b Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Sat, 23 May 2026 22:53:04 +0800 Subject: [PATCH 033/157] comment: add method comments to scan of PopBufferMergeService --- .../processor/PopBufferMergeService.java | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java index 3a6b617b128..2fc0f3d9727 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java @@ -151,6 +151,7 @@ public void run() { scan(); if (scanTimes % countOfSecond30 == 0) { + // remove checkpoint which are timeout scanGarbage(); } @@ -283,6 +284,26 @@ private boolean isSubscriptionGroupNotExist(PopCheckPointWrapper pointWrapper) { } + /** + * Scan and process all buffered checkpoints, then drain the offset commit queue. + * + *

    For each entry in {@link #buffer}: + *

      + *
    • Consumer group not found — removes the entry silently
    • + *
    • CK done (all sub-messages acked) — removes from buffer, no store write needed
    • + *
    • Just-offset entry — writes the CK to the revive topic if not yet stored
    • + *
    • Needs eviction (service stopped, revive timeout, or stay timeout) — + * writes the CK and all un-persisted acks (batch or individual) to the revive topic, + * then removes the entry when all persisted
    • + *
    • Otherwise — leaves the entry in the buffer for the next scan cycle
    • + *
    + * + *

    After processing the buffer, calls {@link #scanCommitOffset()} to commit offsets + * for finished checkpoints in FIFO order. + * + *

    If the scan duration exceeds {@code popCkStayBufferTimeOut - 1000ms}, the service + * temporarily stops accepting new CKs ({@link #serving} = false) to avoid backlog. + */ private void scan() { long startTime = System.currentTimeMillis(); AtomicInteger count = new AtomicInteger(0); @@ -319,6 +340,7 @@ private void scan() { PopCheckPoint point = pointWrapper.getCk(); long now = System.currentTimeMillis(); + // check whether check point is timeout boolean removeCk = !this.serving; // ck will be timeout if (point.getReviveTime() - now < brokerController.getBrokerConfig().getPopCkStayBufferTimeOut()) { From 848f23f5a1f5b10f0f031f014b7bdd8d9e856a13 Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Tue, 26 May 2026 10:12:00 +0800 Subject: [PATCH 034/157] comment: add process comments to method addAk of PopBufferMergeService --- .../processor/PopBufferMergeService.java | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java index 2fc0f3d9727..e50f7327899 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java @@ -325,7 +325,6 @@ private void scan() { continue; } - // just process offset(already stored at pull thread), or buffer ck(not stored and ack finish) if (pointWrapper.isJustOffset() && pointWrapper.isCkStored() || isCkDone(pointWrapper) || isCkDoneForFinish(pointWrapper) && pointWrapper.isCkStored()) { @@ -357,16 +356,16 @@ private void scan() { } // double check - if (isCkDone(pointWrapper)) { + if (isCkDone(pointWrapper)) { // ck done, do nothing continue; - } else if (pointWrapper.isJustOffset()) { + } else if (pointWrapper.isJustOffset()) { // store check point // just offset should be in store. if (pointWrapper.getReviveQueueOffset() < 0) { putCkToStore(pointWrapper, this.brokerController.getBrokerConfig().isAppendCkAsync()); countCk++; } continue; - } else if (removeCk) { + } else if (removeCk) { // store or merge ack info // put buffer ak to store if (pointWrapper.getReviveQueueOffset() < 0) { putCkToStore(pointWrapper, this.brokerController.getBrokerConfig().isAppendCkAsync()); @@ -377,11 +376,12 @@ private void scan() { continue; } - if (brokerController.getBrokerConfig().isEnablePopBatchAck()) { + if (brokerController.getBrokerConfig().isEnablePopBatchAck()) { // default is false List indexList = this.batchAckIndexList; try { for (byte i = 0; i < point.getNum(); i++) { // reput buffer ak to store + // if checkpoint is acked and not stored, add to indexList if (DataConverter.getBit(pointWrapper.getBits().get(), i) && !DataConverter.getBit(pointWrapper.getToStoreBits().get(), i)) { indexList.add(i); @@ -415,6 +415,7 @@ private void scan() { int offsetBufferSize = scanCommitOffset(); + // calculate scan times long eclipse = System.currentTimeMillis() - startTime; if (eclipse > brokerController.getBrokerConfig().getPopCkStayBufferTimeOut() - 1000) { POP_LOGGER.warn("[PopBuffer]scan stop, because eclipse too long, PopBufferEclipse={}, " + @@ -649,13 +650,16 @@ public boolean addCk(PopCheckPoint point, int reviveQueueId, long reviveQueueOff } public boolean addAk(int reviveQid, AckMsg ackMsg) { + // validate env if (!brokerController.getBrokerConfig().isEnablePopBufferMerge()) { return false; } if (!serving) { return false; } + try { + // get and validate checkpoint PopCheckPointWrapper pointWrapper = this.buffer.get(ackMsg.getTopic() + ackMsg.getConsumerGroup() + ackMsg.getQueueId() + ackMsg.getStartOffset() + ackMsg.getPopTime() + ackMsg.getBrokerName()); if (pointWrapper == null) { if (brokerController.getBrokerConfig().isEnablePopLog()) { @@ -685,7 +689,8 @@ public boolean addAk(int reviveQid, AckMsg ackMsg) { return false; } - if (ackMsg instanceof BatchAckMsg) { + // merge ackMsg with checkpoint + if (ackMsg instanceof BatchAckMsg) { // merge batch ackMsg for (Long ackOffset : ((BatchAckMsg) ackMsg).getAckOffsetList()) { int indexOfAck = point.indexOfAck(ackOffset); if (indexOfAck > -1) { @@ -694,7 +699,7 @@ public boolean addAk(int reviveQid, AckMsg ackMsg) { POP_LOGGER.error("[PopBuffer]Invalid index of ack, reviveQid={}, {}, {}", reviveQid, ackMsg, point); } } - } else { + } else { // merge ackMsg int indexOfAck = point.indexOfAck(ackMsg.getAckOffset()); if (indexOfAck > -1) { markBitCAS(pointWrapper.getBits(), indexOfAck); @@ -704,6 +709,7 @@ public boolean addAk(int reviveQid, AckMsg ackMsg) { } } + // logging if (brokerController.getBrokerConfig().isEnablePopLog()) { POP_LOGGER.info("[PopBuffer]add ack, rqId={}, {}, {}", reviveQid, pointWrapper, ackMsg); } From 00252be826e58656442eb156964bdfff346f9cd8 Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Tue, 26 May 2026 10:19:12 +0800 Subject: [PATCH 035/157] comment: add method comments to addAk of PopBufferMergeService --- .../processor/PopBufferMergeService.java | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java index e50f7327899..4fcbf1df47d 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java @@ -649,6 +649,28 @@ public boolean addCk(PopCheckPoint point, int reviveQueueId, long reviveQueueOff return true; } + /** + * Merge a consumer ack into the buffered checkpoint. + * + *

    The ack is not written to the revive topic immediately. Instead, a flag is + * set in {@link PopCheckPointWrapper#bits} via {@link #markBitCAS}. + * The pending ack will later be flushed to storage by {@link #scan()} when the + * checkpoint is evicted (timeout / buffer full / service stopping). + * + *

    Rejection conditions (return false): + *

      + *
    • {@code enablePopBufferMerge} is disabled
    • + *
    • The service is not serving (too busy)
    • + *
    • No matching checkpoint found in {@link #buffer}
    • + *
    • The checkpoint is a {@code justOffset} entry (no messages to ack)
    • + *
    • The checkpoint is too close to its revive deadline
    • + *
    • The checkpoint has been buffered for too long
    • + *
    + * + * @param reviveQid revive queue id (used only for logging) + * @param ackMsg the ack message from the consumer + * @return true if the ack was merged successfully + */ public boolean addAk(int reviveQid, AckMsg ackMsg) { // validate env if (!brokerController.getBrokerConfig().isEnablePopBufferMerge()) { From 801a041523120242db2a8df1cd959df3c1f96f2e Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Tue, 26 May 2026 11:01:33 +0800 Subject: [PATCH 036/157] comment: add attribute comments to queueOffsetDiff of PopCheckPoint --- .../apache/rocketmq/store/pop/PopCheckPoint.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/store/src/main/java/org/apache/rocketmq/store/pop/PopCheckPoint.java b/store/src/main/java/org/apache/rocketmq/store/pop/PopCheckPoint.java index bbd030650f1..2af92847604 100644 --- a/store/src/main/java/org/apache/rocketmq/store/pop/PopCheckPoint.java +++ b/store/src/main/java/org/apache/rocketmq/store/pop/PopCheckPoint.java @@ -63,6 +63,19 @@ public class PopCheckPoint implements Comparable { */ @JSONField(name = "ro") private long reviveOffset; + /** + * Per-message offset differences from {@link #startOffset}. + * + *

    When a batch of messages is popped, the queue offsets of the messages may not + * be contiguous (e.g. batch messages, ConsumeQueue compaction, filter mismatch gaps). + * This list records {@code actualQueueOffset - startOffset} for each message in the + * batch, so that the system can correctly map an ack offset back to its index within + * the checkpoint via {@link #indexOfAck}, and reconstruct the original offset via + * {@link #ackOffsetByIndex}. + * + *

    When this field is null or empty (old-version CK), offsets are assumed to be + * {@code startOffset + index}. + */ @JSONField(name = "d") private List queueOffsetDiff; @JSONField(name = "bn") From cd92c92740aea3de3a11f519504eab8e6ccf5dbd Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Tue, 26 May 2026 11:20:21 +0800 Subject: [PATCH 037/157] comment: add notes about queueOffsetDiff null scenario of PopCheckPoint --- .../java/org/apache/rocketmq/store/pop/PopCheckPoint.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/store/src/main/java/org/apache/rocketmq/store/pop/PopCheckPoint.java b/store/src/main/java/org/apache/rocketmq/store/pop/PopCheckPoint.java index 2af92847604..0bdb23c9adc 100644 --- a/store/src/main/java/org/apache/rocketmq/store/pop/PopCheckPoint.java +++ b/store/src/main/java/org/apache/rocketmq/store/pop/PopCheckPoint.java @@ -65,6 +65,7 @@ public class PopCheckPoint implements Comparable { private long reviveOffset; /** * Per-message offset differences from {@link #startOffset}. + * queueOffsetDiff will not be null or empty in 5.* * *

    When a batch of messages is popped, the queue offsets of the messages may not * be contiguous (e.g. batch messages, ConsumeQueue compaction, filter mismatch gaps). @@ -207,7 +208,7 @@ public int indexOfAck(long ackOffset) { return -1; } - // old version of checkpoint + // old version of checkpoint, this will not happen in 5.* if (queueOffsetDiff == null || queueOffsetDiff.isEmpty()) { if (ackOffset - startOffset < num) { @@ -222,7 +223,7 @@ public int indexOfAck(long ackOffset) { } public long ackOffsetByIndex(byte index) { - // old version of checkpoint + // old version of checkpoint, this will not happen in 5.* if (queueOffsetDiff == null || queueOffsetDiff.isEmpty()) { return startOffset + index; } From e3287d4bca45f3e4fcdd7dfd9b74acca316208eb Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Tue, 26 May 2026 19:42:29 +0800 Subject: [PATCH 038/157] comment: update attribute comments for bits and toStoreBits of PopCheckPointWrapper --- .../rocketmq/broker/processor/PopBufferMergeService.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java index 4fcbf1df47d..02cdece1a88 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java @@ -997,9 +997,9 @@ public class PopCheckPointWrapper { // -1: not stored, >=0: stored, Long.MAX: storing. private volatile long reviveQueueOffset; private final PopCheckPoint ck; - // bits for concurrent + // store ack states of messages, one byte for each message private final AtomicInteger bits; - // bit for stored buffer ak + // bits for stored buffer ak, one byte for each message private final AtomicInteger toStoreBits; // nextOffset of original topic private final long nextBeginOffset; From 107d6fb1166ad1890b960bdd580a9343a9680047 Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Tue, 26 May 2026 19:47:08 +0800 Subject: [PATCH 039/157] comment: add class and method comments to DataConverter --- .../rocketmq/common/utils/DataConverter.java | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/common/src/main/java/org/apache/rocketmq/common/utils/DataConverter.java b/common/src/main/java/org/apache/rocketmq/common/utils/DataConverter.java index cc96770b22a..474179d52f8 100644 --- a/common/src/main/java/org/apache/rocketmq/common/utils/DataConverter.java +++ b/common/src/main/java/org/apache/rocketmq/common/utils/DataConverter.java @@ -19,15 +19,33 @@ import java.nio.ByteBuffer; import java.nio.charset.Charset; +/** + * Bit-level utility methods, primarily used by Pop-mode ack tracking. + * + *

    An {@code int} bitmask is used to track the ack state of up to 32 sub-messages + * within a single Pop checkpoint (see {@code PopCheckPoint}). + */ public class DataConverter { public static final Charset CHARSET_UTF8 = Charset.forName("UTF-8"); + /** + * Convert a {@code long} to an 8-byte array (big-endian). + */ public static byte[] Long2Byte(Long v) { ByteBuffer tmp = ByteBuffer.allocate(8); tmp.putLong(v); return tmp.array(); } + /** + * Set or clear the bit at {@code index} in an int bitmask. + *

    Uses {@code 1L} (long literal) to avoid signed-int overflow when {@code index == 31}. + * + * @param value the original bitmask + * @param index the bit position (0-based, 0..31) + * @param flag {@code true} to set, {@code false} to clear + * @return the updated bitmask + */ public static int setBit(int value, int index, boolean flag) { if (flag) { return (int) (value | (1L << index)); @@ -36,6 +54,13 @@ public static int setBit(int value, int index, boolean flag) { } } + /** + * Test whether the bit at {@code index} is set in an int bitmask. + * + * @param value the bitmask + * @param index the bit position (0-based, 0..31) + * @return {@code true} if the bit is 1 + */ public static boolean getBit(int value, int index) { return (value & (1L << index)) != 0; } From 404019b34e7996fef24594f88f646107f4cc2553 Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Tue, 26 May 2026 19:49:56 +0800 Subject: [PATCH 040/157] comment: add method comments to markBitCAS of PopBufferMergeService --- .../rocketmq/broker/processor/PopBufferMergeService.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java index 02cdece1a88..6d48ca18970 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java @@ -453,6 +453,15 @@ public int getBufferedCKSize() { return this.counter.get(); } + /** + * Atomically set the bit at {@code index} in an {@link AtomicInteger} bitmask. + * + *

    Uses a CAS (compare-and-swap) loop to ensure thread safety without locking. + * If the bit is already set, this method returns immediately (no-op). + * + * @param setBits the atomic bitmask to update + * @param index the bit position (0-based) + */ private void markBitCAS(AtomicInteger setBits, int index) { while (true) { int bits = setBits.get(); From f49cbe4566e152f95c8fa17d888879ad94589375 Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Tue, 26 May 2026 19:52:43 +0800 Subject: [PATCH 041/157] comment: add method comments to indexOfAck of PopCheckPoint --- .../org/apache/rocketmq/store/pop/PopCheckPoint.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/store/src/main/java/org/apache/rocketmq/store/pop/PopCheckPoint.java b/store/src/main/java/org/apache/rocketmq/store/pop/PopCheckPoint.java index 0bdb23c9adc..1bfdf6c3d83 100644 --- a/store/src/main/java/org/apache/rocketmq/store/pop/PopCheckPoint.java +++ b/store/src/main/java/org/apache/rocketmq/store/pop/PopCheckPoint.java @@ -203,6 +203,17 @@ public void addDiff(int diff) { this.queueOffsetDiff.add(diff); } + /** + * Map an ack offset to its index within the checkpoint batch. + * + *

    The index is used to look up the corresponding bit in the {@link #bitMap} + * (or in {@code PopCheckPointWrapper.bits}) and to retrieve the original + * queue offset via {@link #ackOffsetByIndex}. + * + * @param ackOffset the queue offset being acked + * @return the sub-message index (0-based), or -1 if the offset is not found + * in this checkpoint + */ public int indexOfAck(long ackOffset) { if (ackOffset < startOffset) { return -1; From b98be603aec543918963e7c60721e21b324ca04a Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Tue, 26 May 2026 20:06:09 +0800 Subject: [PATCH 042/157] comment: add method comments to ackOffsetByIndex of PopCheckPoint --- .../java/org/apache/rocketmq/store/pop/PopCheckPoint.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/store/src/main/java/org/apache/rocketmq/store/pop/PopCheckPoint.java b/store/src/main/java/org/apache/rocketmq/store/pop/PopCheckPoint.java index 1bfdf6c3d83..e4ed5c085e8 100644 --- a/store/src/main/java/org/apache/rocketmq/store/pop/PopCheckPoint.java +++ b/store/src/main/java/org/apache/rocketmq/store/pop/PopCheckPoint.java @@ -233,6 +233,14 @@ public int indexOfAck(long ackOffset) { return queueOffsetDiff.indexOf((int) (ackOffset - startOffset)); } + /** + * get original queue offset by index. + * the method name is miss-leading, it should be getQueueOffsetByIndex. + * queueOffset = startOffset + queueOffsetDiff[index] + * + * @param index sub-message index within this checkpoint (0-based) + * @return the original queue offset in the consume queue + */ public long ackOffsetByIndex(byte index) { // old version of checkpoint, this will not happen in 5.* if (queueOffsetDiff == null || queueOffsetDiff.isEmpty()) { From 1049c19258d4414c6f9d649173ab7b1ffc181ffc Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Tue, 26 May 2026 20:38:47 +0800 Subject: [PATCH 043/157] comment: add inline comments to putAckToStore and scan of PopBufferMergeService --- .../rocketmq/broker/processor/PopBufferMergeService.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java index 6d48ca18970..b7be5635e5b 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java @@ -396,6 +396,7 @@ private void scan() { } else { for (byte i = 0; i < point.getNum(); i++) { // reput buffer ak to store + // if checkpoint is acked and not stored, call putAckToStore if (DataConverter.getBit(pointWrapper.getBits().get(), i) && !DataConverter.getBit(pointWrapper.getToStoreBits().get(), i)) { putAckToStore(pointWrapper, i, count); @@ -817,6 +818,7 @@ private void handleCkMessagePutResult(PutMessageResult putMessageResult, final P } private void putAckToStore(final PopCheckPointWrapper pointWrapper, byte msgIndex, AtomicInteger count) { + // build ackMsg and Message by checkpoint PopCheckPoint point = pointWrapper.getCk(); MessageExtBrokerInner msgInner = new MessageExtBrokerInner(); final AckMsg ackMsg = new AckMsg(); @@ -840,7 +842,8 @@ private void putAckToStore(final PopCheckPointWrapper pointWrapper, byte msgInde msgInner.setPropertiesString(MessageDecoder.messageProperties2String(msgInner.getProperties())); - if (brokerController.getBrokerConfig().isAppendAckAsync()) { + // store message then change store status of the checkpoint + if (brokerController.getBrokerConfig().isAppendAckAsync()) { // default value is false brokerController.getEscapeBridge().asyncPutMessageToSpecificQueue(msgInner).thenAccept(putMessageResult -> { handleAckPutMessageResult(ackMsg, putMessageResult, pointWrapper, count, msgIndex); }).exceptionally(throwable -> { @@ -848,7 +851,9 @@ private void putAckToStore(final PopCheckPointWrapper pointWrapper, byte msgInde return null; }); } else { + // store message PutMessageResult putMessageResult = brokerController.getEscapeBridge().putMessageToSpecificQueue(msgInner); + // change store status of the checkpoint handleAckPutMessageResult(ackMsg, putMessageResult, pointWrapper, count, msgIndex); } } From 6c561f5d9a7cf7cf78f195bae98aedec5bc5aab4 Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Tue, 26 May 2026 20:43:45 +0800 Subject: [PATCH 044/157] comment: add method comments to putAckToStore of PopBufferMergeService --- .../broker/processor/PopBufferMergeService.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java index b7be5635e5b..9dce73d48c8 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java @@ -817,6 +817,19 @@ private void handleCkMessagePutResult(PutMessageResult putMessageResult, final P } } + /** + * Persist message which created by checkpoint to the revive topic. + * + *

      + *
    • create message by checkpoint
    • + *
    • write message to revive topic
    • + *
    • update pointWrapper related info
    • + *
    + * + * @param pointWrapper the checkpoint wrapper containing the original CK + * @param msgIndex the sub-message index within the CK batch to ack + * @param count atomic counter incremented on successful persistence + */ private void putAckToStore(final PopCheckPointWrapper pointWrapper, byte msgIndex, AtomicInteger count) { // build ackMsg and Message by checkpoint PopCheckPoint point = pointWrapper.getCk(); From ca473541efe0a935d948a62c7b8ddadc3b1194ae Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Tue, 26 May 2026 20:45:47 +0800 Subject: [PATCH 045/157] comment: add method comments to handleAckPutMessageResult of PopBufferMergeService --- .../rocketmq/broker/processor/PopBufferMergeService.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java index 9dce73d48c8..f71be2dfe52 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java @@ -871,6 +871,15 @@ private void putAckToStore(final PopCheckPointWrapper pointWrapper, byte msgInde } } + /** + * update store status of checkpoint if revive message stored successfully. + * + * @param ackMsg the ack message that was persisted + * @param putMessageResult the result returned by the store + * @param pointWrapper the checkpoint wrapper being processed + * @param count atomic counter incremented on success + * @param msgIndex the sub-message index that was persisted + */ private void handleAckPutMessageResult(AckMsg ackMsg, PutMessageResult putMessageResult, PopCheckPointWrapper pointWrapper, AtomicInteger count, byte msgIndex) { brokerController.getBrokerMetricsManager().getPopMetricsManager().incPopReviveAckPutCount(ackMsg, putMessageResult.getPutMessageStatus()); From 2eb0e9074c8cdc4e9d9dea1605d04eb18cf5f0be Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Tue, 26 May 2026 22:18:46 +0800 Subject: [PATCH 046/157] comment: add attribute comments to justOffset of PopCheckPointWrapper --- .../broker/processor/PopBufferMergeService.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java index f71be2dfe52..491fa68e1d7 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java @@ -1043,6 +1043,20 @@ public class PopCheckPointWrapper { private final String lockKey; // topic + group + queueId + startOffset + popTime + brokerName private final String mergeKey; + /** + * Whether this checkpoint should be written to the revive topic directly. + * + *

    When {@code true}: + *

      + *
    • The CK has already been or will be written to the revive topic directly
    • + *
    • No Ack merging is needed — {@link #addAk} rejects these entries
    • + *
    • The wrapper exists solely to maintain FIFO offset commit order in + * {@link #commitOffsets}
    • + *
    + * + * @see PopBufferMergeService#addCkJustOffset + * @see PopBufferMergeService#addCkMock + */ private final boolean justOffset; // whether check point has stored in revive queue private volatile boolean ckStored = false; From 6c554aeeaecd48f752878e34845976e10415c5a0 Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Wed, 27 May 2026 07:02:57 +0800 Subject: [PATCH 047/157] comment: update attribute comments to reviveQueueOffset of PopCheckPointWrapper --- .../broker/processor/PopBufferMergeService.java | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java index 491fa68e1d7..09f19b4f138 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java @@ -1030,7 +1030,18 @@ public LinkedBlockingDeque get() { public class PopCheckPointWrapper { private final int reviveQueueId; - // -1: not stored, >=0: stored, Long.MAX: storing. + /** + * The consume queue offset of the CK message in the revive topic. + * + *

    Three-state indicator: + *

      + *
    • {@code -1} — not yet stored; {@link #putCkToStore} will write it
    • + *
    • {@code >= 0} — successfully stored; the value is the offset in the + * revive topic's consume queue
    • + *
    • {@link Long#MAX_VALUE} — a write is in progress (prevents duplicate + * writes from concurrent scans)
    • + *
    + */ private volatile long reviveQueueOffset; private final PopCheckPoint ck; // store ack states of messages, one byte for each message From d3d95875621ff9b52c47c193bb840869bebe9775 Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Wed, 27 May 2026 07:04:43 +0800 Subject: [PATCH 048/157] comment: add inline comment for reviveQueueOffset check in scan --- .../apache/rocketmq/broker/processor/PopBufferMergeService.java | 1 + 1 file changed, 1 insertion(+) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java index 09f19b4f138..0fa357979c6 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java @@ -367,6 +367,7 @@ private void scan() { continue; } else if (removeCk) { // store or merge ack info // put buffer ak to store + // revive queue offset < 0 means checkpoint was not stored if (pointWrapper.getReviveQueueOffset() < 0) { putCkToStore(pointWrapper, this.brokerController.getBrokerConfig().isAppendCkAsync()); countCk++; From a56679bc1960e39d8744c422c9c0957f74833ac7 Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Wed, 27 May 2026 07:06:55 +0800 Subject: [PATCH 049/157] comment: fix inline comments in scan --- .../rocketmq/broker/processor/PopBufferMergeService.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java index 0fa357979c6..bc856077f40 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java @@ -358,14 +358,14 @@ private void scan() { // double check if (isCkDone(pointWrapper)) { // ck done, do nothing continue; - } else if (pointWrapper.isJustOffset()) { // store check point + } else if (pointWrapper.isJustOffset()) { // store checkpoint // just offset should be in store. if (pointWrapper.getReviveQueueOffset() < 0) { putCkToStore(pointWrapper, this.brokerController.getBrokerConfig().isAppendCkAsync()); countCk++; } continue; - } else if (removeCk) { // store or merge ack info + } else if (removeCk) { // store checkpoint if needed // put buffer ak to store // revive queue offset < 0 means checkpoint was not stored if (pointWrapper.getReviveQueueOffset() < 0) { From fdfc788c21e30b2e7053b357e18ed034f4d640a1 Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Wed, 27 May 2026 07:08:17 +0800 Subject: [PATCH 050/157] comment: update inline comment for isCkDone check in scan --- .../apache/rocketmq/broker/processor/PopBufferMergeService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java index bc856077f40..b5959804bc7 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java @@ -356,7 +356,7 @@ private void scan() { } // double check - if (isCkDone(pointWrapper)) { // ck done, do nothing + if (isCkDone(pointWrapper)) { // all checkpoint are acked, do nothing continue; } else if (pointWrapper.isJustOffset()) { // store checkpoint // just offset should be in store. From d138c0c2c435ff9c51eb3dfc27fe625f35eee5f6 Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Wed, 27 May 2026 07:11:16 +0800 Subject: [PATCH 051/157] comment: add method comments to isCkDone of PopBufferMergeService --- .../broker/processor/PopBufferMergeService.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java index b5959804bc7..4ee7ec457f6 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java @@ -986,6 +986,17 @@ private boolean cancelCkTimer(final PopCheckPointWrapper pointWrapper) { return true; } + /** + * Check whether all sub-messages in the checkpoint have been acked. + * + *

    Every sub-message has a corresponding bit in + * {@link PopCheckPointWrapper#bits}. This method returns {@code true} when + * all bits are set, meaning the CK can be removed from the buffer without + * writing any ack to the revive topic (clean completion). + * + * @param pointWrapper the checkpoint wrapper to check + * @return {@code true} if every sub-message has been acked + */ private boolean isCkDone(PopCheckPointWrapper pointWrapper) { byte num = pointWrapper.getCk().getNum(); for (byte i = 0; i < num; i++) { From 1246674c0b9f16296331ba5c214c09dd4b8fb677 Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Wed, 27 May 2026 07:14:12 +0800 Subject: [PATCH 052/157] comment: add method comments to isCkDoneForFinish of PopBufferMergeService --- .../broker/processor/PopBufferMergeService.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java index 4ee7ec457f6..bb01f7685f5 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java @@ -1007,6 +1007,18 @@ private boolean isCkDone(PopCheckPointWrapper pointWrapper) { return true; } + /** + * Check whether all acked sub-messages have been fully persisted. + * + *

    Uses XOR: {@code bits ^ toStoreBits}. A bit is set in the result when + * the corresponding sub-message has been acked ({@code bits}) but not yet + * persisted ({@code toStoreBits}). Returns {@code true} only when every + * acked message has also been persisted, meaning the checkpoint is ready + * for final cleanup. + * + * @param pointWrapper the checkpoint wrapper to check + * @return {@code true} if no ack remains to be persisted + */ private boolean isCkDoneForFinish(PopCheckPointWrapper pointWrapper) { byte num = pointWrapper.getCk().getNum(); int bits = pointWrapper.getBits().get() ^ pointWrapper.getToStoreBits().get(); From 0f4a4cb25ee29e8df781a2851c8aeddab3a62ccd Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Wed, 27 May 2026 07:23:39 +0800 Subject: [PATCH 053/157] comment: add method comments to scanCommitOffset of PopBufferMergeService --- .../processor/PopBufferMergeService.java | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java index bb01f7685f5..9a51fe446e3 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java @@ -182,6 +182,26 @@ public void run() { } } + /** + * Drain the {@link #commitOffsets} queues and commit consumer offsets in FIFO order. + * + *

    For each {@code topic@cid@queueId} queue, the method peeks the head (oldest) + * wrapper and checks whether it is ready to commit: + *

      + *
    • Just-offset entry with CK stored
    • + *
    • All sub-messages acked ({@link #isCkDone})
    • + *
    • All acks persisted and CK stored ({@link #isCkDoneForFinish})
    • + *
    + * + *

    If the head is ready, it is committed and removed. Processing continues + * to the next wrapper in the same queue. If the head is not ready, the loop + * breaks — this ensures strict FIFO order and prevents consumer offset + * regression. + * + *

    Called at the end of {@link #scan()} after the buffer has been processed. + * + * @return the total number of remaining wrappers across all queues (for logging) + */ private int scanCommitOffset() { Iterator>> iterator = this.commitOffsets.entrySet().iterator(); int count = 0; From fbd9a5d44cd0df9727698b05b80384d558ffa751 Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Wed, 27 May 2026 07:28:59 +0800 Subject: [PATCH 054/157] comment: add naming note to scanCommitOffset --- .../apache/rocketmq/broker/processor/PopBufferMergeService.java | 1 + 1 file changed, 1 insertion(+) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java index 9a51fe446e3..64126798bbf 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java @@ -184,6 +184,7 @@ public void run() { /** * Drain the {@link #commitOffsets} queues and commit consumer offsets in FIFO order. + * scanAndCommitOffset may be a better name * *

    For each {@code topic@cid@queueId} queue, the method peeks the head (oldest) * wrapper and checks whether it is ready to commit: From 57b50896ce4140e0565dd22ca456ad89b325e25c Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Wed, 27 May 2026 07:29:54 +0800 Subject: [PATCH 055/157] comment: add method comments to commitOffset of PopBufferMergeService --- .../broker/processor/PopBufferMergeService.java | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java index 64126798bbf..36f6e5ae66c 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java @@ -499,6 +499,22 @@ private void markBitCAS(AtomicInteger setBits, int index) { } } + /** + * Commit the consumer offset for the checkpoint's {@code topic@cid@queueId}. + * + *

    Called from {@link #scanCommitOffset()} after the checkpoint is confirmed + * as finished (all acks received or CK stored). The offset is advanced to + * {@link PopCheckPointWrapper#nextBeginOffset}, which is the offset of the + * first message after this batch. + * + *

    The operation is guarded by {@link PopMessageProcessor.QueueLockManager} + * to prevent concurrent offset updates on the same queue. + * + * @param wrapper the finished checkpoint wrapper + * @return {@code true} if the offset was committed or no commit is needed + * ({@code nextBeginOffset < 0}); {@code false} if the lock could + * not be acquired (caller should retry later) + */ private boolean commitOffset(final PopCheckPointWrapper wrapper) { if (wrapper.getNextBeginOffset() < 0) { return true; From 5278b4b36f578634fc99a4d0346a0ea20ad0f20a Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Wed, 27 May 2026 07:31:28 +0800 Subject: [PATCH 056/157] comment: add inline comment for scanCommitOffset call in scan --- .../apache/rocketmq/broker/processor/PopBufferMergeService.java | 1 + 1 file changed, 1 insertion(+) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java index 36f6e5ae66c..c674cb7ef17 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java @@ -436,6 +436,7 @@ private void scan() { } } + // scan commitOffsets and commit offset which is needed. int offsetBufferSize = scanCommitOffset(); // calculate scan times From 224815b44643e9df1b6943bb7324c02339ba9eb6 Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Wed, 27 May 2026 08:51:27 +0800 Subject: [PATCH 057/157] comment: add inline comments for store and remove in scan --- .../apache/rocketmq/broker/processor/PopBufferMergeService.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java index c674cb7ef17..5a745b2a355 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java @@ -398,6 +398,7 @@ private void scan() { continue; } + // store checkpoint if (brokerController.getBrokerConfig().isEnablePopBatchAck()) { // default is false List indexList = this.batchAckIndexList; try { @@ -426,6 +427,7 @@ private void scan() { } } + // remove checkpoint from buffer if (isCkDoneForFinish(pointWrapper) && pointWrapper.isCkStored()) { if (brokerController.getBrokerConfig().isEnablePopLog()) { POP_LOGGER.info("[PopBuffer]ck finish, {}", pointWrapper); From 26c908dada7f127e0152976b4c23a54977985b57 Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Wed, 27 May 2026 08:57:42 +0800 Subject: [PATCH 058/157] comment: add in-line comments to method scanGarbage of PopBufferMergeService --- .../rocketmq/broker/processor/PopBufferMergeService.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java index 5a745b2a355..b17c1c7e410 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java @@ -279,17 +279,23 @@ private void scanGarbage() { } String topic = keyArray[0]; String cid = keyArray[1]; + + // remove if topic no longer exists if (brokerController.getTopicConfigManager().selectTopicConfig(topic) == null) { POP_LOGGER.info("[PopBuffer]remove nonexistent topic {} in buffer!", topic); iterator.remove(); continue; } + + // remove if subscription group no longer exists if (!brokerController.getSubscriptionGroupManager().containsSubscriptionGroup(cid)) { POP_LOGGER.info("[PopBuffer]remove nonexistent subscription group {} of topic {} in buffer!", cid, topic); iterator.remove(); continue; } + // remove if idle + // entry.getValue().getTime() = popTime of last checkpoint enqueued in the queue if (System.currentTimeMillis() - entry.getValue().getTime() > minute5) { POP_LOGGER.info("[PopBuffer]remove long time not used sub {} of topic {} in buffer!", cid, topic); iterator.remove(); From 05e2f4d6d003a381c34df4b79324a789bcebc36e Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Wed, 27 May 2026 09:00:36 +0800 Subject: [PATCH 059/157] comment: add class comments to PopReviveService --- .../broker/processor/PopReviveService.java | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopReviveService.java b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopReviveService.java index 07f16e98965..978b85b06eb 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopReviveService.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopReviveService.java @@ -64,6 +64,23 @@ import static org.apache.rocketmq.broker.metrics.BrokerMetricsConstant.LABEL_IS_SYSTEM; import static org.apache.rocketmq.broker.metrics.BrokerMetricsConstant.LABEL_TOPIC; +/** + * Per-queue service that reads the revive topic, matches checkpoints with AckMsgs, and + * revives timed-out messages by re-publishing them to the retry topic. + * + *

    Each revive queue has its own dedicated {@code PopReviveService} instance. + * The service periodically: + *

      + *
    1. Scans the revive topic ({@link #consumeReviveMessage}) to collect CK + * (checkpoint) and Ack messages, merging Acks into CK's bitMap
    2. + *
    3. Processes expired CKs ({@link #mergeAndRevive}) by re-publishing any + * un-acked sub-messages back to the retry topic via {@link #reviveRetry}
    4. + *
    + * + *

    This is the file-based revive path (CK + Ack messages are stored in + * the system revive topic). It is complemented by the KVStore-based path in + * {@code PopConsumerService} which handles the {@code PopConsumerKVStore} flow. + */ public class PopReviveService extends ServiceThread { private static final Logger POP_LOGGER = LoggerFactory.getLogger(LoggerName.ROCKETMQ_POP_LOGGER_NAME); private final int[] ckRewriteIntervalsInSeconds = new int[] { 10, 20, 30, 60, 120, 180, 240, 300, 360, 420, 480, 540, 600, 1200, 1800, 3600, 7200 }; From 352e5691a18dc34a25a1c039389896cb973f3c08 Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Wed, 27 May 2026 13:30:52 +0800 Subject: [PATCH 060/157] comment: add note about public methods to PopReviveService --- .../org/apache/rocketmq/broker/processor/PopReviveService.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopReviveService.java b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopReviveService.java index 978b85b06eb..03464b12795 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopReviveService.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopReviveService.java @@ -68,6 +68,8 @@ * Per-queue service that reads the revive topic, matches checkpoints with AckMsgs, and * revives timed-out messages by re-publishing them to the retry topic. * + *

    There is only one public method for business: run

    + * *

    Each revive queue has its own dedicated {@code PopReviveService} instance. * The service periodically: *

      From 3ebb73454e0cadea341d84ecacbd92acbbce2360 Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Wed, 27 May 2026 13:39:36 +0800 Subject: [PATCH 061/157] comment: add in-line comments to method run of PopReviveService --- .../apache/rocketmq/broker/processor/PopReviveService.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopReviveService.java b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopReviveService.java index 03464b12795..2c6faf77797 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopReviveService.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopReviveService.java @@ -75,7 +75,7 @@ *
        *
      1. Scans the revive topic ({@link #consumeReviveMessage}) to collect CK * (checkpoint) and Ack messages, merging Acks into CK's bitMap
      2. - *
      3. Processes expired CKs ({@link #mergeAndRevive}) by re-publishing any + *
      4. Processes expired checkpoints ({@link #mergeAndRevive}) by re-publishing any * un-acked sub-messages back to the retry topic via {@link #reviveRetry}
      5. *
      * @@ -678,6 +678,7 @@ public void run() { int slow = 1; while (!this.isStopped()) { try { + // env check if (System.currentTimeMillis() < brokerController.getShouldStartTime()) { POP_LOGGER.info("PopReviveService Ready to run after {}", brokerController.getShouldStartTime()); this.waitForRunning(1000); @@ -695,6 +696,8 @@ public void run() { } POP_LOGGER.info("start revive topic={}, reviveQueueId={}", reviveTopic, queueId); + + // consume revive message ConsumeReviveObj consumeReviveObj = new ConsumeReviveObj(); consumeReviveMessage(consumeReviveObj); @@ -703,8 +706,10 @@ public void run() { continue; } + // merge checkpoint and ackMsg then revive mergeAndRevive(consumeReviveObj); + // wait and logging ArrayList sortList = consumeReviveObj.sortList; long delay = 0; if (sortList != null && !sortList.isEmpty()) { From cd64fa1a6e7a9726928b7648fe8b5f472fb3cf1e Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Wed, 27 May 2026 13:43:40 +0800 Subject: [PATCH 062/157] comment: add method comments to run of PopReviveService --- .../broker/processor/PopReviveService.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopReviveService.java b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopReviveService.java index 2c6faf77797..0969e41ae66 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopReviveService.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopReviveService.java @@ -673,6 +673,21 @@ public long getReviveBehindMessages() throws ConsumeQueueException { return Math.max(0, diff); } + /** + * Main loop: periodically consume revive messages and revive timed-out CKs. + * + *

      Each iteration: + *

        + *
      1. Waits for {@code reviveInterval} (configurable)
      2. + *
      3. Calls {@link #consumeReviveMessage} to scan the revive topic and + * merge checkpoints with their corresponding AckMsg
      4. + *
      5. Calls {@link #mergeAndRevive} to re-publish all un-acked + * sub-messages whose revive time has elapsed
      6. + *
      7. If no checkpoints were processed, increases a {@code slow} counter and + * sleeps longer — the idle interval ramps up to + * {@code reviveMaxSlow * reviveInterval}
      8. + *
      + */ @Override public void run() { int slow = 1; From 72f7c3632d3dbeb39044cb3206ccfbcbc959b7f2 Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Wed, 27 May 2026 13:49:56 +0800 Subject: [PATCH 063/157] comment: add method comments to consumeReviveMessage of PopReviveService --- .../broker/processor/PopReviveService.java | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopReviveService.java b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopReviveService.java index 0969e41ae66..bbe1217f634 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopReviveService.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopReviveService.java @@ -352,6 +352,37 @@ private List decodeMsgList(GetMessageResult getMessageResult, boolea return foundList; } + /** + * Pull Message from revive topic then transfer to checkpoint and ack messages. + * + *

      This method reads messages from the revive topic starting from the + * current offset. Each message is classified by its tag: + *

        + *
      • {@link PopAckConstants#CK_TAG} — a checkpoint, deserialized from + * JSON and stored in the map by its merge key
      • + *
      • {@link PopAckConstants#ACK_TAG} or + * {@link PopAckConstants#BATCH_ACK_TAG} — an ack, matched to its + * corresponding checkpoint via the merge key. The ack offset is translated + * to a sub-message index ({@link PopCheckPoint#indexOfAck}) and + * the checkpoint's bitMap is updated via {@link DataConverter#setBit}
      • + *
      + * + *

      AckMsg that arrive after their checkpoint has already been processed + * ({@code enableSkipLongAwaitingAck}) are handled by creating a mock CK + * via {@link #mockCkForAck} so that the revive offset can still be + * committed correctly. + * + *

      The scan stops when any of: + *

        + *
      • No more messages in the revive topic (tail reached)
      • + *
      • Scan time exceeds {@code reviveScanTime}
      • + *
      • The elapsed time since the first CK's revive time exceeds + * {@code ackTimeInterval + 1s}
      • + *
      + * + * @param consumeReviveObj the mutable container that receives the collected + * CKs and the computed {@code endTime} + */ protected void consumeReviveMessage(ConsumeReviveObj consumeReviveObj) { HashMap map = consumeReviveObj.map; HashMap mockPointMap = new HashMap<>(); From e85b23a5df33d8b540f1dd9958555a6a521bc3d5 Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Wed, 27 May 2026 13:55:17 +0800 Subject: [PATCH 064/157] comment: add in-line comments to method consumeReviveMessage of PopReviveService --- .../apache/rocketmq/broker/processor/PopReviveService.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopReviveService.java b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopReviveService.java index bbe1217f634..9fbcfd3bd5e 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopReviveService.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopReviveService.java @@ -384,6 +384,7 @@ private List decodeMsgList(GetMessageResult getMessageResult, boolea * CKs and the computed {@code endTime} */ protected void consumeReviveMessage(ConsumeReviveObj consumeReviveObj) { + // init context parameters HashMap map = consumeReviveObj.map; HashMap mockPointMap = new HashMap<>(); long startScanTime = System.currentTimeMillis(); @@ -396,11 +397,14 @@ protected void consumeReviveMessage(ConsumeReviveObj consumeReviveObj) { int noMsgCount = 0; long firstRt = 0; // offset self amend + while (true) { if (!shouldRunPopRevive) { POP_LOGGER.info("slave skip scan, revive topic={}, reviveQueueId={}", reviveTopic, queueId); break; } + + // pull revive messages List messageExts = getReviveMessage(offset, queueId); if (messageExts == null || messageExts.isEmpty()) { long old = endTime; @@ -429,10 +433,13 @@ protected void consumeReviveMessage(ConsumeReviveObj consumeReviveObj) { } else { noMsgCount = 0; } + if (System.currentTimeMillis() - startScanTime > brokerController.getBrokerConfig().getReviveScanTime()) { POP_LOGGER.info("reviveQueueId={}, scan timeout ", queueId); break; } + + // convert message to PopCheckPoint and AckMsg for (MessageExt messageExt : messageExts) { if (PopAckConstants.CK_TAG.equals(messageExt.getTags())) { String raw = new String(messageExt.getBody(), DataConverter.CHARSET_UTF8); From 6947ab3ae4137c77db336a79cfc99258077a0a26 Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Wed, 27 May 2026 14:02:53 +0800 Subject: [PATCH 065/157] comment: add method comments to getReviveMessage of PopReviveService --- .../rocketmq/broker/processor/PopReviveService.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopReviveService.java b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopReviveService.java index 9fbcfd3bd5e..5abdb30fd2a 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopReviveService.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopReviveService.java @@ -224,6 +224,17 @@ private int getRetryQueueId(String retryTopic, MessageExt messageExt) { return oriQueueId; } + /** + * Pull a batch of messages from the revive topic at the given offset. + * + *

      If the offset becomes illegal (e.g. the revive topic was truncated), + * the revive offset is corrected to {@code nextBeginOffset - 1} so that + * the next scan starts from a valid position. + * + * @param offset the queue offset to start reading from + * @param queueId the revive queue id + * @return a list of decoded messages, or {@code null} if at the tail + */ protected List getReviveMessage(long offset, int queueId) { PullResult pullResult = getMessage(PopAckConstants.REVIVE_GROUP, reviveTopic, queueId, offset, 32, true); if (pullResult == null) { From 4874a13a16d646c82429fad5e27f9542a41ff17b Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Wed, 27 May 2026 14:57:21 +0800 Subject: [PATCH 066/157] comment: add in-line comments to method consumeReviveMessage of PopReviveService --- .../apache/rocketmq/broker/processor/PopReviveService.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopReviveService.java b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopReviveService.java index 5abdb30fd2a..3b947c4f02a 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopReviveService.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopReviveService.java @@ -479,6 +479,7 @@ protected void consumeReviveMessage(ConsumeReviveObj consumeReviveObj) { String mergeKey = ackMsg.getTopic() + ackMsg.getConsumerGroup() + ackMsg.getQueueId() + ackMsg.getStartOffset() + ackMsg.getPopTime() + brokerName; PopCheckPoint point = map.get(mergeKey); if (point == null) { + // default value of enableSkipLongAwaitingAck is false if (!brokerController.getBrokerConfig().isEnableSkipLongAwaitingAck()) { continue; } @@ -486,6 +487,7 @@ protected void consumeReviveMessage(ConsumeReviveObj consumeReviveObj) { firstRt = mockPointMap.get(mergeKey).getReviveTime(); } } else { + // merge ackMsg into checkpoint int indexOfAck = point.indexOfAck(ackMsg.getAckOffset()); if (indexOfAck > -1) { point.setBitMap(DataConverter.setBit(point.getBitMap(), indexOfAck, true)); @@ -506,6 +508,7 @@ protected void consumeReviveMessage(ConsumeReviveObj consumeReviveObj) { String mergeKey = bAckMsg.getTopic() + bAckMsg.getConsumerGroup() + bAckMsg.getQueueId() + bAckMsg.getStartOffset() + bAckMsg.getPopTime() + brokerName; PopCheckPoint point = map.get(mergeKey); if (point == null) { + // default value of enableSkipLongAwaitingAck is false if (!brokerController.getBrokerConfig().isEnableSkipLongAwaitingAck()) { continue; } @@ -513,6 +516,7 @@ protected void consumeReviveMessage(ConsumeReviveObj consumeReviveObj) { firstRt = mockPointMap.get(mergeKey).getReviveTime(); } } else { + // merge ackMsgs into checkpoint List ackOffsetList = bAckMsg.getAckOffsetList(); for (Long ackOffset : ackOffsetList) { int indexOfAck = point.indexOfAck(ackOffset); From a0246597ebb7b52d5e23fcf87cb814c324d975ed Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Wed, 27 May 2026 15:01:08 +0800 Subject: [PATCH 067/157] comment: add method comments to mockCkForAck and createMockCkForAck of PopReviveService --- .../broker/processor/PopReviveService.java | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopReviveService.java b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopReviveService.java index 3b947c4f02a..6795cf371cc 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopReviveService.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopReviveService.java @@ -539,6 +539,20 @@ protected void consumeReviveMessage(ConsumeReviveObj consumeReviveObj) { consumeReviveObj.endTime = endTime; } + /** + * Create a mock CK for an ack whose original CK has already been processed. + * + *

      When an ack arrives long after its CK has been consumed (e.g. network + * delay), the CK is no longer in the scan map. If {@code enableSkipLongAwaitingAck} + * is enabled, this method creates a synthetic CK so that the revive offset + * can still be advanced correctly in {@link #mergeAndRevive}. + * + * @param messageExt the revive topic message that carried the ack + * @param ackMsg the decoded ack + * @param mergeKey the merge key for the CK lookup + * @param mockPointMap map to collect the mock CKs + * @return {@code true} if a mock CK was created + */ private boolean mockCkForAck(MessageExt messageExt, AckMsg ackMsg, String mergeKey, HashMap mockPointMap) { long ackWaitTime = System.currentTimeMillis() - messageExt.getDeliverTimeMs(); long reviveAckWaitMs = brokerController.getBrokerConfig().getReviveAckWaitMs(); @@ -554,6 +568,17 @@ private boolean mockCkForAck(MessageExt messageExt, AckMsg ackMsg, String mergeK return false; } + /** + * Build a synthetic checkpoint from an ack message. + * + *

      The mock CK has {@code num = 0} and empty bitMap, meaning no actual + * messages to revive. Its only purpose is to carry the {@code reviveOffset} + * so that the revive consumer offset can be committed past this ack. + * + * @param ackMsg the ack message + * @param reviveOffset the queue offset of the ack message in the revive topic + * @return a mock checkpoint with no sub-messages + */ private PopCheckPoint createMockCkForAck(AckMsg ackMsg, long reviveOffset) { PopCheckPoint point = new PopCheckPoint(); point.setStartOffset(ackMsg.getStartOffset()); From 0e682f3b0a8ffce3821e243bd74dc00765327ea3 Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Wed, 27 May 2026 15:08:07 +0800 Subject: [PATCH 068/157] comment: add method comments to mergeAndRevive of PopReviveService --- .../broker/processor/PopReviveService.java | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopReviveService.java b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopReviveService.java index 6795cf371cc..0899b401bc8 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopReviveService.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopReviveService.java @@ -593,6 +593,24 @@ private PopCheckPoint createMockCkForAck(AckMsg ackMsg, long reviveOffset) { return point; } + /** + * Process collected checkpoints and revive all un-acked sub-messages. + * + *

      Checkpoints are sorted by revive offset. For each one: + *

        + *
      • Skip if the revive time has not yet elapsed (within + * {@code ackTimeInterval + 1s} of {@code endTime})
      • + *
      • Skip if the normal topic or consumer group no longer exists
      • + *
      • Wait if too many revives are already in-flight (max 3)
      • + *
      • Call {@link #reviveMsgFromCk} to re-publish un-acked messages
      • + *
      + * + *

      After processing, the revive topic offset is advanced past all + * processed checkpoints. + * + * @param consumeReviveObj the container with collected CKs and scan state + * @throws Throwable if any revive operation fails + */ protected void mergeAndRevive(ConsumeReviveObj consumeReviveObj) throws Throwable { ArrayList sortList = consumeReviveObj.genSortList(); POP_LOGGER.info("reviveQueueId={}, ck listSize={}", queueId, sortList.size()); From 8fbe4225fd8ef293036539e878dd947f81151bf7 Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Wed, 27 May 2026 15:18:07 +0800 Subject: [PATCH 069/157] comment: add in-line comments to method mergeAndRevive of PopReviveService --- .../apache/rocketmq/broker/processor/PopReviveService.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopReviveService.java b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopReviveService.java index 0899b401bc8..1720c3b63b7 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopReviveService.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopReviveService.java @@ -612,6 +612,7 @@ private PopCheckPoint createMockCkForAck(AckMsg ackMsg, long reviveOffset) { * @throws Throwable if any revive operation fails */ protected void mergeAndRevive(ConsumeReviveObj consumeReviveObj) throws Throwable { + // sort checkpoints and init newOffset ArrayList sortList = consumeReviveObj.genSortList(); POP_LOGGER.info("reviveQueueId={}, ck listSize={}", queueId, sortList.size()); if (sortList.size() != 0) { @@ -619,6 +620,7 @@ protected void mergeAndRevive(ConsumeReviveObj consumeReviveObj) throws Throwabl sortList.get(0).getReviveOffset(), sortList.get(sortList.size() - 1).getStartOffset(), sortList.get(sortList.size() - 1).getReviveOffset()); } long newOffset = consumeReviveObj.oldOffset; + for (PopCheckPoint popCheckPoint : sortList) { if (!shouldRunPopRevive) { POP_LOGGER.info("slave skip ck process, revive topic={}, reviveQueueId={}", reviveTopic, queueId); @@ -641,6 +643,7 @@ protected void mergeAndRevive(ConsumeReviveObj consumeReviveObj) throws Throwabl continue; } + // restore checkpoint while (inflightReviveRequestMap.size() > 3) { waitForRunning(100); Pair pair = inflightReviveRequestMap.firstEntry().getValue(); @@ -653,10 +656,12 @@ protected void mergeAndRevive(ConsumeReviveObj consumeReviveObj) throws Throwabl } } + // revive message reviveMsgFromCk(popCheckPoint); - newOffset = popCheckPoint.getReviveOffset(); } + + // commit offset if (newOffset > consumeReviveObj.oldOffset) { if (!shouldRunPopRevive) { POP_LOGGER.info("slave skip commit, revive topic={}, reviveQueueId={}", reviveTopic, queueId); From f163d0daa8df16f9e2d714fc2343ff8c4a103d3d Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Wed, 27 May 2026 15:23:55 +0800 Subject: [PATCH 070/157] comment: add method comments to rePutCK of PopReviveService --- .../broker/processor/PopReviveService.java | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopReviveService.java b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopReviveService.java index 1720c3b63b7..cf3428b4d36 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopReviveService.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopReviveService.java @@ -725,6 +725,24 @@ private void reviveMsgFromCk(PopCheckPoint popCheckPoint) { }); } + /** + * Re-write a checkpoint to the revive topic after a failed revive attempt. + * + *

      When a sub-message cannot be revived (e.g. the original message is + * temporarily unavailable), the CK is re-published with: + *

        + *
      • A single sub-message targeting the failed offset
      • + *
      • An increased {@code rePutTimes} and an extended invisible time + * based on the backoff interval
      • + *
      • A cleared bitMap, so the next revive cycle will retry it
      • + *
      + * + *

      If {@code rePutTimes} exceeds the backoff table length and + * {@code skipWhenCKRePutReachMaxTimes} is set, the CK is dropped. + * + * @param oldCK the original checkpoint that failed to revive + * @param pair the failed offset and result (object1 = offset, object2 = result) + */ private void rePutCK(PopCheckPoint oldCK, Pair pair) { int rePutTimes = oldCK.parseRePutTimes(); if (rePutTimes >= ckRewriteIntervalsInSeconds.length && brokerController.getBrokerConfig().isSkipWhenCKRePutReachMaxTimes()) { From fe4ff189e7ed2976491182581134c79a39710ab5 Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Wed, 27 May 2026 19:14:21 +0800 Subject: [PATCH 071/157] comment: add method comments to reviveMsgFromCk of PopReviveService --- .../broker/processor/PopReviveService.java | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopReviveService.java b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopReviveService.java index cf3428b4d36..578f3f90d28 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopReviveService.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopReviveService.java @@ -673,6 +673,25 @@ protected void mergeAndRevive(ConsumeReviveObj consumeReviveObj) throws Throwabl consumeReviveObj.newOffset = newOffset; } + /** + * Revive all un-acked sub-messages in a checkpoint: + * - reput message to revive topic + * - put message to retry topic + * + *

      For each sub-message whose bit is not set in the bitMap, the original + * message is fetched via {@link #getBizMessage} and re-published to the + * retry topic via {@link #reviveRetry}. All revive attempts run + * concurrently via {@link CompletableFuture#allOf}. + * + *

      After all attempts complete: + *

        + *
      • Failed offsets are re-queued via {@link #rePutCK}
      • + *
      • The {@link #inflightReviveRequestMap} is updated and completed + * entries are removed in order, advancing the revive offset
      • + *
      + * + * @param popCheckPoint the checkpoint whose un-acked messages should be revived + */ private void reviveMsgFromCk(PopCheckPoint popCheckPoint) { if (!shouldRunPopRevive) { POP_LOGGER.info("slave skip retry, revive topic={}, reviveQueueId={}", reviveTopic, queueId); From 4891cffc93c049cc97e761d5704a741bff0a45d7 Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Wed, 27 May 2026 19:52:34 +0800 Subject: [PATCH 072/157] comment: add in-line comments to method reviveMsgFromCk of PopReviveService --- .../rocketmq/broker/processor/PopReviveService.java | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopReviveService.java b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopReviveService.java index 578f3f90d28..e20747b920c 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopReviveService.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopReviveService.java @@ -693,21 +693,26 @@ protected void mergeAndRevive(ConsumeReviveObj consumeReviveObj) throws Throwabl * @param popCheckPoint the checkpoint whose un-acked messages should be revived */ private void reviveMsgFromCk(PopCheckPoint popCheckPoint) { + // env check and init if (!shouldRunPopRevive) { POP_LOGGER.info("slave skip retry, revive topic={}, reviveQueueId={}", reviveTopic, queueId); return; } inflightReviveRequestMap.put(popCheckPoint, new Pair<>(System.currentTimeMillis(), false)); List>> futureList = new ArrayList<>(popCheckPoint.getNum()); + + // put message to retry topic if checkpoint was not acked for (int j = 0; j < popCheckPoint.getNum(); j++) { + // if checkpoint was acked, skip if (DataConverter.getBit(popCheckPoint.getBitMap(), j)) { continue; } - // retry msg + // get message by checkpoint, then put message to retry topic long msgOffset = popCheckPoint.ackOffsetByIndex((byte) j); CompletableFuture> future = getBizMessage(popCheckPoint, msgOffset) .thenApply(rst -> { + // validate message MessageExt message = rst.getLeft(); if (message == null) { POP_LOGGER.info("reviveQueueId={}, can not get biz msg, topic:{}, qid:{}, offset:{}, brokerName:{}, info:{}, retry:{}, then continue", @@ -719,8 +724,11 @@ private void reviveMsgFromCk(PopCheckPoint popCheckPoint) { }); futureList.add(future); } + + // reput checkpoint to revive topic if retry failed CompletableFuture.allOf(futureList.toArray(new CompletableFuture[0])) .whenComplete((v, e) -> { + // reput checkpoint for (CompletableFuture> future : futureList) { Pair pair = future.getNow(new Pair<>(0L, false)); if (!pair.getObject2()) { @@ -728,9 +736,12 @@ private void reviveMsgFromCk(PopCheckPoint popCheckPoint) { } } + // update ack status of inflight checkpoint if (inflightReviveRequestMap.containsKey(popCheckPoint)) { inflightReviveRequestMap.get(popCheckPoint).setObject2(true); } + + // commit offset and remove inflight checkpoint for (Map.Entry> entry : inflightReviveRequestMap.entrySet()) { PopCheckPoint oldCK = entry.getKey(); Pair pair = entry.getValue(); From 10a7fe81507b46e92b5a889d09344271a3c08484 Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Wed, 27 May 2026 21:47:33 +0800 Subject: [PATCH 073/157] comment: add concurrency control comments to method mergeAndRevive of PopReviveService --- .../apache/rocketmq/broker/processor/PopReviveService.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopReviveService.java b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopReviveService.java index e20747b920c..5f53ee9c8d7 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopReviveService.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopReviveService.java @@ -643,12 +643,14 @@ protected void mergeAndRevive(ConsumeReviveObj consumeReviveObj) throws Throwabl continue; } - // restore checkpoint + // Concurrency control for revive: skip first long-running revive task. while (inflightReviveRequestMap.size() > 3) { waitForRunning(100); Pair pair = inflightReviveRequestMap.firstEntry().getValue(); + // if first revive task is timeout, reput it to revive topic, then skip if (!pair.getObject2() && System.currentTimeMillis() - pair.getObject1() > 1000 * 30) { PopCheckPoint oldCK = inflightReviveRequestMap.firstKey(); + // reput checkpoint to revive topic rePutCK(oldCK, pair); inflightReviveRequestMap.remove(oldCK); POP_LOGGER.warn("stay too long, remove from reviveRequestMap, {}, {}, {}, {}", popCheckPoint.getTopic(), From 614544453b75e1f2c2e044afced09ee4180a4fa5 Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Wed, 27 May 2026 21:51:06 +0800 Subject: [PATCH 074/157] comment: add attribute comments to inflightReviveRequestMap of PopReviveService --- .../broker/processor/PopReviveService.java | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopReviveService.java b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopReviveService.java index 5f53ee9c8d7..bbaf35946ce 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopReviveService.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopReviveService.java @@ -93,6 +93,25 @@ public class PopReviveService extends ServiceThread { private long currentReviveMessageTimestamp = -1; private volatile boolean shouldRunPopRevive = false; + /** + * Tracks checkpoints that are currently being revived. + * + *

      Key — the checkpoint being processed. + * Value — a pair of (startTime, completed), where: + *

        + *
      • {@code startTime} is the timestamp when revival began
      • + *
      • {@code completed} is {@code true} once all sub-messages have been + * processed (success or failure)
      • + *
      + * + *

      The map is sorted by {@link PopCheckPoint#compareTo} (by startOffset). + * This ordering is used to drain completed entries from the head, ensuring + * the revive topic offset is committed strictly in sequence. + * + *

      Concurrency is limited to at most 3 entries at a time (see + * {@link #mergeAndRevive}). If an entry stays incomplete for over 30 + * seconds, it is considered hung and is skipped via {@link #rePutCK}. + */ private final NavigableMap> inflightReviveRequestMap = Collections.synchronizedNavigableMap(new TreeMap<>()); private long reviveOffset; From f24cae00fc75a9c78f22e7dbf8e8117397573ef1 Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Wed, 27 May 2026 21:54:41 +0800 Subject: [PATCH 075/157] comment: add in-line comments to method reviveRetry of PopReviveService --- .../apache/rocketmq/broker/processor/PopReviveService.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopReviveService.java b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopReviveService.java index bbaf35946ce..a39d4ffac64 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopReviveService.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopReviveService.java @@ -143,6 +143,7 @@ public boolean isShouldRunPopRevive() { } private boolean reviveRetry(PopCheckPoint popCheckPoint, MessageExt messageExt) { + // convert checkpoint to inner message MessageExtBrokerInner msgInner = new MessageExtBrokerInner(); if (!popCheckPoint.getTopic().startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX)) { msgInner.setTopic(KeyBuilder.buildPopRetryTopic(popCheckPoint.getTopic(), popCheckPoint.getCId(), brokerController.getBrokerConfig().isEnableRetryTopicV2())); @@ -171,9 +172,15 @@ private boolean reviveRetry(PopCheckPoint popCheckPoint, MessageExt messageExt) } msgInner.getProperties().put(MessageConst.PROPERTY_ORIGIN_GROUP, popCheckPoint.getCId()); msgInner.setPropertiesString(MessageDecoder.messageProperties2String(msgInner.getProperties())); + + // set topic and queueId addRetryTopicIfNotExist(msgInner.getTopic(), popCheckPoint.getCId()); msgInner.setQueueId(getRetryQueueId(msgInner.getTopic(), messageExt)); + + // store message PutMessageResult putMessageResult = brokerController.getEscapeBridge().putMessageToSpecificQueue(msgInner); + + // logging and metric brokerController.getBrokerMetricsManager().getPopMetricsManager().incPopReviveRetryMessageCount(popCheckPoint, putMessageResult.getPutMessageStatus()); if (brokerController.getBrokerConfig().isEnablePopLog()) { POP_LOGGER.info("reviveQueueId={},retry msg, ck={}, msg queueId {}, offset {}, reviveDelay={}, result is {} ", From 241951de59e0363b03647869dbf4d687f4eb8fad Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Wed, 27 May 2026 22:09:39 +0800 Subject: [PATCH 076/157] comment: add method comments to reviveRetry of PopReviveService --- .../broker/processor/PopReviveService.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopReviveService.java b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopReviveService.java index a39d4ffac64..a6048c39ed7 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopReviveService.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopReviveService.java @@ -142,6 +142,21 @@ public boolean isShouldRunPopRevive() { return shouldRunPopRevive; } + /** + * Re-publish a timed-out message to the retry topic. + * + *

      Constructs a new {@link MessageExtBrokerInner} from the original + * message, increments the reconsume count (unless suspended), sets the + * first-pop time and origin group properties, and writes it to the + * appropriate retry topic (V1 or V2 depending on configuration). + * + *

      If the retry topic does not exist, it is created automatically + * via {@link #addRetryTopicIfNotExist}. + * + * @param popCheckPoint the checkpoint that triggered the revive + * @param messageExt the original message to re-publish + * @return {@code true} if the message was written successfully + */ private boolean reviveRetry(PopCheckPoint popCheckPoint, MessageExt messageExt) { // convert checkpoint to inner message MessageExtBrokerInner msgInner = new MessageExtBrokerInner(); From d51909c19e4afb3d2c4b4631e23e064e6820d4c8 Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Wed, 27 May 2026 22:38:24 +0800 Subject: [PATCH 077/157] comment: add class comments to AckMessageProcessor --- .../broker/processor/AckMessageProcessor.java | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/processor/AckMessageProcessor.java b/broker/src/main/java/org/apache/rocketmq/broker/processor/AckMessageProcessor.java index 34a790efca7..af33c747205 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/processor/AckMessageProcessor.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/AckMessageProcessor.java @@ -54,6 +54,27 @@ import org.apache.rocketmq.store.pop.AckMsg; import org.apache.rocketmq.store.pop.BatchAckMsg; +/** + * Processes consumer ack messages in Pop consumption mode. + * + *

      Handles both single ({@link RequestCode#ACK_MESSAGE}) and batch + * ({@link RequestCode#BATCH_ACK_MESSAGE}) acks. Each ack is processed + * through one of two paths: + *

        + *
      • KVStore path ({@code popConsumerKVServiceEnable=true}) — + * delegates to {@link PopConsumerService#ackAsync}
      • + *
      • File-based path — tries {@link PopBufferMergeService#addAk} + * first; if the buffer merge is not available, writes the ack as a + * message to the system revive topic
      • + *
      + * + *

      Orderly ack is handled separately by {@link #ackOrderly} / + * {@link #ackOrderlyNew}, which update the consumer order info and advance + * the consumer offset while notifying any long-polling waiters. + * + *

      This class also owns and manages the {@link PopReviveService} instances + * for the file-based revive path. + */ public class AckMessageProcessor implements NettyRequestProcessor { private static final Logger POP_LOGGER = LoggerFactory.getLogger(LoggerName.ROCKETMQ_POP_LOGGER_NAME); From 7a35bb4ffd4381bbfb3e169f9a016cde2b784217 Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Wed, 27 May 2026 22:40:23 +0800 Subject: [PATCH 078/157] comment: add class comments to ChangeInvisibleTimeProcessor --- .../ChangeInvisibleTimeProcessor.java | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/processor/ChangeInvisibleTimeProcessor.java b/broker/src/main/java/org/apache/rocketmq/broker/processor/ChangeInvisibleTimeProcessor.java index 5ff132ca237..b351fa85787 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/processor/ChangeInvisibleTimeProcessor.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/ChangeInvisibleTimeProcessor.java @@ -27,6 +27,7 @@ import org.apache.rocketmq.broker.offset.ConsumerOffsetManager; import org.apache.rocketmq.broker.offset.MemoryConsumerOrderInfoManager; import org.apache.rocketmq.broker.pop.PopConsumerLockService; +import org.apache.rocketmq.broker.pop.PopConsumerService; import org.apache.rocketmq.broker.pop.orderly.ConsumerOrderInfoManager; import org.apache.rocketmq.common.PopAckConstants; import org.apache.rocketmq.common.TopicConfig; @@ -52,6 +53,23 @@ import org.apache.rocketmq.store.pop.AckMsg; import org.apache.rocketmq.store.pop.PopCheckPoint; +/** + * Processes the nack {@code ChangeInvisibleTime} request from consumers. + * + *

      When a consumer needs more time to process a message (or wants to + * suspend/nack it), this processor updates the message's visibility + * timeout. The implementation varies by the ack mode: + *

        + *
      • KVStore path — delegates to + * {@link PopConsumerService#changeInvisibilityDuration}
      • + *
      • File-based path — writes a new CK to the revive topic with + * the updated invisible time, then acks the original CK so that + * the message will not be revived until the new timeout expires
      • + *
      + * + *

      For orderly consumption, the next visible time is updated directly in + * the {@link ConsumerOrderInfoManager} without writing to the revive topic. + */ public class ChangeInvisibleTimeProcessor implements NettyRequestProcessor { private static final Logger POP_LOGGER = LoggerFactory.getLogger(LoggerName.ROCKETMQ_POP_LOGGER_NAME); private final BrokerController brokerController; From 3efe3ba1c7fde7631970a53e92b0eeb6ee767d2e Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Thu, 28 May 2026 12:09:18 +0800 Subject: [PATCH 079/157] comment: add note about default mode in PopMessageProcessor --- .../apache/rocketmq/broker/processor/PopMessageProcessor.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopMessageProcessor.java b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopMessageProcessor.java index d260951ea59..20c49ee8c22 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopMessageProcessor.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopMessageProcessor.java @@ -388,7 +388,7 @@ public RemotingCommand processRequest(final ChannelHandlerContext ctx, RemotingC // There are two type of ack mode: // 1. ack by KV service - // 2. ack by file merge service + // 2. ack by file merge service, default mode if (brokerConfig.isPopConsumerKVServiceEnable()) { CompletableFuture popAsyncFuture = brokerController.getPopConsumerService().popAsync( From 76495cff3619f9ccd874c981b2a05a43f12d6996 Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Thu, 28 May 2026 12:14:59 +0800 Subject: [PATCH 080/157] comment: add in-line comments to method processRequest of AckMessageProcessor --- .../broker/processor/AckMessageProcessor.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/processor/AckMessageProcessor.java b/broker/src/main/java/org/apache/rocketmq/broker/processor/AckMessageProcessor.java index af33c747205..6a508f6e525 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/processor/AckMessageProcessor.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/AckMessageProcessor.java @@ -26,6 +26,7 @@ import org.apache.rocketmq.broker.lite.LiteMetadataUtil; import org.apache.rocketmq.broker.offset.ConsumerOffsetManager; import org.apache.rocketmq.broker.pop.PopConsumerLockService; +import org.apache.rocketmq.broker.pop.PopConsumerService; import org.apache.rocketmq.broker.pop.orderly.ConsumerOrderInfoManager; import org.apache.rocketmq.common.KeyBuilder; import org.apache.rocketmq.common.PopAckConstants; @@ -144,11 +145,14 @@ public boolean rejectRequest() { private RemotingCommand processRequest(final Channel channel, RemotingCommand request, boolean brokerAllowSuspend) throws RemotingCommandException { + // init context params AckMessageRequestHeader requestHeader; BatchAckMessageRequestBody reqBody = null; final RemotingCommand response = RemotingCommand.createResponseCommand(ResponseCode.SUCCESS, null); response.setOpaque(request.getOpaque()); + if (request.getCode() == RequestCode.ACK_MESSAGE) { + // decode and validate request requestHeader = (AckMessageRequestHeader) request.decodeCommandCustomHeader(AckMessageRequestHeader.class); TopicConfig topicConfig = this.brokerController.getTopicConfigManager().selectTopicConfig(requestHeader.getTopic()); @@ -188,12 +192,15 @@ private RemotingCommand processRequest(final Channel channel, RemotingCommand re response.setRemark(errorInfo); return response; } + + // append ack if (brokerController.getBrokerConfig().isPopConsumerKVServiceEnable()) { appendAckNew(requestHeader, null, response, channel, null); } else { appendAck(requestHeader, null, response, channel, null); } } else if (request.getCode() == RequestCode.BATCH_ACK_MESSAGE) { + // decode and validate request if (request.getBody() != null) { reqBody = BatchAckMessageRequestBody.decode(request.getBody(), BatchAckMessageRequestBody.class); } @@ -201,7 +208,10 @@ private RemotingCommand processRequest(final Channel channel, RemotingCommand re response.setCode(ResponseCode.NO_MESSAGE); return response; } + + // process each ack for (BatchAck bAck : reqBody.getAcks()) { + // default value of popConsumerKVServiceEnable is false if (brokerController.getBrokerConfig().isPopConsumerKVServiceEnable()) { appendAckNew(null, bAck, response, channel, reqBody.getBrokerName()); } else { @@ -209,6 +219,7 @@ private RemotingCommand processRequest(final Channel channel, RemotingCommand re } } } else { + // unsupported request, logging and return POP_LOGGER.error("AckMessageProcessor failed to process RequestCode: {}, consumer: {} ", request.getCode(), RemotingHelper.parseChannelRemoteAddr(channel)); response.setCode(ResponseCode.MESSAGE_ILLEGAL); response.setRemark(String.format("AckMessageProcessor failed to process RequestCode: %d", request.getCode())); From 0485831764885b034d412c447e7eaf5cd1801f2a Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Thu, 28 May 2026 12:17:11 +0800 Subject: [PATCH 081/157] comment: add method comments to processRequest of AckMessageProcessor --- .../broker/processor/AckMessageProcessor.java | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/processor/AckMessageProcessor.java b/broker/src/main/java/org/apache/rocketmq/broker/processor/AckMessageProcessor.java index 6a508f6e525..a38b95a6158 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/processor/AckMessageProcessor.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/AckMessageProcessor.java @@ -143,6 +143,26 @@ public boolean rejectRequest() { return false; } + /** + * Process an ack request (single or batch). + * + *

      Routes to one of two paths based on {@code popConsumerKVServiceEnable}: + *

        + *
      • {@code true} — {@link #appendAckNew} (KVStore path, delegates to + * {@link PopConsumerService#ackAsync})
      • + *
      • {@code false} — {@link #appendAck} (file-based path, tries + * {@link PopBufferMergeService#addAk} first, then writes to revive topic)
      • + *
      + * + *

      Orderly acks ({@code rqId == POP_ORDER_REVIVE_QUEUE}) are handled by + * {@link #ackOrderly} / {@link #ackOrderlyNew} instead. + * + * @param channel the Netty channel of the requesting client + * @param request the incoming request + * @param brokerAllowSuspend whether the broker may suspend the request + * @return the response to send back to the client + * @throws RemotingCommandException if the request cannot be decoded + */ private RemotingCommand processRequest(final Channel channel, RemotingCommand request, boolean brokerAllowSuspend) throws RemotingCommandException { // init context params From 7927a88518874c5a3fbad8a1b78455d0075e7ea0 Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Thu, 28 May 2026 12:21:07 +0800 Subject: [PATCH 082/157] comment: add in-line comments to method processRequest of ChangeInvisibleTimeProcessor --- .../broker/processor/ChangeInvisibleTimeProcessor.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/processor/ChangeInvisibleTimeProcessor.java b/broker/src/main/java/org/apache/rocketmq/broker/processor/ChangeInvisibleTimeProcessor.java index b351fa85787..889ffe3d286 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/processor/ChangeInvisibleTimeProcessor.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/ChangeInvisibleTimeProcessor.java @@ -94,8 +94,12 @@ public boolean rejectRequest() { private RemotingCommand processRequest(final Channel channel, RemotingCommand request, boolean brokerAllowSuspend) throws RemotingCommandException { + // process request async CompletableFuture responseFuture = processRequestAsync(channel, request, brokerAllowSuspend); + // process response sync or a sync + // default value of appendCkAsync is false + // default value of appendAckAsync is false if (brokerController.getBrokerConfig().isAppendCkAsync() && brokerController.getBrokerConfig().isAppendAckAsync()) { responseFuture.thenAccept(response -> doResponse(channel, request, response)).exceptionally(throwable -> { RemotingCommand response = RemotingCommand.createResponseCommand(ChangeInvisibleTimeResponseHeader.class); From 0d5ebc9ef057535d088f63f57635ece98f33c3ed Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Thu, 28 May 2026 12:26:38 +0800 Subject: [PATCH 083/157] comment: add in-line comments to method processRequestAsync of ChangeInvisibleTimeProcessor --- .../broker/processor/ChangeInvisibleTimeProcessor.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/processor/ChangeInvisibleTimeProcessor.java b/broker/src/main/java/org/apache/rocketmq/broker/processor/ChangeInvisibleTimeProcessor.java index 889ffe3d286..bff34264e19 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/processor/ChangeInvisibleTimeProcessor.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/ChangeInvisibleTimeProcessor.java @@ -126,6 +126,7 @@ private RemotingCommand processRequest(final Channel channel, RemotingCommand re public CompletableFuture processRequestAsync(final Channel channel, RemotingCommand request, boolean brokerAllowSuspend) throws RemotingCommandException { + // decode and validate request final ChangeInvisibleTimeRequestHeader requestHeader = (ChangeInvisibleTimeRequestHeader) request.decodeCommandCustomHeader(ChangeInvisibleTimeRequestHeader.class); RemotingCommand response = RemotingCommand.createResponseCommand(ChangeInvisibleTimeResponseHeader.class); response.setCode(ResponseCode.SUCCESS); @@ -148,11 +149,13 @@ public CompletableFuture processRequestAsync(final Channel chan return CompletableFuture.completedFuture(response); } + // lite topic process CompletableFuture future = processChangeInvisibleTimeForLite(requestHeader, response, responseHeader); if (future != null) { return future; } + // offset check long minOffset = this.brokerController.getMessageStore().getMinOffsetInQueue(requestHeader.getTopic(), requestHeader.getQueueId()); long maxOffset; try { @@ -166,6 +169,9 @@ public CompletableFuture processRequestAsync(final Channel chan } String[] extraInfo = ExtraInfoUtil.split(requestHeader.getExtraInfo()); + + // default value of popConsumerKVServiceEnable is false + // kv based ack service if (brokerController.getBrokerConfig().isPopConsumerKVServiceEnable()) { if (ExtraInfoUtil.isOrder(extraInfo)) { return this.processChangeInvisibleTimeForOrderNew( @@ -186,6 +192,9 @@ public CompletableFuture processRequestAsync(final Channel chan return CompletableFuture.completedFuture(response); } + // file merge based ack service + + // orderly topic if (ExtraInfoUtil.isOrder(extraInfo)) { return CompletableFuture.completedFuture( processChangeInvisibleTimeForOrder(requestHeader, extraInfo, response, responseHeader)); @@ -196,6 +205,7 @@ public CompletableFuture processRequestAsync(final Channel chan CompletableFuture futureResult = appendCheckPointThenAckOrigin(requestHeader, ExtraInfoUtil.getReviveQid(extraInfo), requestHeader.getQueueId(), requestHeader.getOffset(), now, extraInfo); + // format response return futureResult.thenCompose(result -> { if (result) { responseHeader.setInvisibleTime(requestHeader.getInvisibleTime()); From 7b762e02b52dfc4e5279dee2f0db7c372863db58 Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Thu, 28 May 2026 12:33:22 +0800 Subject: [PATCH 084/157] comment: add in-line comments to method appendCheckPointThenAckOrigin of ChangeInvisibleTimeProcessor --- .../broker/processor/ChangeInvisibleTimeProcessor.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/processor/ChangeInvisibleTimeProcessor.java b/broker/src/main/java/org/apache/rocketmq/broker/processor/ChangeInvisibleTimeProcessor.java index bff34264e19..2dfa4c9d38c 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/processor/ChangeInvisibleTimeProcessor.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/ChangeInvisibleTimeProcessor.java @@ -346,6 +346,8 @@ private CompletableFuture appendCheckPointThenAckOrigin( // add check point msg to revive log MessageExtBrokerInner msgInner = new MessageExtBrokerInner(); msgInner.setTopic(reviveTopic); + + // create checkpoint PopCheckPoint ck = new PopCheckPoint(); ck.setBitMap(0); ck.setNum((byte) 1); @@ -359,6 +361,7 @@ private CompletableFuture appendCheckPointThenAckOrigin( ck.setBrokerName(ExtraInfoUtil.getBrokerName(extraInfo)); ck.setSuspend(requestHeader.isSuspend()); + // init message with checkpoint msgInner.setBody(JSON.toJSONString(ck).getBytes(StandardCharsets.UTF_8)); msgInner.setQueueId(reviveQid); msgInner.setTags(PopAckConstants.CK_TAG); @@ -368,6 +371,8 @@ private CompletableFuture appendCheckPointThenAckOrigin( msgInner.setDeliverTimeMs(ck.getReviveTime() - PopAckConstants.ackTimeInterval); msgInner.getProperties().put(MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX, PopMessageProcessor.genCkUniqueId(ck)); msgInner.setPropertiesString(MessageDecoder.messageProperties2String(msgInner.getProperties())); + + // store message then call ackOrigin return this.brokerController.getEscapeBridge().asyncPutMessageToSpecificQueue(msgInner).thenCompose(putMessageResult -> { if (brokerController.getBrokerConfig().isEnablePopLog()) { POP_LOGGER.info("change Invisible, appendCheckPoint, topic {}, queueId {},reviveId {}, cid {}, startOffset {}, rt {}, result {}", requestHeader.getTopic(), queueId, reviveQid, requestHeader.getConsumerGroup(), offset, @@ -381,6 +386,7 @@ private CompletableFuture appendCheckPointThenAckOrigin( this.brokerController.getBrokerStatsManager().incGroupCkNums(requestHeader.getConsumerGroup(), requestHeader.getTopic(), 1); } } + if (putMessageResult.getPutMessageStatus() != PutMessageStatus.PUT_OK && putMessageResult.getPutMessageStatus() != PutMessageStatus.FLUSH_DISK_TIMEOUT && putMessageResult.getPutMessageStatus() != PutMessageStatus.FLUSH_SLAVE_TIMEOUT From 328195d49af217358fe141cf4a30dfb99f0934dc Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Thu, 28 May 2026 12:36:25 +0800 Subject: [PATCH 085/157] comment: add in-line comments to method ackOrigin of ChangeInvisibleTimeProcessor --- .../broker/processor/ChangeInvisibleTimeProcessor.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/processor/ChangeInvisibleTimeProcessor.java b/broker/src/main/java/org/apache/rocketmq/broker/processor/ChangeInvisibleTimeProcessor.java index 2dfa4c9d38c..44840e256a1 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/processor/ChangeInvisibleTimeProcessor.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/ChangeInvisibleTimeProcessor.java @@ -294,6 +294,7 @@ protected RemotingCommand processChangeInvisibleTimeForOrder(ChangeInvisibleTime private CompletableFuture ackOrigin(final ChangeInvisibleTimeRequestHeader requestHeader, String[] extraInfo) { + // create ackMsg and related message MessageExtBrokerInner msgInner = new MessageExtBrokerInner(); AckMsg ackMsg = new AckMsg(); @@ -310,10 +311,12 @@ private CompletableFuture ackOrigin(final ChangeInvisibleTimeRequestHea this.brokerController.getBrokerStatsManager().incBrokerAckNums(1); this.brokerController.getBrokerStatsManager().incGroupAckNums(requestHeader.getConsumerGroup(), requestHeader.getTopic(), 1); + // add ackMsg if (brokerController.getPopMessageProcessor().getPopBufferMergeService().addAk(rqId, ackMsg)) { return CompletableFuture.completedFuture(true); } + // init message msgInner.setTopic(reviveTopic); msgInner.setBody(JSON.toJSONString(ackMsg).getBytes(StandardCharsets.UTF_8)); msgInner.setQueueId(rqId); @@ -324,6 +327,8 @@ private CompletableFuture ackOrigin(final ChangeInvisibleTimeRequestHea msgInner.setDeliverTimeMs(ExtraInfoUtil.getPopTime(extraInfo) + ExtraInfoUtil.getInvisibleTime(extraInfo)); msgInner.getProperties().put(MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX, PopMessageProcessor.genAckUniqueId(ackMsg)); msgInner.setPropertiesString(MessageDecoder.messageProperties2String(msgInner.getProperties())); + + // store message return this.brokerController.getEscapeBridge().asyncPutMessageToSpecificQueue(msgInner).thenCompose(putMessageResult -> { if (putMessageResult.getPutMessageStatus() != PutMessageStatus.PUT_OK && putMessageResult.getPutMessageStatus() != PutMessageStatus.FLUSH_DISK_TIMEOUT From aa04f66e976d32fa98ab2051e5255546292f70ce Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Thu, 28 May 2026 15:07:06 +0800 Subject: [PATCH 086/157] comment: update inline comment for appendCheckPointThenAckOrigin --- .../broker/processor/ChangeInvisibleTimeProcessor.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/processor/ChangeInvisibleTimeProcessor.java b/broker/src/main/java/org/apache/rocketmq/broker/processor/ChangeInvisibleTimeProcessor.java index 44840e256a1..b28513aff75 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/processor/ChangeInvisibleTimeProcessor.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/ChangeInvisibleTimeProcessor.java @@ -377,7 +377,8 @@ private CompletableFuture appendCheckPointThenAckOrigin( msgInner.getProperties().put(MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX, PopMessageProcessor.genCkUniqueId(ck)); msgInner.setPropertiesString(MessageDecoder.messageProperties2String(msgInner.getProperties())); - // store message then call ackOrigin + // store new checkpoint to extend invisible time + // then ack origin checkpoint return this.brokerController.getEscapeBridge().asyncPutMessageToSpecificQueue(msgInner).thenCompose(putMessageResult -> { if (brokerController.getBrokerConfig().isEnablePopLog()) { POP_LOGGER.info("change Invisible, appendCheckPoint, topic {}, queueId {},reviveId {}, cid {}, startOffset {}, rt {}, result {}", requestHeader.getTopic(), queueId, reviveQid, requestHeader.getConsumerGroup(), offset, From b72eb4e8dd3c45d768d9f068099a36c6fa5287a6 Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Thu, 28 May 2026 15:14:52 +0800 Subject: [PATCH 087/157] comment: add method comments to processRequestAsync of ChangeInvisibleTimeProcessor --- .../ChangeInvisibleTimeProcessor.java | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/processor/ChangeInvisibleTimeProcessor.java b/broker/src/main/java/org/apache/rocketmq/broker/processor/ChangeInvisibleTimeProcessor.java index b28513aff75..364e850df95 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/processor/ChangeInvisibleTimeProcessor.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/ChangeInvisibleTimeProcessor.java @@ -124,6 +124,24 @@ private RemotingCommand processRequest(final Channel channel, RemotingCommand re return null; } + /** + * Asynchronously process a ChangeInvisibleTime request. + * + *

      Routes to the appropriate handler based on message type: + *

        + *
      • Lite message — {@link #processChangeInvisibleTimeForLite}
      • + *
      • KVStore path + orderly — {@link #processChangeInvisibleTimeForOrderNew}
      • + *
      • KVStore path + non-orderly — {@link PopConsumerService#changeInvisibilityDuration}
      • + *
      • File-based path + orderly — {@link #processChangeInvisibleTimeForOrder}
      • + *
      • File-based path + non-orderly — {@link #appendCheckPointThenAckOrigin}
      • + *
      + * + * @param channel the Netty channel + * @param request the incoming request + * @param brokerAllowSuspend whether the broker may suspend + * @return a future that completes with the response + * @throws RemotingCommandException if the request cannot be decoded + */ public CompletableFuture processRequestAsync(final Channel channel, RemotingCommand request, boolean brokerAllowSuspend) throws RemotingCommandException { // decode and validate request @@ -200,7 +218,7 @@ public CompletableFuture processRequestAsync(final Channel chan processChangeInvisibleTimeForOrder(requestHeader, extraInfo, response, responseHeader)); } - // add new ck + // add new checkpoint then ack origin checkpoint long now = System.currentTimeMillis(); CompletableFuture futureResult = appendCheckPointThenAckOrigin(requestHeader, ExtraInfoUtil.getReviveQid(extraInfo), requestHeader.getQueueId(), requestHeader.getOffset(), now, extraInfo); From 37c4ae2905800f112b202012c73602783c4c367c Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Thu, 28 May 2026 15:18:33 +0800 Subject: [PATCH 088/157] comment: add method comments to appendCheckPointThenAckOrigin of ChangeInvisibleTimeProcessor --- .../ChangeInvisibleTimeProcessor.java | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/processor/ChangeInvisibleTimeProcessor.java b/broker/src/main/java/org/apache/rocketmq/broker/processor/ChangeInvisibleTimeProcessor.java index 364e850df95..d90252cea69 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/processor/ChangeInvisibleTimeProcessor.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/ChangeInvisibleTimeProcessor.java @@ -362,6 +362,27 @@ private CompletableFuture ackOrigin(final ChangeInvisibleTimeRequestHea }); } + /** + * Extend the visibility timeout by writing a new checkpoint and ack the old one. + * + *

      This is the core of the file-based non-orderly ChangeInvisibleTime path: + *

        + *
      1. Writes a new CK ({@link PopAckConstants#CK_TAG}) to the revive + * topic with the updated {@code invisibleTime}. This CK will trigger a + * revive at the new timeout if not acked.
      2. + *
      3. If the CK is stored successfully, calls {@link #ackOrigin} to write + * an Ack ({@link PopAckConstants#ACK_TAG}) for the original CK, + * preventing the old CK from triggering a premature revive.
      4. + *
      + * + * @param requestHeader the original request header + * @param reviveQid the revive queue to write to + * @param queueId the original queue id + * @param offset the message offset being extended + * @param popTime the new pop time (current time) + * @param extraInfo the extra info from the original pop request + * @return a future that completes with {@code true} on success + */ private CompletableFuture appendCheckPointThenAckOrigin( final ChangeInvisibleTimeRequestHeader requestHeader, int reviveQid, @@ -411,6 +432,7 @@ private CompletableFuture appendCheckPointThenAckOrigin( } } + // if success, ack origin checkpoint if (putMessageResult.getPutMessageStatus() != PutMessageStatus.PUT_OK && putMessageResult.getPutMessageStatus() != PutMessageStatus.FLUSH_DISK_TIMEOUT && putMessageResult.getPutMessageStatus() != PutMessageStatus.FLUSH_SLAVE_TIMEOUT From 92cf992309b47b9b6c0c9b48166d1e95332bdde7 Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Thu, 28 May 2026 15:20:55 +0800 Subject: [PATCH 089/157] comment: add method comments to ackOrigin of ChangeInvisibleTimeProcessor --- .../processor/ChangeInvisibleTimeProcessor.java | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/processor/ChangeInvisibleTimeProcessor.java b/broker/src/main/java/org/apache/rocketmq/broker/processor/ChangeInvisibleTimeProcessor.java index d90252cea69..5de0c3eac78 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/processor/ChangeInvisibleTimeProcessor.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/ChangeInvisibleTimeProcessor.java @@ -310,6 +310,22 @@ protected RemotingCommand processChangeInvisibleTimeForOrder(ChangeInvisibleTime return response; } + /** + * Ack the original checkpoint after created a new checkpoint successfully. + * + *

      Called after the new checkpoint has been written successfully. This method + * writes an {@link PopAckConstants#ACK_TAG} message that matches the + * original checkpoint's merge key. When {@link PopReviveService} processes this + * ack, it sets the corresponding bit in the old CK's bitMap, causing + * the old CK to be treated as fully acked and skipped during revive. + * + *

      If {@link PopBufferMergeService#addAk} accepts the ack (buffer + * merge enabled), it is merged in memory without writing to the store. + * + * @param requestHeader the original request header + * @param extraInfo the extra info from the original pop request + * @return a future that completes with {@code true} on success + */ private CompletableFuture ackOrigin(final ChangeInvisibleTimeRequestHeader requestHeader, String[] extraInfo) { // create ackMsg and related message From 97d68c8abf1ce362ff2c002a160efdf608bdbadf Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Thu, 28 May 2026 15:38:49 +0800 Subject: [PATCH 090/157] comment: update inline comment for ack mode in AckMessageProcessor --- .../apache/rocketmq/broker/processor/AckMessageProcessor.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/processor/AckMessageProcessor.java b/broker/src/main/java/org/apache/rocketmq/broker/processor/AckMessageProcessor.java index a38b95a6158..c24f7299284 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/processor/AckMessageProcessor.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/AckMessageProcessor.java @@ -213,7 +213,7 @@ private RemotingCommand processRequest(final Channel channel, RemotingCommand re return response; } - // append ack + // append ack, default mode is file based merge, call appendAck if (brokerController.getBrokerConfig().isPopConsumerKVServiceEnable()) { appendAckNew(requestHeader, null, response, channel, null); } else { From fe753cb6e401d7947299b9422ecf6c2b72c53043 Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Thu, 28 May 2026 15:56:10 +0800 Subject: [PATCH 091/157] comment: add in-line comments to method appendAck of AckMessageProcessor --- .../broker/processor/AckMessageProcessor.java | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/processor/AckMessageProcessor.java b/broker/src/main/java/org/apache/rocketmq/broker/processor/AckMessageProcessor.java index c24f7299284..2d28c05fbfc 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/processor/AckMessageProcessor.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/AckMessageProcessor.java @@ -250,6 +250,7 @@ private RemotingCommand processRequest(final Channel channel, RemotingCommand re private void appendAck(final AckMessageRequestHeader requestHeader, final BatchAck batchAck, final RemotingCommand response, final Channel channel, String brokerName) throws RemotingCommandException { + // init context params String[] extraInfo; String consumeGroup, topic; int qId, rqId; @@ -257,8 +258,11 @@ private void appendAck(final AckMessageRequestHeader requestHeader, final BatchA long popTime, invisibleTime; AckMsg ackMsg; int ackCount = 0; + + // ack orderly or set context params if (batchAck == null) { // single ack + // set context params extraInfo = ExtraInfoUtil.split(requestHeader.getExtraInfo()); brokerName = ExtraInfoUtil.getBrokerName(extraInfo); consumeGroup = requestHeader.getConsumerGroup(); @@ -270,15 +274,18 @@ private void appendAck(final AckMessageRequestHeader requestHeader, final BatchA popTime = ExtraInfoUtil.getPopTime(extraInfo); invisibleTime = ExtraInfoUtil.getInvisibleTime(extraInfo); + // ack orderly if revive queue if (rqId == KeyBuilder.POP_ORDER_REVIVE_QUEUE) { ackOrderly(topic, consumeGroup, qId, ackOffset, popTime, invisibleTime, channel, response); return; } + // set ackMsg and ackCount ackMsg = new AckMsg(); ackCount = 1; } else { // batch ack + // set context params consumeGroup = batchAck.getConsumerGroup(); topic = ExtraInfoUtil.getRealTopic(batchAck.getTopic(), batchAck.getConsumerGroup(), batchAck.getRetry()); qId = batchAck.getQueueId(); @@ -288,6 +295,7 @@ private void appendAck(final AckMessageRequestHeader requestHeader, final BatchA popTime = batchAck.getPopTime(); invisibleTime = batchAck.getInvisibleTime(); + // offset check long minOffset = this.brokerController.getMessageStore().getMinOffsetInQueue(topic, qId); long maxOffset; try { @@ -300,6 +308,7 @@ private void appendAck(final AckMessageRequestHeader requestHeader, final BatchA return; } + // ack orderly or add offset to batchAckMsg BatchAckMsg batchAckMsg = new BatchAckMsg(); BitSet bitSet = batchAck.getBitSet(); for (int i = bitSet.nextSetBit(0); i >= 0; i = bitSet.nextSetBit(i + 1)) { @@ -316,10 +325,13 @@ private void appendAck(final AckMessageRequestHeader requestHeader, final BatchA batchAckMsg.getAckOffsetList().add(offset); } } + + // skip if empty or is revive queue if (rqId == KeyBuilder.POP_ORDER_REVIVE_QUEUE || batchAckMsg.getAckOffsetList().isEmpty()) { return; } + // set ackMsg and ackCount ackMsg = batchAckMsg; ackCount = batchAckMsg.getAckOffsetList().size(); } @@ -327,6 +339,7 @@ private void appendAck(final AckMessageRequestHeader requestHeader, final BatchA this.brokerController.getBrokerStatsManager().incBrokerAckNums(ackCount); this.brokerController.getBrokerStatsManager().incGroupAckNums(consumeGroup, topic, ackCount); + // set ackMsg ackMsg.setConsumerGroup(consumeGroup); ackMsg.setTopic(topic); ackMsg.setQueueId(qId); @@ -335,11 +348,13 @@ private void appendAck(final AckMessageRequestHeader requestHeader, final BatchA ackMsg.setPopTime(popTime); ackMsg.setBrokerName(brokerName); + // add ackMsg if (this.brokerController.getPopMessageProcessor().getPopBufferMergeService().addAk(rqId, ackMsg)) { brokerController.getPopInflightMessageCounter().decrementInFlightMessageNum(topic, consumeGroup, popTime, qId, ackCount); return; } + // create revive message by ackMsg, if add ackMsg failed MessageExtBrokerInner msgInner = new MessageExtBrokerInner(); msgInner.setTopic(reviveTopic); msgInner.setBody(JSON.toJSONString(ackMsg).getBytes(StandardCharsets.UTF_8)); @@ -357,7 +372,9 @@ private void appendAck(final AckMessageRequestHeader requestHeader, final BatchA msgInner.setDeliverTimeMs(popTime + invisibleTime); msgInner.getProperties().put(MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX, PopMessageProcessor.genAckUniqueId(ackMsg)); msgInner.setPropertiesString(MessageDecoder.messageProperties2String(msgInner.getProperties())); - if (brokerController.getBrokerConfig().isAppendAckAsync()) { + + // store revive message + if (brokerController.getBrokerConfig().isAppendAckAsync()) { // default is false int finalAckCount = ackCount; this.brokerController.getEscapeBridge().asyncPutMessageToSpecificQueue(msgInner).thenAccept(putMessageResult -> { handlePutMessageResult(putMessageResult, ackMsg, topic, consumeGroup, popTime, qId, finalAckCount); From f92c4e83b7e4447cc22d6700ba3d4b1997efd9f8 Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Thu, 28 May 2026 15:57:28 +0800 Subject: [PATCH 092/157] comment: add method comments to appendAck of AckMessageProcessor --- .../broker/processor/AckMessageProcessor.java | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/processor/AckMessageProcessor.java b/broker/src/main/java/org/apache/rocketmq/broker/processor/AckMessageProcessor.java index 2d28c05fbfc..7e044c38db3 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/processor/AckMessageProcessor.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/AckMessageProcessor.java @@ -248,6 +248,28 @@ private RemotingCommand processRequest(final Channel channel, RemotingCommand re return response; } + /** + * Append an ack (single or batch) in the file-based path. + * + *

      For single ack: parses the extra info from the request header, + * routes orderly acks to {@link #ackOrderly}, or creates a single {@link AckMsg}. + * + *

      For batch ack: expands the {@link BitSet} from the + * {@link BatchAck} into individual offsets, routes orderly acks individually, + * and packs the remaining offsets into a {@link BatchAckMsg}. + * + *

      The ack is first offered to {@link PopBufferMergeService#addAk}. + * If the buffer merge is not available, the ack is serialized as JSON and + * written to the revive topic with tag {@link PopAckConstants#ACK_TAG} + * or {@link PopAckConstants#BATCH_ACK_TAG}. + * + * @param requestHeader the single-ack request header (null for batch) + * @param batchAck the batch ack body (null for single) + * @param response the response to modify on error + * @param channel the Netty channel + * @param brokerName the broker name + * @throws RemotingCommandException if offset validation fails + */ private void appendAck(final AckMessageRequestHeader requestHeader, final BatchAck batchAck, final RemotingCommand response, final Channel channel, String brokerName) throws RemotingCommandException { // init context params From d5f43487d3a612388d09db8ce4b9b17ccfa3b9a6 Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Thu, 28 May 2026 16:12:49 +0800 Subject: [PATCH 093/157] comment: add class comments to PopMessageProcessor --- .../broker/processor/PopMessageProcessor.java | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopMessageProcessor.java b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopMessageProcessor.java index 20c49ee8c22..fa49ec211e0 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopMessageProcessor.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopMessageProcessor.java @@ -33,6 +33,7 @@ import org.apache.rocketmq.broker.longpolling.PopRequest; import org.apache.rocketmq.broker.pagecache.ManyMessageTransfer; import org.apache.rocketmq.broker.pop.PopConsumerContext; +import org.apache.rocketmq.broker.pop.PopConsumerService; import org.apache.rocketmq.common.BrokerConfig; import org.apache.rocketmq.common.KeyBuilder; import org.apache.rocketmq.common.MixAll; @@ -99,6 +100,24 @@ import static org.apache.rocketmq.remoting.metrics.RemotingMetricsConstant.LABEL_RESPONSE_CODE; import static org.apache.rocketmq.remoting.metrics.RemotingMetricsConstant.LABEL_RESULT; +/** + * Processes PopMessage requests from consumers. + * + *

      This is the core processor for the Pop consumption mode. It handles: + *

        + *
      • Validating the request (topic, group, queue, subscription, permissions)
      • + *
      • Routing to the {@link PopConsumerService} (KVStore path) or the + * inline file-based path
      • + *
      • Popping messages from normal and retry topics (V1/V2)
      • + *
      • Creating checkpoints and writing them to the revive topic
      • + *
      • Long-polling suspension via {@link PopLongPollingService}
      • + *
      • Transferring messages to the client (heap copy or zero-copy)
      • + *
      + * + *

      This class also owns the {@link PopLongPollingService}, + * {@link PopBufferMergeService}, and {@link QueueLockManager} instances + * used by the file-based ack path. + */ public class PopMessageProcessor implements NettyRequestProcessor { private static final Logger POP_LOGGER = LoggerFactory.getLogger(LoggerName.ROCKETMQ_POP_LOGGER_NAME); @@ -615,6 +634,7 @@ public RemotingCommand processRequest(final ChannelHandlerContext ctx, RemotingC getMessageResult.setStatus(GetMessageStatus.NO_MESSAGE_IN_QUEUE); } + // format response responseHeader.setInvisibleTime(requestHeader.getInvisibleTime()); responseHeader.setPopTime(popTime); responseHeader.setReviveQid(reviveQid); From 72698c1da4daecab0f03c279644089d82e847c7a Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Thu, 28 May 2026 16:15:22 +0800 Subject: [PATCH 094/157] comment: add method comments to processRequest of PopMessageProcessor --- .../broker/processor/PopMessageProcessor.java | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopMessageProcessor.java b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopMessageProcessor.java index fa49ec211e0..299579b70ac 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopMessageProcessor.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopMessageProcessor.java @@ -241,6 +241,27 @@ public void notifyMessageArriving(final String topic, final int queueId, final S topic, queueId, cid, false, null, 0L, null, null); } + /** + * Process a PopMessage request. + * + *

      This method handles the full Pop lifecycle: + *

        + *
      1. Validates the request (topic, group, permissions, subscription)
      2. + *
      3. Routes to the KVStore path (via {@link PopConsumerService#popAsync}) + * or the file-based path (inline CompletableFuture chain)
      4. + *
      5. Pops messages from normal and retry topics (V1/V2)
      6. + *
      7. Creates checkpoints and appends them to the revive topic
      8. + *
      9. Suspends the request via {@link PopLongPollingService#polling} if + * no messages are available
      10. + *
      11. Transfers messages via heap copy or zero-copy ({@code FileRegion})
      12. + *
      + * + * @param ctx the Netty channel handler context + * @param request the incoming PopMessage request + * @return the response, or {@code null} if the response is sent asynchronously + * (zero-copy path or long-polling suspension) + * @throws RemotingCommandException if the request cannot be decoded + */ @Override public RemotingCommand processRequest(final ChannelHandlerContext ctx, RemotingCommand request) throws RemotingCommandException { From 24d2877fff3f7cf506c9cde86cb1590ddd3d9037 Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Thu, 28 May 2026 16:19:22 +0800 Subject: [PATCH 095/157] comment: add inline comments for kv path in PopMessageProcessor --- .../apache/rocketmq/broker/processor/PopMessageProcessor.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopMessageProcessor.java b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopMessageProcessor.java index 299579b70ac..1b27a1b546b 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopMessageProcessor.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopMessageProcessor.java @@ -438,6 +438,7 @@ public RemotingCommand processRequest(final ChannelHandlerContext ctx, RemotingC requestHeader.getAttemptId(), requestHeader.getInitMode(), messageFilter); popAsyncFuture.thenApply(result -> { + // callback try { if (request.getCallbackList() != null) { request.getCallbackList().forEach(CommandCallback::accept); @@ -447,6 +448,7 @@ public RemotingCommand processRequest(final ChannelHandlerContext ctx, RemotingC POP_LOGGER.error("PopProcessor execute callback error", t); } + // long polling process if (result.isFound()) { response.setCode(ResponseCode.SUCCESS); getMessageResult.setStatus(GetMessageStatus.FOUND); @@ -479,6 +481,7 @@ public RemotingCommand processRequest(final ChannelHandlerContext ctx, RemotingC getMessageResult.setStatus(GetMessageStatus.NO_MESSAGE_IN_QUEUE); } + // format response responseHeader.setPopTime(result.getPopTime()); responseHeader.setInvisibleTime(result.getInvisibleTime()); responseHeader.setReviveQid( From 318426e795da29c8f5ed9a1dba5a8fcb3ceba67b Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Thu, 28 May 2026 18:04:17 +0800 Subject: [PATCH 096/157] comment: update inline comment for long polling in PopMessageProcessor --- .../apache/rocketmq/broker/processor/PopMessageProcessor.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopMessageProcessor.java b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopMessageProcessor.java index 1b27a1b546b..e863ae7ac79 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopMessageProcessor.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopMessageProcessor.java @@ -448,7 +448,7 @@ public RemotingCommand processRequest(final ChannelHandlerContext ctx, RemotingC POP_LOGGER.error("PopProcessor execute callback error", t); } - // long polling process + // long polling process, useless in rocketmq 5.* if (result.isFound()) { response.setCode(ResponseCode.SUCCESS); getMessageResult.setStatus(GetMessageStatus.FOUND); From ed4aa415cef7f06abe00d83dccbe7b861e860fd7 Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Thu, 28 May 2026 18:31:02 +0800 Subject: [PATCH 097/157] comment: add in-line comments to method popAsync of PopConsumerService --- .../apache/rocketmq/broker/pop/PopConsumerService.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerService.java b/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerService.java index 9ab5eb651be..d28cc513b7c 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerService.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerService.java @@ -356,6 +356,7 @@ public CompletableFuture popAsync(String clientHost, long po String groupId, String topicId, int queueId, int batchSize, boolean fifo, String attemptId, int initMode, MessageFilter filter) { + // init context params PopConsumerContext popConsumerContext = new PopConsumerContext(clientHost, popTime, invisibleTime, groupId, fifo, initMode, attemptId); @@ -391,18 +392,22 @@ public CompletableFuture popAsync(String clientHost, long po CompletableFuture.completedFuture(popConsumerContext); try { + // get message from retry topic, if (!fifo && preferRetry) { + // default config of retrieveMessageFromPopRetryTopicV1 is true, if (brokerConfig.isRetrieveMessageFromPopRetryTopicV1()) { getMessageFuture = this.getMessageFromTopicAsync(getMessageFuture, clientHost, groupId, retryTopicV1, requestCount, batchSize, filter, PopConsumerRecord.RetryType.RETRY_TOPIC_V1); } + // default config of enableRetryTopicV2 is false if (brokerConfig.isEnableRetryTopicV2()) { getMessageFuture = this.getMessageFromTopicAsync(getMessageFuture, clientHost, groupId, retryTopicV2, requestCount, batchSize, filter, PopConsumerRecord.RetryType.RETRY_TOPIC_V2); } } + // get message from normal topic if (queueId != -1) { getMessageFuture = this.getMessageAsync(getMessageFuture, clientHost, groupId, topicId, queueId, batchSize, filter, PopConsumerRecord.RetryType.NORMAL_TOPIC); @@ -410,6 +415,7 @@ public CompletableFuture popAsync(String clientHost, long po getMessageFuture = this.getMessageFromTopicAsync(getMessageFuture, clientHost, groupId, topicId, requestCount, batchSize, filter, PopConsumerRecord.RetryType.NORMAL_TOPIC); + // get message from retry topic if (!fifo && !preferRetry) { if (brokerConfig.isRetrieveMessageFromPopRetryTopicV1()) { getMessageFuture = this.getMessageFromTopicAsync(getMessageFuture, clientHost, groupId, @@ -425,6 +431,8 @@ public CompletableFuture popAsync(String clientHost, long po return getMessageFuture.thenCompose(result -> { if (result.isFound() && !result.isFifo()) { + // write checkpoint to cache or store + // default config of enablePopBufferMerge is false if (brokerConfig.isEnablePopBufferMerge() && popConsumerCache != null && !popConsumerCache.isCacheFull()) { this.popConsumerCache.writeRecords(result.getPopConsumerRecordList()); @@ -432,6 +440,7 @@ public CompletableFuture popAsync(String clientHost, long po this.popConsumerStore.writeRecords(result.getPopConsumerRecordList()); } + // format result for (int i = 0; i < result.getGetMessageResultList().size(); i++) { GetMessageResult getMessageResult = result.getGetMessageResultList().get(i); PopConsumerRecord popConsumerRecord = result.getPopConsumerRecordList().get(i); @@ -449,6 +458,7 @@ public CompletableFuture popAsync(String clientHost, long po } return CompletableFuture.completedFuture(result); }).whenComplete((result, throwable) -> { + // unlock by consumerLockService try { if (throwable != null) { log.error("PopConsumerService popAsync get message error", From de70be6855765607f5e3881b47e3154bda038d4e Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Thu, 28 May 2026 18:34:27 +0800 Subject: [PATCH 098/157] comment: add method comments to popAsync of PopConsumerService --- .../broker/pop/PopConsumerService.java | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerService.java b/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerService.java index d28cc513b7c..eac85a28e4f 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerService.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerService.java @@ -352,6 +352,33 @@ protected CompletableFuture getMessageFromTopicAsync(Complet return future; } + /** + * Asynchronously pop messages for the KVStore-based ack path. + * + *

      This method coordinates the full Pop lifecycle: + *

        + *
      1. Validates topic, group, and acquires the consumer lock
      2. + *
      3. Determines whether to pull from retry topic first + * (based on {@code popFromRetryProbability})
      4. + *
      5. Pulls messages from normal topic (and retry topic V1/V2 if configured)
      6. + *
      7. Writes checkpoints to {@link PopConsumerCache} (buffer merge) or + * {@link PopConsumerKVStore} (RocksDB)
      8. + *
      9. Re-encodes retry messages if needed
      10. + *
      + * + * @param clientHost the client address + * @param popTime the pop invocation timestamp + * @param invisibleTime the message visibility timeout + * @param groupId consumer group id + * @param topicId topic name + * @param queueId queue id (-1 for all queues) + * @param batchSize max number of messages to return + * @param fifo whether this is a FIFO ordered consumption + * @param attemptId attempt id for idempotent consumption + * @param initMode consume init mode (min/max) + * @param filter message filter expression + * @return a future that completes with the pop result context + */ public CompletableFuture popAsync(String clientHost, long popTime, long invisibleTime, String groupId, String topicId, int queueId, int batchSize, boolean fifo, String attemptId, int initMode, MessageFilter filter) { From 31b092be3fafdeae2b278d080e9f69e66854ed08 Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Thu, 28 May 2026 18:43:26 +0800 Subject: [PATCH 099/157] comment: add method comments to getMessageFromTopicAsync of PopConsumerService --- .../broker/pop/PopConsumerService.java | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerService.java b/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerService.java index eac85a28e4f..a619890fc36 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerService.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerService.java @@ -335,6 +335,28 @@ protected CompletableFuture getMessageAsync(CompletableFutur }); } + /** + * Fetch messages from every read queue of a topic via a CompletableFuture chain. + * + *

      Each queue is visited once. For each queue the + * {@link #getMessageAsync(CompletableFuture, String, String, String, int, int, MessageFilter, PopConsumerRecord.RetryType)} + * method is chained via {@link CompletableFuture#thenCompose}. The chain carries + * the accumulated result through all queues, stopping early when the batch is + * filled, the queue is blocked, or the inflight threshold is reached. + * + *

      Queue iteration order respects {@code priorityOrderAsc} and uses + * {@code requestCount} as a round-robin offset for load balancing. + * + * @param future the accumulator future + * @param clientHost the client address + * @param groupId consumer group id + * @param topicId topic name + * @param requestCount round-robin counter for queue selection + * @param batchSize max number of messages to return + * @param filter message filter expression + * @param retryType whether this is a retry topic V1/V2 + * @return a future completing with the pop result context + */ protected CompletableFuture getMessageFromTopicAsync(CompletableFuture future, String clientHost, String groupId, String topicId, long requestCount, int batchSize, MessageFilter filter, PopConsumerRecord.RetryType retryType) { From 353f71b713066fd7e6ec7eb8b85fe0a94576e300 Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Thu, 28 May 2026 18:44:22 +0800 Subject: [PATCH 100/157] comment: add in-line comments to method getMessageFromTopicAsync of PopConsumerService --- .../org/apache/rocketmq/broker/pop/PopConsumerService.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerService.java b/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerService.java index a619890fc36..d28f9be2583 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerService.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerService.java @@ -360,10 +360,13 @@ protected CompletableFuture getMessageAsync(CompletableFutur protected CompletableFuture getMessageFromTopicAsync(CompletableFuture future, String clientHost, String groupId, String topicId, long requestCount, int batchSize, MessageFilter filter, PopConsumerRecord.RetryType retryType) { + // get topic config TopicConfig topicConfig = this.brokerController.getTopicConfigManager().selectTopicConfig(topicId); if (null == topicConfig) { return future; } + + // iterate all queues of the topic for (int i = 0; i < topicConfig.getReadQueueNums(); i++) { long index = (brokerController.getBrokerConfig().isPriorityOrderAsc() ? topicConfig.getReadQueueNums() - 1 - i : i) + requestCount; From d2dc76431e2f5d40296998263a8c2ba3f7e60752 Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Thu, 28 May 2026 19:28:16 +0800 Subject: [PATCH 101/157] comment: add method comments to getMessageAsync of PopConsumerService --- .../broker/pop/PopConsumerService.java | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerService.java b/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerService.java index d28f9be2583..e3f4f1e21c6 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerService.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerService.java @@ -298,6 +298,31 @@ public boolean isFifoBlocked(PopConsumerContext context, String groupId, String context.getAttemptId(), topicId, groupId, queueId, context.getInvisibleTime()); } + /** + * Fetch messages from a single queue and append them to the pop context. + * + *

      Chained via {@link CompletableFuture#thenCompose} from + * {@link #getMessageFromTopicAsync}. When the batch is already full + * ({@code remain <= 0}), the pending count is added to the context and + * the chain stops. Otherwise, messages are fetched from the store and + * the result is merged into the context via {@link #handleGetMessageResult}. + * + *

      Early termination can occur inside this method when: + *

        + *
      • Too many inflight (un-acked) messages exist
      • + *
      • A FIFO queue is blocked
      • + *
      + * + * @param future the accumulator future carrying the pop context + * @param clientHost the client address + * @param groupId consumer group id + * @param topicId topic name + * @param queueId queue id + * @param batchSize max number of messages still needed + * @param filter message filter + * @param retryType whether this is a retry topic V1/V2 + * @return a future completing with the pop context updated with results + */ protected CompletableFuture getMessageAsync(CompletableFuture future, String clientHost, String groupId, String topicId, int queueId, int batchSize, MessageFilter filter, PopConsumerRecord.RetryType retryType) { From 7737ed741bfa3d7857eb31d2d1d408f9ed447261 Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Fri, 29 May 2026 10:12:23 +0800 Subject: [PATCH 102/157] comment: add in-line comments to method getPopOffset of PopConsumerService --- .../org/apache/rocketmq/broker/pop/PopConsumerService.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerService.java b/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerService.java index e3f4f1e21c6..067b7d73335 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerService.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerService.java @@ -213,6 +213,7 @@ public long getPopOffset(String groupId, String topicId, int queueId, int initMo this.brokerController.getConsumerOffsetManager().queryOffset(groupId, topicId, queueId) : this.brokerController.getConsumerOffsetManager().queryPullOffset(groupId, topicId, queueId); + // init offset if (offset < 0L) { try { offset = this.brokerController.getPopMessageProcessor() @@ -223,6 +224,8 @@ public long getPopOffset(String groupId, String topicId, int queueId, int initMo throw new RuntimeException(e); } } + + // get reset offset Long resetOffset = this.brokerController.getConsumerOffsetManager().queryThenEraseResetOffset(topicId, groupId, queueId); if (resetOffset != null) { @@ -231,6 +234,7 @@ public long getPopOffset(String groupId, String topicId, int queueId, int initMo this.brokerController.getConsumerOffsetManager() .commitOffset("ResetPopOffset", groupId, topicId, queueId, resetOffset); } + return resetOffset != null ? resetOffset : offset; } From 487dc96efc4b077a9f36b04f990184406dcc3e6d Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Fri, 29 May 2026 10:16:52 +0800 Subject: [PATCH 103/157] comment: add method comments to getPopOffset of PopConsumerService --- .../broker/pop/PopConsumerService.java | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerService.java b/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerService.java index 067b7d73335..02886fef8d2 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerService.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerService.java @@ -205,6 +205,28 @@ public PopConsumerContext handleGetMessageResult(PopConsumerContext context, Get return context; } + /** + * Retrieve the starting consume offset for a pop request. + * + *

      For FIFO consumers, the offset is read from the regular consumer offset. + * For non-FIFO consumers, a separate pull offset is used (compatibility with + * pull consumer switchover). + * + *

      If no offset is stored (first pop), it is initialized via + * {@code PopMessageProcessor#getInitOffset} based on {@code initMode} + * (beginning or end of the queue). + * + *

      If a reset offset exists (offset reset command issued), the cache is + * cleared, FIFO lock unlock, and the reset offset takes effect + * immediately. + * + * @param groupId consumer group id + * @param topicId topic name + * @param queueId queue id + * @param initMode consume init mode (min/max) + * @param fifo whether this is a FIFO ordered consumption + * @return the consume offset to start popping from + */ public long getPopOffset(String groupId, String topicId, int queueId, int initMode, boolean fifo) { // For FIFO messages, the pull offset is not used. From 84b55858699baecb46c9c7f716171ea803074e13 Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Fri, 29 May 2026 16:36:25 +0800 Subject: [PATCH 104/157] comment: add method comments to handleGetMessageResult of PopConsumerService --- .../broker/pop/PopConsumerService.java | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerService.java b/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerService.java index 02886fef8d2..e527fb7ad0c 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerService.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerService.java @@ -168,6 +168,33 @@ public GetMessageResult recodeRetryMessage(GetMessageResult getMessageResult, return result; } + /** + * Merge a GetMessageResult into the pop context and commit the consumer offset. + * + *

      If messages were found: + *

        + *
      • For FIFO — the queue is blocked via {@link #setFifoBlocked} so that + * subsequent pops on the same queue wait for the ack
      • + *
      • The result is appended to the context along with the topic, queue, + * and retry type metadata
      • + *
      + * + *

      The consumer offset is then committed: + *

        + *
      • For FIFO when no messages found — committed to the next begin offset
      • + *
      • For non-FIFO — the pull offset is updated. If buffer merge is enabled, + * the offset is clamped to the minimum offset still in the cache to + * prevent regression
      • + *
      + * + * @param context the pop context to update + * @param result the result from the message store + * @param topicId topic name + * @param queueId queue id + * @param retryType whether this is a retry topic V1/V2 + * @param offset the original consume offset used for this fetch + * @return the updated pop context + */ public PopConsumerContext handleGetMessageResult(PopConsumerContext context, GetMessageResult result, String topicId, int queueId, PopConsumerRecord.RetryType retryType, long offset) { From d6a82d6f9620c39bd7bb1780de1ceada6f43c6da Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Fri, 29 May 2026 17:52:13 +0800 Subject: [PATCH 105/157] comment: add class comments to PopConsumerCache --- .../rocketmq/broker/pop/PopConsumerCache.java | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerCache.java b/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerCache.java index c74c5793a5c..5c363d89196 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerCache.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerCache.java @@ -35,6 +35,30 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +/** + * In-memory cache for un-acked Pop consumer records, used when + * {@code enablePopBufferMerge} is enabled in the KVStore path. + * + *

      Popped messages are stored here by + * {@link PopConsumerService#popAsync}. The background {@link #run()} thread + * periodically scans the cache and processes expired records: + *

        + *
      • Visibility timeout expired — the record is passed to the + * {@code reviveConsumer} (which calls + * {@link PopConsumerService#revive}) to re-publish the message to + * the retry topic
      • + *
      • Consumer offline (lock timeout) — all records for that + * {code groupId, topicId} pair are flushed to + * {@link PopConsumerKVStore} without revival
      • + *
      • Consumer acked — the record is removed via + * {@link #deleteRecords} when a matching ack arrives
      • + *
      + * + *

      Each {@code groupId@topicId@queueId} entry is backed by a + * {@link ConsumerRecords} instance containing two + * {@link ConcurrentSkipListMap}s — one for active records and one for + * records staged for removal. + */ public class PopConsumerCache extends ServiceThread { private static final Logger log = LoggerFactory.getLogger(LoggerName.ROCKETMQ_POP_LOGGER_NAME); From ba20e3278e6954ccdef0947e2347205ef88b9734 Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Fri, 29 May 2026 17:53:51 +0800 Subject: [PATCH 106/157] comment: add interface comments to PopConsumerKVStore --- .../rocketmq/broker/pop/PopConsumerKVStore.java | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerKVStore.java b/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerKVStore.java index 33072d699b5..fc2c9739a14 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerKVStore.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerKVStore.java @@ -18,6 +18,22 @@ import java.util.List; +/** + * Persistent key-value store for un-acked Pop consumer records. + * + *

      Used by the KVStore-based ack path ({@code popConsumerKVServiceEnable=true}). + * When a message is popped, a record is written here. When the consumer acks + * the message or the visibility timeout expires, the record is deleted or + * revived. The default implementation is {@code PopConsumerRocksdbStore}. + * + *

      This interface supports three operations: + *

        + *
      • {@link #writeRecords} — persist popped records
      • + *
      • {@link #deleteRecords} — remove acked records
      • + *
      • {@link #scanExpiredRecords} — find records whose visibility timeout + * has elapsed for revival
      • + *
      + */ public interface PopConsumerKVStore { /** From 9b9c92aec1bd85b6640d1ef7d9ff15fa824f73ee Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Fri, 29 May 2026 18:10:55 +0800 Subject: [PATCH 107/157] comment: add attribute comments to attemptId of PopConsumerRecord --- .../rocketmq/broker/pop/PopConsumerRecord.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerRecord.java b/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerRecord.java index d10b584ef69..52fd468f8f9 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerRecord.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerRecord.java @@ -67,6 +67,20 @@ public int getCode() { @JSONField(ordinal = 7) private int attemptTimes; + /** + * Client-generated idempotency key for FIFO ordered consumption. + * + *

      Possible values: + *

        + *
      • Client request — a unique id from {@code PopMessageRequestHeader#getAttemptId}, + * used by {@code ConsumerOrderInfoManager} to block subsequent pops on the same + * queue until the current batch is acked
      • + *
      • {@code null} — for ack records ({@code PopConsumerService#ackAsync}) and + * change-invisibility records, where FIFO ordering is not applicable
      • + *
      • Copied from the original record — for revive retry records, the attemptId is + * inherited from the expired record
      • + *
      + */ @JSONField(ordinal = 8) private String attemptId; From 4003b67ba321ae86de46a4ce12e779ef7423a2eb Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Fri, 29 May 2026 18:20:54 +0800 Subject: [PATCH 108/157] comment: add attribute comments to suspend of PopConsumerRecord --- .../org/apache/rocketmq/broker/pop/PopConsumerRecord.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerRecord.java b/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerRecord.java index 52fd468f8f9..8b268a9fc6f 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerRecord.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerRecord.java @@ -84,6 +84,14 @@ public int getCode() { @JSONField(ordinal = 8) private String attemptId; + /** + * Whether the consumer has suspended (nacked) this message. + * + *

      When {@code true}, the reconsume count is not incremented on + * revive, so the message will not be prematurely sent to the DLQ due to + * repeated visibility timeout extensions. Set via + * {@code ChangeInvisibleTimeRequestHeader#isSuspend}. + */ @JSONField(ordinal = 9) private boolean suspend; From f185dedf52c6c6543394519f7e3ced7ae12f1469 Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Sat, 30 May 2026 11:24:07 +0800 Subject: [PATCH 109/157] comment: add class and attribute comments to ConsumerRecords of PopConsumerCache --- .../rocketmq/broker/pop/PopConsumerCache.java | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerCache.java b/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerCache.java index 5c363d89196..2fa2b2b95da 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerCache.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerCache.java @@ -70,6 +70,7 @@ public class PopConsumerCache extends ServiceThread { private final Consumer reviveConsumer; private final AtomicInteger estimateCacheSize; + // key: groupId@topicId@queueId private final ConcurrentMap consumerRecordTable; public PopConsumerCache(BrokerController brokerController, PopConsumerKVStore consumerRecordStore, @@ -229,13 +230,46 @@ public void run() { } } + /** + * Records for one {@code consumerGroupId@topicId@queueId} in the Pop cache. + * + *

      Uses two {@link ConcurrentSkipListMap}s to separate active and + * expiring records for safe two-phase cleanup: + *

        + *
      1. {@link #stageExpiredRecords} moves timed-out records from + * {@link #recordTreeMap} to {@link #removeTreeMap}
      2. + *
      3. {@link PopConsumerCache#cleanupRecords} drains + * {@link #removeTreeMap} — true-expired records are revived, + * approaching-expired records are written to the KVStore
      4. + *
      + */ protected static class ConsumerRecords { private final String groupId; private final String topicId; private final int queueId; private final BrokerConfig brokerConfig; + /** + * Staged records awaiting cleanup (revival or KVStore write). + * + *

      Populated by {@link #stageExpiredRecords} and drained by + * {@link PopConsumerCache#cleanupRecords}. Sorted by offset + * so that {@link #getMinOffset} can include these records in + * the minimum offset computation. + */ private final ConcurrentSkipListMap removeTreeMap; + /** + * Active (in-flight) records that have been popped but not yet + * acked by the consumer. + * + *

      Records are added via {@link #write} when messages are popped, + * removed via {@link #delete} when an ack arrives, and moved to + * {@link #removeTreeMap} via {@link #stageExpiredRecords} when + * the visibility timeout or stay-buffer time expires. + * + *

      Sorted by offset for efficient minimum-offset queries + * ({@link #getMinOffset}). + */ private final ConcurrentSkipListMap recordTreeMap; public ConsumerRecords(BrokerConfig brokerConfig, String groupId, String topicId, int queueId) { From 9623e4937c8f2dc77a172806313d39e8159a6729 Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Sat, 30 May 2026 11:33:48 +0800 Subject: [PATCH 110/157] comment: add attribute comments to consumerRecordTable of PopConsumerCache --- .../org/apache/rocketmq/broker/pop/PopConsumerCache.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerCache.java b/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerCache.java index 2fa2b2b95da..b7795f1883e 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerCache.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerCache.java @@ -70,7 +70,14 @@ public class PopConsumerCache extends ServiceThread { private final Consumer reviveConsumer; private final AtomicInteger estimateCacheSize; - // key: groupId@topicId@queueId + /** + * Maps {@code consumerGroupId@topicId@queueId} to the buffered records for that + * consumer-queue. + * + *

      Used by {@link #writeRecords} to add popped messages, + * {@link #deleteRecords} to remove acked messages, and + * {@link #cleanupRecords} to process expired records. + */ private final ConcurrentMap consumerRecordTable; public PopConsumerCache(BrokerController brokerController, PopConsumerKVStore consumerRecordStore, From 85dd75273b98134b9c5b19b69f613e2674a68356 Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Sat, 30 May 2026 11:41:32 +0800 Subject: [PATCH 111/157] comment: add method comments to writeRecords of PopConsumerCache --- .../apache/rocketmq/broker/pop/PopConsumerCache.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerCache.java b/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerCache.java index b7795f1883e..d9017f2ff73 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerCache.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerCache.java @@ -121,9 +121,20 @@ public long getPopInFlightMessageCount(String groupId, String topicId, int queue return consumerRecords != null ? consumerRecords.getInFlightRecordCount() : 0L; } + /** + * Write popped records into the cache. + * + *

      Each record is inserted into the {@link ConsumerRecords} for its + * {@code groupId@topicId@queueId}. If no entry exists for that key, a + * new one is created. The cache size estimate is incremented. + * + * @param consumerRecordList the popped records to cache + */ public void writeRecords(List consumerRecordList) { this.estimateCacheSize.addAndGet(consumerRecordList.size()); consumerRecordList.forEach(consumerRecord -> { + // consumerRecords is the recordMap in cache + // it contains two maps of PopConsumerRecord ConsumerRecords consumerRecords = ConcurrentHashMapUtils.computeIfAbsent(consumerRecordTable, this.getKey(consumerRecord), k -> new ConsumerRecords(brokerController.getBrokerConfig(), consumerRecord.getGroupId(), consumerRecord.getTopicId(), consumerRecord.getQueueId())); From d7078e1f5f48490b9b226c64b5722411d65f907c Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Sat, 30 May 2026 11:44:34 +0800 Subject: [PATCH 112/157] comment: add cache structure to PopConsumerCache --- .../apache/rocketmq/broker/pop/PopConsumerCache.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerCache.java b/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerCache.java index d9017f2ff73..94a69135d81 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerCache.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerCache.java @@ -39,6 +39,17 @@ * In-memory cache for un-acked Pop consumer records, used when * {@code enablePopBufferMerge} is enabled in the KVStore path. * + *

      + * The cache structure is as follows: { + * groupId@topicId@queueId: { + * active: ConcurrentSkipListMap, + * removed: ConcurrentSkipListMap + * } + * } + * active(recordTreeMap): in-flight records + * removed(removedTreeMap): records to be removed + *

      + * *

      Popped messages are stored here by * {@link PopConsumerService#popAsync}. The background {@link #run()} thread * periodically scans the cache and processes expired records: From 60fd9fad8d5f04a953e63adfd7021105b764b47a Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Sat, 30 May 2026 12:12:04 +0800 Subject: [PATCH 113/157] comment: add method comments to hold of AbstractRocksDBStorage --- .../rocketmq/common/config/AbstractRocksDBStorage.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/common/src/main/java/org/apache/rocketmq/common/config/AbstractRocksDBStorage.java b/common/src/main/java/org/apache/rocketmq/common/config/AbstractRocksDBStorage.java index 4875ce43e22..9261ab2e819 100644 --- a/common/src/main/java/org/apache/rocketmq/common/config/AbstractRocksDBStorage.java +++ b/common/src/main/java/org/apache/rocketmq/common/config/AbstractRocksDBStorage.java @@ -175,6 +175,16 @@ protected void initFlushOptions() { this.flushOptions = new FlushOptions(); } + /** + * check RocksDB status. isReady maybe a better name. + * + *

      Called before every read/write operation. Returns {@code true} if the + * database is fully loaded, the handle is non-null, and the instance has not + * been closed (e.g. due to a scheduled reload). Subclasses may override + * {@link #release()} to pair with this call (e.g. for reference counting). + * + * @return {@code true} if the database is ready for operations + */ public boolean hold() { if (!this.loaded || this.db == null || this.closed) { LOGGER.error("hold rocksdb Failed. {}", this.dbPath); From 0534da3ca5c124bb6a17df2845bb380503a88458 Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Sat, 30 May 2026 18:46:47 +0800 Subject: [PATCH 114/157] comment: add in-line comments to method initOptions of PopConsumerRocksdbStore --- .../broker/pop/PopConsumerRocksdbStore.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerRocksdbStore.java b/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerRocksdbStore.java index dc68f9d9fe5..a74be34a415 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerRocksdbStore.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerRocksdbStore.java @@ -58,25 +58,39 @@ public PopConsumerRocksdbStore(String filePath, long blockCacheSize, long writeB // https://www.cnblogs.com/renjc/p/rocksdb-class-db.html // https://github.com/johnzeng/rocksdb-doc-cn/blob/master/doc/RocksDB-Tuning-Guide.md protected void initOptions() { + // durability-first: enable WAL and sync flush for pop state recovery this.options = RocksDBOptionsFactory.createDBOptions(); this.writeOptions = new WriteOptions(); + // fsync every write to disk this.writeOptions.setSync(true); + // enable WAL this.writeOptions.setDisableWAL(false); + // allow writing throttling under pressure this.writeOptions.setNoSlowdown(false); + // delete must be durable too — otherwise ack can be lost and message revived incorrectly this.deleteOptions = new WriteOptions(); this.deleteOptions.setSync(true); this.deleteOptions.setDisableWAL(false); this.deleteOptions.setNoSlowdown(false); + // aggressive compaction to purge expired pop records and reclaim space this.compactRangeOptions = new CompactRangeOptions(); + // force compact bottom level this.compactRangeOptions.setBottommostLevelCompaction( CompactRangeOptions.BottommostLevelCompaction.kForce); + // allow compaction to pause writes this.compactRangeOptions.setAllowWriteStall(true); + // manual compaction runs in parallel with auto-compaction. + // Appropriate here because expired Pop records generate tombstones continuously, + // and cleanup should not starve RocksDB's normal background work this.compactRangeOptions.setExclusiveManualCompaction(false); + // Allows compaction to move data across levels this.compactRangeOptions.setChangeLevel(true); + // -1 delegates level selection to RocksDB's internal heuristics this.compactRangeOptions.setTargetLevel(-1); + // Splits the compaction work into at most 4 parallel sub-tasks this.compactRangeOptions.setMaxSubcompactions(4); } From b7c998822072bb9995e9cff092f9a13f77c697c0 Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Sat, 30 May 2026 18:49:59 +0800 Subject: [PATCH 115/157] comment: add method and inline comments to initOptions of PopConsumerRocksdbStore --- .../broker/pop/PopConsumerRocksdbStore.java | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerRocksdbStore.java b/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerRocksdbStore.java index a74be34a415..3b77cb534ad 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerRocksdbStore.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerRocksdbStore.java @@ -55,8 +55,18 @@ public PopConsumerRocksdbStore(String filePath, long blockCacheSize, long writeB this.writeBufferSize = writeBufferSize; } - // https://www.cnblogs.com/renjc/p/rocksdb-class-db.html - // https://github.com/johnzeng/rocksdb-doc-cn/blob/master/doc/RocksDB-Tuning-Guide.md + /** + * Configure RocksDB options for Pop consumer record storage. + * + *

      Unlike the parent class defaults, write and delete options enable + * WAL and synchronous flush — Pop visibility state is the sole source + * of truth and must survive crashes. Compaction is configured to be + * aggressive so that expired-then-deleted records are purged promptly, + * reclaiming disk space. + * + * @see rocksdb-class-db + * @see RocksDB-Tuning-Guide + */ protected void initOptions() { // durability-first: enable WAL and sync flush for pop state recovery this.options = RocksDBOptionsFactory.createDBOptions(); From 0852219ffc4227ea1c5650eb16eefcfd3edb17b8 Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Sat, 30 May 2026 22:56:43 +0800 Subject: [PATCH 116/157] comment: add method comments to postLoad of PopConsumerRocksdbStore --- .../broker/pop/PopConsumerRocksdbStore.java | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerRocksdbStore.java b/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerRocksdbStore.java index 3b77cb534ad..554af8b1ea2 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerRocksdbStore.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerRocksdbStore.java @@ -104,6 +104,22 @@ protected void initOptions() { this.compactRangeOptions.setMaxSubcompactions(4); } + /** + * Initialise the RocksDB instance with a dedicated column family for Pop state. + * + *

      Two column families are created: + *

        + *
      1. {@code default} — unused, required by RocksDB
      2. + *
      3. {@code "popState"} — stores Pop consumer records keyed by + * {@code visibilityTimeout|groupId@topicId@queueId@offset}
      4. + *
      + * + *

      Called by {@link AbstractRocksDBStorage#start()} before the storage + * is marked as loaded. Returns {@code false} if any step fails, preventing + * all subsequent read/write operations via {@link #hold()}. + * + * @return {@code true} if the database was opened successfully + */ @Override protected boolean postLoad() { try { From 8a7d4d3e7cf83cbad1425ec1cbd9b4a478f6d065 Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Sat, 30 May 2026 22:59:19 +0800 Subject: [PATCH 117/157] comment: add method comments to writeRecords of PopConsumerRocksdbStore --- .../rocketmq/broker/pop/PopConsumerRocksdbStore.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerRocksdbStore.java b/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerRocksdbStore.java index 554af8b1ea2..26073f1f0bc 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerRocksdbStore.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerRocksdbStore.java @@ -151,6 +151,14 @@ public String getFilePath() { return this.dbPath; } + /** + * Batch-write consumer records to RocksDB via a single {@link WriteBatch}. + * + *

      Each record is serialized with its visibility-timeout-prefixed key + * so that {@link #scanExpiredRecords} can efficiently scan by time range. + * + * @param consumerRecordList the records to persist + */ @Override public void writeRecords(List consumerRecordList) { if (!consumerRecordList.isEmpty()) { From 7571b119d5688c94d0207909b5e2b920b2540eb8 Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Sat, 30 May 2026 23:05:54 +0800 Subject: [PATCH 118/157] comment: add method comments to deleteRecords of PopConsumerRocksdbStore --- .../rocketmq/broker/pop/PopConsumerRocksdbStore.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerRocksdbStore.java b/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerRocksdbStore.java index 26073f1f0bc..78244fe1bc3 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerRocksdbStore.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerRocksdbStore.java @@ -173,6 +173,14 @@ public void writeRecords(List consumerRecordList) { } } + /** + * Batch-delete consumer records from RocksDB via a single {@link WriteBatch}. + * + *

      Deletion uses the same durability guarantees as writes ({@code sync=true}, + * WAL enabled) + * + * @param consumerRecordList the records to remove + */ @Override public void deleteRecords(List consumerRecordList) { if (!consumerRecordList.isEmpty()) { From cc63393d5c44e8427f0d79f3dfaf0e325a5d0969 Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Sat, 30 May 2026 23:08:48 +0800 Subject: [PATCH 119/157] comment: add method comments to scanExpiredRecords of PopConsumerRocksdbStore --- .../broker/pop/PopConsumerRocksdbStore.java | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerRocksdbStore.java b/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerRocksdbStore.java index 78244fe1bc3..19f4f28c2ca 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerRocksdbStore.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerRocksdbStore.java @@ -153,6 +153,8 @@ public String getFilePath() { /** * Batch-write consumer records to RocksDB via a single {@link WriteBatch}. + * Key: timestamp(8) + groupId + topicId + queueId + offset + * value: PopConsumerRecord.toJsonBytes * *

      Each record is serialized with its visibility-timeout-prefixed key * so that {@link #scanExpiredRecords} can efficiently scan by time range. @@ -195,8 +197,19 @@ public void deleteRecords(List consumerRecordList) { } } + /** + * Scan and return expired consumer records within a visibility-timeout range. + * + *

      Because each record's key is prefixed with {@code visibilityTimeout}, + * this method uses a RocksDB iterator bounded by {@code [lower, upper)} to + * efficiently scan only the relevant time window without a full table scan. + * + * @param lower inclusive lower bound of the visibility timeout (ms) + * @param upper exclusive upper bound of the visibility timeout (ms) + * @param maxCount maximum number of records to return + * @return up to {@code maxCount} expired records, or an empty list + */ @Override - // https://github.com/facebook/rocksdb/issues/10300 public List scanExpiredRecords(long lower, long upper, int maxCount) { // In RocksDB, we can use SstPartitionerFixedPrefixFactory in cfOptions // and new ColumnFamilyOptions().useFixedLengthPrefixExtractor() to From 627c1f922565ddd85cb470251bb6eff1514c31c8 Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Sun, 31 May 2026 15:55:08 +0800 Subject: [PATCH 120/157] comment: add method comments to ackAsync of PopConsumerService --- .../broker/pop/PopConsumerService.java | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerService.java b/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerService.java index e527fb7ad0c..2ef2f7f81d8 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerService.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerService.java @@ -610,6 +610,26 @@ public CompletableFuture popAsync(String clientHost, long po return getMessageFuture; } + /** + * Delete the acked record from the cache and/or RocksDB store. + * + *

      The deletion is a two-step fallback: + *

        + *
      • First, the record is deleted from {@link PopConsumerCache} (if buffer + * merge is enabled). If the record was present in the cache and removed + * successfully, the operation returns immediately without touching RocksDB
      • + *
      • If the cache is not enabled or the record was not found in the cache, + * deletion falls through to {@link PopConsumerKVStore#deleteRecords}
      • + *
      + * + * @param popTime the original pop time of the message + * @param invisibleTime the original visibility timeout + * @param groupId consumer group id + * @param topicId topic name + * @param queueId queue id + * @param offset the acked offset + * @return a future that completes with {@code true} on success + */ // Notify polling request when receive orderly ack public CompletableFuture ackAsync( long popTime, long invisibleTime, String groupId, String topicId, int queueId, long offset) { From 0bc9ab2645831bfa61007a0475308a9a656d4c5e Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Sun, 31 May 2026 15:58:24 +0800 Subject: [PATCH 121/157] comment: add method comments to changeInvisibilityDuration of PopConsumerService --- .../broker/pop/PopConsumerService.java | 28 +++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerService.java b/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerService.java index 2ef2f7f81d8..76ca601f64e 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerService.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerService.java @@ -622,6 +622,8 @@ public CompletableFuture popAsync(String clientHost, long po * deletion falls through to {@link PopConsumerKVStore#deleteRecords} * * + *

      memo: Notify polling request when receive orderly ack + * * @param popTime the original pop time of the message * @param invisibleTime the original visibility timeout * @param groupId consumer group id @@ -630,7 +632,6 @@ public CompletableFuture popAsync(String clientHost, long po * @param offset the acked offset * @return a future that completes with {@code true} on success */ - // Notify polling request when receive orderly ack public CompletableFuture ackAsync( long popTime, long invisibleTime, String groupId, String topicId, int queueId, long offset) { @@ -652,7 +653,30 @@ public CompletableFuture ackAsync( return CompletableFuture.completedFuture(true); } - // refer ChangeInvisibleTimeProcessor.appendCheckPointThenAckOrigin + /** + * Extend the visibility timeout of a popped message (KVStore path). + * + *

      refer: ChangeInvisibleTimeProcessor.appendCheckPointThenAckOrigin + * This is the KVStore equivalent of {@code ChangeInvisibleTimeProcessor#appendCheckPointThenAckOrigin}. + * + *

      A new record with the updated timeout is written to the KVStore, and the + * old record (identified by the original {@code popTime + invisibleTime}) is + * deleted from the cache and KVStore. + * + *

      If the new and old records have the same visibility timeout (e.g. the + * consumer extended by the same duration it already had), the delete one is + * skipped because the write one already overwrites the old record in RocksDB. + * + * @param popTime the original pop time + * @param invisibleTime the original visibility timeout + * @param changedPopTime the new pop time (typically current time) + * @param changedInvisibleTime the new visibility timeout + * @param groupId consumer group id + * @param topicId topic name + * @param queueId queue id + * @param offset the message offset + * @param suspend whether to suspend (nack without incrementing reconsume count) + */ public void changeInvisibilityDuration(long popTime, long invisibleTime, long changedPopTime, long changedInvisibleTime, String groupId, String topicId, int queueId, long offset, boolean suspend) { From af6ec7b3d9c395eac492d93f10355544795fdfdb Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Sun, 31 May 2026 23:37:17 +0800 Subject: [PATCH 122/157] comment: add in-line comments to attribute invisibleTime --- .../org/apache/rocketmq/broker/pop/PopConsumerService.java | 3 ++- .../proxy/grpc/v2/consumer/ReceiveMessageActivity.java | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerService.java b/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerService.java index 76ca601f64e..5aa0e23b20a 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerService.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerService.java @@ -695,6 +695,7 @@ public void changeInvisibilityDuration(long popTime, long invisibleTime, long ch // No need to generate new records when the group does not exist, // because these retry messages will not be consumed by anyone. + // default value of popReviveSkipIfGroupAbsent is true boolean skipWrite = brokerConfig.isPopReviveSkipIfGroupAbsent() && !brokerController.getSubscriptionGroupManager().containsSubscriptionGroup(groupId); @@ -712,7 +713,7 @@ public void changeInvisibilityDuration(long popTime, long invisibleTime, long ch } // If the new CK has the same key as the old CK (same visibilityTimeout), - // the write already overwrites the old record in RocksDB, skip delete + // the write one already overwrites the old record in RocksDB, skip delete // to avoid removing the newly written record. if (skipWrite || ckRecord.getVisibilityTimeout() != ackRecord.getVisibilityTimeout()) { this.popConsumerStore.deleteRecords(Collections.singletonList(ackRecord)); diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/consumer/ReceiveMessageActivity.java b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/consumer/ReceiveMessageActivity.java index ed32e3d5e61..4ecafdf5ce0 100644 --- a/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/consumer/ReceiveMessageActivity.java +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/consumer/ReceiveMessageActivity.java @@ -115,9 +115,13 @@ public void receiveMessage(ProxyContext ctx, ReceiveMessageRequest request, String topic = request.getMessageQueue().getTopic().getName(); String group = request.getGroup().getName(); + // invisibleTime was set by client + // proxy can override it long actualInvisibleTime = Durations.toMillis(request.getInvisibleDuration()); ProxyConfig proxyConfig = ConfigurationManager.getProxyConfig(); + // default enableProxyAutoRenew is true if (proxyConfig.isEnableProxyAutoRenew() && request.getAutoRenew()) { + // default defaultInvisibleTimeMills is 60s actualInvisibleTime = proxyConfig.getDefaultInvisibleTimeMills(); } else { validateInvisibleTime(actualInvisibleTime, From 2434070fb21863413d77f46057795f85bf87c7a1 Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Mon, 1 Jun 2026 10:19:11 +0800 Subject: [PATCH 123/157] comment: optimize getKeyBytes comment in PopConsumerRecord --- .../rocketmq/broker/pop/PopConsumerRecord.java | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerRecord.java b/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerRecord.java index 8b268a9fc6f..73e0ac7b6f9 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerRecord.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerRecord.java @@ -124,7 +124,19 @@ public long getVisibilityTimeout() { } /** - * Key: timestamp(8) + groupId + topicId + queueId + offset + * Build the RocksDB key for this record. + * + *

      Format: + *

      +     * visibilityTimeout(8B) + groupId + '@' + topicId + '@' + queueId(4B) + '@' + offset(8B)
      +     * 
      + * + *

      The {@code visibilityTimeout} is placed first so that records are ordered + * by expiration time in RocksDB's SST files. This allows + * {@code PopConsumerRocksdbStore#scanExpiredRecords} to use a bounded iterator + * to scan only the relevant time window without a full table scan. + * + *

      NACK(changeInvisibleTime) will create a new record, and the old one will be deleted. */ @JSONField(serialize = false) public byte[] getKeyBytes() { From 68c2ec58a5ac2515307df472188790dbab3ea00a Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Mon, 1 Jun 2026 10:19:28 +0800 Subject: [PATCH 124/157] comment: update writeRecords comment to match getKeyBytes --- .../org/apache/rocketmq/broker/pop/PopConsumerRocksdbStore.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerRocksdbStore.java b/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerRocksdbStore.java index 19f4f28c2ca..a19c2c293bb 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerRocksdbStore.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerRocksdbStore.java @@ -153,7 +153,7 @@ public String getFilePath() { /** * Batch-write consumer records to RocksDB via a single {@link WriteBatch}. - * Key: timestamp(8) + groupId + topicId + queueId + offset + * Key: (popTime + invisibleTime) + groupId + topicId + queueId + offset * value: PopConsumerRecord.toJsonBytes * *

      Each record is serialized with its visibility-timeout-prefixed key From 8ecba02bb66a11b90a70afe94e9de8a20bf4f9e9 Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Mon, 1 Jun 2026 10:23:34 +0800 Subject: [PATCH 125/157] comment: add attribute comments to invisibleTime of PopConsumerRecord --- .../apache/rocketmq/broker/pop/PopConsumerRecord.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerRecord.java b/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerRecord.java index 73e0ac7b6f9..73c85311614 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerRecord.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerRecord.java @@ -58,6 +58,15 @@ public int getCode() { @JSONField(ordinal = 4) private int retryFlag; + /** + * Message visibility timeout in milliseconds. + * + *

      The visibility timeout ({@code popTime + invisibleTime}) determines when + * a popped-but-unacked message becomes eligible for revival. Set by the + * consumer (default 60s via {@code DefaultMQPushConsumer#setPopInvisibleTime}). + * Can be changed by proxy with config. + * Can be extended via {@code ChangeInvisibleTime}. + */ @JSONField(ordinal = 5) private long invisibleTime; From 3fb081f33e1aacf6efb4df0c45be3c6175e11f6d Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Mon, 1 Jun 2026 11:58:30 +0800 Subject: [PATCH 126/157] comment: add method comments to getMessageAsync overloads of PopConsumerService --- .../broker/pop/PopConsumerService.java | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerService.java b/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerService.java index 5aa0e23b20a..1ff6f31b818 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerService.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerService.java @@ -234,6 +234,7 @@ public PopConsumerContext handleGetMessageResult(PopConsumerContext context, Get /** * Retrieve the starting consume offset for a pop request. + * should be private, no external callers. * *

      For FIFO consumers, the offset is read from the regular consumer offset. * For non-FIFO consumers, a separate pull offset is used (compatibility with @@ -287,6 +288,25 @@ public long getPopOffset(String groupId, String topicId, int queueId, int initMo return resetOffset != null ? resetOffset : offset; } + /** + * Fetch messages from the store with automatic offset correction. + * No external callers, except unit tests. + * + *

      If the stored offset is behind the actual consume queue offset + * ({@code OFFSET_TOO_SMALL}, {@code OFFSET_OVERFLOW_BADLY}, + * {@code OFFSET_FOUND_NULL}), the offset is corrected and a retry is + * issued with the corrected offset. This prevents duplicate messages + * when the Pop buffer offset has not yet been committed. + * + * @param clientHost the client address + * @param groupId consumer group id + * @param topicId topic name + * @param queueId queue id + * @param offset the consume offset to start from + * @param batchSize max number of messages + * @param filter message filter + * @return a future completing with the fetch result + */ public CompletableFuture getMessageAsync(String clientHost, String groupId, String topicId, int queueId, long offset, int batchSize, MessageFilter filter) { @@ -353,6 +373,7 @@ public boolean isFifoBlocked(PopConsumerContext context, String groupId, String /** * Fetch messages from a single queue and append them to the pop context. + * No external callers, except unit tests. * *

      Chained via {@link CompletableFuture#thenCompose} from * {@link #getMessageFromTopicAsync}. When the batch is already full @@ -720,6 +741,17 @@ public void changeInvisibilityDuration(long popTime, long invisibleTime, long ch } } + /** + * Read the original message from storage for revival. + * No external callers, except unit tests. + * + *

      Used by {@link #revive(PopConsumerRecord)} when a visibility timeout + * expires. Delegates to {@link org.apache.rocketmq.broker.EscapeBridge} + * which can read from either the local store or a remote broker's store. + * + * @param consumerRecord the expired record + * @return a triple of (message, info, needRetry) + */ // Use broker escape bridge to support remote read public CompletableFuture> getMessageAsync(PopConsumerRecord consumerRecord) { return this.brokerController.getEscapeBridge().getMessageAsync(consumerRecord.getTopicId(), From 057eb184fc50120d237ea22a7fa0f333a765043d Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Mon, 1 Jun 2026 12:12:11 +0800 Subject: [PATCH 127/157] comment: add external caller info to methods of PopConsumerService --- .../rocketmq/broker/pop/PopConsumerService.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerService.java b/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerService.java index 1ff6f31b818..c9f88273830 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerService.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerService.java @@ -109,6 +109,7 @@ public PopConsumerService(BrokerController brokerController) { } /** + * No external callers, only called by unit tests. * In-flight messages are those that have been received from a queue * by a consumer but have not yet been deleted. For standard queues, * there is a limit on the number of in-flight messages, depending on queue traffic and message backlog. @@ -119,6 +120,7 @@ public boolean isPopShouldStop(String group, String topic, int queueId) { brokerConfig.getPopInflightMessageThreshold(); } + // No external callers, only called by unit tests. public long getPendingFilterCount(String groupId, String topicId, int queueId) { try { long maxOffset = this.brokerController.getMessageStore().getMaxOffsetInQueue(topicId, queueId); @@ -129,6 +131,7 @@ public long getPendingFilterCount(String groupId, String topicId, int queueId) { } } + // No external callers, only called by unit tests. public GetMessageResult recodeRetryMessage(GetMessageResult getMessageResult, String topicId, long offset, long popTime, long invisibleTime) { @@ -170,6 +173,7 @@ public GetMessageResult recodeRetryMessage(GetMessageResult getMessageResult, /** * Merge a GetMessageResult into the pop context and commit the consumer offset. + * No external callers, only called by unit tests. * *

      If messages were found: *

        @@ -352,6 +356,7 @@ public CompletableFuture getMessageAsync(String clientHost, /** * Fifo message does not have retry feature in broker + * No external callers, only called by unit tests. */ public void setFifoBlocked(PopConsumerContext context, String groupId, String topicId, int queueId, List queueOffsetList, GetMessageResult getMessageResult) { @@ -360,6 +365,7 @@ public void setFifoBlocked(PopConsumerContext context, context.getPopTime(), context.getInvisibleTime(), queueOffsetList, context.getOrderCountInfoBuilder(), getMessageResult); } + // No external callers, only called by unit tests. public boolean isFifoBlocked(PopConsumerContext context, String groupId, String topicId, int queueId) { // If server-side reset offset is enabled, and there is a reset offset, // then return false to make sure that the reset offset takes effect. @@ -758,6 +764,7 @@ public CompletableFuture> getMessageAsync(Po consumerRecord.getOffset(), consumerRecord.getQueueId(), brokerConfig.getBrokerName(), false); } + // No external callers, only called by unit tests. public CompletableFuture revive(PopConsumerRecord record) { if (brokerConfig.isPopReviveSkipIfGroupAbsent() && @@ -781,12 +788,14 @@ public CompletableFuture revive(PopConsumerRecord record) { }); } + // No external callers, only called by unit tests. public void clearCache(String groupId, String topicId, int queueId) { if (popConsumerCache != null) { popConsumerCache.removeRecords(groupId, topicId, queueId); } } + // No external callers, only called by unit tests. public long revive(AtomicLong currentTime, int maxCount) { Stopwatch stopwatch = Stopwatch.createStarted(); long upperTime = System.currentTimeMillis() - 50L; @@ -853,6 +862,7 @@ public long revive(AtomicLong currentTime, int maxCount) { return consumerRecords.size(); } + // No external callers, only called by unit tests. public void createRetryTopicIfNeeded(String groupId, String retryTopic) { TopicConfig topicConfig = brokerController.getTopicConfigManager().selectTopicConfig(retryTopic); if (topicConfig != null && !brokerController.getBrokerConfig().isUseSeparateRetryQueue()) { @@ -885,6 +895,7 @@ public void createRetryTopicIfNeeded(String groupId, String retryTopic) { @SuppressWarnings("DuplicatedCode") // org.apache.rocketmq.broker.processor.PopReviveService#reviveRetry + // No external callers, only called by unit tests. public boolean reviveRetry(PopConsumerRecord record, MessageExt messageExt) { if (brokerConfig.isPopConsumerKVServiceLog()) { @@ -962,6 +973,7 @@ private int getRetryQueueId(String retryTopic, MessageExt oriMsg) { } // Export kv store record to revive topic + // admin service @SuppressWarnings("ExtractMethodRecommender") public synchronized void transferToFsStore() { Stopwatch stopwatch = Stopwatch.createStarted(); From 39eab9f5f232df57ed5f66d38db1e23ae0afc855 Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Mon, 1 Jun 2026 15:19:26 +0800 Subject: [PATCH 128/157] comment: add method comments to run of PopConsumerService --- .../rocketmq/broker/pop/PopConsumerService.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerService.java b/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerService.java index c9f88273830..17511785b34 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerService.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerService.java @@ -1053,6 +1053,19 @@ public void shutdown() { } } + /** + * Background thread that periodically revives expired Pop records. + * + *

        Each iteration: + *

          + *
        1. Calls {@link #revive(AtomicLong, int)} to scan the RocksDB store for + * records whose visibility timeout has elapsed, fetch the original + * message, and re-publish it to the retry topic
        2. + *
        3. Cleans up stale consumer locks every minute
        4. + *
        5. When the number of revived records is below the batch limit, sleeps + * for a short interval to avoid busy-waiting
        6. + *
        + */ @Override public void run() { this.consumerRunning.set(true); From 30932d09bb625d6731dfe027c999630cd93c6e47 Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Mon, 1 Jun 2026 15:44:05 +0800 Subject: [PATCH 129/157] comment: add method comments to revive overloads of PopConsumerService --- .../broker/pop/PopConsumerService.java | 34 +++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerService.java b/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerService.java index 17511785b34..f17121d17ff 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerService.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerService.java @@ -764,7 +764,17 @@ public CompletableFuture> getMessageAsync(Po consumerRecord.getOffset(), consumerRecord.getQueueId(), brokerConfig.getBrokerName(), false); } - // No external callers, only called by unit tests. + /** + * Revive a single expired record by re-publishing it to the retry topic. + * No external callers, only called by unit tests. + * + *

        Skips the record if the consumer group no longer exists. + * Otherwise, reads the original message, + * and re-publishes it via {@link #reviveRetry}. + * + * @param record the expired record to revive + * @return a future completing with {@code true} on success + */ public CompletableFuture revive(PopConsumerRecord record) { if (brokerConfig.isPopReviveSkipIfGroupAbsent() && @@ -795,7 +805,27 @@ public void clearCache(String groupId, String topicId, int queueId) { } } - // No external callers, only called by unit tests. + /** + * Scan the KVStore for expired records and revive them. + * No external callers, only called by unit tests. + * + *

        This is the core revival loop called by {@link #run()}: + *

          + *
        1. Scans {@link PopConsumerKVStore#scanExpiredRecords} for records + * whose visibility timeout falls within {@code [currentTime-3s, now)}
        2. + *
        3. For each expired record, calls {@link #revive(PopConsumerRecord)} to + * read the original message and re-publish it to the retry topic. + * Concurrency is controlled by a semaphore
        4. + *
        5. Failed revive attempts are retried with exponential backoff via a + * new record with increased {@code invisibleTime} and + * {@code attemptTimes}
        6. + *
        7. After the maximum retry attempts, the record is dropped
        8. + *
        + * + * @param currentTime tracks the last scanned visibility timeout (for incremental progress) + * @param maxCount maximum number of records to process per batch + * @return the number of consumed (revived) records + */ public long revive(AtomicLong currentTime, int maxCount) { Stopwatch stopwatch = Stopwatch.createStarted(); long upperTime = System.currentTimeMillis() - 50L; From ce2758dac572c00c963f5e4abf202e0bd8aaf3c3 Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Mon, 1 Jun 2026 16:04:16 +0800 Subject: [PATCH 130/157] comment: update revive maxCount comment --- .../java/org/apache/rocketmq/broker/pop/PopConsumerService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerService.java b/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerService.java index f17121d17ff..453c4057542 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerService.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerService.java @@ -823,7 +823,7 @@ public void clearCache(String groupId, String topicId, int queueId) { *
    * * @param currentTime tracks the last scanned visibility timeout (for incremental progress) - * @param maxCount maximum number of records to process per batch + * @param maxCount maximum number of records to process per batch(load from config: 16 * 1024) * @return the number of consumed (revived) records */ public long revive(AtomicLong currentTime, int maxCount) { From d0c1ea0f147903b83d9a7704e1f7210e100882b3 Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Mon, 1 Jun 2026 17:26:50 +0800 Subject: [PATCH 131/157] comment: add in-line method revive of PopConsumerService --- .../rocketmq/broker/pop/PopConsumerService.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerService.java b/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerService.java index 453c4057542..3e7c4cde4f4 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerService.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerService.java @@ -829,8 +829,12 @@ public void clearCache(String groupId, String topicId, int queueId) { public long revive(AtomicLong currentTime, int maxCount) { Stopwatch stopwatch = Stopwatch.createStarted(); long upperTime = System.currentTimeMillis() - 50L; + + // scan expired records List consumerRecords = this.popConsumerStore.scanExpiredRecords( currentTime.get() - TimeUnit.SECONDS.toMillis(3), upperTime, maxCount); + + // init context params long scanCostTime = stopwatch.elapsed(TimeUnit.MILLISECONDS); // When reading messages from local storage, the current thread is used @@ -844,6 +848,7 @@ public long revive(AtomicLong currentTime, int maxCount) { // could merge read operation here for (PopConsumerRecord record : consumerRecords) { CompletableFuture future; + // revive record try { semaphore.acquire(); future = this.revive(record); @@ -851,6 +856,8 @@ public long revive(AtomicLong currentTime, int maxCount) { semaphore.release(); throw new RuntimeException(e); } + + // add future result to futureList futureList.add(future.thenAccept(result -> { if (!result) { if (record.getAttemptTimes() < brokerConfig.getPopReviveMaxAttemptTimes()) { @@ -870,9 +877,14 @@ public long revive(AtomicLong currentTime, int maxCount) { }).whenComplete((result, ex) -> semaphore.release())); } + // wait for all futures to complete CompletableFuture.allOf(futureList.toArray(new CompletableFuture[0])).join(); + + // then restore failure records and delete successful records this.popConsumerStore.writeRecords(new ArrayList<>(failureList)); this.popConsumerStore.deleteRecords(consumerRecords); + + // set currentTime and logging currentTime.set(consumerRecords.isEmpty() ? upperTime : consumerRecords.get(consumerRecords.size() - 1).getVisibilityTimeout()); From dd187454ab8d606ab670b660c8286d864067c26c Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Mon, 1 Jun 2026 17:30:51 +0800 Subject: [PATCH 132/157] comment: add in-line method reviveRetry of PopConsumerService --- .../org/apache/rocketmq/broker/pop/PopConsumerService.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerService.java b/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerService.java index 3e7c4cde4f4..5aa09d5d43a 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerService.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerService.java @@ -794,6 +794,7 @@ public CompletableFuture revive(PopConsumerRecord record) { log.info("PopConsumerService revive no need retry, record={}", record); return CompletableFuture.completedFuture(!result.getRight()); } + return CompletableFuture.completedFuture(this.reviveRetry(record, result.getLeft())); }); } @@ -946,11 +947,13 @@ public boolean reviveRetry(PopConsumerRecord record, MessageExt messageExt) { record.getQueueId(), record.getOffset()); } + // create retry topic if needed boolean retry = StringUtils.startsWith(record.getTopicId(), MixAll.RETRY_GROUP_TOPIC_PREFIX); String retryTopic = retry ? record.getTopicId() : KeyBuilder.buildPopRetryTopic( record.getTopicId(), record.getGroupId(), brokerConfig.isEnableRetryTopicV2()); this.createRetryTopicIfNeeded(record.getGroupId(), retryTopic); + // create retry message // deep copy here MessageExtBrokerInner msgInner = new MessageExtBrokerInner(); msgInner.setTopic(retryTopic); From e4bdb2b53e85a0a56fb1ff9dd7ee80be8c481b86 Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Mon, 1 Jun 2026 17:34:48 +0800 Subject: [PATCH 133/157] comment: add scan time comments to method revive of PopConsumerService --- .../java/org/apache/rocketmq/broker/pop/PopConsumerService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerService.java b/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerService.java index 5aa09d5d43a..a1ed90cc339 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerService.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerService.java @@ -831,7 +831,7 @@ public long revive(AtomicLong currentTime, int maxCount) { Stopwatch stopwatch = Stopwatch.createStarted(); long upperTime = System.currentTimeMillis() - 50L; - // scan expired records + // scan expired records between [currentTime-3s, now-50ms)] List consumerRecords = this.popConsumerStore.scanExpiredRecords( currentTime.get() - TimeUnit.SECONDS.toMillis(3), upperTime, maxCount); From 397eb379433e9e09ec1687837f3754248c6c8788 Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Mon, 1 Jun 2026 17:37:13 +0800 Subject: [PATCH 134/157] comment: add in-line method scanExpiredRecords of PopConsumerService --- .../org/apache/rocketmq/broker/pop/PopConsumerRocksdbStore.java | 1 + 1 file changed, 1 insertion(+) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerRocksdbStore.java b/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerRocksdbStore.java index a19c2c293bb..b58bbf2945b 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerRocksdbStore.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerRocksdbStore.java @@ -222,6 +222,7 @@ public List scanExpiredRecords(long lower, long upper, int ma RocksIterator iterator = db.newIterator(this.columnFamilyHandle, scanOptions)) { iterator.seek(ByteBuffer.allocate(Long.BYTES).putLong(lower).array()); while (iterator.isValid() && consumerRecordList.size() < maxCount) { + // decode json bytes to PopConsumerRecord consumerRecordList.add(PopConsumerRecord.decode(iterator.value())); iterator.next(); } From 1bf22dc64654bb0166749ddc0414c8411e10c222 Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Mon, 1 Jun 2026 17:51:45 +0800 Subject: [PATCH 135/157] comment: add class comments to PopConsumerRocksdbStore --- .../broker/pop/PopConsumerRocksdbStore.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerRocksdbStore.java b/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerRocksdbStore.java index b58bbf2945b..fcd5826853e 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerRocksdbStore.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerRocksdbStore.java @@ -38,6 +38,19 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +/** + * RocksDB-backed implementation of {@link PopConsumerKVStore} for the + * KVStore-based Pop ack path. + * + *

    Stores Pop consumer records in a dedicated {@code "popState"} column + * family. Each record is keyed by {@code visibilityTimeout|groupId@topicId@queueId@offset} + * so that {@link #scanExpiredRecords} can efficiently scan only expired + * records within a time window without a full table scan. + * + *

    Write and delete operations use synchronous flush and WAL for + * durability — Pop visibility state is the sole source of truth in the + * KVStore path and must survive crashes. + */ public class PopConsumerRocksdbStore extends AbstractRocksDBStorage implements PopConsumerKVStore { private static final Logger log = LoggerFactory.getLogger(LoggerName.ROCKETMQ_POP_LOGGER_NAME); From 80e6f98a6a8f2ca5cc658ed62d84639dd96625fd Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Mon, 1 Jun 2026 21:52:07 +0800 Subject: [PATCH 136/157] comment: add attribute comments to batchDispatchRequestQueue of DefaultMessageStore --- .../org/apache/rocketmq/store/DefaultMessageStore.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/store/src/main/java/org/apache/rocketmq/store/DefaultMessageStore.java b/store/src/main/java/org/apache/rocketmq/store/DefaultMessageStore.java index cb8389111a0..9f43892e303 100644 --- a/store/src/main/java/org/apache/rocketmq/store/DefaultMessageStore.java +++ b/store/src/main/java/org/apache/rocketmq/store/DefaultMessageStore.java @@ -211,6 +211,13 @@ public class DefaultMessageStore implements MessageStore { private final AtomicInteger mappedPageHoldCount = new AtomicInteger(0); + /** + * BatchDispatchRequest queue + * offer by ConcurrentReputMessageService.createBatchDispatchRequest() + * poll by MainBatchDispatchRequestService.pollBatchDispatchRequest() + * + *

    if enableBuildConsumeQueueConcurrently is false, It is useless + */ private final ConcurrentLinkedQueue batchDispatchRequestQueue = new ConcurrentLinkedQueue<>(); private final int dispatchRequestOrderlyQueueSize = 16; From 5327c636b82e4eeb8922d6fcb067405fd1b9702e Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Mon, 1 Jun 2026 22:08:01 +0800 Subject: [PATCH 137/157] comment: add in-line comments to method asyncPutMessage of DefaultMessageStore --- .../java/org/apache/rocketmq/store/DefaultMessageStore.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/store/src/main/java/org/apache/rocketmq/store/DefaultMessageStore.java b/store/src/main/java/org/apache/rocketmq/store/DefaultMessageStore.java index 9f43892e303..f0d9ab25a1a 100644 --- a/store/src/main/java/org/apache/rocketmq/store/DefaultMessageStore.java +++ b/store/src/main/java/org/apache/rocketmq/store/DefaultMessageStore.java @@ -652,7 +652,7 @@ public long getMajorFileSize() { @Override public CompletableFuture asyncPutMessage(MessageExtBrokerInner msg) { - + // execute beforePutMessage hooks for (PutMessageHook putMessageHook : putMessageHookList) { PutMessageResult handleResult = putMessageHook.executeBeforePutMessage(msg); if (handleResult != null) { @@ -660,12 +660,14 @@ public CompletableFuture asyncPutMessage(MessageExtBrokerInner } } + // check inner batch message num if (msg.getProperties().containsKey(MessageConst.PROPERTY_INNER_NUM) && !MessageSysFlag.check(msg.getSysFlag(), MessageSysFlag.INNER_BATCH_FLAG)) { LOGGER.warn("[BUG]The message had property {} but is not an inner batch", MessageConst.PROPERTY_INNER_NUM); return CompletableFuture.completedFuture(new PutMessageResult(PutMessageStatus.MESSAGE_ILLEGAL, null)); } + // check inner batch message topic if (MessageSysFlag.check(msg.getSysFlag(), MessageSysFlag.INNER_BATCH_FLAG)) { Optional topicConfig = this.getTopicConfig(msg.getTopic()); if (!QueueTypeUtils.isBatchCq(topicConfig)) { @@ -674,9 +676,11 @@ public CompletableFuture asyncPutMessage(MessageExtBrokerInner } } + // put message long beginTime = this.getSystemClock().now(); CompletableFuture putResultFuture = this.commitLog.asyncPutMessage(msg); + // metrics putResultFuture.thenAccept(result -> { long elapsedTime = this.getSystemClock().now() - beginTime; if (elapsedTime > 500) { From 0294b853f98424d3bd938e7ba98a3e404f504c18 Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Mon, 1 Jun 2026 22:11:36 +0800 Subject: [PATCH 138/157] comment: add method comments to asyncPutMessage of DefaultMessageStore --- .../apache/rocketmq/store/DefaultMessageStore.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/store/src/main/java/org/apache/rocketmq/store/DefaultMessageStore.java b/store/src/main/java/org/apache/rocketmq/store/DefaultMessageStore.java index f0d9ab25a1a..c59803c4cf2 100644 --- a/store/src/main/java/org/apache/rocketmq/store/DefaultMessageStore.java +++ b/store/src/main/java/org/apache/rocketmq/store/DefaultMessageStore.java @@ -650,6 +650,17 @@ public long getMajorFileSize() { return commitLogSize + consumeQueueSize + indexFileSize; } + /** + * Asynchronously write a message to the commit log. + * + *

    Before writing, any registered {@link PutMessageHook} instances are + * invoked — a non-null result from a hook short-circuits the process. + * Inner-batch message flags are validated + * then the actual write is delegated to {@link CommitLog#asyncPutMessage}. + * + * @param msg the message to write + * @return a future that completes with the put result + */ @Override public CompletableFuture asyncPutMessage(MessageExtBrokerInner msg) { // execute beforePutMessage hooks From 9eefc30f9432d0e33bb969a4102388ead9f89e81 Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Mon, 1 Jun 2026 22:32:35 +0800 Subject: [PATCH 139/157] comment: add in-line comments to method asyncPutMessage of CommitLog --- .../java/org/apache/rocketmq/store/CommitLog.java | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/store/src/main/java/org/apache/rocketmq/store/CommitLog.java b/store/src/main/java/org/apache/rocketmq/store/CommitLog.java index 1c46f9e2ce5..97aa8940ac7 100644 --- a/store/src/main/java/org/apache/rocketmq/store/CommitLog.java +++ b/store/src/main/java/org/apache/rocketmq/store/CommitLog.java @@ -621,7 +621,7 @@ public DispatchRequest checkMessageAndReturnSize(java.nio.ByteBuffer byteBuffer, fullMessageBuffer.limit(messageStartPos + totalSize); byte[] fullMessageBytes = new byte[totalSize]; fullMessageBuffer.get(fullMessageBytes, 0, totalSize); - + // Print full message and especially properties log.warn( "CommitLog#checkAndDispatchMessage: failed to check message CRC, not found CRC in properties. topic={}, properties={}, propertiesLength={}, fullMessageHex={}", @@ -967,6 +967,7 @@ public void updateMaxMessageSize(PutMessageThreadLocal putMessageThreadLocal) { } public CompletableFuture asyncPutMessage(final MessageExtBrokerInner msg) { + // format message and int context params // Set the storage time if (!defaultMessageStore.getMessageStoreConfig().isDuplicationEnable()) { msg.setStoreTimestamp(System.currentTimeMillis()); @@ -1004,6 +1005,8 @@ public CompletableFuture asyncPutMessage(final MessageExtBroke updateMaxMessageSize(putMessageThreadLocal); String topicQueueKey = generateKey(putMessageThreadLocal.getKeyBuilder(), msg); long elapsedTimeInLock = 0; + + // locate mappedFile and get write position MappedFile unlockMappedFile = null; MappedFile mappedFile = this.mappedFileQueue.getLastMappedFile(); @@ -1014,6 +1017,7 @@ public CompletableFuture asyncPutMessage(final MessageExtBroke currOffset = mappedFile.getFileFromOffset() + mappedFile.getWrotePosition(); } + // handle HA int needAckNums = this.defaultMessageStore.getMessageStoreConfig().getInSyncReplicas(); boolean needHandleHA = needHandleHA(msg); @@ -1037,7 +1041,7 @@ public CompletableFuture asyncPutMessage(final MessageExtBroke topicQueueLock.lock(topicQueueKey); try { - + // assign consume queue offset boolean needAssignOffset = true; if (defaultMessageStore.getMessageStoreConfig().isDuplicationEnable() && defaultMessageStore.getMessageStoreConfig().getBrokerRole() != BrokerRole.SLAVE) { @@ -1047,6 +1051,7 @@ public CompletableFuture asyncPutMessage(final MessageExtBroke defaultMessageStore.assignOffset(msg); } + // encode message and ... PutMessageResult encodeResult = putMessageThreadLocal.getEncoder().encode(msg); if (encodeResult != null) { return CompletableFuture.completedFuture(encodeResult); @@ -1056,6 +1061,7 @@ public CompletableFuture asyncPutMessage(final MessageExtBroke putMessageLock.lock(); //spin or ReentrantLock, depending on store config try { + // validate and init some context params long beginLockTimestamp = this.defaultMessageStore.getSystemClock().now(); this.beginTimeInLock = beginLockTimestamp; @@ -1076,7 +1082,10 @@ public CompletableFuture asyncPutMessage(final MessageExtBroke return CompletableFuture.completedFuture(new PutMessageResult(PutMessageStatus.CREATE_MAPPED_FILE_FAILED, null)); } + // append to mappedFile result = mappedFile.appendMessage(msg, this.appendMessageCallback, putMessageContext); + + // check result, retry if needed, switch (result.getStatus()) { case PUT_OK: onCommitLogAppend(msg, result, mappedFile); @@ -1112,6 +1121,7 @@ public CompletableFuture asyncPutMessage(final MessageExtBroke beginTimeInLock = 0; putMessageLock.unlock(); } + // Increase queue offset when messages are successfully written if (AppendMessageStatus.PUT_OK.equals(result.getStatus())) { this.defaultMessageStore.increaseOffset(msg, getMessageNum(msg)); @@ -1122,6 +1132,7 @@ public CompletableFuture asyncPutMessage(final MessageExtBroke topicQueueLock.unlock(topicQueueKey); } + // unlock MappedFile and metrics if (elapsedTimeInLock > 500) { log.warn("[NOTIFYME]putMessage in lock cost time(ms)={}, bodyLength={} AppendMessageResult={}", elapsedTimeInLock, msg.getBody().length, result); } From 5a39553658babe89b383c8e12bab533ef6029375 Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Mon, 1 Jun 2026 22:35:31 +0800 Subject: [PATCH 140/157] comment: add method comments to asyncPutMessage of CommitLog --- .../org/apache/rocketmq/store/CommitLog.java | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/store/src/main/java/org/apache/rocketmq/store/CommitLog.java b/store/src/main/java/org/apache/rocketmq/store/CommitLog.java index 97aa8940ac7..7fc6e015a75 100644 --- a/store/src/main/java/org/apache/rocketmq/store/CommitLog.java +++ b/store/src/main/java/org/apache/rocketmq/store/CommitLog.java @@ -966,6 +966,24 @@ public void updateMaxMessageSize(PutMessageThreadLocal putMessageThreadLocal) { } } + /** + * Asynchronously encode and append a single message to the commit log. + * + *

    The method: + *

      + *
    1. Encodes the message and validates compat flags (V1/V2, IPv6)
    2. + *
    3. Acquires a per-topic-queue-lock for offset assignment, then the + * global put-message lock for the append
    4. + *
    5. Acquires the commitLog write lock for MappedFile writing
    6. + *
    7. Appends to the current mapped file; if full, opens a new file + * and retries
    8. + *
    9. After the append, increments the consume queue offset and + * triggers HA replication if configured
    10. + *
    + * + * @param msg the message to write + * @return a future that completes with the append result + */ public CompletableFuture asyncPutMessage(final MessageExtBrokerInner msg) { // format message and int context params // Set the storage time From 6a79a9b79d59b4c4743988bf2a76275c6c70badb Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Mon, 1 Jun 2026 22:39:19 +0800 Subject: [PATCH 141/157] comment: add method comments to handleDiskFlushAndHA of CommitLog --- .../java/org/apache/rocketmq/store/CommitLog.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/store/src/main/java/org/apache/rocketmq/store/CommitLog.java b/store/src/main/java/org/apache/rocketmq/store/CommitLog.java index 7fc6e015a75..28abebcf64d 100644 --- a/store/src/main/java/org/apache/rocketmq/store/CommitLog.java +++ b/store/src/main/java/org/apache/rocketmq/store/CommitLog.java @@ -1356,6 +1356,20 @@ private boolean needHandleHA(MessageExt messageExt) { return true; } + /** + * Wait for both disk flush and HA replication to complete, then merge results. + * + *

    Disk flush and HA replication run in parallel via + * {@link CompletableFuture#thenCombine}. If either fails, the combined + * result is updated with the failure status — both must succeed for + * the overall result to be {@code PUT_OK}. + * + * @param putMessageResult the append result to update + * @param messageExt the original message (needed by flush) + * @param needAckNums number of slave acks required (0/1 = no HA) + * @param needHandleHA whether HA replication is configured + * @return a future completing with the merged result + */ private CompletableFuture handleDiskFlushAndHA(PutMessageResult putMessageResult, MessageExt messageExt, int needAckNums, boolean needHandleHA) { CompletableFuture flushResultFuture = handleDiskFlush(putMessageResult.getAppendMessageResult(), messageExt); From 8d6c1897df4c10d09a794dfb94660dc9fd0073fe Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Tue, 2 Jun 2026 12:05:17 +0800 Subject: [PATCH 142/157] comment: add in-line comments to method getMessage of DefaultMessageStore --- .../org/apache/rocketmq/store/DefaultMessageStore.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/store/src/main/java/org/apache/rocketmq/store/DefaultMessageStore.java b/store/src/main/java/org/apache/rocketmq/store/DefaultMessageStore.java index c59803c4cf2..4529f6713a2 100644 --- a/store/src/main/java/org/apache/rocketmq/store/DefaultMessageStore.java +++ b/store/src/main/java/org/apache/rocketmq/store/DefaultMessageStore.java @@ -898,6 +898,7 @@ public GetMessageResult getMessage(final String group, final String topic, final return null; } + // try to get from compaction store Optional topicConfig = getTopicConfig(topic); CleanupPolicy policy = CleanupPolicyUtils.getDeletePolicy(topicConfig); //check request topic flag @@ -905,6 +906,7 @@ public GetMessageResult getMessage(final String group, final String topic, final return compactionStore.getMessage(group, topic, queueId, offset, maxMsgNums, maxTotalMsgSize); } // else skip + // init context vars long beginTime = this.getSystemClock().now(); GetMessageStatus status = GetMessageStatus.NO_MESSAGE_IN_QUEUE; @@ -935,6 +937,7 @@ public GetMessageResult getMessage(final String group, final String topic, final status = GetMessageStatus.OFFSET_OVERFLOW_BADLY; nextBeginOffset = nextOffsetCorrection(offset, maxOffset); } else { // offset is ok + // init context vars final int maxFilterMessageSize = Math.max(this.messageStoreConfig.getMaxFilterMessageSize(), maxMsgNums * consumeQueue.getUnitSize()); final boolean diskFallRecorded = this.messageStoreConfig.isDiskFallRecorded(); @@ -974,6 +977,7 @@ public GetMessageResult getMessage(final String group, final String topic, final long nextPhyFileStartOffset = Long.MIN_VALUE; while (bufferConsumeQueue.hasNext() && nextBeginOffset < maxOffset) { + // init context params and validate CqUnit cqUnit = bufferConsumeQueue.next(); long offsetPy = cqUnit.getPos(); int sizePy = cqUnit.getSize(); @@ -1012,6 +1016,7 @@ public GetMessageResult getMessage(final String group, final String topic, final continue; } + // get message, roll to next file if needed SelectMappedBufferResult selectResult = this.commitLog.getMessage(offsetPy, sizePy); if (null == selectResult) { if (getResult.getBufferTotalSize() == 0) { @@ -1022,6 +1027,7 @@ public GetMessageResult getMessage(final String group, final String topic, final continue; } + // handle result if (messageStoreConfig.isColdDataFlowControlEnable() && !MixAll.isSysConsumerGroupPullMessage(group) && !selectResult.isInCache()) { getResult.setColdDataSum(getResult.getColdDataSum() + sizePy); } @@ -1036,6 +1042,7 @@ public GetMessageResult getMessage(final String group, final String topic, final filterMessageCount++; continue; } + this.storeStatsService.getGetMessageTransferredMsgCount().add(cqUnit.getBatchNum()); getResult.addMessage(selectResult, cqUnit.getQueueOffset(), cqUnit.getBatchNum()); status = GetMessageStatus.FOUND; @@ -1051,6 +1058,7 @@ public GetMessageResult getMessage(final String group, final String topic, final } } + // ... if (diskFallRecorded) { long fallBehind = maxOffsetPy - maxPhyOffsetPulling; brokerStatsManager.recordDiskFallBehindSize(group, topic, queueId, fallBehind); @@ -1066,6 +1074,7 @@ public GetMessageResult getMessage(final String group, final String topic, final nextBeginOffset = nextOffsetCorrection(offset, 0); } + // metrics if (GetMessageStatus.FOUND == status) { this.storeStatsService.getGetMessageTimesTotalFound().add(1); } else { @@ -1080,6 +1089,7 @@ public GetMessageResult getMessage(final String group, final String topic, final long elapsedTime = this.getSystemClock().now() - beginTime; this.storeStatsService.setGetMessageEntireTimeMax(elapsedTime); + // format result // lazy init no data found. if (getResult == null) { getResult = new GetMessageResult(0); From 75995e39cfa4686189b3efd05c7bed3372b51a4f Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Tue, 2 Jun 2026 12:07:16 +0800 Subject: [PATCH 143/157] comment: add method comments to getMessage of DefaultMessageStore --- .../rocketmq/store/DefaultMessageStore.java | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/store/src/main/java/org/apache/rocketmq/store/DefaultMessageStore.java b/store/src/main/java/org/apache/rocketmq/store/DefaultMessageStore.java index 4529f6713a2..57fb7eb599c 100644 --- a/store/src/main/java/org/apache/rocketmq/store/DefaultMessageStore.java +++ b/store/src/main/java/org/apache/rocketmq/store/DefaultMessageStore.java @@ -885,6 +885,33 @@ public CompletableFuture getMessageAsync(String group, String return CompletableFuture.completedFuture(getMessage(group, topic, queueId, offset, maxMsgNums, messageFilter)); } + /** + * Pull messages from the consume queue, applying filters and reading bodies + * from the commit log. + * + *

    The method: + *

      + *
    1. Validates the store state and finds the consume queue for the + * topic
    2. + *
    3. Checks the offset against the queue bounds and sets the + * appropriate status if out of range
    4. + *
    5. Iterates through the consume queue entries, applies both + * consume-queue-level and commit-log-level message filters
    6. + *
    7. Reads message bodies from the commit log and appends them to the + * result until the size or count limit is reached
    8. + *
    9. Reports disk-fall-behind metrics and suggests pulling from a + * slave if the data is too far behind in physical offset
    10. + *
    + * + * @param group consumer group + * @param topic topic name + * @param queueId queue id + * @param offset starting offset in the consume queue + * @param maxMsgNums maximum number of messages to return + * @param maxTotalMsgSize maximum total message body size + * @param messageFilter message filter (may be null) + * @return the pull result with status, messages, and next offset + */ @Override public GetMessageResult getMessage(final String group, final String topic, final int queueId, final long offset, final int maxMsgNums, final int maxTotalMsgSize, final MessageFilter messageFilter) { From 2ccd095bcce5b2fc4628bfe30615eaf891b463fc Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Tue, 2 Jun 2026 13:19:36 +0800 Subject: [PATCH 144/157] comment: add method comments to getMessage of CommitLog --- .../java/org/apache/rocketmq/store/CommitLog.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/store/src/main/java/org/apache/rocketmq/store/CommitLog.java b/store/src/main/java/org/apache/rocketmq/store/CommitLog.java index 28abebcf64d..1e65ad09a07 100644 --- a/store/src/main/java/org/apache/rocketmq/store/CommitLog.java +++ b/store/src/main/java/org/apache/rocketmq/store/CommitLog.java @@ -1449,6 +1449,20 @@ public long getMinOffset() { return -1; } + /** + * Read a message body from the commit log at the given physical offset. + * + *

    The difference between getData is: + * getMessage add process: setInCache + * + *

    Finds the mapped file containing the offset and selects a buffer + * for the given size. The returned buffer includes cache-status metadata + * for cold-data flow control. + * + * @param offset physical offset in the commit log + * @param size number of bytes to read + * @return the mapped buffer, or {@code null} if the file is unavailable + */ public SelectMappedBufferResult getMessage(final long offset, final int size) { int mappedFileSize = this.defaultMessageStore.getMessageStoreConfig().getMappedFileSizeCommitLog(); MappedFile mappedFile = this.mappedFileQueue.findMappedFileByOffset(offset, offset == 0); From 57b3beb321fd7f5a0a8ba0ed4aa76112fc542694 Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Tue, 2 Jun 2026 13:19:45 +0800 Subject: [PATCH 145/157] comment: update cache-status comment in log --- .../java/org/apache/rocketmq/store/DefaultMessageStore.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/store/src/main/java/org/apache/rocketmq/store/DefaultMessageStore.java b/store/src/main/java/org/apache/rocketmq/store/DefaultMessageStore.java index 57fb7eb599c..9bfbfca6961 100644 --- a/store/src/main/java/org/apache/rocketmq/store/DefaultMessageStore.java +++ b/store/src/main/java/org/apache/rocketmq/store/DefaultMessageStore.java @@ -909,7 +909,7 @@ public CompletableFuture getMessageAsync(String group, String * @param offset starting offset in the consume queue * @param maxMsgNums maximum number of messages to return * @param maxTotalMsgSize maximum total message body size - * @param messageFilter message filter (may be null) + * @param messageFilter message filter (maybe null) * @return the pull result with status, messages, and next offset */ @Override From a637850f6d6fce88ee3f876fe01b59a53851fb52 Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Tue, 2 Jun 2026 16:13:08 +0800 Subject: [PATCH 146/157] comment: add in-line comments to method findMappedFileByOffset MappedFileQueue --- .../main/java/org/apache/rocketmq/store/MappedFileQueue.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/store/src/main/java/org/apache/rocketmq/store/MappedFileQueue.java b/store/src/main/java/org/apache/rocketmq/store/MappedFileQueue.java index 94235024da9..250e27b292a 100644 --- a/store/src/main/java/org/apache/rocketmq/store/MappedFileQueue.java +++ b/store/src/main/java/org/apache/rocketmq/store/MappedFileQueue.java @@ -697,6 +697,7 @@ public MappedFile findMappedFileByOffset(final long offset, final boolean return MappedFile firstMappedFile = this.getFirstMappedFile(); MappedFile lastMappedFile = this.getLastMappedFile(); if (firstMappedFile != null && lastMappedFile != null) { + // offset is not in range of [firstOffset, lastOffset] if (offset < firstMappedFile.getFileFromOffset() || offset >= lastMappedFile.getFileFromOffset() + this.mappedFileSize) { LOG_ERROR.warn("Offset not matched. Request offset: {}, firstOffset: {}, lastOffset: {}, mappedFileSize: {}, mappedFiles count: {}", offset, @@ -705,6 +706,7 @@ public MappedFile findMappedFileByOffset(final long offset, final boolean return this.mappedFileSize, this.mappedFiles.size()); } else { + // get file by index int index = (int) ((offset / this.mappedFileSize) - (firstMappedFile.getFileFromOffset() / this.mappedFileSize)); MappedFile targetFile = null; try { @@ -717,6 +719,7 @@ public MappedFile findMappedFileByOffset(final long offset, final boolean return return targetFile; } + // iterate to find file for (MappedFile tmpMappedFile : this.mappedFiles) { if (offset >= tmpMappedFile.getFileFromOffset() && offset < tmpMappedFile.getFileFromOffset() + this.mappedFileSize) { From 63ed93bb51fc298c28d59fe13dcada0309475f9f Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Tue, 2 Jun 2026 16:21:25 +0800 Subject: [PATCH 147/157] comment: optimize findMappedFileByOffset comment in MappedFileQueue --- .../rocketmq/store/MappedFileQueue.java | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/store/src/main/java/org/apache/rocketmq/store/MappedFileQueue.java b/store/src/main/java/org/apache/rocketmq/store/MappedFileQueue.java index 250e27b292a..afb6e005dc2 100644 --- a/store/src/main/java/org/apache/rocketmq/store/MappedFileQueue.java +++ b/store/src/main/java/org/apache/rocketmq/store/MappedFileQueue.java @@ -686,11 +686,23 @@ public synchronized boolean commit(final int commitLeastPages) { } /** - * Finds a mapped file by offset. + * Locate the mapped file containing the given physical offset. * - * @param offset Offset. - * @param returnFirstOnNotFound If the mapped file is not found, then return the first one. - * @return Mapped file or null (when not found and returnFirstOnNotFound is false). + *

    The lookup strategy: + *

      + *
    1. Index-based (O(1)) — computes the index from the offset + * and the first file's offset and accesses the list directly
    2. + *
    3. Iteration (O(n)) — falls back to a linear scan when the + * index-based result does not match the expected range
    4. + *
    5. return first if returnFirstOnNotFound is true
    6. + *
    7. return null if not found
    8. + *
    + * + * @param offset physical offset to find + * @param returnFirstOnNotFound if {@code true}, returns the first mapped + * file when the offset is outside the range + * @return the mapped file, or {@code null} if not found and + * {@code returnFirstOnNotFound} is {@code false} */ public MappedFile findMappedFileByOffset(final long offset, final boolean returnFirstOnNotFound) { try { From 366ce8f25925a57703d9fab9541e8878a2663916 Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Tue, 2 Jun 2026 17:55:08 +0800 Subject: [PATCH 148/157] comment: add method comments to handleAutoRenew of ReceiveMessageActivity --- .../v2/consumer/ReceiveMessageActivity.java | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/consumer/ReceiveMessageActivity.java b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/consumer/ReceiveMessageActivity.java index 4ecafdf5ce0..d795d6bf71f 100644 --- a/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/consumer/ReceiveMessageActivity.java +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/consumer/ReceiveMessageActivity.java @@ -207,6 +207,28 @@ public void receiveMessage(ProxyContext ctx, ReceiveMessageRequest request, } } + /** + * Register receipt handles for auto-renewal of message visibility timeouts. + * + *

    When auto-renew is enabled ({@code enableProxyAutoRenew}), the proxy + * periodically extends the invisible time of delivered but unacked messages + * so that they are not revived while the consumer is still processing them. + * + *

    This method extracts the {@code PROPERTY_POP_CK} from each popped + * message, wraps it into a {@link MessageReceiptHandle}, and registers it + * via {@link MessagingProcessor#addReceiptHandle}. The returned + * {@link Runnable} is executed after the response has been written to the + * client stream. + * + * @param ctx the proxy context + * @param request the original receive-message request + * @param group consumer group + * @param topic topic name + * @param popResult the pop result returned from the broker + * @param writer the response stream writer + * @return a runnable to execute after the response write, or {@code null} + * if no messages were found + */ private Runnable handleAutoRenew(ProxyContext ctx, ReceiveMessageRequest request, String group, String topic, PopResult popResult, ReceiveMessageResponseStreamWriter writer ) { From c16179cfd6ce2e52507ee66ec646db05b5883a3e Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Tue, 2 Jun 2026 20:22:59 +0800 Subject: [PATCH 149/157] comment: delete useless comments of PopMessageProcessor --- .../apache/rocketmq/broker/processor/PopMessageProcessor.java | 3 --- .../proxy/grpc/v2/consumer/ReceiveMessageActivity.java | 3 +++ 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopMessageProcessor.java b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopMessageProcessor.java index e863ae7ac79..81c6aeb1411 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopMessageProcessor.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopMessageProcessor.java @@ -555,11 +555,8 @@ public RemotingCommand processRequest(final ChannelHandlerContext ctx, RemotingC this.brokerController.getBrokerConfig().getReviveQueueNum()); } - // properties of rocketmq 4.x, useless in 5.x StringBuilder startOffsetInfo = new StringBuilder(64); - // properties of rocketmq 4.x, useless in 5.x StringBuilder msgOffsetInfo = new StringBuilder(64); - // properties of rocketmq 4.x, useless in 5.x StringBuilder orderCountInfo = requestHeader.isOrder() ? new StringBuilder(64) : null; // Due to the design of the fields startOffsetInfo, msgOffsetInfo, and orderCountInfo, diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/consumer/ReceiveMessageActivity.java b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/consumer/ReceiveMessageActivity.java index d795d6bf71f..b5c4b0ad7f5 100644 --- a/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/consumer/ReceiveMessageActivity.java +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/consumer/ReceiveMessageActivity.java @@ -232,10 +232,12 @@ public void receiveMessage(ProxyContext ctx, ReceiveMessageRequest request, private Runnable handleAutoRenew(ProxyContext ctx, ReceiveMessageRequest request, String group, String topic, PopResult popResult, ReceiveMessageResponseStreamWriter writer ) { + // check result status if (!PopStatus.FOUND.equals(popResult.getPopStatus())) { return null; } + // get socket channel GrpcClientChannel clientChannel = grpcChannelManager.getChannel(ctx.getClientID()); if (clientChannel == null) { GrpcProxyException e = new GrpcProxyException(Code.MESSAGE_NOT_FOUND, @@ -244,6 +246,7 @@ private Runnable handleAutoRenew(ProxyContext ctx, ReceiveMessageRequest request writer.processThrowableWhenWriteMessage(e, ctx, request, messageExt)); throw e; } + return () -> { List messageExtList = popResult.getMsgFoundList(); for (MessageExt messageExt : messageExtList) { From 791f374a11e5e521908cd930e2ba48518694aa9f Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Tue, 2 Jun 2026 20:31:46 +0800 Subject: [PATCH 150/157] comment: add class comments to DefaultReceiptHandleManager --- .../service/receipt/DefaultReceiptHandleManager.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/service/receipt/DefaultReceiptHandleManager.java b/proxy/src/main/java/org/apache/rocketmq/proxy/service/receipt/DefaultReceiptHandleManager.java index f9dfd825337..e0ac9efb895 100644 --- a/proxy/src/main/java/org/apache/rocketmq/proxy/service/receipt/DefaultReceiptHandleManager.java +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/service/receipt/DefaultReceiptHandleManager.java @@ -60,6 +60,18 @@ import org.apache.rocketmq.remoting.protocol.subscription.RetryPolicy; import org.apache.rocketmq.remoting.protocol.subscription.SubscriptionGroupConfig; +/** + * Manages receipt handles for gRPC proxy auto-renewal of message visibility timeouts. + * + *

    When auto-renew is enabled, popped messages are registered here with their + * {@code PROPERTY_POP_CK} data. A periodic {@link #scheduledExecutorService} scans + * all registered handles and extends the invisible time for messages that are + * about to expire. When the total renewal duration exceeds + * {@code renewMaxTimeMillis}, the message is nack'd and returned to the broker. + * + *

    Handles are grouped by {@link ReceiptHandleGroupKey} (channel + consumer group) + * and cleaned up automatically when a gRPC client disconnects. + */ public class DefaultReceiptHandleManager extends AbstractStartAndShutdown implements ReceiptHandleManager { protected final static Logger log = LoggerFactory.getLogger(LoggerName.PROXY_LOGGER_NAME); protected final MetadataService metadataService; From 62a35753e639113abb1b5cad9d23e5cc5aa76298 Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Tue, 2 Jun 2026 20:40:48 +0800 Subject: [PATCH 151/157] comment: add class comments to ReceiptHandleProcessor --- .../proxy/processor/ReceiptHandleProcessor.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/processor/ReceiptHandleProcessor.java b/proxy/src/main/java/org/apache/rocketmq/proxy/processor/ReceiptHandleProcessor.java index bc3730aed9a..370420ea5ba 100644 --- a/proxy/src/main/java/org/apache/rocketmq/proxy/processor/ReceiptHandleProcessor.java +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/processor/ReceiptHandleProcessor.java @@ -29,6 +29,19 @@ import org.apache.rocketmq.proxy.service.ServiceManager; import org.apache.rocketmq.proxy.service.receipt.DefaultReceiptHandleManager; +/** + * Bridges receipt handle renewal events to the messaging processor. + * + *

    Owns a {@link DefaultReceiptHandleManager} and wires its + * {@link RenewEvent} listener to {@link MessagingProcessor#changeInvisibleTime}. + * When a receipt handle is about to expire, the manager fires a {@code RENEW} + * event which this processor translates into a + * {@code ChangeInvisibleTime} call. + * + *

    When the renewal limit is reached, a {@code STOP_RENEW} event fires + * which nacks the message via {@code changeInvisibleTime} with the group's + * retry policy delay. + */ public class ReceiptHandleProcessor extends AbstractProcessor { protected final static Logger log = LoggerFactory.getLogger(LoggerName.PROXY_LOGGER_NAME); protected DefaultReceiptHandleManager receiptHandleManager; From d0230d18a9287a704ea98a33030b45ca3d1b7b34 Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Tue, 2 Jun 2026 21:29:52 +0800 Subject: [PATCH 152/157] comment: add constructor comments to ReceiptHandleProcessor --- .../proxy/processor/ReceiptHandleProcessor.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/processor/ReceiptHandleProcessor.java b/proxy/src/main/java/org/apache/rocketmq/proxy/processor/ReceiptHandleProcessor.java index 370420ea5ba..3fffee4eb64 100644 --- a/proxy/src/main/java/org/apache/rocketmq/proxy/processor/ReceiptHandleProcessor.java +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/processor/ReceiptHandleProcessor.java @@ -46,6 +46,21 @@ public class ReceiptHandleProcessor extends AbstractProcessor { protected final static Logger log = LoggerFactory.getLogger(LoggerName.PROXY_LOGGER_NAME); protected DefaultReceiptHandleManager receiptHandleManager; + /** + * Wire the receipt handle manager to the messaging processor. + * + *

    pass StateEventListener to DefaultReceiptHandleManager + * so that when DefaultReceiptHandleManager find the message is expired, + * call StateEventListener to change the invisible time of the message. + * + *

    Creates an event listener that translates all {@link RenewEvent} + * types ({@code RENEW}, {@code STOP_RENEW}, {@code CLEAR_GROUP}) into + * {@link MessagingProcessor#changeInvisibleTime} calls, which update + * the message's visibility timeout on the broker. + * + * @param messagingProcessor the core messaging processor + * @param serviceManager the service manager providing metadata and consumer services + */ public ReceiptHandleProcessor(MessagingProcessor messagingProcessor, ServiceManager serviceManager) { super(messagingProcessor, serviceManager); StateEventListener eventListener = event -> { From 6112186e521f59c03571f41939a15fd0cb4e4bb2 Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Tue, 2 Jun 2026 21:36:46 +0800 Subject: [PATCH 153/157] comment: add inline comments to constructor of ReceiptHandleProcessor --- .../rocketmq/proxy/processor/ReceiptHandleProcessor.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/processor/ReceiptHandleProcessor.java b/proxy/src/main/java/org/apache/rocketmq/proxy/processor/ReceiptHandleProcessor.java index 3fffee4eb64..555b78f1906 100644 --- a/proxy/src/main/java/org/apache/rocketmq/proxy/processor/ReceiptHandleProcessor.java +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/processor/ReceiptHandleProcessor.java @@ -63,11 +63,16 @@ public class ReceiptHandleProcessor extends AbstractProcessor { */ public ReceiptHandleProcessor(MessagingProcessor messagingProcessor, ServiceManager serviceManager) { super(messagingProcessor, serviceManager); + + // create event listener StateEventListener eventListener = event -> { + // convert event to ReceiptHandle ProxyContext context = createContext(event.getEventType().name()) .setChannel(event.getKey().getChannel()); MessageReceiptHandle messageReceiptHandle = event.getMessageReceiptHandle(); ReceiptHandle handle = ReceiptHandle.decode(messageReceiptHandle.getReceiptHandleStr()); + + // change invisible time messagingProcessor .changeInvisibleTime(context, handle, messageReceiptHandle.getMessageId(), messageReceiptHandle.getGroup(), messageReceiptHandle.getTopic(), @@ -80,6 +85,8 @@ public ReceiptHandleProcessor(MessagingProcessor messagingProcessor, ServiceMana event.getFuture().complete(v); }); }; + + // pass event listener to DefaultReceiptHandleManager this.receiptHandleManager = new DefaultReceiptHandleManager(serviceManager.getMetadataService(), serviceManager.getConsumerManager(), eventListener); this.appendStartAndShutdown(receiptHandleManager); } From 6fd2dbe556d5b63ea76c49680b1c83163e4dd119 Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Tue, 2 Jun 2026 21:53:18 +0800 Subject: [PATCH 154/157] comment: add inline comments to constructor of DefaultReceiptHandleManager --- .../service/receipt/DefaultReceiptHandleManager.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/service/receipt/DefaultReceiptHandleManager.java b/proxy/src/main/java/org/apache/rocketmq/proxy/service/receipt/DefaultReceiptHandleManager.java index e0ac9efb895..b4799d01cf4 100644 --- a/proxy/src/main/java/org/apache/rocketmq/proxy/service/receipt/DefaultReceiptHandleManager.java +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/service/receipt/DefaultReceiptHandleManager.java @@ -89,6 +89,8 @@ public DefaultReceiptHandleManager(MetadataService metadataService, ConsumerMana this.consumerManager = consumerManager; this.eventListener = eventListener; ProxyConfig proxyConfig = ConfigurationManager.getProxyConfig(); + + // by default, minThreadNum is 2, maxThreadNum is 4 this.renewalWorkerService = ThreadPoolMonitor.createAndMonitor( proxyConfig.getRenewThreadPoolNums(), proxyConfig.getRenewMaxThreadPoolNums(), @@ -96,6 +98,8 @@ public DefaultReceiptHandleManager(MetadataService metadataService, ConsumerMana "RenewalWorkerThread", proxyConfig.getRenewThreadPoolQueueCapacity() ); + + // by default, minThreadNum is 2, maxThreadNum is 4 this.returnHandleGroupWorkerService = ThreadPoolMonitor.createAndMonitor( proxyConfig.getReturnHandleGroupThreadPoolNums(), proxyConfig.getReturnHandleGroupThreadPoolNums() * 2, @@ -103,6 +107,8 @@ public DefaultReceiptHandleManager(MetadataService metadataService, ConsumerMana "ReturnHandleGroupWorkerThread", proxyConfig.getRenewThreadPoolQueueCapacity() ); + + // clear receipt by group when consumer unregister consumerManager.appendConsumerIdsChangeListener(new ConsumerIdsChangeListener() { @Override public void handle(ConsumerGroupEvent event, String group, Object... args) { @@ -127,11 +133,15 @@ public void shutdown() { } }); + this.receiptHandleGroupMap = new ConcurrentHashMap<>(); this.renewalWorkerService.setRejectedExecutionHandler((r, executor) -> log.warn("add renew task failed. queueSize:{}", executor.getQueue().size())); + + // add periodic scan task this.appendStartAndShutdown(new StartAndShutdown() { @Override public void start() throws Exception { + // by default, interval is 5000ms scheduledExecutorService.scheduleWithFixedDelay(() -> scheduleRenewTask(), 0, ConfigurationManager.getProxyConfig().getRenewSchedulePeriodMillis(), TimeUnit.MILLISECONDS); } From 2540bb5e5450435c2e73f82f8b8a37945edc4be2 Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Tue, 2 Jun 2026 21:56:32 +0800 Subject: [PATCH 155/157] comment: add method comments to scheduleRenewTask of DefaultReceiptHandleManager --- .../receipt/DefaultReceiptHandleManager.java | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/service/receipt/DefaultReceiptHandleManager.java b/proxy/src/main/java/org/apache/rocketmq/proxy/service/receipt/DefaultReceiptHandleManager.java index b4799d01cf4..5724e90b196 100644 --- a/proxy/src/main/java/org/apache/rocketmq/proxy/service/receipt/DefaultReceiptHandleManager.java +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/service/receipt/DefaultReceiptHandleManager.java @@ -176,6 +176,23 @@ protected boolean clientIsOffline(ReceiptHandleGroupKey groupKey) { return this.consumerManager.findChannel(groupKey.getGroup(), groupKey.getChannel()) == null; } + /** + * Periodic scan of all receipt handle groups, called by the + * {@link #scheduledExecutorService} at a fixed interval. + * + *

    For each group: + *

      + *
    • If the client has gone offline, the entire group is cleared + * immediately
    • + *
    • Otherwise, each handle is inspected — if its next visible time + * minus the current time is within the {@code renewAheadTimeMillis} + * threshold, a renewal is submitted to the + * {@link #renewalWorkerService} thread pool
    • + *
    + * + *

    The scan runs synchronously in the scheduler thread; the actual + * renewal work is dispatched asynchronously to the worker pool. + */ protected void scheduleRenewTask() { Stopwatch stopwatch = Stopwatch.createStarted(); try { From fe4271b225b1721589dc6d9c6b7d478f1004dbd1 Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Tue, 2 Jun 2026 22:01:15 +0800 Subject: [PATCH 156/157] comment: add method comments to startRenewMessage of DefaultReceiptHandleManager --- .../receipt/DefaultReceiptHandleManager.java | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/service/receipt/DefaultReceiptHandleManager.java b/proxy/src/main/java/org/apache/rocketmq/proxy/service/receipt/DefaultReceiptHandleManager.java index 5724e90b196..de678eea811 100644 --- a/proxy/src/main/java/org/apache/rocketmq/proxy/service/receipt/DefaultReceiptHandleManager.java +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/service/receipt/DefaultReceiptHandleManager.java @@ -230,6 +230,28 @@ protected void renewMessage(ProxyContext context, ReceiptHandleGroupKey key, Rec } } + /** + * Renew a single message's visibility timeout, or stop if the renewal + * limit has been reached. + * + *

    Decision logic: + *

      + *
    • If the handle has exceeded {@code maxRenewRetryTimes}, it is + * dropped
    • + *
    • If the total renewal duration is within {@code renewMaxTimeMillis}, + * a {@link RenewEvent.EventType#RENEW} event is fired to extend the + * invisible time
    • + *
    • If the renewal duration has exceeded {@code renewMaxTimeMillis}, + * a {@link RenewEvent.EventType#STOP_RENEW} event is fired which + * nacks the message with the group's retry policy delay
    • + *
    + * + * @param context the proxy context + * @param key the receipt handle group key + * @param messageReceiptHandle the handle to renew + * @return a future completing with the updated handle (or {@code null} if + * renewal is stopped) + */ protected CompletableFuture startRenewMessage(ProxyContext context, ReceiptHandleGroupKey key, MessageReceiptHandle messageReceiptHandle) { CompletableFuture resFuture = new CompletableFuture<>(); ProxyConfig proxyConfig = ConfigurationManager.getProxyConfig(); @@ -239,6 +261,7 @@ protected CompletableFuture startRenewMessage(ProxyContext log.warn("handle has exceed max renewRetryTimes. handle:{}", messageReceiptHandle); return CompletableFuture.completedFuture(null); } + if (current - messageReceiptHandle.getConsumeTimestamp() < proxyConfig.getRenewMaxTimeMillis()) { CompletableFuture future = new CompletableFuture<>(); eventListener.fireEvent(new RenewEvent(key, messageReceiptHandle, RENEW_POLICY.nextDelayDuration(messageReceiptHandle.getRenewTimes()), RenewEvent.EventType.RENEW, future)); From 06cdb22204c0369fb1c50fca6c8f3e53e988c7ab Mon Sep 17 00:00:00 2001 From: "winglechen@aliyun.com" Date: Fri, 5 Jun 2026 18:05:21 +0800 Subject: [PATCH 157/157] comment: add method comments to queryRoute of RouteActivity --- .../proxy/grpc/v2/route/RouteActivity.java | 24 +++++++++++++++++++ .../receipt/DefaultReceiptHandleManager.java | 3 ++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/route/RouteActivity.java b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/route/RouteActivity.java index 75f7089c5e0..5c6e2c3044b 100644 --- a/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/route/RouteActivity.java +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/route/RouteActivity.java @@ -60,6 +60,30 @@ public RouteActivity(MessagingProcessor messagingProcessor, super(messagingProcessor, grpcClientSettingsManager, grpcChannelManager); } + /** + * query route info by topic + * + * @param ctx ctx + * @param request { + * topic: xxx, + * endpoints: from client config, it is an endpoint list, it's a bad design + * } + * @return route info { + * status: xxx, + * message_queues: [ + * { + * topic: xxx, + * permission: (enum)xxx, + * broker: { + * name: xxx, + * id: xxx, + * endPoints: xxx, + * }, + * accept_message_type: xxx + * }, ... + * ] + * } + */ public CompletableFuture queryRoute(ProxyContext ctx, QueryRouteRequest request) { CompletableFuture future = new CompletableFuture<>(); try { diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/service/receipt/DefaultReceiptHandleManager.java b/proxy/src/main/java/org/apache/rocketmq/proxy/service/receipt/DefaultReceiptHandleManager.java index de678eea811..9f168a3128b 100644 --- a/proxy/src/main/java/org/apache/rocketmq/proxy/service/receipt/DefaultReceiptHandleManager.java +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/service/receipt/DefaultReceiptHandleManager.java @@ -257,11 +257,12 @@ protected CompletableFuture startRenewMessage(ProxyContext ProxyConfig proxyConfig = ConfigurationManager.getProxyConfig(); long current = System.currentTimeMillis(); try { + // by default, maxRenewRetryTimes is 3 if (messageReceiptHandle.getRenewRetryTimes() >= proxyConfig.getMaxRenewRetryTimes()) { log.warn("handle has exceed max renewRetryTimes. handle:{}", messageReceiptHandle); return CompletableFuture.completedFuture(null); } - + if (current - messageReceiptHandle.getConsumeTimestamp() < proxyConfig.getRenewMaxTimeMillis()) { CompletableFuture future = new CompletableFuture<>(); eventListener.fireEvent(new RenewEvent(key, messageReceiptHandle, RENEW_POLICY.nextDelayDuration(messageReceiptHandle.getRenewTimes()), RenewEvent.EventType.RENEW, future));