apache/pulsar · error · IllegalArgumentException

Segments are not adjacent: ${hashRange1} and ${hashRange2}

Error message

Segments are not adjacent: ${hashRange1} and ${hashRange2}

What it means

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.

Source

Thrown at pulsar-broker/src/main/java/org/apache/pulsar/broker/service/scalable/SegmentLayout.java:226

    }

    /**
     * As {@link #mergeSegments(long, long, long)}, clamping the merged segment's entry-bucket
     * count to {@code maxBucketsPerSegment} (the configured per-segment ceiling): the merged
     * segment recovers the parents' buckets, but never past the hard ceiling.
     */
    public SegmentLayout mergeSegments(long segmentId1, long segmentId2, long nowMs,
                                       int maxBucketsPerSegment) {
        SegmentInfo seg1 = allSegments.get(segmentId1);
        SegmentInfo seg2 = allSegments.get(segmentId2);
        if (seg1 == null || seg2 == null) {
            throw new IllegalArgumentException("Segment not found");
        }
        if (!seg1.isActive() || !seg2.isActive()) {
            throw new IllegalArgumentException("Both segments must be active");
        }
        if (!seg1.hashRange().isAdjacentTo(seg2.hashRange())) {
            throw new IllegalArgumentException("Segments are not adjacent: "
                    + seg1.hashRange() + " and " + seg2.hashRange());
        }

        long newEpoch = epoch + 1;
        long mergedId = nextSegmentId;
        HashRange mergedRange = seg1.hashRange().merge(seg2.hashRange());

        // PIP-486: a merge is the inverse of a split — the merged segment recovers both parents' buckets
        // (N1 + N2), so the topic's total entry-bucket count stays ≈ the budget as segments coalesce.
        List<Integer> mergedEntryBucketSplits = EntryBucketSplits.equalWidth(
                Math.min(seg1.bucketCount() + seg2.bucketCount(), maxBucketsPerSegment));
        SegmentInfo sealed1 = seg1.sealed(newEpoch, nowMs, List.of(mergedId));
        SegmentInfo sealed2 = seg2.sealed(newEpoch, nowMs, List.of(mergedId));
        SegmentInfo merged = SegmentInfo.active(mergedId, mergedRange,
                List.of(segmentId1, segmentId2), newEpoch, nowMs)
                .withEntryBucketSplits(mergedEntryBucketSplits);

        Map<Long, SegmentInfo> newSegments = new LinkedHashMap<>(allSegments);

View on GitHub (pinned to 820761864e)

Solutions

  1. Before merging, check seg1.hashRange().isAdjacentTo(seg2.hashRange()) and skip/choose another pair if false
  2. Select merge candidates as adjacent pairs by scanning active segments sorted by range start
  3. Merge the intermediate segment(s) first, or split-and-merge stepwise, to bring the desired ranges adjacent
  4. Group candidates by parent ID (children of one split are adjacent by construction)

Example fix

// before
SegmentInfo s1 = layout.getAllSegments().get(id1);
SegmentInfo s2 = layout.getAllSegments().get(id2);
layout.mergeSegments(id1, id2, nowMs);
// after
if (s1.hashRange().isAdjacentTo(s2.hashRange())) {
    layout.mergeSegments(id1, id2, nowMs);
} else {
    // pick an adjacent partner from layout.getActiveSegments()
}
Defensive patterns

Strategy: validation

Validate before calling

boolean mergeable(SegmentLayout layout, long id1, long id2) {
    var segs = layout.getAllSegments();
    return segs.containsKey(id1) && segs.containsKey(id2)
        && segs.get(id1).hashRange().isAdjacentTo(segs.get(id2).hashRange());
}

Try / catch

try {
    newLayout = layout.mergeSegments(id1, id2, nowMs);
} catch (IllegalArgumentException e) {
    if (e.getMessage().startsWith("Segments are not adjacent")) {
        // choose a different candidate pair among active segments
    } else {
        throw e;
    }
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


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