{"record":{"id":"b17e6cb85bd98fd4","repo":"apache/pulsar","slug":"segment-not-found","errorCode":null,"errorMessage":"Segment not found","messagePattern":"Segment not found","errorType":"validation","errorClass":"IllegalArgumentException","httpStatus":null,"severity":"error","filePath":"pulsar-broker/src/main/java/org/apache/pulsar/broker/service/scalable/SegmentLayout.java","lineNumber":220,"sourceCode":"     * @param nowMs      wall-clock millis used as the parents' seal time and the\n     *                   merged child's create time\n     * @return a new SegmentLayout with the merge applied\n     */\n    public SegmentLayout mergeSegments(long segmentId1, long segmentId2, long nowMs) {\n        return mergeSegments(segmentId1, segmentId2, nowMs, EntryBucketSplits.MAX_BUCKETS);\n    }\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));","sourceCodeStart":202,"sourceCodeEnd":238,"githubUrl":"https://github.com/apache/pulsar/blob/820761864ed8e2a7d2e52dd9763ad2ae117c1395/pulsar-broker/src/main/java/org/apache/pulsar/broker/service/scalable/SegmentLayout.java#L202-L238","documentation":"SegmentLayout.mergeSegments(long,long,long,int) looks up both segment IDs in the immutable layout snapshot (allSegments map) before merging. If either ID is absent from this snapshot it throws IllegalArgumentException(\"Segment not found\"). It is a programmatic precondition check: the layout is an immutable in-memory view of a scalable topic's segment DAG, so a missing ID means the caller is operating on stale metadata or a fabricated ID.","triggerScenarios":"Calling mergeSegments(segmentId1, segmentId2, nowMs[, maxBucketsPerSegment]) with an ID that is not in the layout — typically an ID that was already pruned, an ID from a previous (older-epoch) SegmentLayout snapshot, or a hardcoded/guessed ID.","commonSituations":"Reusing segment IDs captured from an earlier layout after another rebalance/merge/split changed the DAG; applying an auto-scale policy against a refreshed layout from ScalableTopicMetadata where the segments were concurrently modified; unit tests constructing partial SegmentLayout maps and merging IDs not present.","solutions":["Reload the current layout (SegmentLayout.fromMetadata(metadata)) immediately before merging so IDs are current","Verify both IDs with layout.getAllSegments().containsKey(id) before calling mergeSegments","Use getActiveSegments() / activeSegments.values() to pick merge candidates instead of stale cached IDs","After a CAS retry on the metadata, rebuild the layout and re-validate the segment pair before retrying the merge"],"exampleFix":"// before\nlayout.mergeSegments(cachedSegIdA, cachedSegIdB, nowMs);\n// after\nSegmentLayout fresh = SegmentLayout.fromMetadata(latestMetadata);\nif (fresh.getAllSegments().containsKey(cachedSegIdA) && fresh.getAllSegments().containsKey(cachedSegIdB)) {\n    fresh.mergeSegments(cachedSegIdA, cachedSegIdB, nowMs);\n}","handlingStrategy":"validation","validationCode":"boolean canMerge(SegmentLayout layout, long id1, long id2) {\n    var segs = layout.getAllSegments();\n    return segs.containsKey(id1) && segs.containsKey(id2)\n        && segs.get(id1).isActive() && segs.get(id2).isActive()\n        && segs.get(id1).hashRange().isAdjacentTo(segs.get(id2).hashRange());\n}","typeGuard":"boolean segmentExists(SegmentLayout layout, long id) {\n    return id > 0 && layout.getAllSegments().containsKey(id);\n}","tryCatchPattern":"try {\n    newLayout = layout.mergeSegments(id1, id2, nowMs);\n} catch (IllegalArgumentException e) {\n    if (e.getMessage().startsWith(\"Segment not found\")) {\n        layout = SegmentLayout.fromMetadata(refreshMetadata()); // stale snapshot; retry on fresh layout\n    } else {\n        throw e;\n    }\n}","preventionTips":["Always re-derive segment IDs from the latest layout snapshot before mutating operations","Never cache segment IDs across metadata CAS cycles","Select merge candidates from getActiveSegments(), not allSegments","Validate the merge triplet (exists, active, adjacent) in one precondition check"],"tags":["java","illegal-argument","segment-not-found","segment-layout"],"backgroundTag":"segment-not-found","analyzedSha":"820761864ed8e2a7d2e52dd9763ad2ae117c1395","analyzedAt":"2026-09-06T00:14:20.138Z","contentChangedAt":"2026-09-06T00:14:20.138Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}