private void onComplete(HBaseRpcController controller, ScanResponse resp) {
    if (controller.failed()) {
      onError(controller.getFailed());
      return;
    }
    updateServerSideMetrics(scanMetrics, resp);
    boolean isHeartbeatMessage = resp.hasHeartbeatMessage() && resp.getHeartbeatMessage();
    Result[] rawResults;
    Result[] results;
    int numberOfCompleteRowsBefore = resultCache.numberOfCompleteRows();
    try {
      rawResults = ResponseConverter.getResults(controller.cellScanner(), resp);
      updateResultsMetrics(scanMetrics, rawResults, isHeartbeatMessage);
      results = resultCache.addAndGet(
        Optional.ofNullable(rawResults).orElse(ScanResultCache.EMPTY_RESULT_ARRAY),
        isHeartbeatMessage);
    } catch (IOException e) {
      // We can not retry here. The server has responded normally and the call sequence has been
      // increased so a new scan with the same call sequence will cause an
      // OutOfOrderScannerNextException. Let the upper layer open a new scanner.
      LOG.warn("decode scan response failed", e);
      completeWhenError(true);
      return;
    }

    ScanControllerImpl scanController;
    if (results.length > 0) {
      scanController = new ScanControllerImpl(
        resp.hasCursor() ? Optional.of(ProtobufUtil.toCursor(resp.getCursor())) : Optional.empty());
      updateNextStartRowWhenError(results[results.length - 1]);
      consumer.onNext(results, scanController);
    } else {
      Optional<Cursor> cursor = Optional.empty();
      if (resp.hasCursor()) {
        cursor = Optional.of(ProtobufUtil.toCursor(resp.getCursor()));
      } else if (scan.isNeedCursorResult() && rawResults.length > 0) {
        // It is size limit exceed and we need to return the last Result's row.
        // When user setBatch and the scanner is reopened, the server may return Results that
        // user has seen and the last Result can not be seen because the number is not enough.
        // So the row keys of results may not be same, we must use the last one.
        cursor = Optional.of(new Cursor(rawResults[rawResults.length - 1].getRow()));
      }
      scanController = new ScanControllerImpl(cursor);
      if (isHeartbeatMessage || cursor.isPresent()) {
        // only call onHeartbeat if server tells us explicitly this is a heartbeat message, or we
        // want to pass a cursor to upper layer.
        consumer.onHeartbeat(scanController);
      }
    }
    ScanControllerState state = scanController.destroy();
    if (state == ScanControllerState.TERMINATED) {
      stopScan(resp);
      return;
    }
    int numberOfCompleteRows = resultCache.numberOfCompleteRows() - numberOfCompleteRowsBefore;
    if (state == ScanControllerState.SUSPENDED) {
      if (scanController.resumer.prepare(resp, numberOfCompleteRows)) {
        return;
      }
    }
    completeOrNext(resp, numberOfCompleteRows);
  }
This method is used when {@link ScanControllerImpl#suspend} had ever been called to get a
{@link ScanResumerImpl}, but now user stops scan and does not need any more scan results.
hbase-client/src/main/java/org/apache/hadoop/hbase/client/AsyncScanSingleRegionRpcRetryingCaller.java