@Override
  public void start() {
    stateMachine.transitionToDispatching();
    long startTime = System.nanoTime();
    Future<FragInstanceDispatchResult> dispatchResultFuture = dispatcher.dispatch(instances);

    // NOTICE: the FragmentInstance may be dispatched to another Host due to consensus redirect.
    // So we need to start the state fetcher after the dispatching stage.
    try {
      FragInstanceDispatchResult result = dispatchResultFuture.get();
      if (!result.isSuccessful()) {
        if (needRetry(result.getFailureStatus())) {
          stateMachine.transitionToPendingRetry(result.getFailureStatus());
        } else {
          stateMachine.transitionToFailed(result.getFailureStatus());
        }
        return;
      }
    } catch (InterruptedException | ExecutionException e) {
      // If the dispatch request cannot be sent or TException is caught, we will retry this query.
      if (e instanceof InterruptedException) {
        Thread.currentThread().interrupt();
      }
      stateMachine.transitionToFailed(e);
      return;
    } finally {
      QUERY_EXECUTION_METRICS.recordExecutionCost(WAIT_FOR_DISPATCH, System.nanoTime() - startTime);
    }

    // For the FragmentInstance of WRITE, it will be executed directly when dispatching.
    if (queryType == QueryType.WRITE) {
      stateMachine.transitionToFinished();
      return;
    }

    // The FragmentInstances has been dispatched successfully to corresponding host, we mark the
    // QueryState to Running
    stateMachine.transitionToRunning();

    this.stateTracker.start();
    logger.debug("state tracker starts");
  }
QueryScheduler is used to dispatch the fragment instances of a query to target nodes. And it will
continue to collect and monitor the query execution before the query is finished.

<p>Later, we can add more control logic for a QueryExecution such as retry, kill and so on by
this scheduler.
iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/scheduler/ClusterScheduler.java