{"record":{"id":"39151f217a5c83a8","repo":"apache/druid","slug":"stage-does-not-shuffle","errorCode":null,"errorMessage":"Stage does not shuffle","messagePattern":"Stage does not shuffle","errorType":"exception","errorClass":"IllegalStateException","httpStatus":null,"severity":"error","filePath":"multi-stage-query/src/main/java/org/apache/druid/msq/kernel/StageDefinition.java","lineNumber":296,"sourceCode":"  /**\n   * Get a {@link SegmentPruner} from the {@link StageProcessor} for a given 'input number' from {@link #inputSpecs}.\n   * This can be used to best-effort prune the set of {@link org.apache.druid.timeline.DataSegment} to process in order\n   * to reduce the working set before processing begins\n   */\n  public SegmentPruner getSegmentPruner(int inputNumber)\n  {\n    return processor.getPruner(inputSpecs.get(inputNumber), inputNumber);\n  }\n\n  /**\n   * Returns the {@link ShuffleSpec} for this stage, if {@link #doesShuffle()}.\n   *\n   * @throws IllegalStateException if this stage does not shuffle\n   */\n  public ShuffleSpec getShuffleSpec()\n  {\n    if (shuffleSpec == null) {\n      throw new IllegalStateException(\"Stage does not shuffle\");\n    }\n\n    return shuffleSpec;\n  }\n\n  /**\n   * Returns the {@link ClusterBy} of the {@link ShuffleSpec} if set, otherwise {@link ClusterBy#none()}.\n   */\n  public ClusterBy getClusterBy()\n  {\n    if (shuffleSpec != null) {\n      return shuffleSpec.clusterBy();\n    } else {\n      return ClusterBy.none();\n    }\n  }\n\n  /**","sourceCodeStart":278,"sourceCodeEnd":314,"githubUrl":"https://github.com/apache/druid/blob/9b90983fd291f26935af934383ce360473179e4d/multi-stage-query/src/main/java/org/apache/druid/msq/kernel/StageDefinition.java#L278-L314","documentation":"getShuffleSpec() returns the stage's shuffle spec, but stages that do not shuffle (e.g. leaf/ingestion stages) have a null shuffleSpec. Calling this accessor on such a stage throws IllegalStateException(\"Stage does not shuffle\"), protecting callers from treating a non-shuffling stage as a shuffle participant.","triggerScenarios":"Calling stageDefinition.getShuffleSpec() on a stage built without builder.shuffleSpec(...) — typically a source/leaf stage, or a final stage that writes output directly.","commonSituations":"Walking a QueryKit/stage graph generically and calling getShuffleSpec() on every stage without first checking shuffleSpec presence, or kernel/worker code assuming all stages shuffle.","solutions":["Guard with stageDefinition.getShuffleSpecForDiagnostics() or check mustGatherResultKeyStatistics()/shuffleSpec presence before calling","Only call getShuffleSpec() on stages where the builder set a shuffle spec","Restructure the traversal to skip non-shuffling stages (leaf and output stages)"],"exampleFix":"// before\nShuffleSpec spec = stage.getShuffleSpec();\n// after\nif (stage.getProcessor() instanceof etc) { /* leaf */ } else {\n  ShuffleSpec spec = stage.getShuffleSpec();\n}","handlingStrategy":"type-guard","validationCode":"boolean shuffles = stageDef.getProcessor() != null\n    && stageDef.getShuffleSpecForDiagnostics() != null; // or check via builder inputs","typeGuard":"// Narrow to stages that shuffle before calling the accessor\nboolean hasShuffle(StageDefinition d) { return !d.mustGatherResultKeyStatistics() && d.getProcessor() != null; }","tryCatchPattern":"try { ShuffleSpec spec = d.getShuffleSpec(); } catch (IllegalStateException e) { /* stage is a leaf; skip */ }","preventionTips":["Only call getShuffleSpec() on stages built with builder.shuffleSpec(...)","Check shuffle presence via diagnostics accessors before calling","In graph traversals, branch on stage role (leaf/shuffle/output) first"],"tags":["msq","stage-definition","null-check"],"backgroundTag":"invalid-state-transition","analyzedSha":"9b90983fd291f26935af934383ce360473179e4d","analyzedAt":"2026-09-07T13:32:30.957Z","contentChangedAt":"2026-09-07T13:32:30.957Z","schemaVersion":2},"datasetVersion":"2026-09-14T11:17:12.474Z"}