public void retry() throws InterruptedException {
    // 1. rebuild the batchRequests using remaining failed FIs, change the replica for each failed
    // FI in this step
    batchRequests.clear();
    for (int fragmentInstanceIndex : needRetryInstanceIndex) {
      this.batchRequests
          .computeIfAbsent(
              instances
                  .get(fragmentInstanceIndex)
                  .getNextRetriedHostDataNode()
                  .getInternalEndPoint(),
              x -> new BatchRequestWithIndex())
          .addSinglePlanNodeReq(
              fragmentInstanceIndex,
              new TSendSinglePlanNodeReq(
                  new TPlanNode(
                      instances
                          .get(fragmentInstanceIndex)
                          .getFragment()
                          .getPlanNodeTree()
                          .serializeToByteBuffer()),
                  instances.get(fragmentInstanceIndex).getRegionReplicaSet().getRegionId()));
    }

    // 2. reset the pendingNumber, needRetryInstanceIds and startSendTime
    needRetryInstanceIndex.clear();
    pendingNumber.set(batchRequests.keySet().size());
    startSendTime = System.nanoTime();

    // 3. call sendAll() to retry
    sendAll();

    // 4. call waitUntilCompleted() to wait for the responses
    waitUntilCompleted();
  }
This function should be called after all last batch responses were received. This function will
do the cleaning work and caller won't need to do the cleaning work outside this function. We
will retry all failed FIs in last batch whose id were all saved in needRetryInstanceIds, if
there are still failed FIs this time, they will be also saved in needRetryInstanceIds.

<p>It's a sync function which means that once this function returned, the results of this retry
have been received, and you don't need to call waitUntilCompleted.
iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/scheduler/AsyncPlanNodeSender.java