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

AndrewJSchofield pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/kafka.git


The following commit(s) were added to refs/heads/trunk by this push:
     new 998ed9b2b68 KAFKA-20690: silent test-coverage hole in 
PersisterStateBatchCombinerTest (#22571)
998ed9b2b68 is described below

commit 998ed9b2b68f9008aa5088b67e79176417bce295
Author: Shekhar Prasad Rajak <[email protected]>
AuthorDate: Mon Jun 15 12:57:37 2026 +0530

    KAFKA-20690: silent test-coverage hole in PersisterStateBatchCombinerTest 
(#22571)
    
    Fixes https://issues.apache.org/jira/browse/KAFKA-20690 a silent
    test-coverage hole in PersisterStateBatchCombinerTest, corrects one
    wrong expected output it exposed, and removes a few hot-path allocations
    in PersisterStateBatchCombiner.
    
    Corrected the expectedResult for the "Handle overlapping batches in
    newBatches, same state" case to include the trailing (124..130, 0, 1)
    range the algorithm correctly emits.
    
    No  output change.
    
    Updated for the improvements and more tests :
    
    pruneBatches short-circuits when startOffset == -1 or the list is empty.
    getMergeCandidatePair reuses a single ArrayList buffer instead of
    allocating a LinkedList per call; uses bulk removeAll.
    mergeBatches caches compareBatchDeliveryInfo(candidate, prev) into a
    local instead of recomputing.
    
    Reviewers: Sushant Mahajan <[email protected]>, Andrew Schofield
    <[email protected]>
---
 .../share/PersisterStateBatchCombiner.java         |  64 ++++++------
 .../PersisterStateBatchCombinerStressTest.java     | 116 +++++++++++++++++++++
 .../share/PersisterStateBatchCombinerTest.java     |  91 +++++++---------
 3 files changed, 184 insertions(+), 87 deletions(-)

diff --git 
a/share-coordinator/src/main/java/org/apache/kafka/coordinator/share/PersisterStateBatchCombiner.java
 
b/share-coordinator/src/main/java/org/apache/kafka/coordinator/share/PersisterStateBatchCombiner.java
index 9078c5fee65..b4eda98cd66 100644
--- 
a/share-coordinator/src/main/java/org/apache/kafka/coordinator/share/PersisterStateBatchCombiner.java
+++ 
b/share-coordinator/src/main/java/org/apache/kafka/coordinator/share/PersisterStateBatchCombiner.java
@@ -21,7 +21,6 @@ import 
org.apache.kafka.server.share.persister.PersisterStateBatch;
 
 import java.util.ArrayList;
 import java.util.Iterator;
-import java.util.LinkedList;
 import java.util.List;
 import java.util.Objects;
 import java.util.TreeSet;
@@ -31,6 +30,7 @@ public class PersisterStateBatchCombiner {
     private final long startOffset;
     private TreeSet<PersisterStateBatch> sortedBatches;
     private List<PersisterStateBatch> finalBatchList;   // final list is built 
here
+    private final List<PersisterStateBatch> nonOverlappingBuffer = new 
ArrayList<>();   // reused per findMergeCandidatePair call
 
     public PersisterStateBatchCombiner(
         List<PersisterStateBatch> batchesSoFar,
@@ -111,7 +111,8 @@ public class PersisterStateBatchCombiner {
             sortedBatches.remove(prev);
             sortedBatches.remove(candidate);
 
-            if (compareBatchDeliveryInfo(candidate, prev) == 0) {  // same 
state and overlap or contiguous
+            int cmp = compareBatchDeliveryInfo(candidate, prev);
+            if (cmp == 0) {  // same state and overlap or contiguous
                 // overlap and same state (prev.firstOffset <= 
candidate.firstOffset) due to sort
                 // covers:
                 // case:        1        2          3            4          5  
         6          7 (contiguous)
@@ -180,27 +181,27 @@ public class PersisterStateBatchCombiner {
         }
         Iterator<PersisterStateBatch> iter = sortedBatches.iterator();
         PersisterStateBatch prev = iter.next();
-        List<PersisterStateBatch> nonOverlapping = new LinkedList<>();
+        nonOverlappingBuffer.clear();
         while (iter.hasNext()) {
             PersisterStateBatch candidate = iter.next();
             if (candidate.firstOffset() <= prev.lastOffset() || // overlap
                 prev.lastOffset() + 1 == candidate.firstOffset() && 
compareBatchDeliveryInfo(prev, candidate) == 0) {  // contiguous
-                updateBatchContainers(nonOverlapping);
+                updateBatchContainers(nonOverlappingBuffer);
                 return new MergeCandidatePair(
                     prev,
                     candidate
                 );
             }
-            nonOverlapping.add(prev);
+            nonOverlappingBuffer.add(prev);
             prev = candidate;
         }
 
-        updateBatchContainers(nonOverlapping);
+        updateBatchContainers(nonOverlappingBuffer);
         return MergeCandidatePair.EMPTY;
     }
 
     private void updateBatchContainers(List<PersisterStateBatch> 
nonOverlappingBatches) {
-        nonOverlappingBatches.forEach(sortedBatches::remove);
+        sortedBatches.removeAll(nonOverlappingBatches);
         finalBatchList.addAll(nonOverlappingBatches);
     }
 
@@ -212,31 +213,32 @@ public class PersisterStateBatchCombiner {
      * the part after it is preserved.
      */
     private void pruneBatches() {
-        if (startOffset != -1) {
-            List<PersisterStateBatch> retainedBatches = new 
ArrayList<>(combinedBatchList.size());
-            combinedBatchList.forEach(batch -> {
-                if (batch.lastOffset() < startOffset) {
-                    // batch is expired, skip current iteration
-                    // -------
-                    //         | -> start offset
-                    return;
-                }
-
-                if (batch.firstOffset() >= startOffset) {
-                    // complete batch is valid
-                    //    ---------
-                    //  | -> start offset
-                    retainedBatches.add(batch);
-                } else {
-                    // start offset intersects batch
-                    //   ---------
-                    //       |     -> start offset
-                    retainedBatches.add(new PersisterStateBatch(startOffset, 
batch.lastOffset(), batch.deliveryState(), batch.deliveryCount()));
-                }
-            });
-            // update the instance variable
-            combinedBatchList = retainedBatches;
+        if (startOffset == -1 || combinedBatchList.isEmpty()) {
+            return;
         }
+        List<PersisterStateBatch> retainedBatches = new 
ArrayList<>(combinedBatchList.size());
+        combinedBatchList.forEach(batch -> {
+            if (batch.lastOffset() < startOffset) {
+                // batch is expired, skip current iteration
+                // -------
+                //         | -> start offset
+                return;
+            }
+
+            if (batch.firstOffset() >= startOffset) {
+                // complete batch is valid
+                //    ---------
+                //  | -> start offset
+                retainedBatches.add(batch);
+            } else {
+                // start offset intersects batch
+                //   ---------
+                //       |     -> start offset
+                retainedBatches.add(new PersisterStateBatch(startOffset, 
batch.lastOffset(), batch.deliveryState(), batch.deliveryCount()));
+            }
+        });
+        // update the instance variable
+        combinedBatchList = retainedBatches;
     }
 
     private void handleSameStateMerge(PersisterStateBatch prev, 
PersisterStateBatch candidate) {
diff --git 
a/share-coordinator/src/test/java/org/apache/kafka/coordinator/share/PersisterStateBatchCombinerStressTest.java
 
b/share-coordinator/src/test/java/org/apache/kafka/coordinator/share/PersisterStateBatchCombinerStressTest.java
new file mode 100644
index 00000000000..5eafff1554d
--- /dev/null
+++ 
b/share-coordinator/src/test/java/org/apache/kafka/coordinator/share/PersisterStateBatchCombinerStressTest.java
@@ -0,0 +1,116 @@
+/*
+ * 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.kafka.coordinator.share;
+
+import org.apache.kafka.server.share.persister.PersisterStateBatch;
+
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Random;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class PersisterStateBatchCombinerStressTest {
+
+    @Test
+    public void manyContiguousSameStateBatchesCoalesceIntoOne() {
+        final int batchCount = 10_000;
+        List<PersisterStateBatch> input = new ArrayList<>(batchCount);
+        for (int i = 0; i < batchCount; i++) {
+            long first = i * 10L;
+            input.add(new PersisterStateBatch(first, first + 9, (byte) 0, 
(short) 1));
+        }
+
+        List<PersisterStateBatch> result = new 
PersisterStateBatchCombiner(input, List.of(), -1)
+            .combineStateBatches();
+
+        assertEquals(1, result.size(), "all contiguous same-state batches must 
coalesce");
+        PersisterStateBatch only = result.get(0);
+        assertEquals(0L, only.firstOffset());
+        assertEquals(batchCount * 10L - 1, only.lastOffset());
+        assertEquals((byte) 0, only.deliveryState());
+        assertEquals((short) 1, only.deliveryCount());
+    }
+
+    @Test
+    public void manyShuffledSameStateBatchesCoalesceIntoOne() {
+        final int batchCount = 5_000;
+        List<PersisterStateBatch> input = new ArrayList<>(batchCount);
+        for (int i = 0; i < batchCount; i++) {
+            long first = i * 10L;
+            input.add(new PersisterStateBatch(first, first + 9, (byte) 0, 
(short) 1));
+        }
+        Collections.shuffle(input, new Random(42));
+
+        List<PersisterStateBatch> result = new 
PersisterStateBatchCombiner(input, List.of(), -1)
+            .combineStateBatches();
+
+        assertEquals(1, result.size());
+        assertEquals(0L, result.get(0).firstOffset());
+        assertEquals(batchCount * 10L - 1, result.get(0).lastOffset());
+    }
+
+    @Test
+    public void alternatingStateBatchesAreNotMerged() {
+        final int batchCount = 1_000;
+        List<PersisterStateBatch> input = new ArrayList<>(batchCount);
+        for (int i = 0; i < batchCount; i++) {
+            long first = i * 10L;
+            byte state = (byte) (i % 2 == 0 ? 0 : 2);
+            input.add(new PersisterStateBatch(first, first + 9, state, (short) 
1));
+        }
+
+        List<PersisterStateBatch> result = new 
PersisterStateBatchCombiner(input, List.of(), -1)
+            .combineStateBatches();
+
+        assertEquals(batchCount, result.size(), "alternating-state batches 
must not coalesce");
+        for (int i = 0; i < batchCount; i++) {
+            assertEquals(i * 10L, result.get(i).firstOffset());
+            assertEquals(i * 10L + 9, result.get(i).lastOffset());
+        }
+    }
+
+    @Test
+    public void resultIsSortedDisjointCoverage() {
+        final int batchCount = 2_000;
+        List<PersisterStateBatch> input = new ArrayList<>(batchCount);
+        Random rng = new Random(7);
+        for (int i = 0; i < batchCount; i++) {
+            long first = rng.nextInt(100_000);
+            long last = first + rng.nextInt(50);
+            byte state = (byte) (rng.nextInt(4));
+            short count = (short) (rng.nextInt(3) + 1);
+            input.add(new PersisterStateBatch(first, last, state, count));
+        }
+
+        List<PersisterStateBatch> result = new 
PersisterStateBatchCombiner(input, List.of(), -1)
+            .combineStateBatches();
+
+        for (int i = 1; i < result.size(); i++) {
+            PersisterStateBatch a = result.get(i - 1);
+            PersisterStateBatch b = result.get(i);
+            assertTrue(a.firstOffset() <= a.lastOffset(), "batch " + a + " 
must have first <= last");
+            assertTrue(a.lastOffset() < b.firstOffset(),
+                "batches must be strictly disjoint: " + a + " vs " + b);
+        }
+    }
+}
diff --git 
a/share-coordinator/src/test/java/org/apache/kafka/coordinator/share/PersisterStateBatchCombinerTest.java
 
b/share-coordinator/src/test/java/org/apache/kafka/coordinator/share/PersisterStateBatchCombinerTest.java
index 26ddbea2c14..1a74a9bd8b3 100644
--- 
a/share-coordinator/src/test/java/org/apache/kafka/coordinator/share/PersisterStateBatchCombinerTest.java
+++ 
b/share-coordinator/src/test/java/org/apache/kafka/coordinator/share/PersisterStateBatchCombinerTest.java
@@ -35,7 +35,6 @@ public class PersisterStateBatchCombinerTest {
         final List<PersisterStateBatch> newBatches;
         final List<PersisterStateBatch> expectedResult;
         final long startOffset;
-        final boolean shouldRun;
 
         BatchTestHolder(
             String testName,
@@ -43,24 +42,12 @@ public class PersisterStateBatchCombinerTest {
             List<PersisterStateBatch> newBatches,
             List<PersisterStateBatch> expectedResult,
             long startOffset
-        ) {
-            this(testName, batchesSoFar, newBatches, expectedResult, 
startOffset, false);
-        }
-
-        BatchTestHolder(
-            String testName,
-            List<PersisterStateBatch> batchesSoFar,
-            List<PersisterStateBatch> newBatches,
-            List<PersisterStateBatch> expectedResult,
-            long startOffset,
-            boolean shouldRun
         ) {
             this.testName = testName;
             this.batchesSoFar = batchesSoFar;
             this.newBatches = newBatches;
             this.expectedResult = expectedResult;
             this.startOffset = startOffset;
-            this.shouldRun = shouldRun;
         }
 
         static List<PersisterStateBatch> singleBatch(
@@ -162,8 +149,7 @@ public class PersisterStateBatchCombinerTest {
                 BatchTestHolder.singleBatch(100, 110, 0, 1),
                 BatchTestHolder.singleBatch(105, 108, 0, 1),
                 BatchTestHolder.singleBatch(100, 110, 0, 1),
-                -1,
-                true
+                -1
             ),
 
             new BatchTestHolder(
@@ -203,10 +189,11 @@ public class PersisterStateBatchCombinerTest {
                 BatchTestHolder.multiBatch()
                     .addBatch(111, 119, 2, 2)
                     .addBatch(116, 123, 2, 2)  // overlap with first batch
-                    .build(),       // ,  //[(111-123, 2, 2)]
+                    .build(),
                 BatchTestHolder.multiBatch()
                     .addBatch(100, 110, 0, 1)
                     .addBatch(111, 123, 2, 2)
+                    .addBatch(124, 130, 0, 1)
                     .build(),
                 -1
             ),
@@ -385,60 +372,52 @@ public class PersisterStateBatchCombinerTest {
     @ParameterizedTest
     @MethodSource("generatorDifferentStates")
     public void testStateBatchCombineDifferentStates(BatchTestHolder test) {
-        if (test.shouldRun) {
-            assertEquals(test.expectedResult,
-                new PersisterStateBatchCombiner(
-                    test.batchesSoFar,
-                    test.newBatches,
-                    test.startOffset)
-                    .combineStateBatches(),
-                test.testName
-            );
-        }
+        assertEquals(test.expectedResult,
+            new PersisterStateBatchCombiner(
+                test.batchesSoFar,
+                test.newBatches,
+                test.startOffset)
+                .combineStateBatches(),
+            test.testName
+        );
     }
 
     @ParameterizedTest
     @MethodSource("generatorSameState")
     public void testStateBatchCombineSameState(BatchTestHolder test) {
-        if (test.shouldRun) {
-            assertEquals(test.expectedResult,
-                new PersisterStateBatchCombiner(
-                    test.batchesSoFar,
-                    test.newBatches,
-                    test.startOffset)
-                    .combineStateBatches(),
-                test.testName
-            );
-        }
+        assertEquals(test.expectedResult,
+            new PersisterStateBatchCombiner(
+                test.batchesSoFar,
+                test.newBatches,
+                test.startOffset)
+                .combineStateBatches(),
+            test.testName
+        );
     }
 
     @ParameterizedTest
     @MethodSource("generatorComplex")
     public void testStateBatchCombineComplexCases(BatchTestHolder test) {
-        if (test.shouldRun) {
-            assertEquals(test.expectedResult,
-                new PersisterStateBatchCombiner(
-                    test.batchesSoFar,
-                    test.newBatches,
-                    test.startOffset)
-                    .combineStateBatches(),
-                test.testName
-            );
-        }
+        assertEquals(test.expectedResult,
+            new PersisterStateBatchCombiner(
+                test.batchesSoFar,
+                test.newBatches,
+                test.startOffset)
+                .combineStateBatches(),
+            test.testName
+        );
     }
 
     @ParameterizedTest
     @MethodSource("generatorCornerCases")
     public void testStateBatchCombineCornerCases(BatchTestHolder test) {
-        if (test.shouldRun) {
-            assertEquals(test.expectedResult,
-                new PersisterStateBatchCombiner(
-                    test.batchesSoFar,
-                    test.newBatches,
-                    test.startOffset)
-                    .combineStateBatches(),
-                test.testName
-            );
-        }
+        assertEquals(test.expectedResult,
+            new PersisterStateBatchCombiner(
+                test.batchesSoFar,
+                test.newBatches,
+                test.startOffset)
+                .combineStateBatches(),
+            test.testName
+        );
     }
 }

Reply via email to