apache/druid · error · IllegalStateException

Cannot start the stage: [%s]

Error message

Cannot start the stage: [%s]

What it means

Thrown by ControllerQueryKernel.startStage when the stage's ControllerStageTracker is not in ControllerStagePhase.NEW, meaning the stage has already been started (or finished/failed). Starting a stage is a one-time phase transition, so a repeat call is an illegal state.

Source

Thrown at multi-stage-query/src/main/java/org/apache/druid/msq/kernel/controller/ControllerQueryKernel.java:595

  public Object getResultObjectForStage(final StageId stageId)
  {
    return getStageTrackerOrThrow(stageId).getResultObject();
  }

  /**
   * Checks if the stage can be started, delegates call to {@link ControllerStageTracker#start()} for internal phase
   * transition and registers the transition in this queryKernel. Work orders need to be created via
   * {@link ControllerQueryKernel#createWorkOrders(int, int, Int2ObjectMap)} before calling this method.
   */
  public void startStage(final StageId stageId)
  {
    if (stageWorkOrders.get(stageId) == null) {
      throw new ISE("Work order not present for stage[%s]", stageId);
    }

    doWithStageTracker(stageId, stageTracker -> {
      if (stageTracker.getPhase() != ControllerStagePhase.NEW) {
        throw new ISE("Cannot start the stage: [%s]", stageId);
      }

      stageTracker.start();
    });
  }

  /**
   * Checks if the stage can be finished, delegates call to {@link ControllerStageTracker#finish()} for internal phase
   * transition and registers the transition in this query kernel
   * <p>
   * If the method is called with strict = true, we confirm if the stage can be marked as finished or else
   * throw illegal argument exception
   */
  public void finishStage(final StageId stageId, final boolean strict)
  {
    if (strict && !effectivelyFinishedStages.contains(stageId)) {
      throw new IAE("Cannot mark the stage: [%s] finished", stageId);
    }

View on GitHub (pinned to 9b90983fd2)

Solutions

  1. Check the stage phase before calling startStage (only proceed when phase == ControllerStagePhase.NEW)
  2. Deduplicate the controller event/callback that triggers startWorkForStage so the transition is applied once
  3. If this occurs after restart, confirm replay logic marks already-started stages instead of re-starting them
  4. Make the call idempotent in caller code by catching ISE and treating an already-started stage as a no-op

Example fix

// before
kernel.startStage(stageId);

// after
if (kernel.getStagePhase(stageId) == ControllerStagePhase.NEW) {
  kernel.startStage(stageId);
}
Defensive patterns

Strategy: type-guard

Validate before calling

if (queryKernel.getStagePhase(stageId) == ControllerStagePhase.NEW) {
  queryKernel.startStage(stageId);
}

Type guard

boolean isNewPhase(ControllerQueryKernel kernel, StageId stageId) {
  try {
    return kernel.getStagePhase(stageId) == ControllerStagePhase.NEW;
  } catch (IllegalArgumentException e) {
    return false;
  }
}

Try / catch

try {
  queryKernel.startStage(stageId);
} catch (IllegalStateException e) {
  LOG.debug(e, "Stage %s already started; treating as no-op", stageId);
}

Prevention

When it happens

Trigger: Calling startStage(stageId) twice for the same stage, or calling it on a stage already in READING_INPUT / POST_READING / RESULTS_COMPLETE / FINISHED phases — typically via startWorkForStage after a state-machine re-entry or duplicate stage-start event.

Common situations: Duplicate success/counter messages from workers causing the controller to re-run the stage-start step; idempotency bugs in controller retry logic; state-machine replay after controller restart re-executing an already-applied transition.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


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