apache/druid · error · IllegalStateException

Cannot run() more than once.

Error message

Cannot run() more than once.

What it means

SuperSorter.run() starts the sorting pipeline and may only be called once per SuperSorter instance; it records completion in an internal SettableFuture (allDone). A second call finds allDone already set and throws IllegalStateException. The instance is single-use by design because its internal channels and worker state are consumed by the first run().

Source

Thrown at processing/src/main/java/org/apache/druid/frame/processor/SuperSorter.java:291

      throw new IAE("maxChannelsPerMerger[%d] < 2", maxChannelsPerMerger);
    }

    if (rowLimit != UNLIMITED && rowLimit <= 0) {
      throw new IAE("rowLimit[%d] must be positive", rowLimit);
    }
  }

  /**
   * Starts sorting. Can only be called once. Work is performed in the {@link FrameProcessorExecutor} that was
   * passed to the constructor.
   *
   * Returns a future containing partitioned sorted output channels.
   */
  public ListenableFuture<OutputChannels> run()
  {
    synchronized (runWorkersLock) {
      if (allDone != null) {
        throw new ISE("Cannot run() more than once.");
      }

      allDone = SettableFuture.create();
      runWorkersIfPossible();

      // When output partitions become known, that may unblock some additional layers of merging.
      outputPartitionsFuture.addListener(
          () -> {
            synchronized (runWorkersLock) {
              if (outputPartitionsFuture.isDone()) { // Update the progress tracker
                superSorterProgressTracker.setTotalMergersForUltimateLevel(getOutputPartitions().size());
              }
              runWorkersIfPossible();
              setAllDoneIfPossible();
            }
          },
          exec.asExecutor(cancellationId)
      );

View on GitHub (pinned to 9b90983fd2)

Solutions

  1. Create a new SuperSorter instance for each sorting operation and call run() exactly once.
  2. Guard call sites so run() executes once, e.g. capture the returned future and reuse it instead of calling run() again.
  3. If you need the result, hold the ListenableFuture<OutputChannels> returned by the first run() and await it rather than re-running.
  4. Restructure retry logic to retry the whole pipeline (new SuperSorter) rather than the run() call.

Example fix

// before
sorter.run();
OutputChannels channels = sorter.run().get(); // second call -> ISE
// after
ListenableFuture<OutputChannels> future = sorter.run();
OutputChannels channels = FutureUtils.getUnchecked(future, true);
Defensive patterns

Strategy: try-catch

Validate before calling

// track one-time use explicitly
private final AtomicBoolean started = new AtomicBoolean();
if (!started.compareAndSet(false, true)) {
  throw new IllegalStateException("SuperSorter already started");
}

Try / catch

try {
  future = sorter.run();
} catch (IllegalStateException e) {
  // already run: reuse previously captured future
  future = previouslyCapturedFuture;
}

Prevention

When it happens

Trigger: Calling run() a second time on the same SuperSorter instance — e.g. retry logic that re-invokes run() after a failure, code that calls run() in both an initialization path and an execution path, or reusing a cached SuperSorter across queries instead of constructing a new one.

Common situations: Application code memoizing/reusing a SuperSorter field across requests; retry wrappers that assume restartable operations; refactoring that moved run() into a method invoked more than once (directly or via the channels()/outputChannels() accessors that can trigger workers).

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/560dd8fc2b6c659c. Report an issue: GitHub.