{"record":{"id":"d145a6288f696ae2","repo":"apache/pulsar","slug":"segments-are-not-adjacent-hashrange1-and-has","errorCode":null,"errorMessage":"Segments are not adjacent: ${hashRange1} and ${hashRange2}","messagePattern":"Segments are not adjacent: (.+?) and (.+?)","errorType":"validation","errorClass":"IllegalArgumentException","httpStatus":null,"severity":"error","filePath":"pulsar-broker/src/main/java/org/apache/pulsar/broker/service/scalable/SegmentLayout.java","lineNumber":226,"sourceCode":"    }\n\n    /**\n     * As {@link #mergeSegments(long, long, long)}, clamping the merged segment's entry-bucket\n     * count to {@code maxBucketsPerSegment} (the configured per-segment ceiling): the merged\n     * segment recovers the parents' buckets, but never past the hard ceiling.\n     */\n    public SegmentLayout mergeSegments(long segmentId1, long segmentId2, long nowMs,\n                                       int maxBucketsPerSegment) {\n        SegmentInfo seg1 = allSegments.get(segmentId1);\n        SegmentInfo seg2 = allSegments.get(segmentId2);\n        if (seg1 == null || seg2 == null) {\n            throw new IllegalArgumentException(\"Segment not found\");\n        }\n        if (!seg1.isActive() || !seg2.isActive()) {\n            throw new IllegalArgumentException(\"Both segments must be active\");\n        }\n        if (!seg1.hashRange().isAdjacentTo(seg2.hashRange())) {\n            throw new IllegalArgumentException(\"Segments are not adjacent: \"\n                    + seg1.hashRange() + \" and \" + seg2.hashRange());\n        }\n\n        long newEpoch = epoch + 1;\n        long mergedId = nextSegmentId;\n        HashRange mergedRange = seg1.hashRange().merge(seg2.hashRange());\n\n        // PIP-486: a merge is the inverse of a split — the merged segment recovers both parents' buckets\n        // (N1 + N2), so the topic's total entry-bucket count stays ≈ the budget as segments coalesce.\n        List<Integer> mergedEntryBucketSplits = EntryBucketSplits.equalWidth(\n                Math.min(seg1.bucketCount() + seg2.bucketCount(), maxBucketsPerSegment));\n        SegmentInfo sealed1 = seg1.sealed(newEpoch, nowMs, List.of(mergedId));\n        SegmentInfo sealed2 = seg2.sealed(newEpoch, nowMs, List.of(mergedId));\n        SegmentInfo merged = SegmentInfo.active(mergedId, mergedRange,\n                List.of(segmentId1, segmentId2), newEpoch, nowMs)\n                .withEntryBucketSplits(mergedEntryBucketSplits);\n\n        Map<Long, SegmentInfo> newSegments = new LinkedHashMap<>(allSegments);","sourceCodeStart":208,"sourceCodeEnd":244,"githubUrl":"https://github.com/apache/pulsar/blob/820761864ed8e2a7d2e52dd9763ad2ae117c1395/pulsar-broker/src/main/java/org/apache/pulsar/broker/service/scalable/SegmentLayout.java#L208-L244","documentation":"mergeSegments only merges segments whose HashRanges are adjacent on the hash ring (a merge is defined as the inverse of a split). When the two active segments do not cover contiguous ranges it throws IllegalArgumentException(\"Segments are not adjacent: <range1> and <range2>\") including both ranges for diagnosis.","triggerScenarios":"Calling mergeSegments(segId1, segId2, ...) with two active segments whose hash ranges are contiguous-adjacent fails isAdjacentTo — e.g. segments separated by another segment between them on the ring, or ranges from opposite ends of the ring.","commonSituations":"Auto-scale merge heuristics picking the two lowest-load segments without checking adjacency; merging segments across different split trees (siblings of different parents); tests selecting arbitrary active segment pairs.","solutions":["Before merging, check seg1.hashRange().isAdjacentTo(seg2.hashRange()) and skip/choose another pair if false","Select merge candidates as adjacent pairs by scanning active segments sorted by range start","Merge the intermediate segment(s) first, or split-and-merge stepwise, to bring the desired ranges adjacent","Group candidates by parent ID (children of one split are adjacent by construction)"],"exampleFix":"// before\nSegmentInfo s1 = layout.getAllSegments().get(id1);\nSegmentInfo s2 = layout.getAllSegments().get(id2);\nlayout.mergeSegments(id1, id2, nowMs);\n// after\nif (s1.hashRange().isAdjacentTo(s2.hashRange())) {\n    layout.mergeSegments(id1, id2, nowMs);\n} else {\n    // pick an adjacent partner from layout.getActiveSegments()\n}","handlingStrategy":"validation","validationCode":"boolean mergeable(SegmentLayout layout, long id1, long id2) {\n    var segs = layout.getAllSegments();\n    return segs.containsKey(id1) && segs.containsKey(id2)\n        && segs.get(id1).hashRange().isAdjacentTo(segs.get(id2).hashRange());\n}","typeGuard":null,"tryCatchPattern":"try {\n    newLayout = layout.mergeSegments(id1, id2, nowMs);\n} catch (IllegalArgumentException e) {\n    if (e.getMessage().startsWith(\"Segments are not adjacent\")) {\n        // choose a different candidate pair among active segments\n    } else {\n        throw e;\n    }\n}","preventionTips":["Scan active segments sorted by range start and merge only immediate neighbors","Prefer merging full sibling sets produced by the same split","Check isAdjacentTo before proposing any merge in policy code","Log both hash ranges on rejection to aid ring-layout debugging"],"tags":["java","illegal-argument","hash-range","segment-layout"],"backgroundTag":"segments-not-adjacent","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"}