apache/pulsar · error · java.lang.IllegalArgumentException

Entry-bucket boundaries must be ascending, contiguous and…

Error message

Entry-bucket boundaries must be ascending, contiguous and start at 0: found [${start},${end}] where start ${expectedStart} was expected

What it means

Each declared boundary range must start exactly where the previous one ended plus one (contiguity), ranges must be ascending, each range must be non-empty (end >= start), and the first must start at 0. validateBucketBoundaries enforces this while converting the IntRange list into broker-side Range objects; a gap, overlap, descending order, or inverted range means entries in the skipped or duplicated bucket span would have no owner or two owners, so the subscription is rejected.

Solutions

  1. Recompute the boundary list so range i starts at range i-1's end + 1, the first range starts at 0, and every range has end >= start.
  2. Generate boundaries programmatically (e.g. split 0..65535 into N equal-width segments in a loop) instead of hand-writing constants.
  3. Verify all consumers and the producing tooling use the identical, sorted boundary list; fix any stale client that sends a divergent list.

Example fix

// before (gap + inverted range)
ksm.addHashRange().setStart(0).setEnd(16383);
ksm.addHashRange().setStart(17000).setEnd(16999); // wrong start, end < start
// after
int segments = 4, size = 65536 / segments;
for (int i = 0; i < segments; i++) {
    ksm.addHashRange().setStart(i * size).setEnd((i + 1) * size - 1);
}
Defensive patterns

Strategy: validation

Validate before calling

static void validateRangesLocal(KeySharedMeta ksm) {
    int expectedStart = 0;
    for (int i = 0; i < ksm.getHashRangesCount(); i++) {
        var r = ksm.getHashRangeAt(i);
        if (r.getStart() != expectedStart || r.getEnd() < r.getStart()) {
            throw new IllegalArgumentException("bad range [" + r.getStart() + "," + r.getEnd()
                + "] at index " + i + ", expected start " + expectedStart);
        }
        expectedStart = r.getEnd() + 1;
    }
}

Type guard

static boolean rangesAreContiguousFromZero(KeySharedMeta ksm) {
    int expectedStart = 0;
    for (int i = 0; i < ksm.getHashRangesCount(); i++) {
        var r = ksm.getHashRangeAt(i);
        if (r.getStart() != expectedStart || r.getEnd() < r.getStart()) return false;
        expectedStart = r.getEnd() + 1;
    }
    return true;
}

Try / catch

try {
    subscribeWithEntryBuckets(ksm);
} catch (IllegalArgumentException e) {
    if (e.getMessage().contains("ascending, contiguous")) {
        regenerateAndResubscribeWithComputedBoundaries();
    } else throw e;
}

Prevention

When it happens

Trigger: Calling subscribe with hashRanges that start above 0, omit a stretch of the ring (gap between range i's end+1 and range i+1's start), overlap (start < previous end+1), are listed out of ascending order, or contain start > end (inverted range).

Common situations: Hand-computed bucket boundaries with arithmetic mistakes (off-by-one between segments); boundary lists generated by buggy tooling that doesn't sort or dedupe ranges; copying a single consumer's ranges but editing one segment without adjusting the neighbors; producers/consumers built from different layout snapshots.

Related errors


AI-assisted analysis of apache/pulsar@820761864e (2026-09-06). Data as JSON: /api/errors/99c770330802da48. Report an issue: GitHub.

Appendix: source

Thrown at pulsar-broker/src/main/java/org/apache/pulsar/broker/service/persistent/PersistentEntryBucketDispatcherMultipleConsumers.java:89

    }

    /**
     * Validate and convert the boundaries declared at subscribe time: ascending, inclusive,
     * contiguous ranges tiling the whole 16-bit entry-bucket ring, with bucket 0 wide enough to
     * contain the canonical hash 1.
     */
    static List<Range> validateBucketBoundaries(KeySharedMeta ksm) {
        int count = ksm.getHashRangesCount();
        if (count == 0) {
            throw new IllegalArgumentException(
                    "Entry-bucket subscription must declare the segment's bucket boundaries");
        }
        List<Range> ranges = new ArrayList<>(count);
        int expectedStart = 0;
        for (int i = 0; i < count; i++) {
            IntRange r = ksm.getHashRangeAt(i);
            if (r.getStart() != expectedStart || r.getEnd() < r.getStart()) {
                throw new IllegalArgumentException("Entry-bucket boundaries must be ascending, "
                        + "contiguous and start at 0: found [" + r.getStart() + "," + r.getEnd()
                        + "] where start " + expectedStart + " was expected");
            }
            ranges.add(Range.of(r.getStart(), r.getEnd()));
            expectedStart = r.getEnd() + 1;
        }
        if (expectedStart != EntryBucketConsumerSelector.DEFAULT_RANGE_SIZE) {
            throw new IllegalArgumentException("Entry-bucket boundaries must tile the 16-bit ring: "
                    + "last range ends at " + (expectedStart - 1));
        }
        if (ranges.get(0).getEnd() < 1) {
            throw new IllegalArgumentException(
                    "Entry-bucket 0 must span at least [0,1] to hold the canonical hash");
        }
        return ranges;
    }

    @Override

View on GitHub (pinned to 820761864e)