void performOnReplica(
        final PrimaryResponse<Response, ReplicaRequest> response,
        final AtomicInteger counter,
        final ShardRouting shard,
        String nodeId) {
      // if we don't have that node, it means that it might have failed and will be created again,
      // in
      // this case, we don't have to do the operation, and just let it failover
      if (!nodes.nodeExists(nodeId)) {
        if (counter.decrementAndGet() == 0) {
          listener.onResponse(response.response());
        }
        return;
      }

      final ReplicaOperationRequest shardRequest =
          new ReplicaOperationRequest(shardIt.shardId().id(), response.replicaRequest());
      if (!nodeId.equals(nodes.localNodeId())) {
        DiscoveryNode node = nodes.get(nodeId);
        transportService.sendRequest(
            node,
            transportReplicaAction,
            shardRequest,
            transportOptions,
            new VoidTransportResponseHandler(ThreadPool.Names.SAME) {
              @Override
              public void handleResponse(VoidStreamable vResponse) {
                finishIfPossible();
              }

              @Override
              public void handleException(TransportException exp) {
                if (!ignoreReplicaException(exp.unwrapCause())) {
                  logger.warn(
                      "Failed to perform " + transportAction + " on replica " + shardIt.shardId(),
                      exp);
                  shardStateAction.shardFailed(
                      shard,
                      "Failed to perform ["
                          + transportAction
                          + "] on replica, message ["
                          + detailedMessage(exp)
                          + "]");
                }
                finishIfPossible();
              }

              private void finishIfPossible() {
                if (counter.decrementAndGet() == 0) {
                  listener.onResponse(response.response());
                }
              }
            });
      } else {
        if (request.operationThreaded()) {
          request.beforeLocalFork();
          threadPool
              .executor(executor)
              .execute(
                  new Runnable() {
                    @Override
                    public void run() {
                      try {
                        shardOperationOnReplica(shardRequest);
                      } catch (Exception e) {
                        if (!ignoreReplicaException(e)) {
                          logger.warn(
                              "Failed to perform "
                                  + transportAction
                                  + " on replica "
                                  + shardIt.shardId(),
                              e);
                          shardStateAction.shardFailed(
                              shard,
                              "Failed to perform ["
                                  + transportAction
                                  + "] on replica, message ["
                                  + detailedMessage(e)
                                  + "]");
                        }
                      }
                      if (counter.decrementAndGet() == 0) {
                        listener.onResponse(response.response());
                      }
                    }
                  });
        } else {
          try {
            shardOperationOnReplica(shardRequest);
          } catch (Exception e) {
            if (!ignoreReplicaException(e)) {
              logger.warn(
                  "Failed to perform " + transportAction + " on replica" + shardIt.shardId(), e);
              shardStateAction.shardFailed(
                  shard,
                  "Failed to perform ["
                      + transportAction
                      + "] on replica, message ["
                      + detailedMessage(e)
                      + "]");
            }
          }
          if (counter.decrementAndGet() == 0) {
            listener.onResponse(response.response());
          }
        }
      }
    }
    void performReplicas(final PrimaryResponse<Response, ReplicaRequest> response) {
      if (ignoreReplicas() || shardIt.size() == 1 /* no replicas */) {
        postPrimaryOperation(request, response);
        listener.onResponse(response.response());
        return;
      }

      // initialize the counter
      int replicaCounter = shardIt.assignedReplicasIncludingRelocating();

      if (replicaCounter == 0) {
        postPrimaryOperation(request, response);
        listener.onResponse(response.response());
        return;
      }

      if (replicationType == ReplicationType.ASYNC) {
        postPrimaryOperation(request, response);
        // async replication, notify the listener
        listener.onResponse(response.response());
        // now, trick the counter so it won't decrease to 0 and notify the listeners
        replicaCounter = Integer.MIN_VALUE;
      }

      // we add one to the replica count to do the postPrimaryOperation
      replicaCounter++;

      AtomicInteger counter = new AtomicInteger(replicaCounter);
      shardIt.reset(); // reset the iterator
      ShardRouting shard;
      while ((shard = shardIt.nextOrNull()) != null) {
        // if its unassigned, nothing to do here...
        if (shard.unassigned()) {
          continue;
        }

        // if the shard is primary and relocating, add one to the counter since we perform it on the
        // replica as well
        // (and we already did it on the primary)
        boolean doOnlyOnRelocating = false;
        if (shard.primary()) {
          if (shard.relocating()) {
            doOnlyOnRelocating = true;
          } else {
            continue;
          }
        }
        // we index on a replica that is initializing as well since we might not have got the event
        // yet that it was started. We will get an exception IllegalShardState exception if its not
        // started
        // and that's fine, we will ignore it
        if (!doOnlyOnRelocating) {
          performOnReplica(response, counter, shard, shard.currentNodeId());
        }
        if (shard.relocating()) {
          performOnReplica(response, counter, shard, shard.relocatingNodeId());
        }
      }

      // now do the postPrimary operation, and check if the listener needs to be invoked
      postPrimaryOperation(request, response);
      // we also invoke here in case replicas finish before postPrimaryAction does
      if (counter.decrementAndGet() == 0) {
        listener.onResponse(response.response());
      }
    }