{"record":{"id":"99c770330802da48","repo":"apache/pulsar","slug":"entry-bucket-boundaries-must-be-ascending-contigu","errorCode":null,"errorMessage":"Entry-bucket boundaries must be ascending, contiguous and start at 0: found [${start},${end}] where start ${expectedStart} was expected","messagePattern":"Entry-bucket boundaries must be ascending, contiguous and start at 0: found \\[(.+?),(.+?)\\] where start (.+?) was expected","errorType":"validation","errorClass":"java.lang.IllegalArgumentException","httpStatus":null,"severity":"error","filePath":"pulsar-broker/src/main/java/org/apache/pulsar/broker/service/persistent/PersistentEntryBucketDispatcherMultipleConsumers.java","lineNumber":89,"sourceCode":"    }\n\n    /**\n     * Validate and convert the boundaries declared at subscribe time: ascending, inclusive,\n     * contiguous ranges tiling the whole 16-bit entry-bucket ring, with bucket 0 wide enough to\n     * contain the canonical hash 1.\n     */\n    static List<Range> validateBucketBoundaries(KeySharedMeta ksm) {\n        int count = ksm.getHashRangesCount();\n        if (count == 0) {\n            throw new IllegalArgumentException(\n                    \"Entry-bucket subscription must declare the segment's bucket boundaries\");\n        }\n        List<Range> ranges = new ArrayList<>(count);\n        int expectedStart = 0;\n        for (int i = 0; i < count; i++) {\n            IntRange r = ksm.getHashRangeAt(i);\n            if (r.getStart() != expectedStart || r.getEnd() < r.getStart()) {\n                throw new IllegalArgumentException(\"Entry-bucket boundaries must be ascending, \"\n                        + \"contiguous and start at 0: found [\" + r.getStart() + \",\" + r.getEnd()\n                        + \"] where start \" + expectedStart + \" was expected\");\n            }\n            ranges.add(Range.of(r.getStart(), r.getEnd()));\n            expectedStart = r.getEnd() + 1;\n        }\n        if (expectedStart != EntryBucketConsumerSelector.DEFAULT_RANGE_SIZE) {\n            throw new IllegalArgumentException(\"Entry-bucket boundaries must tile the 16-bit ring: \"\n                    + \"last range ends at \" + (expectedStart - 1));\n        }\n        if (ranges.get(0).getEnd() < 1) {\n            throw new IllegalArgumentException(\n                    \"Entry-bucket 0 must span at least [0,1] to hold the canonical hash\");\n        }\n        return ranges;\n    }\n\n    @Override","sourceCodeStart":71,"sourceCodeEnd":107,"githubUrl":"https://github.com/apache/pulsar/blob/820761864ed8e2a7d2e52dd9763ad2ae117c1395/pulsar-broker/src/main/java/org/apache/pulsar/broker/service/persistent/PersistentEntryBucketDispatcherMultipleConsumers.java#L71-L107","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","solutions":["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.","Generate boundaries programmatically (e.g. split 0..65535 into N equal-width segments in a loop) instead of hand-writing constants.","Verify all consumers and the producing tooling use the identical, sorted boundary list; fix any stale client that sends a divergent list."],"exampleFix":"// before (gap + inverted range)\nksm.addHashRange().setStart(0).setEnd(16383);\nksm.addHashRange().setStart(17000).setEnd(16999); // wrong start, end < start\n// after\nint segments = 4, size = 65536 / segments;\nfor (int i = 0; i < segments; i++) {\n    ksm.addHashRange().setStart(i * size).setEnd((i + 1) * size - 1);\n}","handlingStrategy":"validation","validationCode":"static void validateRangesLocal(KeySharedMeta ksm) {\n    int expectedStart = 0;\n    for (int i = 0; i < ksm.getHashRangesCount(); i++) {\n        var r = ksm.getHashRangeAt(i);\n        if (r.getStart() != expectedStart || r.getEnd() < r.getStart()) {\n            throw new IllegalArgumentException(\"bad range [\" + r.getStart() + \",\" + r.getEnd()\n                + \"] at index \" + i + \", expected start \" + expectedStart);\n        }\n        expectedStart = r.getEnd() + 1;\n    }\n}","typeGuard":"static boolean rangesAreContiguousFromZero(KeySharedMeta ksm) {\n    int expectedStart = 0;\n    for (int i = 0; i < ksm.getHashRangesCount(); i++) {\n        var r = ksm.getHashRangeAt(i);\n        if (r.getStart() != expectedStart || r.getEnd() < r.getStart()) return false;\n        expectedStart = r.getEnd() + 1;\n    }\n    return true;\n}","tryCatchPattern":"try {\n    subscribeWithEntryBuckets(ksm);\n} catch (IllegalArgumentException e) {\n    if (e.getMessage().contains(\"ascending, contiguous\")) {\n        regenerateAndResubscribeWithComputedBoundaries();\n    } else throw e;\n}","preventionTips":["Generate boundaries programmatically from segment count instead of hardcoding constants.","Sort and normalize ranges before sending.","Unit-test the boundary builder: assert contiguity, start at 0, end >= start."],"tags":["pulsar","key-shared","entry-bucket","validation"],"backgroundTag":"invalid-hash-range-partition","analyzedSha":"820761864ed8e2a7d2e52dd9763ad2ae117c1395","analyzedAt":"2026-09-06T00:14:20.138Z","contentChangedAt":"2026-09-06T00:14:20.138Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}