{"record":{"id":"560dd8fc2b6c659c","repo":"apache/druid","slug":"cannot-run-more-than-once","errorCode":null,"errorMessage":"Cannot run() more than once.","messagePattern":"Cannot run\\(\\) more than once\\.","errorType":"exception","errorClass":"IllegalStateException","httpStatus":null,"severity":"error","filePath":"processing/src/main/java/org/apache/druid/frame/processor/SuperSorter.java","lineNumber":291,"sourceCode":"      throw new IAE(\"maxChannelsPerMerger[%d] < 2\", maxChannelsPerMerger);\n    }\n\n    if (rowLimit != UNLIMITED && rowLimit <= 0) {\n      throw new IAE(\"rowLimit[%d] must be positive\", rowLimit);\n    }\n  }\n\n  /**\n   * Starts sorting. Can only be called once. Work is performed in the {@link FrameProcessorExecutor} that was\n   * passed to the constructor.\n   *\n   * Returns a future containing partitioned sorted output channels.\n   */\n  public ListenableFuture<OutputChannels> run()\n  {\n    synchronized (runWorkersLock) {\n      if (allDone != null) {\n        throw new ISE(\"Cannot run() more than once.\");\n      }\n\n      allDone = SettableFuture.create();\n      runWorkersIfPossible();\n\n      // When output partitions become known, that may unblock some additional layers of merging.\n      outputPartitionsFuture.addListener(\n          () -> {\n            synchronized (runWorkersLock) {\n              if (outputPartitionsFuture.isDone()) { // Update the progress tracker\n                superSorterProgressTracker.setTotalMergersForUltimateLevel(getOutputPartitions().size());\n              }\n              runWorkersIfPossible();\n              setAllDoneIfPossible();\n            }\n          },\n          exec.asExecutor(cancellationId)\n      );","sourceCodeStart":273,"sourceCodeEnd":309,"githubUrl":"https://github.com/apache/druid/blob/9b90983fd291f26935af934383ce360473179e4d/processing/src/main/java/org/apache/druid/frame/processor/SuperSorter.java#L273-L309","documentation":"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().","triggerScenarios":"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.","commonSituations":"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).","solutions":["Create a new SuperSorter instance for each sorting operation and call run() exactly once.","Guard call sites so run() executes once, e.g. capture the returned future and reuse it instead of calling run() again.","If you need the result, hold the ListenableFuture<OutputChannels> returned by the first run() and await it rather than re-running.","Restructure retry logic to retry the whole pipeline (new SuperSorter) rather than the run() call."],"exampleFix":"// before\nsorter.run();\nOutputChannels channels = sorter.run().get(); // second call -> ISE\n// after\nListenableFuture<OutputChannels> future = sorter.run();\nOutputChannels channels = FutureUtils.getUnchecked(future, true);","handlingStrategy":"try-catch","validationCode":"// track one-time use explicitly\nprivate final AtomicBoolean started = new AtomicBoolean();\nif (!started.compareAndSet(false, true)) {\n  throw new IllegalStateException(\"SuperSorter already started\");\n}","typeGuard":null,"tryCatchPattern":"try {\n  future = sorter.run();\n} catch (IllegalStateException e) {\n  // already run: reuse previously captured future\n  future = previouslyCapturedFuture;\n}","preventionTips":["Treat SuperSorter as single-use: one instance per sort operation.","Store the future returned by run() and reuse it instead of re-calling run().","Wrap run() in an idempotent accessor that memoizes the future.","Never cache SuperSorter instances across requests."],"tags":["state","lifecycle","illegal-state"],"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"}