<R> R performOperation(
      Function<? super S2, ? extends R> function,
      ResultsAccumulator<R> remoteResults,
      Predicate<? super R> earlyTerminatePredicate) {
    ConsistentHash ch = dm.getConsistentHash();
    TerminalOperation<R> op =
        new SingleRunOperation(
            intermediateOperations, supplierForSegments(ch, segmentsToFilter, null), function);
    Object id =
        csm.remoteStreamOperation(
            getParallelDistribution(),
            parallel,
            ch,
            segmentsToFilter,
            keysToFilter,
            Collections.emptyMap(),
            includeLoader,
            op,
            remoteResults,
            earlyTerminatePredicate);
    try {
      R localValue = op.performOperation();
      remoteResults.onCompletion(null, Collections.emptySet(), localValue);
      if (id != null) {
        try {
          if ((earlyTerminatePredicate == null || !earlyTerminatePredicate.test(localValue))
              && !csm.awaitCompletion(id, timeout, timeoutUnit)) {
            throw new TimeoutException();
          }
        } catch (InterruptedException e) {
          throw new CacheException(e);
        }
      }

      log.tracef("Finished operation for id %s", id);

      return remoteResults.currentValue;
    } finally {
      csm.forgetOperation(id);
    }
  }
  <R> R performOperationRehashAware(
      Function<? super S2, ? extends R> function,
      boolean retryOnRehash,
      ResultsAccumulator<R> remoteResults,
      Predicate<? super R> earlyTerminatePredicate) {
    Set<Integer> segmentsToProcess = segmentsToFilter;
    TerminalOperation<R> op;
    do {
      ConsistentHash ch = dm.getReadConsistentHash();
      if (retryOnRehash) {
        op =
            new SegmentRetryingOperation(
                intermediateOperations, supplierForSegments(ch, segmentsToProcess, null), function);
      } else {
        op =
            new SingleRunOperation(
                intermediateOperations, supplierForSegments(ch, segmentsToProcess, null), function);
      }
      Object id =
          csm.remoteStreamOperationRehashAware(
              getParallelDistribution(),
              parallel,
              ch,
              segmentsToProcess,
              keysToFilter,
              Collections.emptyMap(),
              includeLoader,
              op,
              remoteResults,
              earlyTerminatePredicate);
      try {
        R localValue;
        boolean localRun = ch.getMembers().contains(localAddress);
        if (localRun) {
          localValue = op.performOperation();
          // TODO: we can do this more efficiently - since we drop all results locally
          if (dm.getReadConsistentHash().equals(ch)) {
            Set<Integer> ourSegments = ch.getPrimarySegmentsForOwner(localAddress);
            if (segmentsToProcess != null) {
              ourSegments.retainAll(segmentsToProcess);
            }
            remoteResults.onCompletion(null, ourSegments, localValue);
          } else {
            if (segmentsToProcess != null) {
              Set<Integer> ourSegments = ch.getPrimarySegmentsForOwner(localAddress);
              ourSegments.retainAll(segmentsToProcess);
              remoteResults.onSegmentsLost(ourSegments);
            } else {
              remoteResults.onSegmentsLost(ch.getPrimarySegmentsForOwner(localAddress));
            }
          }
        } else {
          // This isn't actually used because localRun short circuits first
          localValue = null;
        }
        if (id != null) {
          try {
            if ((!localRun
                    || earlyTerminatePredicate == null
                    || !earlyTerminatePredicate.test(localValue))
                && !csm.awaitCompletion(id, timeout, timeoutUnit)) {
              throw new TimeoutException();
            }
          } catch (InterruptedException e) {
            throw new CacheException(e);
          }
        }

        if (!remoteResults.lostSegments.isEmpty()) {
          segmentsToProcess = new HashSet<>(remoteResults.lostSegments);
          remoteResults.lostSegments.clear();
          log.tracef("Found %s lost segments for identifier %s", segmentsToProcess, id);
        } else {
          // If we didn't lose any segments we don't need to process anymore
          if (segmentsToProcess != null) {
            segmentsToProcess = null;
          }
          log.tracef("Finished rehash aware operation for id %s", id);
        }
      } finally {
        csm.forgetOperation(id);
      }
    } while (segmentsToProcess != null && !segmentsToProcess.isEmpty());

    return remoteResults.currentValue;
  }