This is an automated email from the ASF dual-hosted git repository.

jt2594838 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/iotdb.git


The following commit(s) were added to refs/heads/master by this push:
     new fdce12d7972 Fix subscription event loss on poll payload overflow 
(#18471)
fdce12d7972 is described below

commit fdce12d797234ef139a54206ae4a462e69b127b9
Author: Caideyipi <[email protected]>
AuthorDate: Mon Aug 24 11:38:55 2026 +0800

    Fix subscription event loss on poll payload overflow (#18471)
---
 .../agent/SubscriptionBrokerAgent.java             |  27 +++++-
 .../broker/ConsensusSubscriptionBroker.java        |  23 ++++-
 .../subscription/broker/ISubscriptionBroker.java   |   3 +
 .../db/subscription/broker/SubscriptionBroker.java |  21 ++++-
 .../broker/SubscriptionPrefetchingQueue.java       |  33 +++++++
 .../consensus/ConsensusPrefetchingQueue.java       |  38 ++++++++
 .../db/subscription/event/SubscriptionEvent.java   |   5 +
 .../receiver/SubscriptionReceiverV1.java           |  14 ++-
 ...onsensusSubscriptionBrokerPayloadLimitTest.java |  93 ++++++++++++++++++
 .../SubscriptionBrokerAgentPayloadLimitTest.java   | 104 +++++++++++++++++++++
 .../consensus/ConsensusPrefetchingQueueTest.java   |  62 ++++++++++++
 11 files changed, 415 insertions(+), 8 deletions(-)

diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/agent/SubscriptionBrokerAgent.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/agent/SubscriptionBrokerAgent.java
index da649e75b63..83e816e9cf3 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/agent/SubscriptionBrokerAgent.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/agent/SubscriptionBrokerAgent.java
@@ -125,12 +125,23 @@ public class SubscriptionBrokerAgent {
       }
       final List<SubscriptionEvent> events =
           broker.poll(consumerId, topicNames, remainingBytes, progressByTopic);
-      allEvents.addAll(events);
       for (final SubscriptionEvent event : events) {
         try {
-          remainingBytes -= event.getCurrentResponseSize();
+          final long currentSize = event.getCurrentResponseSize();
+          // Each broker preserves the existing handling for its first 
oversized event. If another
+          // broker already used part of this response, put the event back so 
it can be retried with
+          // the full budget on the next poll.
+          if (!allEvents.isEmpty()
+              && currentSize > remainingBytes
+              && broker.requeue(consumerId, event.getCommitContext())) {
+            remainingBytes = 0;
+            break;
+          }
+          allEvents.add(event);
+          remainingBytes -= currentSize;
         } catch (final IOException ignored) {
           // best effort
+          allEvents.add(event);
         }
       }
     }
@@ -204,6 +215,18 @@ public class SubscriptionBrokerAgent {
     return allSuccessful;
   }
 
+  public boolean requeue(
+      final ConsumerConfig consumerConfig, final SubscriptionCommitContext 
commitContext) {
+    final String consumerGroupId = consumerConfig.getConsumerGroupId();
+    final String consumerId = consumerConfig.getConsumerId();
+    for (final ISubscriptionBroker broker : getBrokers(consumerGroupId)) {
+      if (broker.acceptsCommitContext(commitContext) && 
broker.requeue(consumerId, commitContext)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
   public int refreshInFlightEventLeases(
       final ConsumerConfig consumerConfig,
       final List<SubscriptionCommitContext> processorBufferedCommitContexts) {
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/broker/ConsensusSubscriptionBroker.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/broker/ConsensusSubscriptionBroker.java
index fb739aef349..e1696d59b17 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/broker/ConsensusSubscriptionBroker.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/broker/ConsensusSubscriptionBroker.java
@@ -124,6 +124,7 @@ public class ConsensusSubscriptionBroker implements 
ISubscriptionBroker {
     final List<SubscriptionEvent> eventsToPoll = new ArrayList<>();
     final List<SubscriptionEvent> eventsToNack = new ArrayList<>();
     long totalSize = 0;
+    boolean responseFull = false;
 
     for (final String topicName : topicNames) {
       final List<ConsensusPrefetchingQueue> queues =
@@ -169,6 +170,15 @@ public class ConsensusSubscriptionBroker implements 
ISubscriptionBroker {
           continue;
         }
 
+        // Preserve the existing handling for a single oversized event. Once 
this response already
+        // contains data, defer an event that does not fit instead of 
returning an oversized batch.
+        if (totalSize > 0
+            && currentSize > maxBytes - totalSize
+            && consensusQueue.requeue(consumerId, event.getCommitContext())) {
+          responseFull = true;
+          break;
+        }
+
         eventsToPoll.add(event);
         totalSize += currentSize;
 
@@ -176,7 +186,7 @@ public class ConsensusSubscriptionBroker implements 
ISubscriptionBroker {
           break;
         }
       }
-      if (totalSize >= maxBytes) {
+      if (responseFull || totalSize >= maxBytes) {
         break;
       }
     }
@@ -280,6 +290,17 @@ public class ConsensusSubscriptionBroker implements 
ISubscriptionBroker {
     return successfulCommitContexts;
   }
 
+  @Override
+  public boolean requeue(final String consumerId, final 
SubscriptionCommitContext commitContext) {
+    final List<ConsensusPrefetchingQueue> queues =
+        
topicNameToConsensusPrefetchingQueues.get(commitContext.getTopicName());
+    if (Objects.isNull(queues) || queues.isEmpty()) {
+      return false;
+    }
+    final ConsensusPrefetchingQueue queue = getQueueForCommitContext(queues, 
commitContext);
+    return Objects.nonNull(queue) && queue.requeue(consumerId, commitContext);
+  }
+
   @Override
   public int refreshInFlightEventLeases(
       final String consumerId, final List<SubscriptionCommitContext> 
commitContexts) {
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/broker/ISubscriptionBroker.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/broker/ISubscriptionBroker.java
index 7d0ec6deada..547ffc6b763 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/broker/ISubscriptionBroker.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/broker/ISubscriptionBroker.java
@@ -48,6 +48,9 @@ public interface ISubscriptionBroker {
   List<SubscriptionCommitContext> commit(
       String consumerId, List<SubscriptionCommitContext> commitContexts, 
boolean nack);
 
+  /** Returns an in-flight event to its prefetching queue without incrementing 
its nack count. */
+  boolean requeue(String consumerId, SubscriptionCommitContext commitContext);
+
   default List<SubscriptionCommitContext> selectAcceptedCommitContexts(
       final List<SubscriptionCommitContext> commitContexts) {
     if (Objects.isNull(commitContexts) || commitContexts.isEmpty()) {
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/broker/SubscriptionBroker.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/broker/SubscriptionBroker.java
index 1115c465c15..0353e3d74b1 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/broker/SubscriptionBroker.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/broker/SubscriptionBroker.java
@@ -166,6 +166,14 @@ public class SubscriptionBroker implements 
ISubscriptionBroker {
         continue;
       }
 
+      // Preserve the existing handling for a single oversized event. Once 
this response already
+      // contains data, defer an event that does not fit instead of returning 
an oversized batch.
+      if (totalSize > 0
+          && currentSize > maxBytes - totalSize
+          && prefetchingQueue.requeue(consumerId, event.getCommitContext())) {
+        break;
+      }
+
       // Add the event to the poll list
       eventsToPoll.add(event);
 
@@ -175,8 +183,8 @@ public class SubscriptionBroker implements 
ISubscriptionBroker {
       // Update the total size
       totalSize += currentSize;
 
-      // If adding this event exceeds the maxBytes (pessimistic estimation), 
break the loop
-      if (totalSize + currentSize > maxBytes) {
+      // If the response has reached maxBytes, stop polling more events.
+      if (totalSize >= maxBytes) {
         break;
       }
     }
@@ -375,6 +383,15 @@ public class SubscriptionBroker implements 
ISubscriptionBroker {
     return successfulCommitContexts;
   }
 
+  @Override
+  public boolean requeue(final String consumerId, final 
SubscriptionCommitContext commitContext) {
+    final SubscriptionPrefetchingQueue prefetchingQueue =
+        topicNameToPrefetchingQueue.get(commitContext.getTopicName());
+    return Objects.nonNull(prefetchingQueue)
+        && !prefetchingQueue.isClosed()
+        && prefetchingQueue.requeue(consumerId, commitContext);
+  }
+
   @Override
   public int refreshInFlightEventLeases(
       final String consumerId, final List<SubscriptionCommitContext> 
commitContexts) {
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/broker/SubscriptionPrefetchingQueue.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/broker/SubscriptionPrefetchingQueue.java
index 0bfccd3a78b..801fc03d3b8 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/broker/SubscriptionPrefetchingQueue.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/broker/SubscriptionPrefetchingQueue.java
@@ -817,6 +817,39 @@ public abstract class SubscriptionPrefetchingQueue {
     return refreshed.get();
   }
 
+  /**
+   * Returns an event to the prefetching queue without modifying its response 
or nack count.
+   *
+   * <p>This is used when the server polled the event but cannot fit it in the 
current response.
+   */
+  public boolean requeue(final String consumerId, final 
SubscriptionCommitContext commitContext) {
+    acquireReadLock();
+    try {
+      if (isClosed()) {
+        return false;
+      }
+      final AtomicBoolean requeued = new AtomicBoolean(false);
+      inFlightEvents.compute(
+          new Pair<>(consumerId, commitContext),
+          (key, ev) -> {
+            if (Objects.isNull(ev)) {
+              return null;
+            }
+            if (ev.isCommitted()) {
+              ev.cleanUp(false);
+              return null;
+            }
+            ev.resetLastPolledTimestamp();
+            prefetchEvent(ev);
+            requeued.set(true);
+            return null;
+          });
+      return requeued.get();
+    } finally {
+      releaseReadLock();
+    }
+  }
+
   /**
    * @return {@code true} if ack successfully
    */
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusPrefetchingQueue.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusPrefetchingQueue.java
index 2d2060e15f7..4dd71c3b8f2 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusPrefetchingQueue.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusPrefetchingQueue.java
@@ -2579,6 +2579,44 @@ public class ConsensusPrefetchingQueue {
     return refreshed.get();
   }
 
+  /**
+   * Returns an event to the prefetching queue without modifying its response 
or nack count.
+   *
+   * <p>This is used when the server polled the event but cannot fit it in the 
current response.
+   */
+  public boolean requeue(final String consumerId, final 
SubscriptionCommitContext commitContext) {
+    acquireReadLock();
+    try {
+      if (isClosed || closeRequested || pendingSeekRequest != null || 
!isActive) {
+        return false;
+      }
+      if (Objects.isNull(commitContext)
+          || !commitContext.hasWriterProgress()
+          || isCommitContextOutdated(commitContext)) {
+        return false;
+      }
+      final AtomicBoolean requeued = new AtomicBoolean(false);
+      inFlightEvents.compute(
+          new InFlightEventKey(consumerId, commitContext),
+          (key, ev) -> {
+            if (Objects.isNull(ev)) {
+              return null;
+            }
+            if (ev.isCommitted()) {
+              cleanUpEvent(ev, false);
+              return null;
+            }
+            ev.resetLastPolledTimestamp();
+            prefetchingQueue.add(ev);
+            requeued.set(true);
+            return null;
+          });
+      return requeued.get();
+    } finally {
+      releaseReadLock();
+    }
+  }
+
   private boolean canAcceptCommitContext(
       final SubscriptionCommitContext commitContext, final String action, 
final boolean silent) {
     if (isClosed || closeRequested || pendingSeekRequest != null) {
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/event/SubscriptionEvent.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/event/SubscriptionEvent.java
index 3c99a17f49f..2e58a68411d 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/event/SubscriptionEvent.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/event/SubscriptionEvent.java
@@ -275,6 +275,11 @@ public class SubscriptionEvent implements 
Comparable<SubscriptionEvent> {
     }
   }
 
+  /** Makes this event pollable again without treating local response-size 
control as a nack. */
+  public void resetLastPolledTimestamp() {
+    lastPolledTimestamp.set(INVALID_TIMESTAMP);
+  }
+
   /** Returns the current nack count for this event. */
   public long getNackCount() {
     return nackCount.get();
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/receiver/SubscriptionReceiverV1.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/receiver/SubscriptionReceiverV1.java
index dcc92bc2217..b0eec505a5c 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/receiver/SubscriptionReceiverV1.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/receiver/SubscriptionReceiverV1.java
@@ -798,10 +798,18 @@ public class SubscriptionReceiverV1 implements 
SubscriptionReceiver {
                           req.getRequest(),
                           e);
                     }
-                    // nack
+                    // A response-size overflow caused by events already added 
to this response is
+                    // local batching backpressure, not a consumer rejection. 
Requeue it without
+                    // increasing the poison-message nack counter.
                     if (!isOutdated) {
-                      SubscriptionAgent.broker()
-                          .commit(consumerConfig, 
Collections.singletonList(commitContext), true);
+                      final boolean requeued =
+                          e instanceof SubscriptionPayloadExceedException
+                              && totalSize.get() > 0
+                              && 
SubscriptionAgent.broker().requeue(consumerConfig, commitContext);
+                      if (!requeued) {
+                        SubscriptionAgent.broker()
+                            .commit(consumerConfig, 
Collections.singletonList(commitContext), true);
+                      }
                     }
                     return null;
                   }
diff --git 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/ConsensusSubscriptionBrokerPayloadLimitTest.java
 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/ConsensusSubscriptionBrokerPayloadLimitTest.java
new file mode 100644
index 00000000000..1d2e7137535
--- /dev/null
+++ 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/ConsensusSubscriptionBrokerPayloadLimitTest.java
@@ -0,0 +1,93 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.iotdb.db.subscription.broker;
+
+import org.apache.iotdb.commons.consensus.DataRegionId;
+import 
org.apache.iotdb.db.subscription.broker.consensus.ConsensusPrefetchingQueue;
+import org.apache.iotdb.db.subscription.event.SubscriptionEvent;
+import 
org.apache.iotdb.rpc.subscription.payload.poll.SubscriptionCommitContext;
+
+import org.junit.Test;
+
+import java.lang.reflect.Field;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertSame;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class ConsensusSubscriptionBrokerPayloadLimitTest {
+
+  private static final String CONSUMER_GROUP_ID = "consumerGroup";
+  private static final String CONSUMER_ID = "consumer";
+  private static final String TOPIC_NAME = "topic";
+
+  @Test
+  public void testPollRequeuesEventThatWouldExceedPayloadLimit() throws 
Exception {
+    final ConsensusSubscriptionBroker broker = new 
ConsensusSubscriptionBroker(CONSUMER_GROUP_ID);
+    final ConsensusPrefetchingQueue firstQueue = 
mock(ConsensusPrefetchingQueue.class);
+    final ConsensusPrefetchingQueue secondQueue = 
mock(ConsensusPrefetchingQueue.class);
+    final SubscriptionEvent firstEvent = mock(SubscriptionEvent.class);
+    final SubscriptionEvent secondEvent = mock(SubscriptionEvent.class);
+    final SubscriptionCommitContext firstCommitContext = newCommitContext(1, 
1);
+    final SubscriptionCommitContext secondCommitContext = newCommitContext(2, 
2);
+
+    when(firstQueue.getConsensusGroupId()).thenReturn(new DataRegionId(1));
+    when(secondQueue.getConsensusGroupId()).thenReturn(new DataRegionId(2));
+    when(firstQueue.poll(CONSUMER_ID, null)).thenReturn(firstEvent);
+    when(secondQueue.poll(CONSUMER_ID, null)).thenReturn(secondEvent);
+    when(firstEvent.getCurrentResponseSize()).thenReturn(40);
+    when(secondEvent.getCurrentResponseSize()).thenReturn(30);
+    when(firstEvent.getCommitContext()).thenReturn(firstCommitContext);
+    when(secondEvent.getCommitContext()).thenReturn(secondCommitContext);
+    when(secondQueue.requeue(CONSUMER_ID, 
secondCommitContext)).thenReturn(true);
+    bindQueues(broker, Arrays.asList(firstQueue, secondQueue));
+
+    final List<SubscriptionEvent> events =
+        broker.poll(CONSUMER_ID, Collections.singleton(TOPIC_NAME), 60L);
+
+    assertEquals(1, events.size());
+    assertSame(firstEvent, events.get(0));
+    verify(secondQueue).requeue(CONSUMER_ID, secondCommitContext);
+  }
+
+  private static SubscriptionCommitContext newCommitContext(
+      final int regionId, final int commitId) {
+    return new SubscriptionCommitContext(
+        1, 1, TOPIC_NAME, CONSUMER_GROUP_ID, commitId, "DataRegion[" + 
regionId + "]", 0L);
+  }
+
+  @SuppressWarnings("unchecked")
+  private static void bindQueues(
+      final ConsensusSubscriptionBroker broker, final 
List<ConsensusPrefetchingQueue> queues)
+      throws Exception {
+    final Field field =
+        
ConsensusSubscriptionBroker.class.getDeclaredField("topicNameToConsensusPrefetchingQueues");
+    field.setAccessible(true);
+    final Map<String, List<ConsensusPrefetchingQueue>> queuesByTopic =
+        (Map<String, List<ConsensusPrefetchingQueue>>) field.get(broker);
+    queuesByTopic.put(TOPIC_NAME, queues);
+  }
+}
diff --git 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/SubscriptionBrokerAgentPayloadLimitTest.java
 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/SubscriptionBrokerAgentPayloadLimitTest.java
new file mode 100644
index 00000000000..782f2338e9d
--- /dev/null
+++ 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/SubscriptionBrokerAgentPayloadLimitTest.java
@@ -0,0 +1,104 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.iotdb.db.subscription.broker;
+
+import org.apache.iotdb.db.subscription.agent.SubscriptionBrokerAgent;
+import org.apache.iotdb.db.subscription.event.SubscriptionEvent;
+import org.apache.iotdb.rpc.subscription.config.ConsumerConfig;
+import org.apache.iotdb.rpc.subscription.config.ConsumerConstant;
+import 
org.apache.iotdb.rpc.subscription.payload.poll.SubscriptionCommitContext;
+import 
org.apache.iotdb.rpc.subscription.payload.poll.SubscriptionPollResponseType;
+import org.apache.iotdb.rpc.subscription.payload.poll.TerminationPayload;
+
+import org.junit.Test;
+
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertSame;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class SubscriptionBrokerAgentPayloadLimitTest {
+
+  private static final String CONSUMER_GROUP_ID = "consumerGroup";
+  private static final String CONSUMER_ID = "consumer";
+  private static final String TOPIC_NAME = "topic";
+
+  @Test
+  public void 
testPollRequeuesFirstEventFromNextBrokerWhenRemainingBudgetIsInsufficient()
+      throws Exception {
+    final SubscriptionBrokerAgent agent = new SubscriptionBrokerAgent();
+    final ISubscriptionBroker firstBroker = mock(ISubscriptionBroker.class);
+    final ISubscriptionBroker secondBroker = mock(ISubscriptionBroker.class);
+    final SubscriptionEvent firstEvent = newEvent(1);
+    final SubscriptionEvent secondEvent = newEvent(2);
+    final long firstEventSize = firstEvent.getCurrentResponseSize();
+    final long secondEventSize = secondEvent.getCurrentResponseSize();
+    final long maxBytes = firstEventSize + secondEventSize - 1L;
+    final Set<String> topicNames = Collections.singleton(TOPIC_NAME);
+
+    when(firstBroker.poll(CONSUMER_ID, topicNames, maxBytes, 
Collections.emptyMap()))
+        .thenReturn(Collections.singletonList(firstEvent));
+    when(secondBroker.poll(
+            CONSUMER_ID, topicNames, maxBytes - firstEventSize, 
Collections.emptyMap()))
+        .thenReturn(Collections.singletonList(secondEvent));
+    when(secondBroker.requeue(CONSUMER_ID, 
secondEvent.getCommitContext())).thenReturn(true);
+    bindBrokers(agent, firstBroker, secondBroker);
+
+    final List<SubscriptionEvent> events = agent.poll(createConsumerConfig(), 
topicNames, maxBytes);
+
+    assertEquals(1, events.size());
+    assertSame(firstEvent, events.get(0));
+    verify(secondBroker).requeue(CONSUMER_ID, secondEvent.getCommitContext());
+    assertEquals(0L, secondEvent.getNackCount());
+  }
+
+  private static SubscriptionEvent newEvent(final int commitId) {
+    return new SubscriptionEvent(
+        SubscriptionPollResponseType.TERMINATION.getType(),
+        new TerminationPayload(),
+        new SubscriptionCommitContext(1, 1, TOPIC_NAME, CONSUMER_GROUP_ID, 
commitId));
+  }
+
+  private static ConsumerConfig createConsumerConfig() {
+    final Map<String, String> attributes = new HashMap<>();
+    attributes.put(ConsumerConstant.CONSUMER_ID_KEY, CONSUMER_ID);
+    attributes.put(ConsumerConstant.CONSUMER_GROUP_ID_KEY, CONSUMER_GROUP_ID);
+    return new ConsumerConfig(attributes);
+  }
+
+  @SuppressWarnings("unchecked")
+  private static void bindBrokers(
+      final SubscriptionBrokerAgent agent, final ISubscriptionBroker... 
brokers) throws Exception {
+    final Field field = 
SubscriptionBrokerAgent.class.getDeclaredField("consumerGroupIdToBrokers");
+    field.setAccessible(true);
+    final Map<String, List<ISubscriptionBroker>> brokersByConsumerGroup =
+        (Map<String, List<ISubscriptionBroker>>) field.get(agent);
+    brokersByConsumerGroup.put(CONSUMER_GROUP_ID, new 
ArrayList<>(List.of(brokers)));
+  }
+}
diff --git 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusPrefetchingQueueTest.java
 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusPrefetchingQueueTest.java
index b1fd02cc4c5..5b3fd00b928 100644
--- 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusPrefetchingQueueTest.java
+++ 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusPrefetchingQueueTest.java
@@ -1705,6 +1705,68 @@ public class ConsensusPrefetchingQueueTest {
     }
   }
 
+  @Test
+  public void testRequeueDoesNotIncrementNackCount() throws Exception {
+    final String originalSystemDir = 
IoTDBDescriptor.getInstance().getConfig().getSystemDir();
+    final File systemDir = 
temporaryFolder.newFolder("system-requeue-without-nack");
+    ConsensusPrefetchingQueue queue = null;
+    try {
+      final DataRegionId regionId = new DataRegionId(1);
+      final FakeConsensusReqReader reader = new FakeConsensusReqReader();
+      final IoTConsensusServerImpl serverImpl = 
mock(IoTConsensusServerImpl.class);
+      when(serverImpl.getConsensusReqReader()).thenReturn(reader);
+      when(serverImpl.getWriterSafeFrontierTracker()).thenReturn(new 
WriterSafeFrontierTracker());
+
+      final ConsensusLogToTabletConverter converter = 
mock(ConsensusLogToTabletConverter.class);
+      when(converter.convert(any()))
+          .thenReturn(Collections.singletonList(createTablet()), 
Collections.emptyList());
+      when(converter.getDatabaseName()).thenReturn("db");
+
+      queue =
+          new ConsensusPrefetchingQueue(
+              "consumerGroup",
+              "topic",
+              TopicConstant.ORDER_MODE_LEADER_ONLY_VALUE,
+              regionId,
+              serverImpl,
+              new SubscriptionWalRetentionPolicy(
+                  "topic",
+                  SubscriptionWalRetentionPolicy.UNBOUNDED,
+                  SubscriptionWalRetentionPolicy.UNBOUNDED),
+              converter,
+              newCommitManager(systemDir),
+              new RegionProgress(Collections.emptyMap()),
+              1L,
+              1L,
+              true);
+
+      reader.currentSearchIndex = 2L;
+      assertTrue(pendingEntries(queue).offer(createRequest(1L)));
+      assertTrue(pendingEntries(queue).offer(createRequest(2L)));
+      assertNull(queue.poll("consumer"));
+      queue.drivePrefetchOnce();
+
+      final SubscriptionEvent event = queue.poll("consumer");
+      assertNotNull(event);
+      assertEquals(1L, queue.getSubscriptionUncommittedEventCount());
+
+      assertTrue(queue.requeue("consumer", event.getCommitContext()));
+      assertEquals(0L, event.getNackCount());
+      assertEquals(0L, queue.getSubscriptionUncommittedEventCount());
+      assertEquals(1, queue.getPrefetchedEventCount());
+
+      final SubscriptionEvent redeliveredEvent = queue.poll("consumer");
+      assertSame(event, redeliveredEvent);
+      assertEquals(0L, redeliveredEvent.getNackCount());
+      assertTrue(queue.ack("consumer", redeliveredEvent.getCommitContext()));
+    } finally {
+      if (queue != null) {
+        queue.close();
+      }
+      
IoTDBDescriptor.getInstance().getConfig().setSystemDir(originalSystemDir);
+    }
+  }
+
   @Test
   public void testDeactivationReleasesMaterializedTabletMemory() throws 
Exception {
     final String originalSystemDir = 
IoTDBDescriptor.getInstance().getConfig().getSystemDir();

Reply via email to