{"record":{"id":"0593f70a5253cea6","repo":"apache/pulsar","slug":"entry-bucket-0-must-span-at-least-0-1-to-hold-th","errorCode":null,"errorMessage":"Entry-bucket 0 must span at least [0,1] to hold the canonical hash","messagePattern":"Entry-bucket 0 must span at least \\[0,1\\] to hold the canonical hash","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":101,"sourceCode":"        }\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\n    public synchronized CompletableFuture<Void> addConsumer(Consumer consumer) {\n        // A segment's bucketing is immutable, so every consumer must declare the boundaries the\n        // dispatcher was created with — a mismatch is a client bug or a stale layout, not a race.\n        try {\n            List<Range> declared = validateBucketBoundaries(consumer.getKeySharedMeta());\n            if (!declared.equals(bucketSelector.getBucketRanges())) {\n                return CompletableFuture.failedFuture(new BrokerServiceException.ConsumerAssignException(\n                        \"Consumer declares different entry-bucket boundaries than the subscription: \"\n                                + declared + \" != \" + bucketSelector.getBucketRanges()));\n            }\n        } catch (IllegalArgumentException e) {\n            return CompletableFuture.failedFuture(","sourceCodeStart":83,"sourceCodeEnd":119,"githubUrl":"https://github.com/apache/pulsar/blob/820761864ed8e2a7d2e52dd9763ad2ae117c1395/pulsar-broker/src/main/java/org/apache/pulsar/broker/service/persistent/PersistentEntryBucketDispatcherMultipleConsumers.java#L83-L119","documentation":"Entry hashes are normalized to per-bucket canonical values, and bucket 0's canonical hash is 1. If the first declared range ends below 1 (i.e. bucket 0 is just [0,0]), the canonical hash 1 falls outside every bucket, breaking routing and the draining/handoff machinery. validateBucketBoundaries therefore requires ranges.get(0).getEnd() >= 1.","triggerScenarios":"A boundary list whose first range is exactly [0,0] — typically when a very large number of fine-grained segments each get width 1, or when a generator emits [0,0] as the first single-value bucket.","commonSituations":"Generating one range per hash value (65536 ranges of width 1), which puts the canonical hash 1 in bucket 1's range and leaves bucket 0 unable to hold its canonical value; scripted boundary generation that doesn't special-case the first segment.","solutions":["Widen the first range to at least [0,1] (end >= 1) and merge the following segment accordingly, keeping overall contiguity and full ring coverage.","Adjust the boundary generator so the first segment always spans at least two hash values.","If you need 65536 single-hash buckets, use the regular Key_Shared hashing path instead of entry-bucket dispatch — entry-bucket segmentation assumes coarser buckets."],"exampleFix":"// before\nksm.addHashRange().setStart(0).setEnd(0);    // bucket 0 can't hold canonical hash 1\nksm.addHashRange().setStart(1).setEnd(21844);\n...\n// after\nksm.addHashRange().setStart(0).setEnd(1);    // spans at least [0,1]\nksm.addHashRange().setStart(2).setEnd(21844);\n...","handlingStrategy":"validation","validationCode":"static void assertFirstBucketHoldsCanonicalHash(KeySharedMeta ksm) {\n    if (ksm.getHashRangesCount() == 0 || ksm.getHashRangeAt(0).getEnd() < 1) {\n        throw new IllegalArgumentException(\n            \"first bucket range must end at >= 1 (canonical hash 1)\");\n    }\n}","typeGuard":"static boolean firstBucketSpansCanonicalHash(KeySharedMeta ksm) {\n    return ksm.getHashRangesCount() > 0 && ksm.getHashRangeAt(0).getEnd() >= 1;\n}","tryCatchPattern":null,"preventionTips":["Never generate width-1 first buckets; enforce a minimum first-range width of 2.","Avoid 65536 single-hash segments — entry-bucket dispatch targets coarse segments.","Include firstRange.end >= 1 in the boundary generator's self-check."],"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"}