apache/pulsar · error · IllegalArgumentException

Cannot prune an active segment: ${segmentId}

Error message

Cannot prune an active segment: ${segmentId}

What it means

pruneSegment may only remove segments that are sealed (not active); pruning an active segment would delete live data ownership. If segment.isActive() it throws IllegalArgumentException("Cannot prune an active segment: <id>").

Source

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

        return new SegmentLayout(newEpoch, nextSegmentId + 1, newSegments);
    }

    /**
     * Prune an expired segment from the DAG. The segment must be sealed and have no
     * children that are still in the DAG (i.e., children have already been pruned or
     * the segment is a leaf that was sealed).
     *
     * @param segmentId the segment to prune
     * @return a new SegmentLayout with the segment removed
     */
    public SegmentLayout pruneSegment(long segmentId) {
        SegmentInfo segment = allSegments.get(segmentId);
        if (segment == null) {
            throw new IllegalArgumentException("Segment not found: " + segmentId);
        }
        if (segment.isActive()) {
            throw new IllegalArgumentException("Cannot prune an active segment: " + segmentId);
        }

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

        // Remove this segment from its children's parent lists
        for (long childId : segment.childIds()) {
            SegmentInfo child = newSegments.get(childId);
            if (child != null) {
                List<Long> newParentIds = child.parentIds().stream()
                        .filter(id -> id != segmentId)
                        .collect(Collectors.toList());
                newSegments.put(childId, child.withParentIds(newParentIds));
            }
        }

        // Remove this segment from its parents' child lists
        for (long parentId : segment.parentIds()) {

View on GitHub (pinned to 820761864e)

Solutions

  1. Only prune segments that are sealed AND expired past retention: check !segment.isActive() plus the expiry timestamp before calling
  2. Derive prune candidates from the sealed segments in the layout, never from allSegments
  3. Re-check activity on the freshest layout immediately before each prune to avoid racing a recovery
  4. Let pruneAllAsync drive candidate selection instead of hand-picking IDs

Example fix

// before
layout.pruneSegment(id);
// after
SegmentInfo seg = layout.getAllSegments().get(id);
if (seg != null && !seg.isActive() && isExpired(seg, nowMs)) {
    layout.pruneSegment(id);
}
Defensive patterns

Strategy: validation

Validate before calling

boolean canPrune(SegmentLayout layout, long segmentId, long nowMs) {
    var seg = layout.getAllSegments().get(segmentId);
    return seg != null && !seg.isActive() && isExpired(seg, nowMs);
}

Type guard

boolean isSealedSegment(SegmentLayout layout, long id) {
    var seg = layout.getAllSegments().get(id);
    return seg != null && !seg.isActive();
}

Try / catch

try {
    newLayout = layout.pruneSegment(id);
} catch (IllegalArgumentException e) {
    if (e.getMessage().startsWith("Cannot prune an active segment")) {
        // segment is live (or reactivated) — skip it this round
        return;
    }
    throw e;
}

Prevention

When it happens

Trigger: Calling pruneSegment on an ID that is still an active serving segment — e.g. iterating all segments instead of only expired/sealed ones, or pruning against a stale layout where the segment has since become active again (state restored after failure).

Common situations: Custom retention/prune logic without an expiration check; tests calling testCannotPruneActiveSegment-style negative paths; prune logic racing a recovery that reactivated the segment.

Related errors


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