murblanc commented on a change in pull request #2199:
URL: https://github.com/apache/lucene-solr/pull/2199#discussion_r563645684



##########
File path: 
solr/core/src/java/org/apache/solr/cluster/placement/plugins/AffinityPlacementFactory.java
##########
@@ -238,11 +258,93 @@ public PlacementPlan computePlacement(Cluster cluster, 
PlacementRequest request,
         // 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 
PlacementModificationException, InterruptedException {
+      if (modificationRequest instanceof DeleteShardsRequest) {
+        throw new UnsupportedOperationException("not implemented yet");
+      } else if (modificationRequest instanceof DeleteCollectionRequest) {
+        verifyDeleteCollection((DeleteCollectionRequest) modificationRequest, 
placementContext);
+      } else if (modificationRequest instanceof DeleteReplicasRequest) {
+        verifyDeleteReplicas((DeleteReplicasRequest) modificationRequest, 
placementContext);
+      } else {
+        throw new UnsupportedOperationException("unsupported request type " + 
modificationRequest.getClass().getName());
+      }
+    }
+
+    private void verifyDeleteCollection(DeleteCollectionRequest 
deleteCollectionRequest, PlacementContext placementContext) throws 
PlacementModificationException, InterruptedException {

Review comment:
       Can we have cycles in the `withCollection` graph? Should we allow a way 
to override the vetting checks from the Collection API?

##########
File path: 
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;
+
+/**
+ * Delete shards request.
+ */
+public interface DeleteShardsRequest extends ModificationRequest {

Review comment:
       If we don't use this interface (i.e. the class that implements it) I 
suggest we do not include either in this PR. Or at least define and call the 
corresponding method in `AssignStrategy` from the appropriate `*Cmd` even if 
nothing does a real implementation and vetting based on it (but it would be 
ready to be consumed maybe by another plugin written by some user).

##########
File path: 
solr/core/src/java/org/apache/solr/cluster/placement/plugins/AffinityPlacementFactory.java
##########
@@ -238,11 +258,93 @@ public PlacementPlan computePlacement(Cluster cluster, 
PlacementRequest request,
         // 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 
PlacementModificationException, InterruptedException {
+      if (modificationRequest instanceof DeleteShardsRequest) {
+        throw new UnsupportedOperationException("not implemented yet");
+      } else if (modificationRequest instanceof DeleteCollectionRequest) {
+        verifyDeleteCollection((DeleteCollectionRequest) modificationRequest, 
placementContext);
+      } else if (modificationRequest instanceof DeleteReplicasRequest) {
+        verifyDeleteReplicas((DeleteReplicasRequest) modificationRequest, 
placementContext);
+      } else {
+        throw new UnsupportedOperationException("unsupported request type " + 
modificationRequest.getClass().getName());

Review comment:
       here too. Allow everything we don't plan to explicitly disallow.

##########
File path: 
solr/core/src/java/org/apache/solr/cluster/placement/plugins/AffinityPlacementFactory.java
##########
@@ -467,7 +569,7 @@ private void makePlacementDecisions(SolrCollection 
solrCollection, String shardN
         if (candidateAzEntries == null) {
           // This can happen because not enough nodes for the placement 
request or already too many nodes with replicas of
           // the shard that can't accept new replicas or not enough nodes with 
enough free disk space.
-          throw new PlacementException("Not enough nodes to place " + 
numReplicas + " replica(s) of type " + replicaType +
+          throw new PlacementException("Not enough eligible nodes to place " + 
numReplicas + " replica(s) of type " + replicaType +

Review comment:
       Not sure if it's useful here or output elsewhere already though.

##########
File path: 
solr/core/src/java/org/apache/solr/cluster/placement/plugins/AffinityPlacementConfig.java
##########
@@ -43,14 +46,28 @@
   @JsonProperty
   public long prioritizedFreeDiskGB;
 
+  /**
+   * This property defines an additional constraint that primary collections 
(keys) should be
+   * located on the same nodes as the secondary collections (values). The 
plugin will assume
+   * that the secondary collection replicas are already in place and ignore 
candidate nodes where
+   * they are not already present.
+   */
+  @JsonProperty
+  public Map<String, String> withCollections;
+
   // no-arg public constructor required for deserialization
   public AffinityPlacementConfig() {
-    minimalFreeDiskGB = 20L;
-    prioritizedFreeDiskGB = 100L;
+    this (20L, 100L, Map.of());

Review comment:
       Also, this constructor should call the constructor not requiring a 
`Map`, it will add an empty one.
   
   BTW, the deserialization constructor should not pass "meaningful" default 
values since those will not be used.
   I'd call `this(0L, 0L)` here instead, and switch the 
`AffinityPlacementConfig DEFAULT` static variable to use the constructor 
accepting values. That will also make seeing what are the defaults easier when 
reading the code.

##########
File path: 
solr/core/src/java/org/apache/solr/cluster/placement/plugins/AffinityPlacementFactory.java
##########
@@ -467,7 +569,7 @@ private void makePlacementDecisions(SolrCollection 
solrCollection, String shardN
         if (candidateAzEntries == null) {
           // This can happen because not enough nodes for the placement 
request or already too many nodes with replicas of
           // the shard that can't accept new replicas or not enough nodes with 
enough free disk space.
-          throw new PlacementException("Not enough nodes to place " + 
numReplicas + " replica(s) of type " + replicaType +
+          throw new PlacementException("Not enough eligible nodes to place " + 
numReplicas + " replica(s) of type " + replicaType +

Review comment:
       Can we add in the message if there were `withCollection` constraints 
used in the placement logic? No details, not even indicating that it's the 
failure reason, just mentioning it...

##########
File path: 
solr/core/src/java/org/apache/solr/cluster/placement/plugins/AffinityPlacementFactory.java
##########
@@ -238,11 +258,93 @@ public PlacementPlan computePlacement(Cluster cluster, 
PlacementRequest request,
         // 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 
PlacementModificationException, InterruptedException {
+      if (modificationRequest instanceof DeleteShardsRequest) {
+        throw new UnsupportedOperationException("not implemented yet");

Review comment:
       If not implemented we should let it happen rather than have it fail.

##########
File path: 
solr/core/src/java/org/apache/solr/cluster/placement/plugins/AffinityPlacementFactory.java
##########
@@ -238,11 +258,93 @@ public PlacementPlan computePlacement(Cluster cluster, 
PlacementRequest request,
         // 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 
PlacementModificationException, InterruptedException {
+      if (modificationRequest instanceof DeleteShardsRequest) {
+        throw new UnsupportedOperationException("not implemented yet");
+      } else if (modificationRequest instanceof DeleteCollectionRequest) {
+        verifyDeleteCollection((DeleteCollectionRequest) modificationRequest, 
placementContext);
+      } else if (modificationRequest instanceof DeleteReplicasRequest) {
+        verifyDeleteReplicas((DeleteReplicasRequest) modificationRequest, 
placementContext);
+      } else {
+        throw new UnsupportedOperationException("unsupported request type " + 
modificationRequest.getClass().getName());
+      }
+    }
+
+    private void verifyDeleteCollection(DeleteCollectionRequest 
deleteCollectionRequest, PlacementContext placementContext) throws 
PlacementModificationException, InterruptedException {
+      Cluster cluster = placementContext.getCluster();
+      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);
+        }
+      }
+    }
+
+    private void verifyDeleteReplicas(DeleteReplicasRequest 
deleteReplicasRequest, PlacementContext placementContext) throws 
PlacementModificationException, InterruptedException {
+      Cluster cluster = placementContext.getCluster();
+      SolrCollection secondaryCollection = 
deleteReplicasRequest.getCollection();
+      Set<String> colocatedCollections = 
colocatedWith.get(secondaryCollection.getName());
+      if (colocatedCollections == null) {
+        return;
+      }
+      Map<Node, Map<String, AtomicInteger>> secondaryNodeShardReplicas = new 
HashMap<>();
+      secondaryCollection.shards().forEach(shard ->
+          shard.replicas().forEach(replica -> {
+            secondaryNodeShardReplicas.computeIfAbsent(replica.getNode(), n -> 
new HashMap<>())
+                .computeIfAbsent(replica.getShard().getShardName(), s -> new 
AtomicInteger())
+                .incrementAndGet();
+          }));
+
+      // find the colocated-with collections
+      Map<Node, Set<String>> colocatingNodes = new HashMap<>();
+      try {
+        for (String colocatedCollection : colocatedCollections) {
+          SolrCollection coll = cluster.getCollection(colocatedCollection);
+          coll.shards().forEach(shard ->
+              shard.replicas().forEach(replica -> {
+                colocatingNodes.computeIfAbsent(replica.getNode(), n -> new 
HashSet<>())
+                    .add(coll.getName());
+              }));
+        }
+      } catch (IOException ioe) {
+        throw new PlacementModificationException("failed to retrieve colocated 
collection information", ioe);
+      }
+      PlacementModificationException exception = null;
+      for (Replica replica : deleteReplicasRequest.getReplicas()) {
+        if (!colocatingNodes.containsKey(replica.getNode())) {
+          continue;
+        }
+        // check that there will be at least one replica remaining
+        AtomicInteger secondaryCount = secondaryNodeShardReplicas
+            .getOrDefault(replica.getNode(), Map.of())
+            .getOrDefault(replica.getShard().getShardName(), new 
AtomicInteger());
+        if (secondaryCount.get() > 1) {
+          // we can delete it - record the deletion
+          secondaryCount.decrementAndGet();
+          continue;
+        }
+        // fail - this replica cannot be removed
+        if (exception == null) {
+          exception = new PlacementModificationException("delete replica(s) 
rejected");
+        }
+        exception.addRejectedModification(replica.toString(), "co-located with 
replicas of " + colocatingNodes.get(replica.getNode()));

Review comment:
       Add the problematic node in the rejection reason so that client can 
delete the colocated collection replicas there to fix the issue.

##########
File path: 
solr/core/src/java/org/apache/solr/cluster/placement/plugins/AffinityPlacementFactory.java
##########
@@ -238,11 +258,93 @@ public PlacementPlan computePlacement(Cluster cluster, 
PlacementRequest request,
         // 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 
PlacementModificationException, InterruptedException {
+      if (modificationRequest instanceof DeleteShardsRequest) {
+        throw new UnsupportedOperationException("not implemented yet");
+      } else if (modificationRequest instanceof DeleteCollectionRequest) {
+        verifyDeleteCollection((DeleteCollectionRequest) modificationRequest, 
placementContext);
+      } else if (modificationRequest instanceof DeleteReplicasRequest) {
+        verifyDeleteReplicas((DeleteReplicasRequest) modificationRequest, 
placementContext);
+      } else {
+        throw new UnsupportedOperationException("unsupported request type " + 
modificationRequest.getClass().getName());
+      }
+    }
+
+    private void verifyDeleteCollection(DeleteCollectionRequest 
deleteCollectionRequest, PlacementContext placementContext) throws 
PlacementModificationException, InterruptedException {
+      Cluster cluster = placementContext.getCluster();
+      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");

Review comment:
       The exception should also tell which collection we refuse to delete 
(unless that context will be added higher up the call stack?).

##########
File path: 
solr/core/src/java/org/apache/solr/cluster/placement/impl/ModificationRequestImpl.java
##########
@@ -0,0 +1,107 @@
+/*
+ * 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.DeleteCollectionRequest;
+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;
+
+/**
+ * Helper class to create modification request instances.
+ */
+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
+   * @param replicas replicas to delete
+   */
+  public static DeleteReplicasRequest deleteReplicasRequest(SolrCollection 
collection, Set<Replica> replicas) {

Review comment:
       Method should be called `createDeleteReplicasRequest`?

##########
File path: 
solr/core/src/java/org/apache/solr/cluster/placement/plugins/AffinityPlacementConfig.java
##########
@@ -43,14 +46,28 @@
   @JsonProperty
   public long prioritizedFreeDiskGB;
 
+  /**
+   * This property defines an additional constraint that primary collections 
(keys) should be
+   * located on the same nodes as the secondary collections (values). The 
plugin will assume
+   * that the secondary collection replicas are already in place and ignore 
candidate nodes where
+   * they are not already present.
+   */
+  @JsonProperty
+  public Map<String, String> withCollections;
+
   // no-arg public constructor required for deserialization
   public AffinityPlacementConfig() {
-    minimalFreeDiskGB = 20L;
-    prioritizedFreeDiskGB = 100L;
+    this (20L, 100L, Map.of());

Review comment:
       minor: remove space after `this`.

##########
File path: 
solr/core/src/java/org/apache/solr/cluster/placement/impl/ModificationRequestImpl.java
##########
@@ -0,0 +1,107 @@
+/*
+ * 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.DeleteCollectionRequest;
+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;
+
+/**
+ * Helper class to create modification request instances.
+ */
+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
+   * @param replicas replicas to delete
+   */
+  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;
+      }
+
+      @Override
+      public String toString() {
+        return "DeleteReplicasRequest{collection=" + collection.getName() +
+            ",replicas=" + replicas;
+      }
+    };
+  }
+
+  /**
+   * Create a delete replicas request using the internal Solr API.
+   * @param docCollection Solr collection
+   * @param shardName shard name
+   * @param replicaNames replica names (aka. core-node names)
+   */
+  public static DeleteReplicasRequest deleteReplicasRequest(DocCollection 
docCollection, String shardName, Set<String> replicaNames) {

Review comment:
       The signature is not consistent with `deleteReplicasRequest` above that 
takes `Replica` instances rather than `String` names. I prefer the strongly 
typed signature.

##########
File path: solr/core/src/java/org/apache/solr/cloud/api/collections/Assign.java
##########
@@ -381,7 +382,11 @@ public AssignmentException(String message, Throwable 
cause, boolean enableSuppre
 
   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)

Review comment:
       Maybe add default implementation here to not force empty implementation 
in legacy or other places?
   I'd add javadoc at least for added methods...

##########
File path: 
solr/core/src/java/org/apache/solr/cluster/placement/plugins/AffinityPlacementFactory.java
##########
@@ -238,11 +258,93 @@ public PlacementPlan computePlacement(Cluster cluster, 
PlacementRequest request,
         // 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 
PlacementModificationException, InterruptedException {
+      if (modificationRequest instanceof DeleteShardsRequest) {
+        throw new UnsupportedOperationException("not implemented yet");
+      } else if (modificationRequest instanceof DeleteCollectionRequest) {
+        verifyDeleteCollection((DeleteCollectionRequest) modificationRequest, 
placementContext);
+      } else if (modificationRequest instanceof DeleteReplicasRequest) {
+        verifyDeleteReplicas((DeleteReplicasRequest) modificationRequest, 
placementContext);
+      } else {
+        throw new UnsupportedOperationException("unsupported request type " + 
modificationRequest.getClass().getName());
+      }
+    }
+
+    private void verifyDeleteCollection(DeleteCollectionRequest 
deleteCollectionRequest, PlacementContext placementContext) throws 
PlacementModificationException, InterruptedException {
+      Cluster cluster = placementContext.getCluster();
+      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);
+        }
+      }
+    }
+
+    private void verifyDeleteReplicas(DeleteReplicasRequest 
deleteReplicasRequest, PlacementContext placementContext) throws 
PlacementModificationException, InterruptedException {
+      Cluster cluster = placementContext.getCluster();
+      SolrCollection secondaryCollection = 
deleteReplicasRequest.getCollection();
+      Set<String> colocatedCollections = 
colocatedWith.get(secondaryCollection.getName());
+      if (colocatedCollections == null) {
+        return;
+      }
+      Map<Node, Map<String, AtomicInteger>> secondaryNodeShardReplicas = new 
HashMap<>();
+      secondaryCollection.shards().forEach(shard ->
+          shard.replicas().forEach(replica -> {
+            secondaryNodeShardReplicas.computeIfAbsent(replica.getNode(), n -> 
new HashMap<>())
+                .computeIfAbsent(replica.getShard().getShardName(), s -> new 
AtomicInteger())
+                .incrementAndGet();
+          }));
+
+      // find the colocated-with collections
+      Map<Node, Set<String>> colocatingNodes = new HashMap<>();
+      try {
+        for (String colocatedCollection : colocatedCollections) {
+          SolrCollection coll = cluster.getCollection(colocatedCollection);
+          coll.shards().forEach(shard ->
+              shard.replicas().forEach(replica -> {
+                colocatingNodes.computeIfAbsent(replica.getNode(), n -> new 
HashSet<>())
+                    .add(coll.getName());
+              }));
+        }
+      } catch (IOException ioe) {
+        throw new PlacementModificationException("failed to retrieve colocated 
collection information", ioe);
+      }
+      PlacementModificationException exception = null;
+      for (Replica replica : deleteReplicasRequest.getReplicas()) {
+        if (!colocatingNodes.containsKey(replica.getNode())) {
+          continue;
+        }
+        // check that there will be at least one replica remaining
+        AtomicInteger secondaryCount = secondaryNodeShardReplicas
+            .getOrDefault(replica.getNode(), Map.of())
+            .getOrDefault(replica.getShard().getShardName(), new 
AtomicInteger());
+        if (secondaryCount.get() > 1) {
+          // we can delete it - record the deletion
+          secondaryCount.decrementAndGet();
+          continue;
+        }
+        // fail - this replica cannot be removed
+        if (exception == null) {
+          exception = new PlacementModificationException("delete replica(s) 
rejected");
+        }
+        exception.addRejectedModification(replica.toString(), "co-located with 
replicas of " + colocatingNodes.get(replica.getNode()));

Review comment:
       We do not support in Solr returning errors from Collection API calls in 
a way that is consumable by code, right? Would be a nice addition...

##########
File path: 
solr/core/src/java/org/apache/solr/cluster/placement/impl/ModificationRequestImpl.java
##########
@@ -0,0 +1,107 @@
+/*
+ * 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.DeleteCollectionRequest;
+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;
+
+/**
+ * Helper class to create modification request instances.
+ */
+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
+   * @param replicas replicas to delete
+   */
+  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;
+      }
+
+      @Override
+      public String toString() {
+        return "DeleteReplicasRequest{collection=" + collection.getName() +
+            ",replicas=" + replicas;
+      }
+    };
+  }
+
+  /**
+   * Create a delete replicas request using the internal Solr API.
+   * @param docCollection Solr collection
+   * @param shardName shard name
+   * @param replicaNames replica names (aka. core-node names)
+   */
+  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 -> solrReplicas.add(shard.getReplica(name)));
+    return deleteReplicasRequest(solrCollection, solrReplicas);
+  }
+
+
+  public static DeleteShardsRequest deleteShardsRequest(SolrCollection 
collection, Set<String> shardNames) {

Review comment:
       This and next method should really be called `createDeleteShardsRequest` 
to convey what they really do, right?




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
us...@infra.apache.org



---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscr...@lucene.apache.org
For additional commands, e-mail: issues-h...@lucene.apache.org

Reply via email to