You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@lucene.apache.org by er...@apache.org on 2014/10/08 20:13:15 UTC

svn commit: r1630191 - in /lucene/dev/branches/branch_5x: ./ solr/ solr/core/ solr/core/src/java/org/apache/solr/cloud/ solr/core/src/java/org/apache/solr/handler/admin/ solr/core/src/test/org/apache/solr/cloud/ solr/solrj/ solr/solrj/src/java/org/apac...

Author: erick
Date: Wed Oct  8 18:13:15 2014
New Revision: 1630191

URL: http://svn.apache.org/r1630191
Log:
SOLR-6513: Add a collectionsAPI call BALANCESLICEUNIQUE

Added:
    lucene/dev/branches/branch_5x/solr/core/src/test/org/apache/solr/cloud/ReplicaPropertiesBase.java
      - copied unchanged from r1630143, lucene/dev/trunk/solr/core/src/test/org/apache/solr/cloud/ReplicaPropertiesBase.java
    lucene/dev/branches/branch_5x/solr/core/src/test/org/apache/solr/cloud/TestReplicaProperties.java
      - copied unchanged from r1630143, lucene/dev/trunk/solr/core/src/test/org/apache/solr/cloud/TestReplicaProperties.java
Modified:
    lucene/dev/branches/branch_5x/   (props changed)
    lucene/dev/branches/branch_5x/solr/   (props changed)
    lucene/dev/branches/branch_5x/solr/CHANGES.txt   (contents, props changed)
    lucene/dev/branches/branch_5x/solr/core/   (props changed)
    lucene/dev/branches/branch_5x/solr/core/src/java/org/apache/solr/cloud/Overseer.java
    lucene/dev/branches/branch_5x/solr/core/src/java/org/apache/solr/cloud/OverseerCollectionProcessor.java
    lucene/dev/branches/branch_5x/solr/core/src/java/org/apache/solr/handler/admin/CollectionsHandler.java
    lucene/dev/branches/branch_5x/solr/core/src/test/org/apache/solr/cloud/TestCollectionAPI.java
    lucene/dev/branches/branch_5x/solr/solrj/   (props changed)
    lucene/dev/branches/branch_5x/solr/solrj/src/java/org/apache/solr/common/params/CollectionParams.java

Modified: lucene/dev/branches/branch_5x/solr/CHANGES.txt
URL: http://svn.apache.org/viewvc/lucene/dev/branches/branch_5x/solr/CHANGES.txt?rev=1630191&r1=1630190&r2=1630191&view=diff
==============================================================================
--- lucene/dev/branches/branch_5x/solr/CHANGES.txt (original)
+++ lucene/dev/branches/branch_5x/solr/CHANGES.txt Wed Oct  8 18:13:15 2014
@@ -125,6 +125,11 @@ New Features
   this property from all other replicas within a particular slice. 
   (Erick Erickson)
 
+* SOLR-6513: Add a collectionsAPI call BALANCESLICEUNIQUE. Allows the even
+  distribution of custom replica properties across nodes making up a 
+  collection, at most one node per slice will have the property.
+
+
 Bug Fixes
 ----------------------
 

Modified: lucene/dev/branches/branch_5x/solr/core/src/java/org/apache/solr/cloud/Overseer.java
URL: http://svn.apache.org/viewvc/lucene/dev/branches/branch_5x/solr/core/src/java/org/apache/solr/cloud/Overseer.java?rev=1630191&r1=1630190&r2=1630191&view=diff
==============================================================================
--- lucene/dev/branches/branch_5x/solr/core/src/java/org/apache/solr/cloud/Overseer.java (original)
+++ lucene/dev/branches/branch_5x/solr/core/src/java/org/apache/solr/cloud/Overseer.java Wed Oct  8 18:13:15 2014
@@ -18,7 +18,11 @@ package org.apache.solr.cloud;
  */
 
 import static java.util.Collections.singletonMap;
+import static org.apache.solr.cloud.OverseerCollectionProcessor.SLICE_UNIQUE;
 import static org.apache.solr.common.cloud.ZkNodeProps.makeMap;
+import static org.apache.solr.cloud.OverseerCollectionProcessor.ONLY_ACTIVE_NODES;
+import static org.apache.solr.cloud.OverseerCollectionProcessor.COLL_PROP_PREFIX;
+import static org.apache.solr.common.params.CollectionParams.CollectionAction.BALANCESLICEUNIQUE;
 
 import java.io.Closeable;
 import java.io.IOException;
@@ -26,12 +30,15 @@ import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.LinkedHashMap;
 import java.util.LinkedList;
 import java.util.List;
+import java.util.ListIterator;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Map.Entry;
+import java.util.Random;
 import java.util.Set;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicInteger;
@@ -448,6 +455,13 @@ public class Overseer implements Closeab
           case DELETEREPLICAPROP:
             clusterState = deleteReplicaProp(clusterState, message);
             break;
+          case BALANCESLICEUNIQUE:
+            ExclusiveSliceProperty dProp = new ExclusiveSliceProperty(this, clusterState, message);
+            if (dProp.balanceProperty()) {
+              String collName = message.getStr(ZkStateReader.COLLECTION_PROP);
+              clusterState = newState(clusterState, singletonMap(collName, dProp.getDocCollection()));
+            }
+            break;
           default:
             throw new RuntimeException("unknown operation:" + operation
                 + " contents:" + message.getProperties());
@@ -532,9 +546,10 @@ public class Overseer implements Closeab
       String sliceName = message.getStr(ZkStateReader.SHARD_ID_PROP);
       String replicaName = message.getStr(ZkStateReader.REPLICA_PROP);
       String property = message.getStr(ZkStateReader.PROPERTY_PROP).toLowerCase(Locale.ROOT);
-      if (property.startsWith(OverseerCollectionProcessor.COLL_PROP_PREFIX) == false) {
+      if (StringUtils.startsWith(property, COLL_PROP_PREFIX) == false) {
         property = OverseerCollectionProcessor.COLL_PROP_PREFIX + property;
       }
+      property = property.toLowerCase(Locale.ROOT);
       String propVal = message.getStr(ZkStateReader.PROPERTY_VALUE_PROP);
       String sliceUnique = message.getStr(OverseerCollectionProcessor.SLICE_UNIQUE);
 
@@ -593,7 +608,7 @@ public class Overseer implements Closeab
       String sliceName = message.getStr(ZkStateReader.SHARD_ID_PROP);
       String replicaName = message.getStr(ZkStateReader.REPLICA_PROP);
       String property = message.getStr(ZkStateReader.PROPERTY_PROP).toLowerCase(Locale.ROOT);
-      if (property.startsWith(OverseerCollectionProcessor.COLL_PROP_PREFIX) == false) {
+      if (StringUtils.startsWith(property, COLL_PROP_PREFIX) == false) {
         property = OverseerCollectionProcessor.COLL_PROP_PREFIX + property;
       }
 
@@ -934,8 +949,16 @@ public class Overseer implements Closeab
         // System.out.println("########## UPDATE MESSAGE: " + JSONUtil.toJSON(message));
         if (slice != null) {
           Replica oldReplica = slice.getReplicasMap().get(coreNodeName);
-          if (oldReplica != null && oldReplica.containsKey(ZkStateReader.LEADER_PROP)) {
-            replicaProps.put(ZkStateReader.LEADER_PROP, oldReplica.get(ZkStateReader.LEADER_PROP));
+          if (oldReplica != null) {
+            if (oldReplica.containsKey(ZkStateReader.LEADER_PROP)) {
+              replicaProps.put(ZkStateReader.LEADER_PROP, oldReplica.get(ZkStateReader.LEADER_PROP));
+            }
+            // Move custom props over.
+            for (Map.Entry<String, Object> ent : oldReplica.getProperties().entrySet()) {
+              if (ent.getKey().startsWith(COLL_PROP_PREFIX)) {
+                replicaProps.put(ent.getKey(), ent.getValue());
+              }
+            }
           }
         }
 
@@ -1146,7 +1169,7 @@ public class Overseer implements Closeab
         return null;
       }
 
-      private ClusterState updateSlice(ClusterState state, String collectionName, Slice slice) {
+    ClusterState updateSlice(ClusterState state, String collectionName, Slice slice) {
         // System.out.println("###!!!### OLD CLUSTERSTATE: " + JSONUtil.toJSON(state.getCollectionStates()));
         // System.out.println("Updating slice:" + slice);
         DocCollection newCollection = null;
@@ -1374,6 +1397,307 @@ public class Overseer implements Closeab
 
   }
 
+  // Class to encapsulate processing replica properties that have at most one replica hosting a property per slice.
+  private class ExclusiveSliceProperty {
+    private ClusterStateUpdater updater;
+    private ClusterState clusterState;
+    private final boolean onlyActiveNodes;
+    private final String property;
+    private final DocCollection collection;
+    private final String collectionName;
+
+    // Key structure. For each node, list all replicas on it regardless of whether they have the property or not.
+    private final Map<String, List<SliceReplica>> nodesHostingReplicas = new HashMap<>();
+    // Key structure. For each node, a list of the replicas _currently_ hosting the property.
+    private final Map<String, List<SliceReplica>> nodesHostingProp = new HashMap<>();
+    Set<String> shardsNeedingHosts = new HashSet<String>();
+    Map<String, Slice> changedSlices = new HashMap<>(); // Work on copies rather than the underlying cluster state.
+
+    private int origMaxPropPerNode = 0;
+    private int origModulo = 0;
+    private int tmpMaxPropPerNode = 0;
+    private int tmpModulo = 0;
+    Random rand = new Random();
+
+    private int assigned = 0;
+
+    ExclusiveSliceProperty(ClusterStateUpdater updater, ClusterState clusterState, ZkNodeProps message) {
+      this.updater = updater;
+      this.clusterState = clusterState;
+      String tmp = message.getStr(ZkStateReader.PROPERTY_PROP);
+      if (StringUtils.startsWith(tmp, OverseerCollectionProcessor.COLL_PROP_PREFIX) == false) {
+        tmp = OverseerCollectionProcessor.COLL_PROP_PREFIX + tmp;
+      }
+      this.property = tmp.toLowerCase(Locale.ROOT);
+      collectionName = message.getStr(ZkStateReader.COLLECTION_PROP);
+
+      if (StringUtils.isBlank(collectionName) || StringUtils.isBlank(property)) {
+        throw new SolrException(SolrException.ErrorCode.BAD_REQUEST,
+            "Overseer '" + message.getStr(QUEUE_OPERATION) + "'  requires both the '" + ZkStateReader.COLLECTION_PROP + "' and '" +
+                ZkStateReader.PROPERTY_PROP + "' parameters. No action taken ");
+      }
+
+      Boolean sliceUnique = Boolean.parseBoolean(message.getStr(SLICE_UNIQUE));
+      if (sliceUnique == false &&
+          Overseer.sliceUniqueBooleanProperties.contains(this.property) == false) {
+        throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Balancing properties amongst replicas in a slice requires that"
+            + " the property be a pre-defined property (e.g. 'preferredLeader') or that 'sliceUnique' be set to 'true' " +
+            " Property: " + this.property + " sliceUnique: " + Boolean.toString(sliceUnique));
+      }
+
+      collection = clusterState.getCollection(collectionName);
+      if (collection == null) {
+        throw new SolrException(SolrException.ErrorCode.BAD_REQUEST,
+            "Could not find collection ' " + collectionName + "' for overseer operation '" +
+                message.getStr(QUEUE_OPERATION) + "'. No action taken.");
+      }
+      onlyActiveNodes = Boolean.parseBoolean(message.getStr(ONLY_ACTIVE_NODES, "true"));
+    }
+
+
+    private DocCollection getDocCollection() {
+      return collection;
+    }
+
+    private boolean isActive(Replica replica) {
+      return ZkStateReader.ACTIVE.equals(replica.getStr(ZkStateReader.STATE_PROP));
+    }
+
+    // Collect a list of all the nodes that _can_ host the indicated property. Along the way, also collect any of
+    // the replicas on that node that _already_ host the property as well as any slices that do _not_ have the
+    // property hosted.
+    //
+    // Return true if anything node needs it's property reassigned. False if the property is already balanced for
+    // the collection.
+
+    private boolean collectCurrentPropStats() {
+      int maxAssigned = 0;
+      // Get a list of potential replicas that can host the property _and_ their counts
+      // Move any obvious entries to a list of replicas to change the property on
+      Set<String> allHosts = new HashSet<>();
+      for (Slice slice : collection.getSlices()) {
+        boolean sliceHasProp = false;
+        for (Replica replica : slice.getReplicas()) {
+          if (onlyActiveNodes && isActive(replica) == false) {
+            if (StringUtils.isNotBlank(replica.getStr(property))) {
+              removeProp(slice, replica.getName()); // Note, we won't be committing this to ZK until later.
+            }
+            continue;
+          }
+          allHosts.add(replica.getNodeName());
+          String nodeName = replica.getNodeName();
+          if (StringUtils.isNotBlank(replica.getStr(property))) {
+            if (sliceHasProp) {
+              throw new SolrException(SolrException.ErrorCode.BAD_REQUEST,
+                  "'" + BALANCESLICEUNIQUE + "' should only be called for properties that have at most one member " +
+                      "in any slice with the property set. No action taken.");
+            }
+            if (nodesHostingProp.containsKey(nodeName) == false) {
+              nodesHostingProp.put(nodeName, new ArrayList<SliceReplica>());
+            }
+            nodesHostingProp.get(nodeName).add(new SliceReplica(slice, replica));
+            ++assigned;
+            maxAssigned = Math.max(maxAssigned, nodesHostingProp.get(nodeName).size());
+            sliceHasProp = true;
+          }
+          if (nodesHostingReplicas.containsKey(nodeName) == false) {
+            nodesHostingReplicas.put(nodeName, new ArrayList<SliceReplica>());
+          }
+          nodesHostingReplicas.get(nodeName).add(new SliceReplica(slice, replica));
+        }
+      }
+
+      // If the total number of already-hosted properties assigned to nodes
+      // that have potential to host leaders is equal to the slice count _AND_ none of the current nodes has more than
+      // the max number of properties, there's nothing to do.
+      origMaxPropPerNode = collection.getSlices().size() / allHosts.size();
+
+      // Some nodes can have one more of the proeprty if the numbers aren't exactly even.
+      origModulo = collection.getSlices().size() % allHosts.size();
+      if (origModulo > 0) {
+        origMaxPropPerNode++;  // have to have some nodes with 1 more property.
+      }
+
+      // We can say for sure that we need to rebalance if we don't have as many assigned properties as slices.
+      if (assigned != collection.getSlices().size()) {
+        return true;
+      }
+
+      // Make sure there are no more slices at the limit than the "leftovers"
+      // Let's say there's 7 slices and 3 nodes. We need to distribute the property as 3 on node1, 2 on node2 and 2 on node3
+      // (3, 2, 2) We need to be careful to not distribute them as 3, 3, 1. that's what this check is all about.
+      int counter = origModulo;
+      for (List<SliceReplica> list : nodesHostingProp.values()) {
+        if (list.size() == origMaxPropPerNode) --counter;
+      }
+      if (counter == 0) return false; // nodes with 1 extra leader are exactly the needed number
+
+      return true;
+    }
+
+    private void removeSliceAlreadyHostedFromPossibles(String sliceName) {
+      for (Map.Entry<String, List<SliceReplica>> entReplica : nodesHostingReplicas.entrySet()) {
+
+        ListIterator<SliceReplica> iter = entReplica.getValue().listIterator();
+        while (iter.hasNext()) {
+          SliceReplica sr = iter.next();
+          if (sr.slice.getName().equals(sliceName))
+            iter.remove();
+        }
+      }
+    }
+
+    private void balanceUnassignedReplicas() {
+      tmpMaxPropPerNode = origMaxPropPerNode; // A bit clumsy, but don't want to duplicate code.
+      tmpModulo = origModulo;
+
+      // Get the nodeName and shardName for the node that has the least room for this
+
+      while (shardsNeedingHosts.size() > 0) {
+        String nodeName = "";
+        int minSize = Integer.MAX_VALUE;
+        SliceReplica srToChange = null;
+        for (String slice : shardsNeedingHosts) {
+          for (Map.Entry<String, List<SliceReplica>> ent : nodesHostingReplicas.entrySet()) {
+            // A little tricky. If we don't set this to something below, then it means all possible places to
+            // put this property are full up, so just put it somewhere.
+            if (srToChange == null && ent.getValue().size() > 0) {
+              srToChange = ent.getValue().get(0);
+            }
+            ListIterator<SliceReplica> iter = ent.getValue().listIterator();
+            while (iter.hasNext()) {
+              SliceReplica sr = iter.next();
+              if (StringUtils.equals(slice, sr.slice.getName()) == false) {
+                continue;
+              }
+              if (nodesHostingProp.containsKey(ent.getKey()) == false) {
+                nodesHostingProp.put(ent.getKey(), new ArrayList<SliceReplica>());
+              }
+              if (minSize > nodesHostingReplicas.get(ent.getKey()).size() && nodesHostingProp.get(ent.getKey()).size() < tmpMaxPropPerNode) {
+                minSize = nodesHostingReplicas.get(ent.getKey()).size();
+                srToChange = sr;
+                nodeName = ent.getKey();
+              }
+            }
+          }
+        }
+        // Now, you have a slice and node to put it on
+        shardsNeedingHosts.remove(srToChange.slice.getName());
+        if (nodesHostingProp.containsKey(nodeName) == false) {
+          nodesHostingProp.put(nodeName, new ArrayList<SliceReplica>());
+        }
+        nodesHostingProp.get(nodeName).add(srToChange);
+        adjustLimits(nodesHostingProp.get(nodeName));
+        removeSliceAlreadyHostedFromPossibles(srToChange.slice.getName());
+        addProp(srToChange.slice, srToChange.replica.getName());
+      }
+    }
+
+    // Adjust the min/max counts per allowed per node. Special handling here for dealing with the fact
+    // that no node should have more than 1 more replica with this property than any other.
+    private void adjustLimits(List<SliceReplica> changeList) {
+      if (changeList.size() == tmpMaxPropPerNode) {
+        if (tmpModulo < 0) return;
+
+        --tmpModulo;
+        if (tmpModulo == 0) {
+          --tmpMaxPropPerNode;
+          --tmpModulo;  // Prevent dropping tmpMaxPropPerNode again.
+        }
+      }
+    }
+
+    // Go through the list of presently-hosted proeprties and remove any that have too many replicas that host the property
+    private void removeOverallocatedReplicas() {
+      tmpMaxPropPerNode = origMaxPropPerNode; // A bit clumsy, but don't want to duplicate code.
+      tmpModulo = origModulo;
+
+      for (Map.Entry<String, List<SliceReplica>> ent : nodesHostingProp.entrySet()) {
+        while (ent.getValue().size() > tmpMaxPropPerNode) { // remove delta nodes
+          ent.getValue().remove(rand.nextInt(ent.getValue().size()));
+        }
+        adjustLimits(ent.getValue());
+      }
+    }
+
+    private void removeProp(Slice origSlice, String replicaName) {
+      getReplicaFromChanged(origSlice, replicaName).getProperties().remove(property);
+    }
+
+    private void addProp(Slice origSlice, String replicaName) {
+      getReplicaFromChanged(origSlice, replicaName).getProperties().put(property, "true");
+    }
+
+    // Just a place to encapsulate the fact that we need to have new slices (copy) to update before we
+    // put this all in the cluster state.
+    private Replica getReplicaFromChanged(Slice origSlice, String replicaName) {
+      Slice newSlice = changedSlices.get(origSlice.getName());
+      Replica replica;
+      if (newSlice != null) {
+        replica = newSlice.getReplica(replicaName);
+      } else {
+        newSlice = new Slice(origSlice.getName(), origSlice.getReplicasCopy(), origSlice.shallowCopy());
+        changedSlices.put(origSlice.getName(), newSlice);
+        replica = newSlice.getReplica(replicaName);
+      }
+      if (replica == null) {
+        throw new SolrException(SolrException.ErrorCode.INVALID_STATE, "Should have been able to find replica '" +
+            replicaName + "' in slice '" + origSlice.getName() + "'. No action taken");
+      }
+      return replica;
+
+    }
+    // Main entry point for carrying out the action. Returns "true" if we have actually moved properties around.
+
+    private boolean balanceProperty() {
+      if (collectCurrentPropStats() == false) {
+        return false;
+      }
+
+      // we have two lists based on nodeName
+      // 1> all the nodes that _could_ host a property for the slice
+      // 2> all the nodes that _currently_ host a property for the slice.
+
+      // So, remove a replica from the nodes that have too many
+      removeOverallocatedReplicas();
+
+      // prune replicas belonging to a slice that have the property currently assigned from the list of replicas
+      // that could host the property.
+      for (Map.Entry<String, List<SliceReplica>> entProp : nodesHostingProp.entrySet()) {
+        for (SliceReplica srHosting : entProp.getValue()) {
+          removeSliceAlreadyHostedFromPossibles(srHosting.slice.getName());
+        }
+      }
+
+      // Assemble the list of slices that do not have any replica hosting the property:
+      for (Map.Entry<String, List<SliceReplica>> ent : nodesHostingReplicas.entrySet()) {
+        ListIterator<SliceReplica> iter = ent.getValue().listIterator();
+        while (iter.hasNext()) {
+          SliceReplica sr = iter.next();
+          shardsNeedingHosts.add(sr.slice.getName());
+        }
+      }
+
+      // At this point, nodesHostingProp should contain _only_ lists of replicas that belong to slices that do _not_
+      // have any replica hosting the property. So let's assign them.
+
+      balanceUnassignedReplicas();
+      for (Slice newSlice : changedSlices.values()) {
+        clusterState = updater.updateSlice(clusterState, collectionName, newSlice);
+      }
+      return true;
+    }
+  }
+
+  private class SliceReplica {
+    private Slice slice;
+    private Replica replica;
+
+    SliceReplica(Slice slice, Replica replica) {
+      this.slice = slice;
+      this.replica = replica;
+    }
+  }
   static void getShardNames(Integer numShards, List<String> shardNames) {
     if(numShards == null)
       throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "numShards" + " is a required param");

Modified: lucene/dev/branches/branch_5x/solr/core/src/java/org/apache/solr/cloud/OverseerCollectionProcessor.java
URL: http://svn.apache.org/viewvc/lucene/dev/branches/branch_5x/solr/core/src/java/org/apache/solr/cloud/OverseerCollectionProcessor.java?rev=1630191&r1=1630190&r2=1630191&view=diff
==============================================================================
--- lucene/dev/branches/branch_5x/solr/core/src/java/org/apache/solr/cloud/OverseerCollectionProcessor.java (original)
+++ lucene/dev/branches/branch_5x/solr/core/src/java/org/apache/solr/cloud/OverseerCollectionProcessor.java Wed Oct  8 18:13:15 2014
@@ -26,6 +26,7 @@ import static org.apache.solr.common.clo
 import static org.apache.solr.common.params.CollectionParams.CollectionAction.ADDREPLICA;
 import static org.apache.solr.common.params.CollectionParams.CollectionAction.ADDREPLICAPROP;
 import static org.apache.solr.common.params.CollectionParams.CollectionAction.ADDROLE;
+import static org.apache.solr.common.params.CollectionParams.CollectionAction.BALANCESLICEUNIQUE;
 import static org.apache.solr.common.params.CollectionParams.CollectionAction.CLUSTERSTATUS;
 import static org.apache.solr.common.params.CollectionParams.CollectionAction.CREATE;
 import static org.apache.solr.common.params.CollectionParams.CollectionAction.CREATESHARD;
@@ -154,6 +155,8 @@ public class OverseerCollectionProcessor
 
   public static final String SLICE_UNIQUE = "sliceUnique";
 
+  public static final String ONLY_ACTIVE_NODES = "onlyactivenodes";
+
   public int maxParallelThreads = 10;
 
   public static final Set<String> KNOWN_CLUSTER_PROPS = ImmutableSet.of(ZkStateReader.LEGACY_CLOUD, ZkStateReader.URL_SCHEME);
@@ -645,6 +648,9 @@ public class OverseerCollectionProcessor
           case DELETEREPLICAPROP:
             processReplicaDeletePropertyCommand(message);
             break;
+          case BALANCESLICEUNIQUE:
+            balanceProperty(message);
+            break;
           default:
             throw new SolrException(ErrorCode.BAD_REQUEST, "Unknown operation:"
                 + operation);
@@ -708,6 +714,21 @@ public class OverseerCollectionProcessor
     inQueue.offer(ZkStateReader.toJSON(m));
   }
 
+  private void balanceProperty(ZkNodeProps message) throws KeeperException, InterruptedException {
+    if (StringUtils.isBlank(message.getStr(COLLECTION_PROP)) || StringUtils.isBlank(message.getStr(PROPERTY_PROP))) {
+      throw new SolrException(ErrorCode.BAD_REQUEST,
+          "The '" + COLLECTION_PROP + "' and '" + PROPERTY_PROP +
+              "' parameters are required for the BALANCESLICEUNIQUE operation, no action taken");
+    }
+    SolrZkClient zkClient = zkStateReader.getZkClient();
+    DistributedQueue inQueue = Overseer.getInQueue(zkClient);
+    Map<String, Object> propMap = new HashMap<>();
+    propMap.put(Overseer.QUEUE_OPERATION, BALANCESLICEUNIQUE.toLower());
+    propMap.putAll(message.getProperties());
+    inQueue.offer(ZkStateReader.toJSON(new ZkNodeProps(propMap)));
+  }
+
+
   @SuppressWarnings("unchecked")
   private void getOverseerStatus(ZkNodeProps message, NamedList results) throws KeeperException, InterruptedException {
     String leaderNode = getLeaderNode(zkStateReader.getZkClient());

Modified: lucene/dev/branches/branch_5x/solr/core/src/java/org/apache/solr/handler/admin/CollectionsHandler.java
URL: http://svn.apache.org/viewvc/lucene/dev/branches/branch_5x/solr/core/src/java/org/apache/solr/handler/admin/CollectionsHandler.java?rev=1630191&r1=1630190&r2=1630191&view=diff
==============================================================================
--- lucene/dev/branches/branch_5x/solr/core/src/java/org/apache/solr/handler/admin/CollectionsHandler.java (original)
+++ lucene/dev/branches/branch_5x/solr/core/src/java/org/apache/solr/handler/admin/CollectionsHandler.java Wed Oct  8 18:13:15 2014
@@ -23,6 +23,7 @@ import static org.apache.solr.cloud.Over
 import static org.apache.solr.cloud.OverseerCollectionProcessor.CREATE_NODE_SET;
 import static org.apache.solr.cloud.OverseerCollectionProcessor.SLICE_UNIQUE;
 import static org.apache.solr.cloud.OverseerCollectionProcessor.NUM_SLICES;
+import static org.apache.solr.cloud.OverseerCollectionProcessor.ONLY_ACTIVE_NODES;
 import static org.apache.solr.cloud.OverseerCollectionProcessor.ONLY_IF_DOWN;
 import static org.apache.solr.cloud.OverseerCollectionProcessor.REPLICATION_FACTOR;
 import static org.apache.solr.cloud.OverseerCollectionProcessor.REQUESTID;
@@ -37,6 +38,7 @@ import static org.apache.solr.common.clo
 import static org.apache.solr.common.cloud.ZkStateReader.AUTO_ADD_REPLICAS;
 import static org.apache.solr.common.cloud.ZkStateReader.REPLICA_PROP;
 import static org.apache.solr.common.params.CollectionParams.CollectionAction.ADDROLE;
+import static org.apache.solr.common.params.CollectionParams.CollectionAction.BALANCESLICEUNIQUE;
 import static org.apache.solr.common.params.CollectionParams.CollectionAction.ADDREPLICAPROP;
 import static org.apache.solr.common.params.CollectionParams.CollectionAction.CLUSTERPROP;
 import static org.apache.solr.common.params.CollectionParams.CollectionAction.CREATE;
@@ -246,7 +248,10 @@ public class CollectionsHandler extends 
         this.handleDeleteReplicaProp(req, rsp);
         break;
       }
-
+      case BALANCESLICEUNIQUE: {
+        this.handleBalanceSliceUnique(req, rsp);
+        break;
+      }
       default: {
           throw new RuntimeException("Unknown action: " + action);
       }
@@ -294,6 +299,27 @@ public class CollectionsHandler extends 
 
 
 
+  private void handleBalanceSliceUnique(SolrQueryRequest req, SolrQueryResponse rsp) throws KeeperException, InterruptedException {
+    req.getParams().required().check(COLLECTION_PROP, PROPERTY_PROP);
+    Boolean sliceUnique = Boolean.parseBoolean(req.getParams().get(SLICE_UNIQUE));
+    String prop = req.getParams().get(PROPERTY_PROP).toLowerCase(Locale.ROOT);
+    if (StringUtils.startsWith(prop, OverseerCollectionProcessor.COLL_PROP_PREFIX) == false) {
+      prop = OverseerCollectionProcessor.COLL_PROP_PREFIX + prop;
+    }
+
+    if (sliceUnique == false &&
+        Overseer.sliceUniqueBooleanProperties.contains(prop) == false) {
+      throw new SolrException(ErrorCode.BAD_REQUEST, "Balancing properties amongst replicas in a slice requires that"
+      + " the property be pre-defined as a unique property (e.g. 'preferredLeader') or that 'sliceUnique' be set to 'true'. " +
+      " Property: " + prop + " sliceUnique: " + Boolean.toString(sliceUnique));
+    }
+
+    Map<String, Object> map = ZkNodeProps.makeMap(Overseer.QUEUE_OPERATION, BALANCESLICEUNIQUE.toLower());
+    copyIfNotNull(req.getParams(), map, COLLECTION_PROP, PROPERTY_PROP, ONLY_ACTIVE_NODES, SLICE_UNIQUE);
+
+    handleResponse(BALANCESLICEUNIQUE.toLower(), new ZkNodeProps(map), rsp);
+  }
+
   private void handleOverseerStatus(SolrQueryRequest req, SolrQueryResponse rsp) throws KeeperException, InterruptedException {
     Map<String, Object> props = ZkNodeProps.makeMap(
         Overseer.QUEUE_OPERATION, OVERSEERSTATUS.toLower());

Modified: lucene/dev/branches/branch_5x/solr/core/src/test/org/apache/solr/cloud/TestCollectionAPI.java
URL: http://svn.apache.org/viewvc/lucene/dev/branches/branch_5x/solr/core/src/test/org/apache/solr/cloud/TestCollectionAPI.java?rev=1630191&r1=1630190&r2=1630191&view=diff
==============================================================================
--- lucene/dev/branches/branch_5x/solr/core/src/test/org/apache/solr/cloud/TestCollectionAPI.java (original)
+++ lucene/dev/branches/branch_5x/solr/core/src/test/org/apache/solr/cloud/TestCollectionAPI.java Wed Oct  8 18:13:15 2014
@@ -19,7 +19,6 @@ package org.apache.solr.cloud;
 
 
 import com.google.common.collect.Lists;
-import org.apache.commons.lang.StringUtils;
 import org.apache.solr.client.solrj.SolrRequest;
 import org.apache.solr.client.solrj.SolrServerException;
 import org.apache.solr.client.solrj.impl.CloudSolrServer;
@@ -45,7 +44,7 @@ import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 
-public class TestCollectionAPI extends AbstractFullDistribZkTestBase {
+public class TestCollectionAPI extends ReplicaPropertiesBase {
 
   public static final String COLLECTION_NAME = "testcollection";
   public static final String COLLECTION_NAME1 = "testcollection1";
@@ -338,9 +337,7 @@ public class TestCollectionAPI extends A
 
       // The above should have set exactly one preferredleader...
       verifyPropertyVal(client, COLLECTION_NAME, c1_s1_r1, "property.preferredleader", "true");
-      verifyPropertyNotPresent(client, COLLECTION_NAME, c1_s1_r2, "property.preferredLeader");
-      verifyPropertyNotPresent(client, COLLECTION_NAME, c1_s2_r1, "property.preferredLeader");
-      verifyPropertyNotPresent(client, COLLECTION_NAME, c1_s2_r2, "property.preferredLeader");
+      verifyUniquePropertyWithinCollection(client, COLLECTION_NAME, "property.preferredLeader");
 
       doPropertyAction(client,
           "action", CollectionParams.CollectionAction.ADDREPLICAPROP.toString(),
@@ -351,9 +348,7 @@ public class TestCollectionAPI extends A
           "property.value", "true");
       // The preferred leader property for shard1 should have switched to the other replica.
       verifyPropertyVal(client, COLLECTION_NAME, c1_s1_r2, "property.preferredleader", "true");
-      verifyPropertyNotPresent(client, COLLECTION_NAME, c1_s1_r1, "property.preferredLeader");
-      verifyPropertyNotPresent(client, COLLECTION_NAME, c1_s2_r1, "property.preferredLeader");
-      verifyPropertyNotPresent(client, COLLECTION_NAME, c1_s2_r2, "property.preferredLeader");
+      verifyUniquePropertyWithinCollection(client, COLLECTION_NAME, "property.preferredLeader");
 
       doPropertyAction(client,
           "action", CollectionParams.CollectionAction.ADDREPLICAPROP.toString(),
@@ -365,9 +360,8 @@ public class TestCollectionAPI extends A
 
       // Now we should have a preferred leader in both shards...
       verifyPropertyVal(client, COLLECTION_NAME, c1_s1_r2, "property.preferredleader", "true");
-      verifyPropertyNotPresent(client, COLLECTION_NAME, c1_s1_r1, "property.preferredleader");
       verifyPropertyVal(client, COLLECTION_NAME, c1_s2_r1, "property.preferredleader", "true");
-      verifyPropertyNotPresent(client, COLLECTION_NAME, c1_s2_r2, "property.preferredLeader");
+      verifyUniquePropertyWithinCollection(client, COLLECTION_NAME, "property.preferredLeader");
 
       doPropertyAction(client,
           "action", CollectionParams.CollectionAction.ADDREPLICAPROP.toString(),
@@ -381,6 +375,8 @@ public class TestCollectionAPI extends A
       verifyPropertyVal(client, COLLECTION_NAME, c1_s1_r2, "property.preferredleader", "true");
       verifyPropertyVal(client, COLLECTION_NAME, c1_s2_r1, "property.preferredleader", "true");
       verifyPropertyVal(client, COLLECTION_NAME1, c2_s1_r1, "property.preferredleader", "true");
+      verifyUniquePropertyWithinCollection(client, COLLECTION_NAME, "property.preferredLeader");
+      verifyUniquePropertyWithinCollection(client, COLLECTION_NAME1, "property.preferredLeader");
 
       doPropertyAction(client,
           "action", CollectionParams.CollectionAction.DELETEREPLICAPROP.toString(),
@@ -393,9 +389,8 @@ public class TestCollectionAPI extends A
       // But first we have to wait for the overseer to finish the action
       verifyPropertyVal(client, COLLECTION_NAME, c1_s1_r2, "property.preferredleader", "true");
       verifyPropertyVal(client, COLLECTION_NAME, c1_s2_r1, "property.preferredleader", "true");
-      verifyPropertyNotPresent(client, COLLECTION_NAME, c1_s1_r1, "property.preferredleader");
-      verifyPropertyNotPresent(client, COLLECTION_NAME, c1_s2_r2, "property.preferredleader");
-      verifyPropertyNotPresent(client, COLLECTION_NAME1, c2_s1_r1, "property.preferredleader");
+      verifyUniquePropertyWithinCollection(client, COLLECTION_NAME, "property.preferredLeader");
+      verifyUniquePropertyWithinCollection(client, COLLECTION_NAME1, "property.preferredLeader");
 
       // Try adding an arbitrary property to one that has the leader property
       doPropertyAction(client,
@@ -409,10 +404,8 @@ public class TestCollectionAPI extends A
       verifyPropertyVal(client, COLLECTION_NAME, c1_s1_r2, "property.preferredleader", "true");
       verifyPropertyVal(client, COLLECTION_NAME, c1_s2_r1, "property.preferredleader", "true");
       verifyPropertyVal(client, COLLECTION_NAME, c1_s1_r1, "property.testprop", "true");
-      verifyPropertyNotPresent(client, COLLECTION_NAME, c1_s1_r1, "property.preferredleader");
-      verifyPropertyNotPresent(client, COLLECTION_NAME, c1_s2_r2, "property.preferredleader");
-      verifyPropertyNotPresent(client, COLLECTION_NAME1, c2_s1_r1, "property.preferredleader");
-      verifyPropertyNotPresent(client, COLLECTION_NAME1, c2_s1_r1, "property.preferredleader");
+      verifyUniquePropertyWithinCollection(client, COLLECTION_NAME, "property.preferredLeader");
+      verifyUniquePropertyWithinCollection(client, COLLECTION_NAME1, "property.preferredLeader");
 
       doPropertyAction(client,
           "action", CollectionParams.CollectionAction.ADDREPLICAPROP.toString(),
@@ -426,10 +419,8 @@ public class TestCollectionAPI extends A
       verifyPropertyVal(client, COLLECTION_NAME, c1_s2_r1, "property.preferredleader", "true");
       verifyPropertyVal(client, COLLECTION_NAME, c1_s1_r1, "property.testprop", "true");
       verifyPropertyVal(client, COLLECTION_NAME, c1_s1_r2, "property.prop", "silly");
-      verifyPropertyNotPresent(client, COLLECTION_NAME, c1_s1_r1, "property.preferredleader");
-      verifyPropertyNotPresent(client, COLLECTION_NAME, c1_s2_r2, "property.preferredleader");
-      verifyPropertyNotPresent(client, COLLECTION_NAME1, c2_s1_r1, "property.preferredleader");
-      verifyPropertyNotPresent(client, COLLECTION_NAME1, c2_s1_r1, "property.preferredleader");
+      verifyUniquePropertyWithinCollection(client, COLLECTION_NAME, "property.preferredLeader");
+      verifyUniquePropertyWithinCollection(client, COLLECTION_NAME1, "property.preferredLeader");
 
       doPropertyAction(client,
           "action", CollectionParams.CollectionAction.ADDREPLICAPROP.toLower(),
@@ -444,10 +435,8 @@ public class TestCollectionAPI extends A
       verifyPropertyVal(client, COLLECTION_NAME, c1_s2_r1, "property.preferredleader", "true");
       verifyPropertyVal(client, COLLECTION_NAME, c1_s1_r1, "property.testprop", "nonsense");
       verifyPropertyVal(client, COLLECTION_NAME, c1_s1_r2, "property.prop", "silly");
-      verifyPropertyNotPresent(client, COLLECTION_NAME, c1_s1_r1, "property.preferredleader");
-      verifyPropertyNotPresent(client, COLLECTION_NAME, c1_s2_r2, "property.preferredleader");
-      verifyPropertyNotPresent(client, COLLECTION_NAME1, c2_s1_r1, "property.preferredleader");
-      verifyPropertyNotPresent(client, COLLECTION_NAME1, c2_s1_r1, "property.preferredleader");
+      verifyUniquePropertyWithinCollection(client, COLLECTION_NAME, "property.preferredLeader");
+      verifyUniquePropertyWithinCollection(client, COLLECTION_NAME1, "property.preferredLeader");
 
 
       doPropertyAction(client,
@@ -463,10 +452,8 @@ public class TestCollectionAPI extends A
       verifyPropertyVal(client, COLLECTION_NAME, c1_s2_r1, "property.preferredleader", "true");
       verifyPropertyVal(client, COLLECTION_NAME, c1_s1_r1, "property.testprop", "true");
       verifyPropertyVal(client, COLLECTION_NAME, c1_s1_r2, "property.prop", "silly");
-      verifyPropertyNotPresent(client, COLLECTION_NAME, c1_s1_r1, "property.preferredleader");
-      verifyPropertyNotPresent(client, COLLECTION_NAME, c1_s2_r2, "property.preferredleader");
-      verifyPropertyNotPresent(client, COLLECTION_NAME1, c2_s1_r1, "property.preferredleader");
-      verifyPropertyNotPresent(client, COLLECTION_NAME1, c2_s1_r1, "property.preferredleader");
+      verifyUniquePropertyWithinCollection(client, COLLECTION_NAME, "property.preferredLeader");
+      verifyUniquePropertyWithinCollection(client, COLLECTION_NAME1, "property.preferredLeader");
 
       doPropertyAction(client,
           "action", CollectionParams.CollectionAction.DELETEREPLICAPROP.toLower(),
@@ -479,10 +466,8 @@ public class TestCollectionAPI extends A
       verifyPropertyVal(client, COLLECTION_NAME, c1_s2_r1, "property.preferredleader", "true");
       verifyPropertyNotPresent(client, COLLECTION_NAME, c1_s1_r1, "property.testprop");
       verifyPropertyVal(client, COLLECTION_NAME, c1_s1_r2, "property.prop", "silly");
-      verifyPropertyNotPresent(client, COLLECTION_NAME, c1_s1_r1, "property.preferredleader");
-      verifyPropertyNotPresent(client, COLLECTION_NAME, c1_s2_r2, "property.preferredleader");
-      verifyPropertyNotPresent(client, COLLECTION_NAME1, c2_s1_r1, "property.preferredleader");
-      verifyPropertyNotPresent(client, COLLECTION_NAME1, c2_s1_r1, "property.preferredleader");
+      verifyUniquePropertyWithinCollection(client, COLLECTION_NAME, "property.preferredLeader");
+      verifyUniquePropertyWithinCollection(client, COLLECTION_NAME1, "property.preferredLeader");
 
       try {
         doPropertyAction(client,
@@ -503,10 +488,8 @@ public class TestCollectionAPI extends A
       verifyPropertyVal(client, COLLECTION_NAME, c1_s2_r1, "property.preferredleader", "true");
       verifyPropertyNotPresent(client, COLLECTION_NAME, c1_s1_r1, "property.testprop");
       verifyPropertyVal(client, COLLECTION_NAME, c1_s1_r2, "property.prop", "silly");
-      verifyPropertyNotPresent(client, COLLECTION_NAME, c1_s1_r1, "property.preferredleader");
-      verifyPropertyNotPresent(client, COLLECTION_NAME, c1_s2_r2, "property.preferredleader");
-      verifyPropertyNotPresent(client, COLLECTION_NAME1, c2_s1_r1, "property.preferredleader");
-      verifyPropertyNotPresent(client, COLLECTION_NAME1, c2_s1_r1, "property.preferredleader");
+      verifyUniquePropertyWithinCollection(client, COLLECTION_NAME, "property.preferredLeader");
+      verifyUniquePropertyWithinCollection(client, COLLECTION_NAME1, "property.preferredLeader");
 
       Map<String, String> origProps = getProps(client, COLLECTION_NAME, c1_s1_r1,
           "state", "core", "node_name", "base_url");
@@ -592,68 +575,11 @@ public class TestCollectionAPI extends A
     }
   }
 
-  private void doPropertyAction(CloudSolrServer client, String... paramsIn) throws IOException, SolrServerException {
-    assertTrue("paramsIn must be an even multiple of 2, it is: " + paramsIn.length, (paramsIn.length % 2) == 0);
-    ModifiableSolrParams params = new ModifiableSolrParams();
-    for (int idx = 0; idx < paramsIn.length; idx += 2) {
-      params.set(paramsIn[idx], paramsIn[idx + 1]);
-    }
-    QueryRequest request = new QueryRequest(params);
-    request.setPath("/admin/collections");
-    client.request(request);
-
-  }
 
-  private void verifyPropertyNotPresent(CloudSolrServer client, String collectionName, String replicaName,
-                                        String property)
+  // Expects the map will have keys, but blank values.
+  private Map<String, String> getProps(CloudSolrServer client, String collectionName, String replicaName, String... props)
       throws KeeperException, InterruptedException {
-    ClusterState clusterState = null;
-    Replica replica = null;
-    for (int idx = 0; idx < 300; ++idx) {
-      client.getZkStateReader().updateClusterState(true);
-      clusterState = client.getZkStateReader().getClusterState();
-      replica = clusterState.getReplica(collectionName, replicaName);
-      if (replica == null) {
-        fail("Could not find collection/replica pair! " + collectionName + "/" + replicaName);
-      }
-      if (StringUtils.isBlank(replica.getStr(property))) return;
-      Thread.sleep(100);
-    }
-    fail("Property " + property + " not set correctly for collection/replica pair: " +
-        collectionName + "/" + replicaName + ". Replica props: " + replica.getProperties().toString() +
-        ". Cluster state is " + clusterState.toString());
-
-  }
 
-  // The params are triplets,
-  // collection
-  // shard
-  // replica
-  private void verifyPropertyVal(CloudSolrServer client, String collectionName,
-                                 String replicaName, String property, String val)
-      throws InterruptedException, KeeperException {
-    Replica replica = null;
-    ClusterState clusterState = null;
-
-    for (int idx = 0; idx < 300; ++idx) { // Keep trying while Overseer writes the ZK state for up to 30 seconds.
-      client.getZkStateReader().updateClusterState(true);
-      clusterState = client.getZkStateReader().getClusterState();
-      replica = clusterState.getReplica(collectionName, replicaName);
-      if (replica == null) {
-        fail("Could not find collection/replica pair! " + collectionName + "/" + replicaName);
-      }
-      if (StringUtils.equals(val, replica.getStr(property))) return;
-      Thread.sleep(100);
-    }
-
-    fail("Property '" + property + "' with value " + replica.getStr(property) +
-        " not set correctly for collection/replica pair: " + collectionName + "/" + replicaName + " property map is " +
-        replica.getProperties().toString() + ".");
-
-  }
-
-  // Expects the map will have keys, but blank values.
-  private Map<String, String> getProps(CloudSolrServer client, String collectionName, String replicaName, String... props) throws KeeperException, InterruptedException {
     client.getZkStateReader().updateClusterState(true);
     ClusterState clusterState = client.getZkStateReader().getClusterState();
     Replica replica = clusterState.getReplica(collectionName, replicaName);

Modified: lucene/dev/branches/branch_5x/solr/solrj/src/java/org/apache/solr/common/params/CollectionParams.java
URL: http://svn.apache.org/viewvc/lucene/dev/branches/branch_5x/solr/solrj/src/java/org/apache/solr/common/params/CollectionParams.java?rev=1630191&r1=1630190&r2=1630191&view=diff
==============================================================================
--- lucene/dev/branches/branch_5x/solr/solrj/src/java/org/apache/solr/common/params/CollectionParams.java (original)
+++ lucene/dev/branches/branch_5x/solr/solrj/src/java/org/apache/solr/common/params/CollectionParams.java Wed Oct  8 18:13:15 2014
@@ -48,7 +48,8 @@ public interface CollectionParams 
     LIST,
     CLUSTERSTATUS,
     ADDREPLICAPROP,
-    DELETEREPLICAPROP;
+    DELETEREPLICAPROP,
+    BALANCESLICEUNIQUE;
     
     public static CollectionAction get( String p )
     {