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 46b4636d682 KAFKA-20691: 
PersisterStateBatchCombiner.combineStateBatches() with an event-driven 
sweep-line (#22572)
46b4636d682 is described below

commit 46b4636d682d2134a9af01b37ace2479df09707c
Author: Shekhar Prasad Rajak <[email protected]>
AuthorDate: Mon Jun 15 22:21:42 2026 +0530

    KAFKA-20691: PersisterStateBatchCombiner.combineStateBatches() with an 
event-driven sweep-line (#22572)
    
    Fix https://issues.apache.org/jira/browse/KAFKA-20691
    
     Replaces the iterative TreeSet-based PersisterStateBatchCombiner merge
    logic with a sweep-line implementation. The new approach models state
    batches as start/end events, tracks active (deliveryCount,
    deliveryState) priorities with a counted TreeMap, and emits the winning
    non-overlapping ranges while preserving gaps.
    
    Before:
    
      - Iterative overlap merging with repeated candidate search/reinsert
    cycles.    - Worst-case behavior can grow beyond a single sort/sweep due
    to repeated TreeSet mutations and rescans.
    
    How:
    
    - Emit (firstOffset, BEGIN) and (lastOffset + 1, END) for each pruned
    batch.
    - Sort events by (offset asc, END before BEGIN).
    - Sweep events;
    - At every offset advance, emit a slice with the heap-top's (state,
    count); coalesce contiguous slices with identical (state, count).
    
    Result
    
    - Worst case: O(n^2) went to O((n + k) log p)
      - n = number of input batches
      - k = number of emitted/transition ranges
      - p = number of distinct active (deliveryCount, deliveryState)
    priorities
    
    - Public API unchanged.
    
    Reviewers: Sushant Mahajan <[email protected]>, Andrew Schofield
    <[email protected]>
---
 .../share/PersisterStateBatchCombiner.java         | 449 ++++++---------------
 .../share/PersisterStateBatchCombinerTest.java     |  50 +++
 2 files changed, 167 insertions(+), 332 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 b4eda98cd66..199af8a70dd 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
@@ -20,385 +20,170 @@ package org.apache.kafka.coordinator.share;
 import org.apache.kafka.server.share.persister.PersisterStateBatch;
 
 import java.util.ArrayList;
-import java.util.Iterator;
+import java.util.Comparator;
 import java.util.List;
-import java.util.Objects;
-import java.util.TreeSet;
+import java.util.TreeMap;
 
+/**
+ * Combines an existing list of {@link PersisterStateBatch} entries with a 
list of newly produced
+ * entries and returns the shortest non-overlapping, {@code (deliveryState, 
deliveryCount)}-distinct
+ * cover of the union, clipped at {@code startOffset} (SPSO).
+ *
+ * <p>The merge is performed by an event-driven sweep-line over the union of 
both inputs. Each
+ * input batch contributes one BEGIN event at {@code firstOffset} and one END 
event at
+ * {@code lastOffset + 1}. Events are processed in offset order (END before 
BEGIN at the same
+ * offset). A counted ordered map tracks the currently active priorities; the 
first key defines the
+ * {@code (state, count)} that wins on the current sub-range. Successive 
sub-ranges with identical
+ * {@code (state, count)} are coalesced on the fly.
+ *
+ * <p>Complexity: {@code O((n + k) log p)} where {@code n} is the total number 
of input batches,
+ * {@code k} is the number of overlap transitions encountered, and {@code p} 
is the number of
+ * distinct {@code (deliveryState, deliveryCount)} priorities.
+ */
 public class PersisterStateBatchCombiner {
-    private List<PersisterStateBatch> combinedBatchList;    // link between 
pruning and merging
+    private static final Comparator<BatchPriority> PRIORITY_DESC = (a, b) -> {
+        int cmpCount = Short.compare(b.deliveryCount(), a.deliveryCount());
+        if (cmpCount != 0) {
+            return cmpCount;
+        }
+        return Byte.compare(b.deliveryState(), a.deliveryState());
+    };
+
+    private final List<PersisterStateBatch> batchesSoFar;
+    private final List<PersisterStateBatch> newBatches;
     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,
         List<PersisterStateBatch> newBatches,
         long startOffset
     ) {
-        initializeCombinedList(batchesSoFar, newBatches);
-        int estimatedResultSize = (combinedBatchList.size() * 3) / 2;   // 
heuristic size - 50% overallocation
-        finalBatchList = new ArrayList<>(estimatedResultSize);
+        this.batchesSoFar = batchesSoFar == null ? List.of() : batchesSoFar;
+        this.newBatches = newBatches == null ? List.of() : newBatches;
         this.startOffset = startOffset;
     }
 
-    private void initializeCombinedList(List<PersisterStateBatch> 
batchesSoFar, List<PersisterStateBatch> newBatches) {
-        boolean soFarEmpty = batchesSoFar == null || batchesSoFar.isEmpty();
-        boolean newBatchesEmpty = newBatches == null || newBatches.isEmpty();
-
-        if (soFarEmpty && newBatchesEmpty) {
-            combinedBatchList = new ArrayList<>();
-        } else if (soFarEmpty) {
-            combinedBatchList = new ArrayList<>(newBatches);    // new list as 
the original one could be unmodifiable
-        } else if (newBatchesEmpty) {
-            combinedBatchList = new ArrayList<>(batchesSoFar);  // new list as 
the original one could be unmodifiable
-        } else {
-            combinedBatchList = new ArrayList<>(batchesSoFar.size() + 
newBatches.size());
-            combinedBatchList.addAll(batchesSoFar);
-            combinedBatchList.addAll(newBatches);
-        }
-    }
-
     /**
-     * Algorithm: Merge current state batches and new batches into a single 
non-overlapping batch list.
-     * Input: batchesSoFar, newBatches, startOffset
-     * Output: combined list with non-overlapping batches (finalBatchList)
-     * <p>
-     * - Add both currentBatches and newBatches into a single list 
combinedBatchList
-     * - if combinedBatchList.size() <= 1 return combinedBatchList
-     * <p>
-     * - Remove/prune any batches from the combinedBatchList:
-     * -    if batch.lastOffset < startOffset then remove batch from 
combinedBatchList
-     * -    else if batch.firstOffset > startOffset then we will keep the batch
-     * -    else if batch.firstOffset <= startOffset <= batch.lastOffset then 
keep [startOffset, batch.lastOffset] part only and discard rest.
-     * <p>
-     * - create a treeset sortedBatches using pruned combinedBatchList
-     * - find first 2 mergeable batches in sortedBatches set, say, prev and 
candidate.
-     * - remove any non-overlapping batches from sortedBatches encountered 
during the find operation and add them to a finalBatchList
-     * - do repeat until a mergeable pair is not found:
-     * -    based on various conditions of offset overlap and batch state 
differences combine the batches or
-     *      create new batches, if required, and add to the sortedBatches.
-     * -    find first 2 mergeable batches in sortedBatches set, say, prev and 
candidate.
-     * -    remove any non-mergeable batches from sortedBatches encountered 
during the find operation and add them to a finalBatchList
-     * - done
-     * - return the finalBatchList
-     *
-     * @return list of {@link PersisterStateBatch} representing 
non-overlapping combined batches
+     * Produces the merged, pruned, non-overlapping batch list.
      */
     public List<PersisterStateBatch> combineStateBatches() {
-        pruneBatches();
-        mergeBatches();
-        return finalBatchList;
-    }
-
-    private void mergeBatches() {
-        if (combinedBatchList.size() < 2) {
-            finalBatchList = combinedBatchList;
-            return;
-        }
-
-        sortedBatches = new TreeSet<>(combinedBatchList);
-
-        MergeCandidatePair overlapState = getMergeCandidatePair();
-
-        while (overlapState != MergeCandidatePair.EMPTY) {
-            PersisterStateBatch prev = overlapState.prev();
-            PersisterStateBatch candidate = overlapState.candidate();
-
-            // remove both previous and candidate for easier
-            // assessment about adding batches to sortedBatches
-            sortedBatches.remove(prev);
-            sortedBatches.remove(candidate);
-
-            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)
-                // prev:        ------   -------    -------      -------   
-------   --------    -------
-                // candidate:   ------   ----       ----------     ---        
----       -------        -------
-                handleSameStateMerge(prev, candidate);  // pair can be 
contiguous or overlapping
-            } else {
-                // If we reach here then it is guaranteed that the batch pair 
is overlapping and
-                // non-contiguous because getMergeCandidatePair only returns 
contiguous pair if
-                // the constituents have the same delivery count and state.
-                // covers:
-                // case:        1        2*          3            4          5 
          6             7*
-                // prev:        ------   -------    -------      -------    
-------     --------      ------
-                // candidate:   ------   ----       ---------      ----        
----        -------   -------
-                // max batches: 1           2       2                3         
 2            2          2
-                // min batches: 1           1       1                1         
 1            2          1
-                // * not possible with treeset
-                handleDiffStateOverlap(prev, candidate);
-            }
-            overlapState = getMergeCandidatePair();
+        List<PersisterStateBatch> pruned = prune();
+        if (pruned.isEmpty()) {
+            return pruned;
         }
-        finalBatchList.addAll(sortedBatches);   // some non overlapping 
batches might have remained
-    }
-
-    /**
-     * Compares the non-offset state of 2 batches i.e. the deliveryCount and 
deliverState.
-     * <p>
-     * Uses standard compareTo contract x < y => +int, x > y => -int, x == y 
=> 0
-     *
-     * @param b1 - {@link PersisterStateBatch} to compare
-     * @param b2 - {@link PersisterStateBatch} to compare
-     * @return int representing comparison result.
-     */
-    private int compareBatchDeliveryInfo(PersisterStateBatch b1, 
PersisterStateBatch b2) {
-        int deltaCount = Short.compare(b1.deliveryCount(), b2.deliveryCount());
-
-        // Delivery state could be:
-        // 0 - AVAILABLE (non-terminal)
-        // 1 - ACQUIRED - should not be persisted yet
-        // 2 - ACKNOWLEDGED (terminal)
-        // 3 - ARCHIVING - not implemented in KIP-932 - non-terminal - leads 
only to ARCHIVED
-        // 4 - ARCHIVED (terminal)
-
-        if (deltaCount == 0) {   // same delivery count
-            return Byte.compare(b1.deliveryState(), b2.deliveryState());
+        if (pruned.size() == 1) {
+            return pruned;
         }
-        return deltaCount;
+        return sweepMerge(pruned);
     }
 
     /**
-     * Accepts a sorted set of state batches and finds the first 2 batches 
which can be merged.
-     * Merged implies that they have some offsets in common or, they are 
contiguous with the same state.
-     * <p>
-     * Any non-mergeable batches prefixing a good mergeable pair are removed 
from the sortedBatches.
-     * For example:
-     * ----- ----  ----- -----      -----
-     *                      ------
-     * <---------------> <-------->
-     * non-overlapping   1st overlapping pair
-     *
-     * @return object representing the overlap state
+     * Drops or clips ranges below {@code startOffset}. Returns a new list; 
never modifies inputs.
      */
-    private MergeCandidatePair getMergeCandidatePair() {
-        if (sortedBatches == null || sortedBatches.isEmpty()) {
-            return MergeCandidatePair.EMPTY;
-        }
-        Iterator<PersisterStateBatch> iter = sortedBatches.iterator();
-        PersisterStateBatch prev = iter.next();
-        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(nonOverlappingBuffer);
-                return new MergeCandidatePair(
-                    prev,
-                    candidate
-                );
+    private List<PersisterStateBatch> prune() {
+        int estimate = batchesSoFar.size() + newBatches.size();
+        List<PersisterStateBatch> out = new ArrayList<>(estimate);
+        addPruned(out, batchesSoFar);
+        addPruned(out, newBatches);
+        return out;
+    }
+
+    private void addPruned(List<PersisterStateBatch> out, 
List<PersisterStateBatch> src) {
+        for (PersisterStateBatch b : src) {
+            if (startOffset != -1 && b.lastOffset() < startOffset) {
+                // batch fully expired
+                continue;
+            }
+            if (startOffset == -1 || b.firstOffset() >= startOffset) {
+                out.add(b);
+            } else {
+                // start offset intersects batch -> clip
+                out.add(new PersisterStateBatch(startOffset, b.lastOffset(), 
b.deliveryState(), b.deliveryCount()));
             }
-            nonOverlappingBuffer.add(prev);
-            prev = candidate;
         }
-
-        updateBatchContainers(nonOverlappingBuffer);
-        return MergeCandidatePair.EMPTY;
-    }
-
-    private void updateBatchContainers(List<PersisterStateBatch> 
nonOverlappingBatches) {
-        sortedBatches.removeAll(nonOverlappingBatches);
-        finalBatchList.addAll(nonOverlappingBatches);
     }
 
     /**
-     * Accepts a list of {@link PersisterStateBatch} and checks:
-     * - last offset is < start offset => batch is removed
-     * - first offset > start offset => batch is preserved
-     * - start offset intersects the batch => part of batch before start 
offset is removed and
-     * the part after it is preserved.
+     * Event-driven sweep. Linear time after the initial event sort.
      */
-    private void pruneBatches() {
-        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()));
+    private List<PersisterStateBatch> sweepMerge(List<PersisterStateBatch> 
batches) {
+        int n = batches.size();
+        Event[] events = new Event[n * 2];
+        for (int i = 0; i < n; i++) {
+            PersisterStateBatch b = batches.get(i);
+            BatchPriority priority = BatchPriority.from(b);
+            events[i * 2] = new Event(b.firstOffset(), true, priority);
+            events[i * 2 + 1] = new Event(b.lastOffset() + 1, false, priority);
+        }
+        // END (isBegin=false) sorts before BEGIN (isBegin=true) at the same 
offset so that
+        // contiguous same-state ranges meeting at offset X collapse to a 
single emit.
+        java.util.Arrays.sort(events, (e1, e2) -> {
+            int cmp = Long.compare(e1.offset, e2.offset);
+            if (cmp != 0) {
+                return cmp;
             }
+            return Boolean.compare(e1.isBegin, e2.isBegin);
         });
-        // update the instance variable
-        combinedBatchList = retainedBatches;
-    }
 
-    private void handleSameStateMerge(PersisterStateBatch prev, 
PersisterStateBatch candidate) {
-        sortedBatches.add(new PersisterStateBatch(
-            prev.firstOffset(),
-            // cover cases
-            // prev:      ------   --------       ---------
-            // candidate:   ---       ----------           -----
-            Math.max(candidate.lastOffset(), prev.lastOffset()),
-            prev.deliveryState(),
-            prev.deliveryCount()
-        ));
-    }
+        TreeMap<BatchPriority, Integer> active = new TreeMap<>(PRIORITY_DESC);
+        List<PersisterStateBatch> out = new ArrayList<>();
+        long openFrom = -1;
+        BatchPriority openWinner = null;
 
-    private void handleDiffStateOverlap(PersisterStateBatch prev, 
PersisterStateBatch candidate) {
-        if (candidate.firstOffset() == prev.firstOffset()) {
-            handleDiffStateOverlapFirstOffsetAligned(prev, candidate);
-        } else {    // candidate.firstOffset() > prev.firstOffset()
-            handleDiffStateOverlapFirstOffsetNotAligned(prev, candidate);
-        }
-    }
+        int i = 0;
+        while (i < events.length) {
+            long offset = events[i].offset;
 
-    private void handleDiffStateOverlapFirstOffsetAligned(PersisterStateBatch 
prev, PersisterStateBatch candidate) {
-        if (candidate.lastOffset() == prev.lastOffset()) {  // case 1
-            // candidate can never have lower or equal priority
-            // since sortedBatches order takes that into account.
-            // -------
-            // -------
-            sortedBatches.add(candidate);
-        } else {
-            // case 2 is not possible with TreeSet. It is symmetric to case 3.
-            // case 3
-            // --------
-            // -----------
-            if (compareBatchDeliveryInfo(candidate, prev) < 0) {
-                sortedBatches.add(prev);
-                sortedBatches.add(new PersisterStateBatch(
-                    prev.lastOffset() + 1,
-                    candidate.lastOffset(),
-                    candidate.deliveryState(),
-                    candidate.deliveryCount()
-                ));
-            } else {
-                // candidate priority is >= prev
-                sortedBatches.add(candidate);
+            // Emit the sub-range that ended at `offset - 1` (if one was open).
+            if (openWinner != null && offset > openFrom) {
+                appendCoalesced(out, openFrom, offset - 1, openWinner);
             }
-        }
-    }
-
-    private void 
handleDiffStateOverlapFirstOffsetNotAligned(PersisterStateBatch prev, 
PersisterStateBatch candidate) {
-        if (candidate.lastOffset() < prev.lastOffset()) {    // case 4
-            handleDiffStateOverlapPrevSwallowsCandidate(prev, candidate);
-        } else if (candidate.lastOffset() == prev.lastOffset()) {    // case 5
-            handleDiffStateOverlapLastOffsetAligned(prev, candidate);
-        } else {    // case 6
-            handleDiffStateOverlapCandidateOffsetsLarger(prev, candidate);
-        }
-    }
-
-    private void 
handleDiffStateOverlapPrevSwallowsCandidate(PersisterStateBatch prev, 
PersisterStateBatch candidate) {
-        // --------
-        //   ----
-        if (compareBatchDeliveryInfo(candidate, prev) < 0) {
-            sortedBatches.add(prev);
-        } else {
-            sortedBatches.add(new PersisterStateBatch(
-                prev.firstOffset(),
-                candidate.firstOffset() - 1,
-                prev.deliveryState(),
-                prev.deliveryCount()
-            ));
 
-            sortedBatches.add(candidate);
+            // Apply every event at this offset (END before BEGIN by sort 
order).
+            while (i < events.length && events[i].offset == offset) {
+                Event e = events[i++];
+                if (e.isBegin) {
+                    active.merge(e.priority, 1, Integer::sum);
+                } else {
+                    decrement(active, e.priority);
+                }
+            }
 
-            sortedBatches.add(new PersisterStateBatch(
-                candidate.lastOffset() + 1,
-                prev.lastOffset(),
-                prev.deliveryState(),
-                prev.deliveryCount()
-            ));
+            openWinner = active.isEmpty() ? null : active.firstKey();
+            openFrom = offset;
         }
+        return out;
     }
 
-    private void handleDiffStateOverlapLastOffsetAligned(PersisterStateBatch 
prev, PersisterStateBatch candidate) {
-        // --------
-        //    -----
-        if (compareBatchDeliveryInfo(candidate, prev) < 0) {
-            sortedBatches.add(prev);
+    private void decrement(TreeMap<BatchPriority, Integer> active, 
BatchPriority priority) {
+        int count = active.get(priority);
+        if (count == 1) {
+            active.remove(priority);
         } else {
-            sortedBatches.add(new PersisterStateBatch(
-                prev.firstOffset(),
-                candidate.firstOffset() - 1,
-                prev.deliveryState(),
-                prev.deliveryCount()
-            ));
-
-            sortedBatches.add(candidate);
+            active.put(priority, count - 1);
         }
     }
 
-    private void 
handleDiffStateOverlapCandidateOffsetsLarger(PersisterStateBatch prev, 
PersisterStateBatch candidate) {
-        //   -------
-        //      -------
-        if (compareBatchDeliveryInfo(candidate, prev) < 0) {
-            sortedBatches.add(prev);
-
-            sortedBatches.add(new PersisterStateBatch(
-                prev.lastOffset() + 1,
-                candidate.lastOffset(),
-                candidate.deliveryState(),
-                candidate.deliveryCount()
-            ));
-        } else {
-            // candidate has higher priority
-            sortedBatches.add(new PersisterStateBatch(
-                prev.firstOffset(),
-                candidate.firstOffset() - 1,
-                prev.deliveryState(),
-                prev.deliveryCount()
-            ));
-
-            sortedBatches.add(candidate);
+    private void appendCoalesced(List<PersisterStateBatch> out, long from, 
long to, BatchPriority winner) {
+        if (!out.isEmpty()) {
+            PersisterStateBatch tail = out.get(out.size() - 1);
+            if (tail.lastOffset() + 1 == from
+                && tail.deliveryState() == winner.deliveryState()
+                && tail.deliveryCount() == winner.deliveryCount()) {
+                out.set(out.size() - 1, new PersisterStateBatch(
+                    tail.firstOffset(), to, winner.deliveryState(), 
winner.deliveryCount()));
+                return;
+            }
         }
+        out.add(new PersisterStateBatch(from, to, winner.deliveryState(), 
winner.deliveryCount()));
     }
 
-    /**
-     * Holder class for intermediate state
-     * used in the batch merge algorithm.
-     */
-    static class MergeCandidatePair {
-        private final PersisterStateBatch prev;
-        private final PersisterStateBatch candidate;
-        public static final MergeCandidatePair EMPTY = new 
MergeCandidatePair(null, null);
-
-        public MergeCandidatePair(
-            PersisterStateBatch prev,
-            PersisterStateBatch candidate
-        ) {
-            this.prev = prev;
-            this.candidate = candidate;
-        }
-
-        public PersisterStateBatch prev() {
-            return prev;
-        }
-
-        public PersisterStateBatch candidate() {
-            return candidate;
-        }
-
-        @Override
-        public boolean equals(Object o) {
-            if (this == o) return true;
-            if (!(o instanceof MergeCandidatePair that)) return false;
-            return Objects.equals(prev, that.prev) && 
Objects.equals(candidate, that.candidate);
+    private record BatchPriority(short deliveryCount, byte deliveryState) {
+        private static BatchPriority from(PersisterStateBatch batch) {
+            return new BatchPriority(batch.deliveryCount(), 
batch.deliveryState());
         }
+    }
 
-        @Override
-        public int hashCode() {
-            return Objects.hash(prev, candidate);
-        }
+    private record Event(long offset, boolean isBegin, BatchPriority priority) 
{
     }
 }
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 1a74a9bd8b3..b4e419070cf 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
@@ -113,6 +113,56 @@ public class PersisterStateBatchCombinerTest {
                 List.of(),
                 List.of(),
                 120
+            ),
+
+            new BatchTestHolder(
+                "Null current and new batches are treated as empty.",
+                null,
+                null,
+                List.of(),
+                -1
+            ),
+
+            new BatchTestHolder(
+                "Duplicate equal batches ending before a later batch do not 
fill the gap.",
+                BatchTestHolder.singleBatch(100, 110, 0, 1),
+                BatchTestHolder.multiBatch()
+                    .addBatch(100, 110, 0, 1)
+                    .addBatch(120, 130, 2, 1)
+                    .build(),
+                BatchTestHolder.multiBatch()
+                    .addBatch(100, 110, 0, 1)
+                    .addBatch(120, 130, 2, 1)
+                    .build(),
+                -1
+            ),
+
+            new BatchTestHolder(
+                "Duplicate state-2 batches ending before a later state-0 batch 
do not fill the gap.",
+                BatchTestHolder.singleBatch(0, 5, 2, 1),
+                BatchTestHolder.multiBatch()
+                    .addBatch(0, 5, 2, 1)
+                    .addBatch(11, 15, 0, 1)
+                    .build(),
+                BatchTestHolder.multiBatch()
+                    .addBatch(0, 5, 2, 1)
+                    .addBatch(11, 15, 0, 1)
+                    .build(),
+                -1
+            ),
+
+            new BatchTestHolder(
+                "Duplicate state-0 batches ending before a later state-0 batch 
do not fill the gap.",
+                BatchTestHolder.singleBatch(0, 5, 0, 1),
+                BatchTestHolder.multiBatch()
+                    .addBatch(0, 5, 0, 1)
+                    .addBatch(11, 15, 0, 1)
+                    .build(),
+                BatchTestHolder.multiBatch()
+                    .addBatch(0, 5, 0, 1)
+                    .addBatch(11, 15, 0, 1)
+                    .build(),
+                -1
             )
         );
     }

Reply via email to