@Override
  public boolean nextStep() {
    Member targetMember = model.getMember(target);
    Member sourceMember = model.getMember(source);
    if (targetMember == null) {
      throw new IllegalStateException(
          String.format(
              "Target member does not exist or is not a data store for the partitioned region %s: %s",
              model.getName(), target));
    }

    if (targetMember.equals(sourceMember)) {
      throw new IllegalStateException(String.format(
          "Target member is the same as source member for the partitioned region %s: %s",
          model.getName(), target));
    }

    // if there is no largest bucket that we can move, we are done.
    if (orderedBuckets.isEmpty()) {
      return false;
    }
    // Take the largest bucket, and try to move that.
    Bucket bucket = orderedBuckets.last();

    float load = bucket.getLoad();

    // See if we can move this bucket to the taret node.
    if (targetMember.willAcceptBucket(bucket, sourceMember, model.enforceUniqueZones())
        .willAccept()) {

      if (model.moveBucket(new Move(sourceMember, targetMember, bucket))) {
        // If we had a successful move, decrement the load we should move.
        loadToMove -= load;

        // Remove all of the remaining buckets that are to big to move.
        // TODO - this could be O(log(n)), rather an O(n)
        Iterator<Bucket> itr = orderedBuckets.descendingIterator();
        while (itr.hasNext()) {
          Bucket next = itr.next();
          if (next.getLoad() > loadToMove) {
            itr.remove();
          } else {
            break;
          }
        }
      }
    }

    // In any case, remove the bucket from the list of buckets we'll try to move.
    orderedBuckets.remove(bucket);

    return true;
  }
Move buckets from one node to another, up to a certain percentage of the source node. Each call
to nextStep attempts to move a single bucket.

This uses a first fit decreasing strategy to choose which buckets to move. It sorts the buckets
by size, and then moves the largest bucket that is below the load we are trying to move.

An improvement would be find the bucket that can be moved with the least cost for the most load
change, but because the load probe currently use the same value for load and cost, there's no
need to complicate things now.
geode-core/src/main/java/org/apache/geode/internal/cache/partitioned/rebalance/PercentageMoveDirector.java