apache/druid · error · IllegalStateException

Cannot accept duplicate stage numbers

Error message

Cannot accept duplicate stage numbers

What it means

QueryDefinition.create builds the stage map from the supplied stage definitions and requires each stage to have a unique stage number. If two StageDefinitions share the same StageId/stage number within one query, the internal maps would silently overwrite each other, so create throws ISE on the duplicate.

Source

Thrown at multi-stage-query/src/main/java/org/apache/druid/msq/kernel/QueryDefinition.java:84

  )
  {
    this.stageDefinitions = stageDefinitions;
    this.finalStage = finalStage;
    this.context = context;
  }

  @JsonCreator
  public static QueryDefinition create(
      @JsonProperty("stages") final List<StageDefinition> stageDefinitions,
      @Nullable @JsonProperty("context") final QueryContext context)
  {
    final Map<StageId, StageDefinition> stageMap = new HashMap<>();
    final Set<StageId> nonFinalStages = new HashSet<>();
    final IntSet stageNumbers = new IntOpenHashSet();

    for (final StageDefinition stage : stageDefinitions) {
      if (!stageNumbers.add(stage.getStageNumber())) {
        throw new ISE("Cannot accept duplicate stage numbers");
      }

      stageMap.put(stage.getId(), stage);

      for (int stageNumber : stage.getInputStageNumbers()) {
        nonFinalStages.add(new StageId(stage.getId().getQueryId(), stageNumber));
      }
    }

    for (final StageId nonFinalStageId : nonFinalStages) {
      if (!stageMap.containsKey(nonFinalStageId)) {
        throw new ISE("Stage [%s] is missing a definition", nonFinalStageId);
      }
    }

    final int finalStageCandidates = stageMap.size() - nonFinalStages.size();

    if (finalStageCandidates == 1) {

View on GitHub (pinned to 9b90983fd2)

Solutions

  1. Fix the builder/plan producer so each stage gets a unique stage number (use QueryDefinitionBuilder.add, which auto-numbers)
  2. Deduplicate the stage list before calling create and verify no two stages share getStageNumber()
  3. If merging plans, renumber stages of the second plan before merging

Example fix

// before: manual numbers collide
builder.add(stage0Def); builder.add(stage0Def);
// after
QueryDefinitionBuilder builder = QueryDefinitionBuilder.builder(queryId);
builder.add(StageDefinition.builder().nextStageNumber()...); // unique per stage
Defensive patterns

Strategy: validation

Validate before calling

Set<Integer> seen = new HashSet<>();
for (StageDefinition stage : stageDefinitions) {
  if (!seen.add(stage.getStageNumber())) {
    throw new IllegalArgumentException("Duplicate stage number: " + stage.getStageNumber());
  }
}
QueryDefinition.create(stageDefinitions, context);

Try / catch

try {
  return QueryDefinition.create(stageDefinitions, context);
} catch (IllegalStateException e) {
  throw new PlanValidationException("Duplicate stage numbers in plan", e);
}

Prevention

When it happens

Trigger: Calling QueryDefinition.create(stageDefinitions, context) with a list that contains two StageDefinitions with the same stage number for the same query id — typically a builder bug where a stage was added twice or stage numbering collided.

Common situations: Programmatic construction of query definitions (controller tests, custom engines reusing the MSQ kernel) where stage numbers are assigned manually instead of via QueryDefinitionBuilder.add(); merging plans from two sources without renumbering.

Understand the failure class

Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.

Related errors


AI-assisted analysis of apache/druid@9b90983fd2 (2026-09-07). Data as JSON: /api/errors/ae92a5df58df6646. Report an issue: GitHub.