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
- Re-fetch the layout and confirm both segments are active (getAllSegments().get(id).isActive()) before merging
- Pick merge candidates only from layout.getActiveSegments(), never from allSegments
- On CAS failure, rebuild the layout from fresh ScalableTopicMetadata and recompute the merge pair — the previous merge may have succeeded
- 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
- Only source segment pairs from getActiveSegments()
- After a CAS failure, reload the layout before retrying — the prior attempt may have sealed the segments
- Track epoch numbers so stale layouts are detected immediately
- Never merge segments from lineage/history collections
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
- Segment not found
- Segments are not adjacent: ${hashRange1} and ${hashRange2}
- Cannot rebucket non-active segment: ${segmentId}
- Cannot prune an active segment: ${segmentId}
- configuredService should not be an instance of SystemTopicBa
AI-assisted analysis of apache/pulsar@820761864e (2026-09-06).
Data as JSON: /api/errors/47968d6fd04bf694.
Report an issue: GitHub.