lucene-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From a.@apache.org
Subject [lucene-solr] branch jira/solr-15055-2 updated: SOLR-15055: API refactoring + extensions to support vetoing different types of placement modification requests.
Date Wed, 13 Jan 2021 11:48:09 GMT
This is an automated email from the ASF dual-hosted git repository.

ab pushed a commit to branch jira/solr-15055-2
in repository https://gitbox.apache.org/repos/asf/lucene-solr.git


The following commit(s) were added to refs/heads/jira/solr-15055-2 by this push:
     new 57c2db3  SOLR-15055: API refactoring + extensions to support vetoing different types of placement modification requests.
57c2db3 is described below

commit 57c2db331f41a5954f7ce0b3262d1044b3a4eb35
Author: Andrzej Bialecki <ab@apache.org>
AuthorDate: Wed Jan 13 12:47:17 2021 +0100

    SOLR-15055: API refactoring + extensions to support vetoing different types of
    placement modification requests.
---
 .../solr/cloud/api/collections/DeleteNodeCmd.java  |  3 +-
 .../cloud/api/collections/DeleteReplicaCmd.java    | 76 ++++++++++++++++----
 .../cluster/placement/DeleteReplicasRequest.java   | 29 ++++++++
 .../cluster/placement/DeleteShardsRequest.java     | 27 +++++++
 .../cluster/placement/ModificationRequest.java     | 30 ++++++++
 .../solr/cluster/placement/PlacementContext.java   | 44 ++++++++++++
 .../solr/cluster/placement/PlacementPlugin.java    | 15 ++--
 .../solr/cluster/placement/PlacementRequest.java   |  7 +-
 .../placement/impl/ModificationRequestImpl.java    | 82 ++++++++++++++++++++++
 .../placement/impl/PlacementContextImpl.java       | 39 ++++++++++
 .../impl/PlacementPluginAssignStrategy.java        | 12 ++--
 .../impl/SimpleClusterAbstractionsImpl.java        |  6 +-
 .../plugins/AffinityPlacementFactory.java          | 23 ++++--
 .../plugins/MinimizeCoresPlacementFactory.java     | 16 +++--
 .../placement/plugins/RandomPlacementFactory.java  | 17 +++--
 .../apache/solr/cluster/placement/Builders.java    | 24 +++++++
 .../plugins/AffinityPlacementFactoryTest.java      | 63 ++++++++---------
 17 files changed, 420 insertions(+), 93 deletions(-)

diff --git a/solr/core/src/java/org/apache/solr/cloud/api/collections/DeleteNodeCmd.java b/solr/core/src/java/org/apache/solr/cloud/api/collections/DeleteNodeCmd.java
index 19865d3..c69675b 100644
--- a/solr/core/src/java/org/apache/solr/cloud/api/collections/DeleteNodeCmd.java
+++ b/solr/core/src/java/org/apache/solr/cloud/api/collections/DeleteNodeCmd.java
@@ -18,6 +18,7 @@
 package org.apache.solr.cloud.api.collections;
 
 
+import java.io.IOException;
 import java.lang.invoke.MethodHandles;
 import java.util.ArrayList;
 import java.util.List;
@@ -98,7 +99,7 @@ public class DeleteNodeCmd implements OverseerCollectionMessageHandler.Cmd {
                               List<ZkNodeProps> sourceReplicas,
                               OverseerCollectionMessageHandler ocmh,
                               String node,
-                              String async) throws InterruptedException {
+                              String async) throws IOException, InterruptedException {
     CountDownLatch cleanupLatch = new CountDownLatch(sourceReplicas.size());
     for (ZkNodeProps sourceReplica : sourceReplicas) {
       String coll = sourceReplica.getStr(COLLECTION_PROP);
diff --git a/solr/core/src/java/org/apache/solr/cloud/api/collections/DeleteReplicaCmd.java b/solr/core/src/java/org/apache/solr/cloud/api/collections/DeleteReplicaCmd.java
index 4d7975d..93ababd 100644
--- a/solr/core/src/java/org/apache/solr/cloud/api/collections/DeleteReplicaCmd.java
+++ b/solr/core/src/java/org/apache/solr/cloud/api/collections/DeleteReplicaCmd.java
@@ -23,6 +23,7 @@ import static org.apache.solr.common.params.CollectionAdminParams.COUNT_PROP;
 import static org.apache.solr.common.params.CollectionAdminParams.FOLLOW_ALIASES;
 import static org.apache.solr.common.params.CommonAdminParams.ASYNC;
 
+import java.io.IOException;
 import java.lang.invoke.MethodHandles;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -34,6 +35,14 @@ import java.util.concurrent.Callable;
 
 import org.apache.solr.cloud.api.collections.OverseerCollectionMessageHandler.Cmd;
 import org.apache.solr.cloud.api.collections.OverseerCollectionMessageHandler.ShardRequestTracker;
+import org.apache.solr.cluster.placement.AttributeFetcher;
+import org.apache.solr.cluster.placement.DeleteReplicasRequest;
+import org.apache.solr.cluster.placement.PlacementContext;
+import org.apache.solr.cluster.placement.PlacementException;
+import org.apache.solr.cluster.placement.PlacementPlugin;
+import org.apache.solr.cluster.placement.impl.AttributeFetcherImpl;
+import org.apache.solr.cluster.placement.impl.ModificationRequestImpl;
+import org.apache.solr.cluster.placement.impl.PlacementContextImpl;
 import org.apache.solr.common.SolrException;
 import org.apache.solr.common.cloud.ClusterState;
 import org.apache.solr.common.cloud.DocCollection;
@@ -69,15 +78,22 @@ public class DeleteReplicaCmd implements Cmd {
 
   @SuppressWarnings("unchecked")
   void deleteReplica(ClusterState clusterState, ZkNodeProps message, @SuppressWarnings({"rawtypes"})NamedList results, Runnable onComplete)
-          throws KeeperException, InterruptedException {
+          throws KeeperException, IOException, InterruptedException {
     if (log.isDebugEnabled()) {
       log.debug("deleteReplica() : {}", Utils.toJSONString(message));
     }
     boolean parallel = message.getBool("parallel", false);
 
+    PlacementPlugin placementPlugin = ocmh.overseer.getCoreContainer().getPlacementPluginFactory().createPluginInstance();
+    PlacementContext placementContext = new PlacementContextImpl(ocmh.cloudManager);
     //If a count is specified the strategy needs be different
     if (message.getStr(COUNT_PROP) != null) {
-      deleteReplicaBasedOnCount(clusterState, message, results, onComplete, parallel);
+      try {
+        deleteReplicaBasedOnCount(clusterState, message, results, onComplete, parallel, placementPlugin, placementContext);
+      } catch (PlacementException pe) {
+        throw new SolrException(SolrException.ErrorCode.BAD_REQUEST,
+            "Delete replica(s) rejected by replica placement plugin: " + pe.getMessage(), pe);
+      }
       return;
     }
 
@@ -102,7 +118,12 @@ public class DeleteReplicaCmd implements Cmd {
               "Invalid shard name : " +  shard + " in collection : " +  collectionName);
     }
 
-    deleteCore(slice, collectionName, replicaName, message, shard, results, onComplete,  parallel);
+    try {
+      deleteCore(coll, shard, replicaName, message, results, onComplete, parallel, placementPlugin, placementContext);
+    } catch (PlacementException pe) {
+      throw new SolrException(SolrException.ErrorCode.BAD_REQUEST,
+          "Delete replica rejected by replica placement plugin: " + pe.getMessage(), pe);
+    }
 
   }
 
@@ -116,8 +137,10 @@ public class DeleteReplicaCmd implements Cmd {
                                  ZkNodeProps message,
                                  @SuppressWarnings({"rawtypes"})NamedList results,
                                  Runnable onComplete,
-                                 boolean parallel)
-          throws KeeperException, InterruptedException {
+                                 boolean parallel,
+                                 PlacementPlugin placementPlugin,
+                                 PlacementContext placementContext)
+          throws KeeperException, PlacementException, InterruptedException {
     ocmh.checkRequired(message, COLLECTION_PROP, COUNT_PROP);
     int count = Integer.parseInt(message.getStr(COUNT_PROP));
     String collectionName = message.getStr(COLLECTION_PROP);
@@ -147,6 +170,16 @@ public class DeleteReplicaCmd implements Cmd {
       }
     }
 
+    // verify that all replicas can be deleted
+    for (Map.Entry<Slice, Set<String>> entry : shardToReplicasMapping.entrySet()) {
+      Slice shardSlice = entry.getKey();
+      String shardId = shardSlice.getName();
+      Set<String> replicas = entry.getValue();
+      //verify all replicas
+      DeleteReplicasRequest deleteReplicasRequest = ModificationRequestImpl.deleteReplicasRequest(coll, shardId, replicas);
+      placementPlugin.verifyAllowedModification(deleteReplicasRequest, placementContext);
+    }
+
     for (Map.Entry<Slice, Set<String>> entry : shardToReplicasMapping.entrySet()) {
       Slice shardSlice = entry.getKey();
       String shardId = shardSlice.getName();
@@ -154,7 +187,8 @@ public class DeleteReplicaCmd implements Cmd {
       //callDeleteReplica on all replicas
       for (String replica: replicas) {
         log.debug("Deleting replica {}  for shard {} based on count {}", replica, shardId, count);
-        deleteCore(shardSlice, collectionName, replica, message, shard, results, onComplete, parallel);
+        // don't verify with the placement plugin - we already did it
+        deleteCore(coll, shardId, replica, message, results, onComplete, parallel, null, null);
       }
       results.add("shard_id", shardId);
       results.add("replicas_deleted", replicas);
@@ -212,25 +246,39 @@ public class DeleteReplicaCmd implements Cmd {
   }
 
   @SuppressWarnings({"unchecked"})
-  void deleteCore(Slice slice, String collectionName, String replicaName,ZkNodeProps message, String shard, @SuppressWarnings({"rawtypes"})NamedList results, Runnable onComplete, boolean parallel) throws KeeperException, InterruptedException {
-
+  void deleteCore(DocCollection coll,
+                  String shardId,
+                  String replicaName,
+                  ZkNodeProps message,
+                  @SuppressWarnings({"rawtypes"})NamedList results,
+                  Runnable onComplete,
+                  boolean parallel,
+                  PlacementPlugin placementPlugin,
+                  PlacementContext placementContext) throws KeeperException, PlacementException, InterruptedException {
+
+    Slice slice = coll.getSlice(shardId);
     Replica replica = slice.getReplica(replicaName);
     if (replica == null) {
       ArrayList<String> l = new ArrayList<>();
       for (Replica r : slice.getReplicas())
         l.add(r.getName());
       throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Invalid replica : " +  replicaName + " in shard/collection : " +
-              shard  + "/" + collectionName + " available replicas are " +  StrUtils.join(l, ','));
+              shardId  + "/" + coll.getName() + " available replicas are " +  StrUtils.join(l, ','));
     }
 
     // If users are being safe and only want to remove a shard if it is down, they can specify onlyIfDown=true
     // on the command.
     if (Boolean.parseBoolean(message.getStr(OverseerCollectionMessageHandler.ONLY_IF_DOWN)) && replica.getState() != Replica.State.DOWN) {
       throw new SolrException(SolrException.ErrorCode.BAD_REQUEST,
-              "Attempted to remove replica : " + collectionName + "/"  + shard + "/" + replicaName +
+              "Attempted to remove replica : " + coll.getName() + "/"  + shardId + "/" + replicaName +
               " with onlyIfDown='true', but state is '" + replica.getStr(ZkStateReader.STATE_PROP) + "'");
     }
 
+    if (placementPlugin != null) {
+      // verify that we are allowed to delete this replica
+      DeleteReplicasRequest deleteReplicasRequest = ModificationRequestImpl.deleteReplicasRequest(coll, shardId, Set.of(replicaName));
+      placementPlugin.verifyAllowedModification(deleteReplicasRequest, placementContext);
+    }
     ShardHandler shardHandler = ocmh.shardHandlerFactory.getShardHandler();
     String core = replica.getStr(ZkStateReader.CORE_NAME_PROP);
     String asyncId = message.getStr(ASYNC);
@@ -256,12 +304,12 @@ public class DeleteReplicaCmd implements Cmd {
           shardRequestTracker.processResponses(results, shardHandler, false, null);
 
           //check if the core unload removed the corenode zk entry
-          if (ocmh.waitForCoreNodeGone(collectionName, shard, replicaName, 30000)) return Boolean.TRUE;
+          if (ocmh.waitForCoreNodeGone(coll.getName(), shardId, replicaName, 30000)) return Boolean.TRUE;
         }
 
         // try and ensure core info is removed from cluster state
-        ocmh.deleteCoreNode(collectionName, replicaName, replica, core);
-        if (ocmh.waitForCoreNodeGone(collectionName, shard, replicaName, 30000)) return Boolean.TRUE;
+        ocmh.deleteCoreNode(coll.getName(), replicaName, replica, core);
+        if (ocmh.waitForCoreNodeGone(coll.getName(), shardId, replicaName, 30000)) return Boolean.TRUE;
         return Boolean.FALSE;
       } catch (Exception e) {
         results.add("failure", "Could not complete delete " + e.getMessage());
@@ -275,7 +323,7 @@ public class DeleteReplicaCmd implements Cmd {
       try {
         if (!callable.call())
           throw new SolrException(SolrException.ErrorCode.SERVER_ERROR,
-                  "Could not remove replica : " + collectionName + "/" + shard + "/" + replicaName);
+                  "Could not remove replica : " + coll.getName() + "/" + shardId + "/" + replicaName);
       } catch (InterruptedException | KeeperException e) {
         throw e;
       } catch (Exception ex) {
diff --git a/solr/core/src/java/org/apache/solr/cluster/placement/DeleteReplicasRequest.java b/solr/core/src/java/org/apache/solr/cluster/placement/DeleteReplicasRequest.java
new file mode 100644
index 0000000..09f5762
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/cluster/placement/DeleteReplicasRequest.java
@@ -0,0 +1,29 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.solr.cluster.placement;
+
+import org.apache.solr.cluster.Replica;
+
+import java.util.Set;
+
+/**
+ *
+ */
+public interface DeleteReplicasRequest extends ModificationRequest {
+  Set<Replica> getReplicas();
+}
diff --git a/solr/core/src/java/org/apache/solr/cluster/placement/DeleteShardsRequest.java b/solr/core/src/java/org/apache/solr/cluster/placement/DeleteShardsRequest.java
new file mode 100644
index 0000000..5bff54f
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/cluster/placement/DeleteShardsRequest.java
@@ -0,0 +1,27 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.solr.cluster.placement;
+
+import java.util.Set;
+
+/**
+ *
+ */
+public interface DeleteShardsRequest extends ModificationRequest {
+  Set<String> getShardNames();
+}
diff --git a/solr/core/src/java/org/apache/solr/cluster/placement/ModificationRequest.java b/solr/core/src/java/org/apache/solr/cluster/placement/ModificationRequest.java
new file mode 100644
index 0000000..964d9bf
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/cluster/placement/ModificationRequest.java
@@ -0,0 +1,30 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.solr.cluster.placement;
+
+import org.apache.solr.cluster.SolrCollection;
+
+/**
+ *
+ */
+public interface ModificationRequest {
+  /**
+   * The {@link SolrCollection} to modify.
+   */
+  SolrCollection getCollection();
+}
diff --git a/solr/core/src/java/org/apache/solr/cluster/placement/PlacementContext.java b/solr/core/src/java/org/apache/solr/cluster/placement/PlacementContext.java
new file mode 100644
index 0000000..89deecd
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/cluster/placement/PlacementContext.java
@@ -0,0 +1,44 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.solr.cluster.placement;
+
+import org.apache.solr.cluster.Cluster;
+
+/**
+ *
+ */
+public interface PlacementContext {
+  /**
+   * Initial state of the cluster. Note there are {@link java.util.Set}'s and {@link java.util.Map}'s
+   * accessible from the {@link Cluster} and other reachable instances. These collection will not change
+   * while the plugin is executing and will be thrown away once the plugin is done. The plugin code can
+   * therefore modify them if needed.
+   */
+  Cluster getCluster();
+
+  /**
+   * Factory used by the plugin to fetch additional attributes from the cluster nodes, such as
+   * count of cores, system properties etc..
+   */
+  AttributeFetcher getAttributeFetcher();
+
+  /**
+   * Factory used to create instances of {@link PlacementPlan} to return computed decision.
+   */
+  PlacementPlanFactory getPlacementPlanFactory();
+}
diff --git a/solr/core/src/java/org/apache/solr/cluster/placement/PlacementPlugin.java b/solr/core/src/java/org/apache/solr/cluster/placement/PlacementPlugin.java
index bbb52cb..c1e4c35 100644
--- a/solr/core/src/java/org/apache/solr/cluster/placement/PlacementPlugin.java
+++ b/solr/core/src/java/org/apache/solr/cluster/placement/PlacementPlugin.java
@@ -17,8 +17,6 @@
 
 package org.apache.solr.cluster.placement;
 
-import org.apache.solr.cluster.Cluster;
-
 /**
  * <p>Implemented by external plugins to control replica placement and movement on the search cluster (as well as other things
  * such as cluster elasticity?) when cluster changes are required (initiated elsewhere, most likely following a Collection
@@ -36,16 +34,11 @@ public interface PlacementPlugin {
    *
    * <p>Configuration is passed upon creation of a new instance of this class by {@link PlacementPluginFactory#createPluginInstance}.
    *
-   * @param cluster              initial state of the cluster. Note there are {@link java.util.Set}'s and {@link java.util.Map}'s
-   *                             accessible from the {@link Cluster} and other reachable instances. These collection will not change
-   *                             while the plugin is executing and will be thrown away once the plugin is done. The plugin code can
-   *                             therefore modify them if needed.
    * @param placementRequest     request for placing new replicas or moving existing replicas on the cluster.
-   * @param attributeFetcher     Factory used by the plugin to fetch additional attributes from the cluster nodes, such as
-   *                             count of coresm ssytem properties etc..
-   * @param placementPlanFactory Factory used to create instances of {@link PlacementPlan} to return computed decision.
    * @return plan satisfying the placement request.
    */
-  PlacementPlan computePlacement(Cluster cluster, PlacementRequest placementRequest, AttributeFetcher attributeFetcher,
-                                 PlacementPlanFactory placementPlanFactory) throws PlacementException, InterruptedException;
+  PlacementPlan computePlacement(PlacementRequest placementRequest, PlacementContext placementContext) throws PlacementException, InterruptedException;
+
+  void verifyAllowedModification(ModificationRequest modificationRequest, PlacementContext placementContext)
+    throws PlacementException, InterruptedException;
 }
diff --git a/solr/core/src/java/org/apache/solr/cluster/placement/PlacementRequest.java b/solr/core/src/java/org/apache/solr/cluster/placement/PlacementRequest.java
index 44222a2..0ece962 100644
--- a/solr/core/src/java/org/apache/solr/cluster/placement/PlacementRequest.java
+++ b/solr/core/src/java/org/apache/solr/cluster/placement/PlacementRequest.java
@@ -30,12 +30,7 @@ import java.util.Set;
  * <p>The set of {@link Node}s on which the replicas should be placed
  * is specified (defaults to being equal to the set returned by {@link Cluster#getLiveNodes()}).
  */
-public interface PlacementRequest {
-  /**
-   * The {@link SolrCollection} to add {@link Replica}(s) to.
-   */
-  SolrCollection getCollection();
-
+public interface PlacementRequest extends ModificationRequest {
   /**
    * <p>Shard name(s) for which new replicas placement should be computed. The shard(s) might exist or not (that's why this
    * method returns a {@link Set} of {@link String}'s and not directly a set of {@link Shard} instances).
diff --git a/solr/core/src/java/org/apache/solr/cluster/placement/impl/ModificationRequestImpl.java b/solr/core/src/java/org/apache/solr/cluster/placement/impl/ModificationRequestImpl.java
new file mode 100644
index 0000000..5161869
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/cluster/placement/impl/ModificationRequestImpl.java
@@ -0,0 +1,82 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.solr.cluster.placement.impl;
+
+import org.apache.solr.cluster.Replica;
+import org.apache.solr.cluster.Shard;
+import org.apache.solr.cluster.SolrCollection;
+import org.apache.solr.cluster.placement.DeleteReplicasRequest;
+import org.apache.solr.cluster.placement.DeleteShardsRequest;
+import org.apache.solr.common.cloud.DocCollection;
+import org.apache.solr.common.cloud.Slice;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ *
+ */
+public class ModificationRequestImpl {
+
+  public static DeleteReplicasRequest deleteReplicasRequest(SolrCollection collection, Set<Replica> replicas) {
+    return new DeleteReplicasRequest() {
+      @Override
+      public Set<Replica> getReplicas() {
+        return replicas;
+      }
+
+      @Override
+      public SolrCollection getCollection() {
+        return collection;
+      }
+    };
+  }
+
+  public static DeleteReplicasRequest deleteReplicasRequest(DocCollection docCollection, String shardName, Set<String> replicaNames) {
+    SolrCollection solrCollection = SimpleClusterAbstractionsImpl.SolrCollectionImpl.fromDocCollection(docCollection);
+    Shard shard = solrCollection.getShard(shardName);
+    Slice slice = docCollection.getSlice(shardName);
+    Set<Replica> solrReplicas = new HashSet<>();
+    replicaNames.forEach(name -> {
+      org.apache.solr.common.cloud.Replica replica = slice.getReplica(name);
+      Replica solrReplica = new SimpleClusterAbstractionsImpl.ReplicaImpl(replica.getName(), shard, replica);
+      solrReplicas.add(solrReplica);
+    });
+    return deleteReplicasRequest(solrCollection, solrReplicas);
+  }
+
+
+  public static DeleteShardsRequest deleteShardsRequest(SolrCollection collection, Set<String> shardNames) {
+    return new DeleteShardsRequest() {
+      @Override
+      public Set<String> getShardNames() {
+        return shardNames;
+      }
+
+      @Override
+      public SolrCollection getCollection() {
+        return collection;
+      }
+    };
+  }
+
+  public static DeleteShardsRequest deleteShardsRequest(DocCollection docCollection, Set<String> shardNames) {
+    SolrCollection solrCollection = SimpleClusterAbstractionsImpl.SolrCollectionImpl.fromDocCollection(docCollection);
+    return deleteShardsRequest(solrCollection, shardNames);
+  }
+}
diff --git a/solr/core/src/java/org/apache/solr/cluster/placement/impl/PlacementContextImpl.java b/solr/core/src/java/org/apache/solr/cluster/placement/impl/PlacementContextImpl.java
new file mode 100644
index 0000000..eed649b
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/cluster/placement/impl/PlacementContextImpl.java
@@ -0,0 +1,39 @@
+package org.apache.solr.cluster.placement.impl;
+
+import org.apache.solr.client.solrj.cloud.SolrCloudManager;
+import org.apache.solr.cluster.Cluster;
+import org.apache.solr.cluster.placement.AttributeFetcher;
+import org.apache.solr.cluster.placement.PlacementContext;
+import org.apache.solr.cluster.placement.PlacementPlanFactory;
+
+import java.io.IOException;
+
+/**
+ *
+ */
+public class PlacementContextImpl implements PlacementContext {
+
+  private final Cluster cluster;
+  private final AttributeFetcher attributeFetcher;
+  private final PlacementPlanFactory placementPlanFactory = new PlacementPlanFactoryImpl();
+
+  public PlacementContextImpl(SolrCloudManager solrCloudManager) throws IOException {
+    cluster = new SimpleClusterAbstractionsImpl.ClusterImpl(solrCloudManager);
+    attributeFetcher = new AttributeFetcherImpl(solrCloudManager);
+  }
+
+  @Override
+  public Cluster getCluster() {
+    return cluster;
+  }
+
+  @Override
+  public AttributeFetcher getAttributeFetcher() {
+    return attributeFetcher;
+  }
+
+  @Override
+  public PlacementPlanFactory getPlacementPlanFactory() {
+    return placementPlanFactory;
+  }
+}
diff --git a/solr/core/src/java/org/apache/solr/cluster/placement/impl/PlacementPluginAssignStrategy.java b/solr/core/src/java/org/apache/solr/cluster/placement/impl/PlacementPluginAssignStrategy.java
index c4c5667..a226ab8 100644
--- a/solr/core/src/java/org/apache/solr/cluster/placement/impl/PlacementPluginAssignStrategy.java
+++ b/solr/core/src/java/org/apache/solr/cluster/placement/impl/PlacementPluginAssignStrategy.java
@@ -22,8 +22,8 @@ import java.util.List;
 
 import org.apache.solr.client.solrj.cloud.SolrCloudManager;
 import org.apache.solr.cloud.api.collections.Assign;
-import org.apache.solr.cluster.Cluster;
 import org.apache.solr.cluster.SolrCollection;
+import org.apache.solr.cluster.placement.PlacementContext;
 import org.apache.solr.cluster.placement.PlacementException;
 import org.apache.solr.cluster.placement.PlacementPlugin;
 import org.apache.solr.cluster.placement.PlacementPlan;
@@ -35,8 +35,6 @@ import org.apache.solr.common.cloud.ReplicaPosition;
  */
 public class PlacementPluginAssignStrategy implements Assign.AssignStrategy {
 
-  private static final PlacementPlanFactoryImpl PLACEMENT_PLAN_FACTORY = new PlacementPlanFactoryImpl();
-
   private final PlacementPlugin plugin;
   private final DocCollection collection;
 
@@ -53,14 +51,14 @@ public class PlacementPluginAssignStrategy implements Assign.AssignStrategy {
   public List<ReplicaPosition> assign(SolrCloudManager solrCloudManager, Assign.AssignRequest assignRequest)
       throws Assign.AssignmentException, IOException, InterruptedException {
 
-    Cluster cluster = new SimpleClusterAbstractionsImpl.ClusterImpl(solrCloudManager);
-    SolrCollection solrCollection = new SimpleClusterAbstractionsImpl.SolrCollectionImpl(collection);
+    PlacementContext placementContext = new PlacementContextImpl(solrCloudManager);
+    SolrCollection solrCollection = placementContext.getCluster().getCollection(collection.getName());
 
-    PlacementRequestImpl placementRequest = PlacementRequestImpl.toPlacementRequest(cluster, solrCollection, assignRequest);
+    PlacementRequestImpl placementRequest = PlacementRequestImpl.toPlacementRequest(placementContext.getCluster(), solrCollection, assignRequest);
 
     final PlacementPlan placementPlan;
     try {
-      placementPlan = plugin.computePlacement(cluster, placementRequest, new AttributeFetcherImpl(solrCloudManager), PLACEMENT_PLAN_FACTORY);
+      placementPlan = plugin.computePlacement(placementRequest, placementContext);
     } catch (PlacementException pe) {
       throw new Assign.AssignmentException(pe);
     }
diff --git a/solr/core/src/java/org/apache/solr/cluster/placement/impl/SimpleClusterAbstractionsImpl.java b/solr/core/src/java/org/apache/solr/cluster/placement/impl/SimpleClusterAbstractionsImpl.java
index e26a374..292fe5c 100644
--- a/solr/core/src/java/org/apache/solr/cluster/placement/impl/SimpleClusterAbstractionsImpl.java
+++ b/solr/core/src/java/org/apache/solr/cluster/placement/impl/SimpleClusterAbstractionsImpl.java
@@ -324,7 +324,7 @@ class SimpleClusterAbstractionsImpl {
       return new Pair<>(replicas, leader);
     }
 
-    private ReplicaImpl(String replicaName, Shard shard, org.apache.solr.common.cloud.Replica sliceReplica) {
+    ReplicaImpl(String replicaName, Shard shard, org.apache.solr.common.cloud.Replica sliceReplica) {
       this.replicaName = replicaName;
       this.coreName = sliceReplica.getCoreName();
       this.shard = shard;
@@ -334,7 +334,7 @@ class SimpleClusterAbstractionsImpl {
       this.node = new NodeImpl(sliceReplica.getNodeName());
     }
 
-    private Replica.ReplicaType translateType(org.apache.solr.common.cloud.Replica.Type type) {
+    Replica.ReplicaType translateType(org.apache.solr.common.cloud.Replica.Type type) {
       switch (type) {
         case NRT:
           return Replica.ReplicaType.NRT;
@@ -347,7 +347,7 @@ class SimpleClusterAbstractionsImpl {
       }
     }
 
-    private Replica.ReplicaState translateState(org.apache.solr.common.cloud.Replica.State state) {
+    Replica.ReplicaState translateState(org.apache.solr.common.cloud.Replica.State state) {
       switch (state) {
         case ACTIVE:
           return Replica.ReplicaState.ACTIVE;
diff --git a/solr/core/src/java/org/apache/solr/cluster/placement/plugins/AffinityPlacementFactory.java b/solr/core/src/java/org/apache/solr/cluster/placement/plugins/AffinityPlacementFactory.java
index ce79792..8a561c3 100644
--- a/solr/core/src/java/org/apache/solr/cluster/placement/plugins/AffinityPlacementFactory.java
+++ b/solr/core/src/java/org/apache/solr/cluster/placement/plugins/AffinityPlacementFactory.java
@@ -190,15 +190,16 @@ public class AffinityPlacementFactory implements PlacementPluginFactory<Affinity
       }
     }
 
+    @Override
     @SuppressForbidden(reason = "Ordering.arbitrary() has no equivalent in Comparator class. Rather reuse than copy.")
-    public PlacementPlan computePlacement(Cluster cluster, PlacementRequest request, AttributeFetcher attributeFetcher,
-                                          PlacementPlanFactory placementPlanFactory) throws PlacementException {
+    public PlacementPlan computePlacement(PlacementRequest request, PlacementContext placementContext) throws PlacementException {
       Set<Node> nodes = request.getTargetNodes();
       SolrCollection solrCollection = request.getCollection();
 
-      nodes = filterNodesWithCollection(cluster, request, nodes);
+      nodes = filterNodesWithCollection(placementContext.getCluster(), request, nodes);
 
       // Request all needed attributes
+      AttributeFetcher attributeFetcher = placementContext.getAttributeFetcher();
       attributeFetcher.requestNodeSystemProperty(AVAILABILITY_ZONE_SYSPROP).requestNodeSystemProperty(REPLICA_TYPE_SYSPROP);
       attributeFetcher
           .requestNodeMetric(NodeMetricImpl.NUM_CORES)
@@ -243,11 +244,23 @@ public class AffinityPlacementFactory implements PlacementPluginFactory<Affinity
         // failure. Current code does fail if placement is impossible (constraint is at most one replica of a shard on any node).
         for (Replica.ReplicaType replicaType : Replica.ReplicaType.values()) {
           makePlacementDecisions(solrCollection, shardName, availabilityZones, replicaType, request.getCountReplicasToCreate(replicaType),
-              attrValues, replicaTypeToNodes, nodesWithReplicas, coresOnNodes, placementPlanFactory, replicaPlacements);
+              attrValues, replicaTypeToNodes, nodesWithReplicas, coresOnNodes, placementContext.getPlacementPlanFactory(), replicaPlacements);
         }
       }
 
-      return placementPlanFactory.createPlacementPlan(request, replicaPlacements);
+      return placementContext.getPlacementPlanFactory().createPlacementPlan(request, replicaPlacements);
+    }
+
+    @Override
+    public void verifyAllowedModification(ModificationRequest modificationRequest, PlacementContext placementContext) throws PlacementException, InterruptedException {
+      if (modificationRequest instanceof DeleteShardsRequest) {
+        throw new UnsupportedOperationException("not implemented yet");
+      } else if (!(modificationRequest instanceof DeleteReplicasRequest)) {
+        throw new UnsupportedOperationException("unsupported request type " + modificationRequest.getClass().getName());
+      }
+      DeleteReplicasRequest request = (DeleteReplicasRequest) modificationRequest;
+
+      // XXX to be completed...
     }
 
     private Set<String> getZonesFromNodes(Set<Node> nodes, final AttributeValues attrValues) {
diff --git a/solr/core/src/java/org/apache/solr/cluster/placement/plugins/MinimizeCoresPlacementFactory.java b/solr/core/src/java/org/apache/solr/cluster/placement/plugins/MinimizeCoresPlacementFactory.java
index bb1e762..d8e1370 100644
--- a/solr/core/src/java/org/apache/solr/cluster/placement/plugins/MinimizeCoresPlacementFactory.java
+++ b/solr/core/src/java/org/apache/solr/cluster/placement/plugins/MinimizeCoresPlacementFactory.java
@@ -50,15 +50,15 @@ public class MinimizeCoresPlacementFactory implements PlacementPluginFactory<Pla
 
   static private class MinimizeCoresPlacementPlugin implements PlacementPlugin {
 
+    @Override
     @SuppressForbidden(reason = "Ordering.arbitrary() has no equivalent in Comparator class. Rather reuse than copy.")
-    public PlacementPlan computePlacement(Cluster cluster, PlacementRequest request, AttributeFetcher attributeFetcher,
-                                          PlacementPlanFactory placementPlanFactory) throws PlacementException {
+    public PlacementPlan computePlacement(PlacementRequest request, PlacementContext placementContext) throws PlacementException {
       int totalReplicasPerShard = 0;
       for (Replica.ReplicaType rt : Replica.ReplicaType.values()) {
         totalReplicasPerShard += request.getCountReplicasToCreate(rt);
       }
 
-      if (cluster.getLiveNodes().size() < totalReplicasPerShard) {
+      if (placementContext.getCluster().getLiveNodes().size() < totalReplicasPerShard) {
         throw new PlacementException("Cluster size too small for number of replicas per shard");
       }
 
@@ -67,6 +67,7 @@ public class MinimizeCoresPlacementFactory implements PlacementPluginFactory<Pla
 
       Set<Node> nodes = request.getTargetNodes();
 
+      AttributeFetcher attributeFetcher = placementContext.getAttributeFetcher();
       attributeFetcher.requestNodeMetric(NodeMetricImpl.NUM_CORES);
       attributeFetcher.fetchFrom(nodes);
       AttributeValues attrValues = attributeFetcher.fetchAttributes();
@@ -106,11 +107,16 @@ public class MinimizeCoresPlacementFactory implements PlacementPluginFactory<Pla
         }
 
         for (Replica.ReplicaType replicaType : Replica.ReplicaType.values()) {
-          placeReplicas(request.getCollection(), nodeEntriesToAssign, placementPlanFactory, replicaPlacements, shardName, request, replicaType);
+          placeReplicas(request.getCollection(), nodeEntriesToAssign, placementContext.getPlacementPlanFactory(), replicaPlacements, shardName, request, replicaType);
         }
       }
 
-      return placementPlanFactory.createPlacementPlan(request, replicaPlacements);
+      return placementContext.getPlacementPlanFactory().createPlacementPlan(request, replicaPlacements);
+    }
+
+    @Override
+    public void verifyAllowedModification(ModificationRequest modificationRequest, PlacementContext placementContext) throws PlacementException, InterruptedException {
+      // no-op
     }
 
     private void placeReplicas(SolrCollection solrCollection, ArrayList<Map.Entry<Integer, Node>> nodeEntriesToAssign,
diff --git a/solr/core/src/java/org/apache/solr/cluster/placement/plugins/RandomPlacementFactory.java b/solr/core/src/java/org/apache/solr/cluster/placement/plugins/RandomPlacementFactory.java
index 0b27d21..065353a 100644
--- a/solr/core/src/java/org/apache/solr/cluster/placement/plugins/RandomPlacementFactory.java
+++ b/solr/core/src/java/org/apache/solr/cluster/placement/plugins/RandomPlacementFactory.java
@@ -53,14 +53,14 @@ public class RandomPlacementFactory implements PlacementPluginFactory<PlacementP
       }
     }
 
-    public PlacementPlan computePlacement(Cluster cluster, PlacementRequest request, AttributeFetcher attributeFetcher,
-                                          PlacementPlanFactory placementPlanFactory) throws PlacementException {
+    @Override
+    public PlacementPlan computePlacement(PlacementRequest request, PlacementContext placementContext) throws PlacementException {
       int totalReplicasPerShard = 0;
       for (Replica.ReplicaType rt : Replica.ReplicaType.values()) {
         totalReplicasPerShard += request.getCountReplicasToCreate(rt);
       }
 
-      if (cluster.getLiveNodes().size() < totalReplicasPerShard) {
+      if (placementContext.getCluster().getLiveNodes().size() < totalReplicasPerShard) {
         throw new PlacementException("Cluster size too small for number of replicas per shard");
       }
 
@@ -69,15 +69,20 @@ public class RandomPlacementFactory implements PlacementPluginFactory<PlacementP
       // Now place randomly all replicas of all shards on available nodes
       for (String shardName : request.getShardNames()) {
         // Shuffle the nodes for each shard so that replicas for a shard are placed on distinct yet random nodes
-        ArrayList<Node> nodesToAssign = new ArrayList<>(cluster.getLiveNodes());
+        ArrayList<Node> nodesToAssign = new ArrayList<>(placementContext.getCluster().getLiveNodes());
         Collections.shuffle(nodesToAssign, replicaPlacementRandom);
 
         for (Replica.ReplicaType rt : Replica.ReplicaType.values()) {
-          placeForReplicaType(request.getCollection(), nodesToAssign, placementPlanFactory, replicaPlacements, shardName, request, rt);
+          placeForReplicaType(request.getCollection(), nodesToAssign, placementContext.getPlacementPlanFactory(), replicaPlacements, shardName, request, rt);
         }
       }
 
-      return placementPlanFactory.createPlacementPlan(request, replicaPlacements);
+      return placementContext.getPlacementPlanFactory().createPlacementPlan(request, replicaPlacements);
+    }
+
+    @Override
+    public void verifyAllowedModification(ModificationRequest modificationRequest, PlacementContext placementContext) throws PlacementException, InterruptedException {
+      // no-op
     }
 
     private void placeForReplicaType(SolrCollection solrCollection, ArrayList<Node> nodesToAssign, PlacementPlanFactory placementPlanFactory,
diff --git a/solr/core/src/test/org/apache/solr/cluster/placement/Builders.java b/solr/core/src/test/org/apache/solr/cluster/placement/Builders.java
index 21b8369..43de56e 100644
--- a/solr/core/src/test/org/apache/solr/cluster/placement/Builders.java
+++ b/solr/core/src/test/org/apache/solr/cluster/placement/Builders.java
@@ -22,6 +22,7 @@ import org.apache.solr.cluster.placement.impl.AttributeFetcherImpl;
 import org.apache.solr.cluster.placement.impl.AttributeValuesImpl;
 import org.apache.solr.cluster.placement.impl.CollectionMetricsBuilder;
 import org.apache.solr.cluster.placement.impl.NodeMetricImpl;
+import org.apache.solr.cluster.placement.impl.PlacementPlanFactoryImpl;
 import org.apache.solr.cluster.placement.impl.ReplicaMetricImpl;
 import org.apache.solr.common.util.Pair;
 import org.junit.Assert;
@@ -92,6 +93,29 @@ public class Builders {
       return clusterCollections;
     }
 
+    private static final PlacementPlanFactory PLACEMENT_PLAN_FACTORY = new PlacementPlanFactoryImpl();
+
+    public PlacementContext buildPlacementContext() {
+      Cluster cluster = build();
+      AttributeFetcher attributeFetcher = buildAttributeFetcher();
+      return new PlacementContext() {
+        @Override
+        public Cluster getCluster() {
+          return cluster;
+        }
+
+        @Override
+        public AttributeFetcher getAttributeFetcher() {
+          return attributeFetcher;
+        }
+
+        @Override
+        public PlacementPlanFactory getPlacementPlanFactory() {
+          return PLACEMENT_PLAN_FACTORY;
+        }
+      };
+    }
+
     public AttributeFetcher buildAttributeFetcher() {
       Map<String, Map<Node, String>> sysprops = new HashMap<>();
       Map<NodeMetric<?>, Map<Node, Object>> metrics = new HashMap<>();
diff --git a/solr/core/src/test/org/apache/solr/cluster/placement/plugins/AffinityPlacementFactoryTest.java b/solr/core/src/test/org/apache/solr/cluster/placement/plugins/AffinityPlacementFactoryTest.java
index 80c9b1d..c5cfe2e 100644
--- a/solr/core/src/test/org/apache/solr/cluster/placement/plugins/AffinityPlacementFactoryTest.java
+++ b/solr/core/src/test/org/apache/solr/cluster/placement/plugins/AffinityPlacementFactoryTest.java
@@ -98,8 +98,7 @@ public class AffinityPlacementFactoryTest extends SolrTestCaseJ4 {
       collectionBuilder.initializeShardsReplicas(1, 0, 0, 0, List.of());
     }
 
-    Cluster cluster = clusterBuilder.build();
-    AttributeFetcher attributeFetcher = clusterBuilder.buildAttributeFetcher();
+    PlacementContext placementContext = clusterBuilder.buildPlacementContext();
 
     SolrCollection solrCollection = collectionBuilder.build();
     List<Node> liveNodes = clusterBuilder.buildLiveNodes();
@@ -109,7 +108,7 @@ public class AffinityPlacementFactoryTest extends SolrTestCaseJ4 {
         Set.of(solrCollection.shards().iterator().next().getShardName()), new HashSet<>(liveNodes),
         1, 0, 0);
 
-    PlacementPlan pp = plugin.computePlacement(cluster, placementRequest, attributeFetcher, new PlacementPlanFactoryImpl());
+    PlacementPlan pp = plugin.computePlacement(placementRequest, placementContext);
 
     assertEquals(1, pp.getReplicaPlacements().size());
     ReplicaPlacement rp = pp.getReplicaPlacements().iterator().next();
@@ -149,7 +148,7 @@ public class AffinityPlacementFactoryTest extends SolrTestCaseJ4 {
     PlacementRequestImpl placementRequest = new PlacementRequestImpl(solrCollection, solrCollection.getShardNames(), new HashSet<>(liveNodes),
         2, 2, 2);
 
-    PlacementPlan pp = plugin.computePlacement(clusterBuilder.build(), placementRequest, clusterBuilder.buildAttributeFetcher(), new PlacementPlanFactoryImpl());
+    PlacementPlan pp = plugin.computePlacement(placementRequest, clusterBuilder.buildPlacementContext());
 
     assertEquals(18, pp.getReplicaPlacements().size()); // 3 shards, 6 replicas total each
     Set<Pair<String, Node>> placements = new HashSet<>();
@@ -162,7 +161,7 @@ public class AffinityPlacementFactoryTest extends SolrTestCaseJ4 {
     // Verify that if we ask for 7 replicas, the placement will use the low free space node
     placementRequest = new PlacementRequestImpl(solrCollection, solrCollection.getShardNames(), new HashSet<>(liveNodes),
         7, 0, 0);
-    pp = plugin.computePlacement(clusterBuilder.build(), placementRequest, clusterBuilder.buildAttributeFetcher(), new PlacementPlanFactoryImpl());
+    pp = plugin.computePlacement(placementRequest, clusterBuilder.buildPlacementContext());
     assertEquals(21, pp.getReplicaPlacements().size()); // 3 shards, 7 replicas each
     placements = new HashSet<>();
     for (ReplicaPlacement rp : pp.getReplicaPlacements()) {
@@ -175,7 +174,7 @@ public class AffinityPlacementFactoryTest extends SolrTestCaseJ4 {
     try {
       placementRequest = new PlacementRequestImpl(solrCollection, solrCollection.getShardNames(), new HashSet<>(liveNodes),
           8, 0, 0);
-      plugin.computePlacement(clusterBuilder.build(), placementRequest, clusterBuilder.buildAttributeFetcher(), new PlacementPlanFactoryImpl());
+      plugin.computePlacement(placementRequest, clusterBuilder.buildPlacementContext());
       fail("Placing 8 replicas should not be possible given only 7 nodes have enough space");
     } catch (PlacementException e) {
       // expected
@@ -219,7 +218,7 @@ public class AffinityPlacementFactoryTest extends SolrTestCaseJ4 {
     // The replicas must be placed on the most appropriate nodes, i.e. those that do not already have a replica for the
     // shard and then on the node with the lowest number of cores.
     // NRT are placed first and given the cluster state here the placement is deterministic (easier to test, only one good placement).
-    PlacementPlan pp = plugin.computePlacement(clusterBuilder.build(), placementRequest, clusterBuilder.buildAttributeFetcher(), new PlacementPlanFactoryImpl());
+    PlacementPlan pp = plugin.computePlacement(placementRequest, clusterBuilder.buildPlacementContext());
 
     // Each expected placement is represented as a string "shard replica-type node"
     Set<String> expectedPlacements = Set.of("1 NRT 1", "1 TLOG 2", "2 NRT 0", "2 TLOG 4");
@@ -317,7 +316,7 @@ public class AffinityPlacementFactoryTest extends SolrTestCaseJ4 {
     // Add 2 NRT and one TLOG to each shard.
     PlacementRequestImpl placementRequest = new PlacementRequestImpl(solrCollection, solrCollection.getShardNames(), new HashSet<>(liveNodes),
         2, 1, 0);
-    PlacementPlan pp = plugin.computePlacement(clusterBuilder.build(), placementRequest, clusterBuilder.buildAttributeFetcher(), new PlacementPlanFactoryImpl());
+    PlacementPlan pp = plugin.computePlacement(placementRequest, clusterBuilder.buildPlacementContext());
     // Shard 1: The NRT's should go to the med cores node on AZ2 and low core on az3 (even though
     // a low core node can take the replica in az1, there's already an NRT replica there and we want spreading across AZ's),
     // the TLOG to the TLOG node on AZ2 (because the tlog node on AZ1 has low free disk)
@@ -331,7 +330,7 @@ public class AffinityPlacementFactoryTest extends SolrTestCaseJ4 {
     // If we add instead 2 PULL replicas to each shard
     placementRequest = new PlacementRequestImpl(solrCollection, solrCollection.getShardNames(), new HashSet<>(liveNodes),
         0, 0, 2);
-    pp = plugin.computePlacement(clusterBuilder.build(), placementRequest, clusterBuilder.buildAttributeFetcher(), new PlacementPlanFactoryImpl());
+    pp = plugin.computePlacement(placementRequest, clusterBuilder.buildPlacementContext());
     // Shard 1: Given node AZ3_TLOGPULL is taken by the TLOG replica, the PULL should go to AZ1_TLOGPULL_LOWFREEDISK and AZ2_TLOGPULL
     // Shard 2: Similarly AZ2_TLOGPULL is taken. Replicas should go to AZ1_TLOGPULL_LOWFREEDISK and AZ3_TLOGPULL
     expectedPlacements = Set.of("1 PULL " + AZ1_TLOGPULL_LOWFREEDISK, "1 PULL " + AZ2_TLOGPULL,
@@ -378,7 +377,7 @@ public class AffinityPlacementFactoryTest extends SolrTestCaseJ4 {
     for (int countNrtToPlace = 1; countNrtToPlace <= 9; countNrtToPlace++) {
       PlacementRequestImpl placementRequest = new PlacementRequestImpl(solrCollection, solrCollection.getShardNames(), new HashSet<>(liveNodes),
           countNrtToPlace, 0, 0);
-      PlacementPlan pp = plugin.computePlacement(clusterBuilder.build(), placementRequest, clusterBuilder.buildAttributeFetcher(), new PlacementPlanFactoryImpl());
+      PlacementPlan pp = plugin.computePlacement(placementRequest, clusterBuilder.buildPlacementContext());
       verifyPlacements(placements.get(countNrtToPlace - 1), pp, collectionBuilder.getShardBuilders(), liveNodes);
     }
   }
@@ -414,7 +413,7 @@ public class AffinityPlacementFactoryTest extends SolrTestCaseJ4 {
     PlacementRequestImpl placementRequest = new PlacementRequestImpl(solrCollection, Set.of(solrCollection.iterator().next().getShardName()), new HashSet<>(liveNodes),
         0, 0, 1);
 
-    PlacementPlan pp = plugin.computePlacement(clusterBuilder.build(), placementRequest, clusterBuilder.buildAttributeFetcher(), new PlacementPlanFactoryImpl());
+    PlacementPlan pp = plugin.computePlacement(placementRequest, clusterBuilder.buildPlacementContext());
 
     // Each expected placement is represented as a string "shard replica-type node"
     // Node 0 has less cores than node 1 (0 vs 1) so the placement should go there.
@@ -427,7 +426,7 @@ public class AffinityPlacementFactoryTest extends SolrTestCaseJ4 {
     it.next(); // skip first shard to do placement for the second one...
     placementRequest = new PlacementRequestImpl(solrCollection, Set.of(it.next().getShardName()), new HashSet<>(liveNodes),
         0, 0, 1);
-    pp = plugin.computePlacement(clusterBuilder.build(), placementRequest, clusterBuilder.buildAttributeFetcher(), new PlacementPlanFactoryImpl());
+    pp = plugin.computePlacement(placementRequest, clusterBuilder.buildPlacementContext());
     expectedPlacements = Set.of("2 PULL 0");
     verifyPlacements(expectedPlacements, pp, collectionBuilder.getShardBuilders(), liveNodes);
   }
@@ -510,7 +509,8 @@ public class AffinityPlacementFactoryTest extends SolrTestCaseJ4 {
     collectionBuilder.initializeShardsReplicas(2, 0, 0, 0, clusterBuilder.getLiveNodeBuilders());
     clusterBuilder.addCollection(collectionBuilder);
 
-    Cluster cluster = clusterBuilder.build();
+    PlacementContext placementContext = clusterBuilder.buildPlacementContext();
+    Cluster cluster = placementContext.getCluster();
 
     SolrCollection solrCollection = cluster.getCollection(collectionName);
 
@@ -519,14 +519,12 @@ public class AffinityPlacementFactoryTest extends SolrTestCaseJ4 {
             .map(Shard::getShardName).collect(Collectors.toSet()),
         cluster.getLiveNodes(), 2, 2, 2);
 
-    PlacementPlanFactory placementPlanFactory = new PlacementPlanFactoryImpl();
-    AttributeFetcher attributeFetcher = clusterBuilder.buildAttributeFetcher();
-    PlacementPlan pp = plugin.computePlacement(cluster, placementRequest, attributeFetcher, placementPlanFactory);
+    PlacementPlan pp = plugin.computePlacement(placementRequest, placementContext);
     // 2 shards, 6 replicas
     assertEquals(12, pp.getReplicaPlacements().size());
     // shard -> AZ -> replica count
     Map<Replica.ReplicaType, Map<String, Map<String, AtomicInteger>>> replicas = new HashMap<>();
-    AttributeValues attributeValues = attributeFetcher.fetchAttributes();
+    AttributeValues attributeValues = placementContext.getAttributeFetcher().fetchAttributes();
     for (ReplicaPlacement rp : pp.getReplicaPlacements()) {
       Optional<String> azOptional = attributeValues.getSystemProperty(rp.getNode(), AffinityPlacementFactory.AVAILABILITY_ZONE_SYSPROP);
       if (!azOptional.isPresent()) {
@@ -570,7 +568,8 @@ public class AffinityPlacementFactoryTest extends SolrTestCaseJ4 {
     collectionBuilder.initializeShardsReplicas(2, 0, 0, 0, clusterBuilder.getLiveNodeBuilders());
     clusterBuilder.addCollection(collectionBuilder);
 
-    Cluster cluster = clusterBuilder.build();
+    PlacementContext placementContext = clusterBuilder.buildPlacementContext();
+    Cluster cluster = placementContext.getCluster();
 
     SolrCollection solrCollection = cluster.getCollection(collectionName);
 
@@ -579,14 +578,12 @@ public class AffinityPlacementFactoryTest extends SolrTestCaseJ4 {
             .map(Shard::getShardName).collect(Collectors.toSet()),
         cluster.getLiveNodes(), 2, 2, 2);
 
-    PlacementPlanFactory placementPlanFactory = new PlacementPlanFactoryImpl();
-    AttributeFetcher attributeFetcher = clusterBuilder.buildAttributeFetcher();
-    PlacementPlan pp = plugin.computePlacement(cluster, placementRequest, attributeFetcher, placementPlanFactory);
+    PlacementPlan pp = plugin.computePlacement(placementRequest, placementContext);
     // 2 shards, 6 replicas
     assertEquals(12, pp.getReplicaPlacements().size());
     // shard -> group -> replica count
     Map<Replica.ReplicaType, Map<String, Map<String, AtomicInteger>>> replicas = new HashMap<>();
-    AttributeValues attributeValues = attributeFetcher.fetchAttributes();
+    AttributeValues attributeValues = placementContext.getAttributeFetcher().fetchAttributes();
     for (ReplicaPlacement rp : pp.getReplicaPlacements()) {
       Optional<String> groupOptional = attributeValues.getSystemProperty(rp.getNode(), "group");
       if (!groupOptional.isPresent()) {
@@ -637,7 +634,8 @@ public class AffinityPlacementFactoryTest extends SolrTestCaseJ4 {
     collectionBuilder.initializeShardsReplicas(2, 0, 0, 0, clusterBuilder.getLiveNodeBuilders());
     clusterBuilder.addCollection(collectionBuilder);
 
-    Cluster cluster = clusterBuilder.build();
+    PlacementContext placementContext = clusterBuilder.buildPlacementContext();
+    Cluster cluster = placementContext.getCluster();
 
     SolrCollection solrCollection = cluster.getCollection(collectionName);
 
@@ -646,9 +644,7 @@ public class AffinityPlacementFactoryTest extends SolrTestCaseJ4 {
             .map(Shard::getShardName).collect(Collectors.toSet()),
         cluster.getLiveNodes(), 1, 0, 1);
 
-    PlacementPlanFactory placementPlanFactory = new PlacementPlanFactoryImpl();
-    AttributeFetcher attributeFetcher = clusterBuilder.buildAttributeFetcher();
-    PlacementPlan pp = plugin.computePlacement(cluster, placementRequest, attributeFetcher, placementPlanFactory);
+    PlacementPlan pp = plugin.computePlacement(placementRequest, placementContext);
     assertEquals(4, pp.getReplicaPlacements().size());
     for (ReplicaPlacement rp : pp.getReplicaPlacements()) {
       assertFalse("should not put any replicas on " + smallNode, rp.getNode().equals(smallNode));
@@ -667,7 +663,8 @@ public class AffinityPlacementFactoryTest extends SolrTestCaseJ4 {
     collectionBuilder.initializeShardsReplicas(0, 0, 0, 0, clusterBuilder.getLiveNodeBuilders());
     clusterBuilder.addCollection(collectionBuilder);
 
-    Cluster cluster = clusterBuilder.build();
+    PlacementContext placementContext = clusterBuilder.buildPlacementContext();
+    Cluster cluster = placementContext.getCluster();
 
     SolrCollection secondaryCollection = cluster.getCollection(secondaryCollectionName);
     SolrCollection primaryCollection = cluster.getCollection(primaryCollectionName);
@@ -679,9 +676,7 @@ public class AffinityPlacementFactoryTest extends SolrTestCaseJ4 {
       Set.of("shard1", "shard2"), cluster.getLiveNodes(), 1, 0, 0);
 
 
-    PlacementPlanFactory placementPlanFactory = new PlacementPlanFactoryImpl();
-    AttributeFetcher attributeFetcher = clusterBuilder.buildAttributeFetcher();
-    PlacementPlan pp = plugin.computePlacement(cluster, placementRequest, attributeFetcher, placementPlanFactory);
+    PlacementPlan pp = plugin.computePlacement(placementRequest, placementContext);
     assertEquals(2, pp.getReplicaPlacements().size());
     // verify that all placements are on nodes with the secondary replica
     pp.getReplicaPlacements().forEach(placement ->
@@ -691,7 +686,7 @@ public class AffinityPlacementFactoryTest extends SolrTestCaseJ4 {
     placementRequest = new PlacementRequestImpl(primaryCollection,
         Set.of("shard1"), cluster.getLiveNodes(), 3, 0, 0);
     try {
-      pp = plugin.computePlacement(cluster, placementRequest, attributeFetcher, placementPlanFactory);
+      pp = plugin.computePlacement(placementRequest, placementContext);
       fail("should generate 'Not enough eligible nodes' failure here");
     } catch (PlacementException pe) {
       assertTrue(pe.toString().contains("Not enough eligible nodes"));
@@ -732,9 +727,7 @@ public class AffinityPlacementFactoryTest extends SolrTestCaseJ4 {
     Builders.CollectionBuilder collectionBuilder = Builders.newCollectionBuilder(collectionName);
     collectionBuilder.initializeShardsReplicas(numShards, 0, 0, 0, List.of());
 
-    Cluster cluster = clusterBuilder.build();
-    AttributeFetcher attributeFetcher = clusterBuilder.buildAttributeFetcher();
-
+    PlacementContext placementContext = clusterBuilder.buildPlacementContext();
     SolrCollection solrCollection = collectionBuilder.build();
     List<Node> liveNodes = clusterBuilder.buildLiveNodes();
 
@@ -743,7 +736,7 @@ public class AffinityPlacementFactoryTest extends SolrTestCaseJ4 {
         new HashSet<>(liveNodes), nrtReplicas, tlogReplicas, pullReplicas);
 
     long start = System.nanoTime();
-    PlacementPlan pp = plugin.computePlacement(cluster, placementRequest, attributeFetcher, new PlacementPlanFactoryImpl());
+    PlacementPlan pp = plugin.computePlacement(placementRequest, placementContext);
     long end = System.nanoTime();
 
     final int REPLICAS_PER_SHARD = nrtReplicas + tlogReplicas + pullReplicas;


Mime
View raw message