You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@lucene.apache.org by ab...@apache.org on 2021/01/19 18:04:22 UTC

[lucene-solr] branch jira/solr-15055-2 updated: SOLR-15055: More refactoring after review.

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 cb82e2f  SOLR-15055: More refactoring after review.
cb82e2f is described below

commit cb82e2f13dada9afd60af2259fb2d6da2d68bb5a
Author: Andrzej Bialecki <ab...@apache.org>
AuthorDate: Tue Jan 19 19:03:59 2021 +0100

    SOLR-15055: More refactoring after review.
---
 .../apache/solr/cloud/ExclusiveSliceProperty.java  |  2 +-
 .../apache/solr/cloud/api/collections/Assign.java  | 17 +++++-
 .../cloud/api/collections/DeleteCollectionCmd.java | 20 ++-----
 .../cloud/api/collections/DeleteReplicaCmd.java    | 62 ++++++--------------
 .../apache/solr/cloud/overseer/ReplicaMutator.java |  2 +-
 .../cluster/placement/DeleteCollectionRequest.java | 23 ++++++++
 .../placement/impl/ModificationRequestImpl.java    | 12 ++--
 .../impl/PlacementPluginAssignStrategy.java        | 27 ++++++++-
 .../impl/SimpleClusterAbstractionsImpl.java        |  6 +-
 ...xtImpl.java => SimplePlacementContextImpl.java} |  4 +-
 .../placement/plugins/AffinityPlacementConfig.java |  4 +-
 .../plugins/AffinityPlacementFactory.java          | 68 ++++++++++++++--------
 .../impl/PlacementPluginIntegrationTest.java       |  2 +-
 13 files changed, 147 insertions(+), 102 deletions(-)

diff --git a/solr/core/src/java/org/apache/solr/cloud/ExclusiveSliceProperty.java b/solr/core/src/java/org/apache/solr/cloud/ExclusiveSliceProperty.java
index d17976e..01592ff 100644
--- a/solr/core/src/java/org/apache/solr/cloud/ExclusiveSliceProperty.java
+++ b/solr/core/src/java/org/apache/solr/cloud/ExclusiveSliceProperty.java
@@ -74,7 +74,7 @@ class ExclusiveSliceProperty {
   ExclusiveSliceProperty(ClusterState clusterState, ZkNodeProps message) {
     this.clusterState = clusterState;
     String tmp = message.getStr(ZkStateReader.PROPERTY_PROP);
-    if (StringUtils.startsWith(tmp, CollectionAdminParams.PROPERTY_PREFIX) == false) {
+    if (!StringUtils.startsWith(tmp, CollectionAdminParams.PROPERTY_PREFIX)) {
       tmp = CollectionAdminParams.PROPERTY_PREFIX + tmp;
     }
     this.property = tmp.toLowerCase(Locale.ROOT);
diff --git a/solr/core/src/java/org/apache/solr/cloud/api/collections/Assign.java b/solr/core/src/java/org/apache/solr/cloud/api/collections/Assign.java
index 786bfa9..d0d1f88 100644
--- a/solr/core/src/java/org/apache/solr/cloud/api/collections/Assign.java
+++ b/solr/core/src/java/org/apache/solr/cloud/api/collections/Assign.java
@@ -40,6 +40,7 @@ import org.apache.solr.client.solrj.cloud.SolrCloudManager;
 import org.apache.solr.client.solrj.cloud.AlreadyExistsException;
 import org.apache.solr.client.solrj.cloud.BadVersionException;
 import org.apache.solr.client.solrj.cloud.VersionedData;
+import org.apache.solr.cluster.placement.ModificationRequest;
 import org.apache.solr.cluster.placement.PlacementPlugin;
 import org.apache.solr.cluster.placement.impl.PlacementPluginAssignStrategy;
 import org.apache.solr.common.SolrException;
@@ -381,7 +382,11 @@ public class Assign {
 
   public interface AssignStrategy {
     List<ReplicaPosition> assign(SolrCloudManager solrCloudManager, AssignRequest assignRequest)
-        throws Assign.AssignmentException, IOException, InterruptedException;
+        throws AssignmentException, IOException, InterruptedException;
+    void verifyDeleteCollection(SolrCloudManager solrCloudManager, DocCollection collection)
+        throws AssignmentException, IOException, InterruptedException;
+    void verifyDeleteReplicas(SolrCloudManager solrCloudManager, DocCollection collection, String shardId, Set<String> replicaNames)
+        throws AssignmentException, IOException, InterruptedException;
   }
 
   public static class AssignRequest {
@@ -479,6 +484,16 @@ public class Assign {
       return result;
     }
 
+    @Override
+    public void verifyDeleteCollection(SolrCloudManager solrCloudManager, DocCollection collection) throws AssignmentException, IOException, InterruptedException {
+      // no-op
+    }
+
+    @Override
+    public void verifyDeleteReplicas(SolrCloudManager solrCloudManager, DocCollection collection, String shardId, Set<String> replicaNames) throws AssignmentException, IOException, InterruptedException {
+      // no-op
+    }
+
     // keeps this big ugly construction block out of otherwise legible code
     private ImmutableMap<Replica.Type, Integer> countsPerReplicaType(AssignRequest assignRequest) {
       return ImmutableMap.of(
diff --git a/solr/core/src/java/org/apache/solr/cloud/api/collections/DeleteCollectionCmd.java b/solr/core/src/java/org/apache/solr/cloud/api/collections/DeleteCollectionCmd.java
index 7674577..4f189db 100644
--- a/solr/core/src/java/org/apache/solr/cloud/api/collections/DeleteCollectionCmd.java
+++ b/solr/core/src/java/org/apache/solr/cloud/api/collections/DeleteCollectionCmd.java
@@ -29,18 +29,13 @@ import java.util.concurrent.TimeUnit;
 import java.util.stream.Collectors;
 
 import org.apache.solr.cloud.Overseer;
-import org.apache.solr.cluster.placement.DeleteReplicasRequest;
-import org.apache.solr.cluster.placement.PlacementContext;
 import org.apache.solr.cluster.placement.PlacementPlugin;
-import org.apache.solr.cluster.placement.impl.ModificationRequestImpl;
-import org.apache.solr.cluster.placement.impl.PlacementContextImpl;
 import org.apache.solr.common.NonExistentCoreException;
 import org.apache.solr.common.SolrException;
 import org.apache.solr.common.cloud.Aliases;
 import org.apache.solr.common.cloud.ClusterState;
 import org.apache.solr.common.cloud.DocCollection;
 import org.apache.solr.common.cloud.Replica;
-import org.apache.solr.common.cloud.Slice;
 import org.apache.solr.common.cloud.SolrZkClient;
 import org.apache.solr.common.cloud.ZkNodeProps;
 import org.apache.solr.common.cloud.ZkStateReader;
@@ -99,17 +94,10 @@ public class DeleteCollectionCmd implements OverseerCollectionMessageHandler.Cmd
     }
 
     PlacementPlugin placementPlugin = ocmh.overseer.getCoreContainer().getPlacementPluginFactory().createPluginInstance();
-    if (placementPlugin != null) {
-      // verify the placement modifications caused by the deletion are allowed
-      PlacementContext placementContext = new PlacementContextImpl(ocmh.cloudManager);
-      DocCollection coll = state.getCollection(collection);
-      for (Slice slice : coll.getActiveSlices()) {
-        DeleteReplicasRequest deleteReplicasRequest = ModificationRequestImpl
-            .deleteReplicasRequest(coll, slice.getName(),
-                slice.getReplicas().stream().map(Replica::getName).collect(Collectors.toSet()));
-        placementPlugin.verifyAllowedModification(deleteReplicasRequest, placementContext);
-      }
-    }
+    // verify the placement modifications caused by the deletion are allowed
+    DocCollection coll = state.getCollection(collection);
+    Assign.AssignStrategy assignStrategy = Assign.createAssignStrategy(placementPlugin, state, coll);
+    assignStrategy.verifyDeleteCollection(ocmh.cloudManager, coll);
 
     final boolean deleteHistory = message.getBool(CoreAdminParams.DELETE_METRICS_HISTORY, true);
 
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 f5cf6db..1d4eede 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
@@ -35,14 +35,7 @@ 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;
@@ -85,15 +78,9 @@ public class DeleteReplicaCmd implements Cmd {
     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) {
-      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.toString(), pe);
-      }
+      deleteReplicaBasedOnCount(clusterState, message, results, onComplete, parallel, placementPlugin);
       return;
     }
 
@@ -117,14 +104,7 @@ public class DeleteReplicaCmd implements Cmd {
       throw new SolrException(SolrException.ErrorCode.BAD_REQUEST,
               "Invalid shard name : " +  shard + " in collection : " +  collectionName);
     }
-
-    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.toString(), pe);
-    }
-
+    deleteCore(clusterState, coll, shard, replicaName, message, results, onComplete, parallel, placementPlugin);
   }
 
 
@@ -138,9 +118,8 @@ public class DeleteReplicaCmd implements Cmd {
                                  @SuppressWarnings({"rawtypes"})NamedList results,
                                  Runnable onComplete,
                                  boolean parallel,
-                                 PlacementPlugin placementPlugin,
-                                 PlacementContext placementContext)
-          throws KeeperException, PlacementException, InterruptedException {
+                                 PlacementPlugin placementPlugin)
+          throws KeeperException, IOException, InterruptedException {
     ocmh.checkRequired(message, COLLECTION_PROP, COUNT_PROP);
     int count = Integer.parseInt(message.getStr(COUNT_PROP));
     String collectionName = message.getStr(COLLECTION_PROP);
@@ -170,16 +149,13 @@ public class DeleteReplicaCmd implements Cmd {
       }
     }
 
-    if (placementPlugin != null) {
-      // 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);
-      }
+    // verify that all replicas can be deleted
+    Assign.AssignStrategy assignStrategy = Assign.createAssignStrategy(placementPlugin, clusterState, coll);
+    for (Map.Entry<Slice, Set<String>> entry : shardToReplicasMapping.entrySet()) {
+      Slice shardSlice = entry.getKey();
+      String shardId = shardSlice.getName();
+      Set<String> replicas = entry.getValue();
+      assignStrategy.verifyDeleteReplicas(ocmh.cloudManager, coll, shardId, replicas);
     }
 
     for (Map.Entry<Slice, Set<String>> entry : shardToReplicasMapping.entrySet()) {
@@ -190,7 +166,7 @@ public class DeleteReplicaCmd implements Cmd {
       for (String replica: replicas) {
         log.debug("Deleting replica {}  for shard {} based on count {}", replica, shardId, count);
         // don't verify with the placement plugin - we already did it
-        deleteCore(coll, shardId, replica, message, results, onComplete, parallel, null, null);
+        deleteCore(clusterState, coll, shardId, replica, message, results, onComplete, parallel, null);
       }
       results.add("shard_id", shardId);
       results.add("replicas_deleted", replicas);
@@ -248,15 +224,14 @@ public class DeleteReplicaCmd implements Cmd {
   }
 
   @SuppressWarnings({"unchecked"})
-  void deleteCore(DocCollection coll,
+  void deleteCore(ClusterState clusterState, DocCollection coll,
                   String shardId,
                   String replicaName,
                   ZkNodeProps message,
                   @SuppressWarnings({"rawtypes"})NamedList results,
                   Runnable onComplete,
                   boolean parallel,
-                  PlacementPlugin placementPlugin,
-                  PlacementContext placementContext) throws KeeperException, PlacementException, InterruptedException {
+                  PlacementPlugin placementPlugin) throws KeeperException, IOException, InterruptedException {
 
     Slice slice = coll.getSlice(shardId);
     Replica replica = slice.getReplica(replicaName);
@@ -276,11 +251,10 @@ public class DeleteReplicaCmd implements Cmd {
               " 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);
-    }
+    // verify that we are allowed to delete this replica
+    Assign.AssignStrategy assignStrategy = Assign.createAssignStrategy(placementPlugin, clusterState, coll);
+    assignStrategy.verifyDeleteReplicas(ocmh.cloudManager, coll, shardId, Set.of(replicaName));
+
     ShardHandler shardHandler = ocmh.shardHandlerFactory.getShardHandler();
     String core = replica.getStr(ZkStateReader.CORE_NAME_PROP);
     String asyncId = message.getStr(ASYNC);
diff --git a/solr/core/src/java/org/apache/solr/cloud/overseer/ReplicaMutator.java b/solr/core/src/java/org/apache/solr/cloud/overseer/ReplicaMutator.java
index d386880..be8f07a 100644
--- a/solr/core/src/java/org/apache/solr/cloud/overseer/ReplicaMutator.java
+++ b/solr/core/src/java/org/apache/solr/cloud/overseer/ReplicaMutator.java
@@ -116,7 +116,7 @@ public class ReplicaMutator {
     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 (StringUtils.startsWith(property, CollectionAdminParams.PROPERTY_PREFIX) == false) {
+    if (!StringUtils.startsWith(property, CollectionAdminParams.PROPERTY_PREFIX)) {
       property = CollectionAdminParams.PROPERTY_PREFIX + property;
     }
     property = property.toLowerCase(Locale.ROOT);
diff --git a/solr/core/src/java/org/apache/solr/cluster/placement/DeleteCollectionRequest.java b/solr/core/src/java/org/apache/solr/cluster/placement/DeleteCollectionRequest.java
new file mode 100644
index 0000000..b5dabc5
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/cluster/placement/DeleteCollectionRequest.java
@@ -0,0 +1,23 @@
+/*
+ * 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;
+
+/**
+ *
+ */
+public interface DeleteCollectionRequest extends ModificationRequest {
+}
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
index 7a62e28..f6889d2 100644
--- 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
@@ -20,6 +20,7 @@ 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.DeleteCollectionRequest;
 import org.apache.solr.cluster.placement.DeleteReplicasRequest;
 import org.apache.solr.cluster.placement.DeleteShardsRequest;
 import org.apache.solr.common.cloud.DocCollection;
@@ -33,6 +34,11 @@ import java.util.Set;
  */
 public class ModificationRequestImpl {
 
+  public static DeleteCollectionRequest deleteCollectionRequest(DocCollection docCollection) {
+    SolrCollection solrCollection = SimpleClusterAbstractionsImpl.SolrCollectionImpl.fromDocCollection(docCollection);
+    return () -> solrCollection;
+  }
+
   /**
    * Create a delete replicas request.
    * @param collection collection to delete replicas from
@@ -70,11 +76,7 @@ public class ModificationRequestImpl {
     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);
-    });
+    replicaNames.forEach(name -> solrReplicas.add(shard.getReplica(name)));
     return deleteReplicasRequest(solrCollection, solrReplicas);
   }
 
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 a226ab8..5b7c12f 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
@@ -19,10 +19,13 @@ package org.apache.solr.cluster.placement.impl;
 
 import java.io.IOException;
 import java.util.List;
+import java.util.Set;
 
 import org.apache.solr.client.solrj.cloud.SolrCloudManager;
 import org.apache.solr.cloud.api.collections.Assign;
 import org.apache.solr.cluster.SolrCollection;
+import org.apache.solr.cluster.placement.DeleteCollectionRequest;
+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;
@@ -51,7 +54,7 @@ public class PlacementPluginAssignStrategy implements Assign.AssignStrategy {
   public List<ReplicaPosition> assign(SolrCloudManager solrCloudManager, Assign.AssignRequest assignRequest)
       throws Assign.AssignmentException, IOException, InterruptedException {
 
-    PlacementContext placementContext = new PlacementContextImpl(solrCloudManager);
+    PlacementContext placementContext = new SimplePlacementContextImpl(solrCloudManager);
     SolrCollection solrCollection = placementContext.getCluster().getCollection(collection.getName());
 
     PlacementRequestImpl placementRequest = PlacementRequestImpl.toPlacementRequest(placementContext.getCluster(), solrCollection, assignRequest);
@@ -65,4 +68,26 @@ public class PlacementPluginAssignStrategy implements Assign.AssignStrategy {
 
     return ReplicaPlacementImpl.toReplicaPositions(placementPlan.getReplicaPlacements());
   }
+
+  @Override
+  public void verifyDeleteCollection(SolrCloudManager solrCloudManager, DocCollection collection) throws Assign.AssignmentException, IOException, InterruptedException {
+    PlacementContext placementContext = new SimplePlacementContextImpl(solrCloudManager);
+    DeleteCollectionRequest modificationRequest = ModificationRequestImpl.deleteCollectionRequest(collection);
+    try {
+      plugin.verifyAllowedModification(modificationRequest, placementContext);
+    } catch (PlacementException pe) {
+      throw new Assign.AssignmentException(pe);
+    }
+  }
+
+  @Override
+  public void verifyDeleteReplicas(SolrCloudManager solrCloudManager, DocCollection collection, String shardId, Set<String> replicaNames) throws Assign.AssignmentException, IOException, InterruptedException {
+    PlacementContext placementContext = new SimplePlacementContextImpl(solrCloudManager);
+    DeleteReplicasRequest modificationRequest = ModificationRequestImpl.deleteReplicasRequest(collection, shardId, replicaNames);
+    try {
+      plugin.verifyAllowedModification(modificationRequest, 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 292fe5c..e26a374 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);
     }
 
-    ReplicaImpl(String replicaName, Shard shard, org.apache.solr.common.cloud.Replica sliceReplica) {
+    private 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());
     }
 
-    Replica.ReplicaType translateType(org.apache.solr.common.cloud.Replica.Type type) {
+    private 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 {
       }
     }
 
-    Replica.ReplicaState translateState(org.apache.solr.common.cloud.Replica.State state) {
+    private 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/impl/PlacementContextImpl.java b/solr/core/src/java/org/apache/solr/cluster/placement/impl/SimplePlacementContextImpl.java
similarity index 85%
rename from solr/core/src/java/org/apache/solr/cluster/placement/impl/PlacementContextImpl.java
rename to solr/core/src/java/org/apache/solr/cluster/placement/impl/SimplePlacementContextImpl.java
index eed649b..c04812c 100644
--- a/solr/core/src/java/org/apache/solr/cluster/placement/impl/PlacementContextImpl.java
+++ b/solr/core/src/java/org/apache/solr/cluster/placement/impl/SimplePlacementContextImpl.java
@@ -11,13 +11,13 @@ import java.io.IOException;
 /**
  *
  */
-public class PlacementContextImpl implements PlacementContext {
+public class SimplePlacementContextImpl implements PlacementContext {
 
   private final Cluster cluster;
   private final AttributeFetcher attributeFetcher;
   private final PlacementPlanFactory placementPlanFactory = new PlacementPlanFactoryImpl();
 
-  public PlacementContextImpl(SolrCloudManager solrCloudManager) throws IOException {
+  public SimplePlacementContextImpl(SolrCloudManager solrCloudManager) throws IOException {
     cluster = new SimpleClusterAbstractionsImpl.ClusterImpl(solrCloudManager);
     attributeFetcher = new AttributeFetcherImpl(solrCloudManager);
   }
diff --git a/solr/core/src/java/org/apache/solr/cluster/placement/plugins/AffinityPlacementConfig.java b/solr/core/src/java/org/apache/solr/cluster/placement/plugins/AffinityPlacementConfig.java
index 2866392..f091654 100644
--- a/solr/core/src/java/org/apache/solr/cluster/placement/plugins/AffinityPlacementConfig.java
+++ b/solr/core/src/java/org/apache/solr/cluster/placement/plugins/AffinityPlacementConfig.java
@@ -57,9 +57,7 @@ public class AffinityPlacementConfig implements PlacementPluginConfig {
 
   // no-arg public constructor required for deserialization
   public AffinityPlacementConfig() {
-    minimalFreeDiskGB = 20L;
-    prioritizedFreeDiskGB = 100L;
-    withCollections = Map.of();
+    this (20L, 100L, Map.of());
   }
 
   public AffinityPlacementConfig(long minimalFreeDiskGB, long prioritizedFreeDiskGB) {
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 ad75053..0ad62ac 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
@@ -174,7 +174,10 @@ public class AffinityPlacementFactory implements PlacementPluginFactory<Affinity
 
     private final long prioritizedFreeDiskGB;
 
+    // primary to secondary (1:1)
     private final Map<String, String> withCollections;
+    // secondary to primary (1:N)
+    private final Map<String, Set<String>> colocatedWith;
 
     private final Random replicaPlacementRandom = new Random(); // ok even if random sequence is predictable.
 
@@ -184,7 +187,16 @@ public class AffinityPlacementFactory implements PlacementPluginFactory<Affinity
     private AffinityPlacementPlugin(long minimalFreeDiskGB, long prioritizedFreeDiskGB, Map<String, String> withCollections) {
       this.minimalFreeDiskGB = minimalFreeDiskGB;
       this.prioritizedFreeDiskGB = prioritizedFreeDiskGB;
+      Objects.requireNonNull(withCollections, "withCollections must not be null");
       this.withCollections = withCollections;
+      if (withCollections.isEmpty()) {
+        colocatedWith = Map.of();
+      } else {
+        colocatedWith = new HashMap<>();
+        withCollections.forEach((primary, secondary) ->
+            colocatedWith.computeIfAbsent(secondary, s -> new HashSet<>())
+                .add(primary));
+      }
 
       // We make things reproducible in tests by using test seed if any
       String seed = System.getProperty("tests.seed");
@@ -256,14 +268,30 @@ public class AffinityPlacementFactory implements PlacementPluginFactory<Affinity
 
     @Override
     public void verifyAllowedModification(ModificationRequest modificationRequest, PlacementContext placementContext) throws PlacementModificationException, InterruptedException {
+      Cluster cluster = placementContext.getCluster();
       if (modificationRequest instanceof DeleteShardsRequest) {
         throw new UnsupportedOperationException("not implemented yet");
+      } else if (modificationRequest instanceof DeleteCollectionRequest) {
+        DeleteCollectionRequest deleteCollectionRequest = (DeleteCollectionRequest) modificationRequest;
+        Set<String> colocatedCollections = colocatedWith.getOrDefault(deleteCollectionRequest.getCollection().getName(), Set.of());
+        for (String primaryName : colocatedCollections) {
+          try {
+            if (cluster.getCollection(primaryName) != null) {
+              // still exists
+              throw new PlacementModificationException("colocated collection " + primaryName + " still present");
+            }
+          } catch (IOException e) {
+            throw new PlacementModificationException("failed to retrieve colocated collection information", e);
+          }
+        }
+        return;
       } else if (!(modificationRequest instanceof DeleteReplicasRequest)) {
         throw new UnsupportedOperationException("unsupported request type " + modificationRequest.getClass().getName());
       }
       DeleteReplicasRequest request = (DeleteReplicasRequest) modificationRequest;
       SolrCollection secondaryCollection = request.getCollection();
-      if (!withCollections.values().contains(secondaryCollection.getName())) {
+      Set<String> colocatedCollections = colocatedWith.get(secondaryCollection.getName());
+      if (colocatedCollections == null) {
         return;
       }
       Map<Node, Map<String, AtomicInteger>> secondaryNodeShardReplicas = new HashMap<>();
@@ -275,36 +303,28 @@ public class AffinityPlacementFactory implements PlacementPluginFactory<Affinity
           }));
 
       // find the colocated-with collections
-      Cluster cluster = placementContext.getCluster();
-      Set<SolrCollection> colocatedCollections = new HashSet<>();
-      AtomicReference<IOException> exc = new AtomicReference<>();
-      withCollections.forEach((primaryName, secondaryName) -> {
-        if (exc.get() != null) { // there were errors before
+      Map<Node, Set<String>> colocatingNodes = new HashMap<>();
+      AtomicReference<Exception> exc = new AtomicReference<>();
+      colocatedCollections.forEach(collName -> {
+        if (exc.get() != null) {
           return;
         }
-        if (secondaryCollection.getName().equals(secondaryName)) {
-          try {
-            SolrCollection primary = cluster.getCollection(primaryName);
-            if (primary != null) { // still exists
-              colocatedCollections.add(primary);
-            }
-          } catch (IOException e) {
-            // IO error, not a missing collection - fail
-            exc.set(e);
-            return;
-          }
+        SolrCollection coll;
+        try {
+          coll = cluster.getCollection(collName);
+        } catch (Exception e) {
+          exc.set(e);
+          return;
         }
+        coll.shards().forEach(shard ->
+            shard.replicas().forEach(replica -> {
+              colocatingNodes.computeIfAbsent(replica.getNode(), n -> new HashSet<>())
+                  .add(coll.getName());
+            }));
       });
       if (exc.get() != null) {
         throw new PlacementModificationException("failed to retrieve colocated collection information", exc.get());
       }
-      Map<Node, Set<String>> colocatingNodes = new HashMap<>();
-      colocatedCollections.forEach(coll ->
-          coll.shards().forEach(shard ->
-              shard.replicas().forEach(replica -> {
-                colocatingNodes.computeIfAbsent(replica.getNode(), n -> new HashSet<>())
-                    .add(coll.getName());
-              })));
       PlacementModificationException exception = null;
       for (Replica replica : request.getReplicas()) {
         if (!colocatingNodes.containsKey(replica.getNode())) {
diff --git a/solr/core/src/test/org/apache/solr/cluster/placement/impl/PlacementPluginIntegrationTest.java b/solr/core/src/test/org/apache/solr/cluster/placement/impl/PlacementPluginIntegrationTest.java
index e0dfc46..199d779 100644
--- a/solr/core/src/test/org/apache/solr/cluster/placement/impl/PlacementPluginIntegrationTest.java
+++ b/solr/core/src/test/org/apache/solr/cluster/placement/impl/PlacementPluginIntegrationTest.java
@@ -310,7 +310,7 @@ public class PlacementPluginIntegrationTest extends SolrCloudTestCase {
           .process(cluster.getSolrClient());
       fail("should have failed: " + rsp);
     } catch (Exception e) {
-      assertTrue(e.toString(), e.toString().contains("delete replica(s) rejected"));
+      assertTrue(e.toString(), e.toString().contains("colocated collection"));
     }
   }