apache/pulsar · error · IllegalArgumentException

Both segments must be active

Error message

Both segments must be active

What it means

mergeSegments requires both operands to be ACTIVE segments in the current epoch. If either segment has already been sealed (by a split, merge, rebucket rollover, or expiration) the method throws IllegalArgumentException("Both segments must be active"). Sealed segments are historical DAG nodes that can no longer take part in topology mutations.

Source

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

     */
    public SegmentLayout mergeSegments(long segmentId1, long segmentId2, long nowMs) {
        return mergeSegments(segmentId1, segmentId2, nowMs, EntryBucketSplits.MAX_BUCKETS);
    }

    /**
     * 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)

View on GitHub (pinned to 820761864e)

Solutions

  1. Re-fetch the layout and confirm both segments are active (getAllSegments().get(id).isActive()) before merging
  2. Pick merge candidates only from layout.getActiveSegments(), never from allSegments
  3. On CAS failure, rebuild the layout from fresh ScalableTopicMetadata and recompute the merge pair — the previous merge may have succeeded
  4. Pair merges by adjacency among currently active segments rather than caching segment IDs across operations

Example fix

// before
layout.mergeSegments(idA, idB, nowMs);
// after
SegmentInfo a = layout.getAllSegments().get(idA);
SegmentInfo b = layout.getAllSegments().get(idB);
if (a != null && b != null && a.isActive() && b.isActive()) {
    layout.mergeSegments(idA, idB, nowMs);
}
Defensive patterns

Strategy: validation

Validate before calling

boolean bothActive(SegmentLayout layout, long id1, long id2) {
    var segs = layout.getAllSegments();
    var a = segs.get(id1); var b = segs.get(id2);
    return a != null && b != null && a.isActive() && b.isActive();
}

Type guard

boolean isActiveSegment(SegmentLayout layout, long id) {
    return layout.getActiveSegments().containsKey(id);
}

Try / catch

try {
    newLayout = layout.mergeSegments(id1, id2, nowMs);
} catch (IllegalArgumentException e) {
    if (e.getMessage().equals("Both segments must be active")) {
        // likely already merged or sealed: reload layout, check if merge already applied, skip or retry
        layout = SegmentLayout.fromMetadata(refreshMetadata());
    } else {
        throw e;
    }
}

Prevention

When it happens

Trigger: Calling mergeSegments on a pair where at least one ID refers to a sealed segment — e.g. merging a segment that was just split, merging a segment already consumed by a previous merge, or merging after a rebucket rollover sealed it.

Common situations: Auto-scale evaluator iterating a stale candidate list while a concurrent rebalance sealed the segments; retrying a merge after a failed CAS without re-reading the layout (the first attempt actually succeeded, sealing both parents); tests merging historical segments from the DAG lineage.

Related errors


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