This is an automated email from the ASF dual-hosted git repository.

dsmiley pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/solr.git


The following commit(s) were added to refs/heads/main by this push:
     new 46585917715 SOLR-18413: Handle deleted MIGRATE routing targets (#4862)
46585917715 is described below

commit 465859177153921cd0c09fb9f6c4d643d2f30d41
Author: Zhenyu Li <[email protected]>
AuthorDate: Sun Sep 6 00:14:34 2026 -0400

    SOLR-18413: Handle deleted MIGRATE routing targets (#4862)
    
    Problem fixed:  Say the target of a MIGRATE collection is deleted, yet the 
source collection exists and receives an update request.  The request reports 
failure after partially applying the mutation, leaving replicas of the source 
shard inconsistent. No update request is sent to the source replica. Therefore, 
SolrCmdDistributor does not observe a replica failure. Consequently, this 
request does not lower the replica's shard term or directly trigger recovery.
---
 .../fix-dangling-routing-rule-target.yml           |  8 ++
 .../processor/DistributedZkUpdateProcessor.java    | 95 ++++++++++++----------
 .../org/apache/solr/cloud/MigrateRouteKeyTest.java | 53 ++++++++++++
 3 files changed, 115 insertions(+), 41 deletions(-)

diff --git a/changelog/unreleased/fix-dangling-routing-rule-target.yml 
b/changelog/unreleased/fix-dangling-routing-rule-target.yml
new file mode 100644
index 00000000000..a3ba20b78bc
--- /dev/null
+++ b/changelog/unreleased/fix-dangling-routing-rule-target.yml
@@ -0,0 +1,8 @@
+title: Prevent partial writes when a collection referenced by a MIGRATE 
routing rule has been deleted
+type: fixed
+authors:
+  - name: ZhenyuLi
+    nick: JHSUYU
+links:
+  - name: SOLR-18413
+    url: https://issues.apache.org/jira/browse/SOLR-18413
diff --git 
a/solr/core/src/java/org/apache/solr/update/processor/DistributedZkUpdateProcessor.java
 
b/solr/core/src/java/org/apache/solr/update/processor/DistributedZkUpdateProcessor.java
index 49c9fc717a7..fad45bb70cb 100644
--- 
a/solr/core/src/java/org/apache/solr/update/processor/DistributedZkUpdateProcessor.java
+++ 
b/solr/core/src/java/org/apache/solr/update/processor/DistributedZkUpdateProcessor.java
@@ -1011,7 +1011,17 @@ public class DistributedZkUpdateProcessor extends 
DistributedUpdateProcessor {
                 int hash = compositeIdRouter.sliceHash(id, doc, null, coll);
                 for (DocRouter.Range range : ranges) {
                   if (range.includes(hash)) {
-                    DocCollection targetColl = 
cstate.getCollection(rule.getTargetCollectionName());
+                    DocCollection targetColl =
+                        
cstate.getCollectionOrNull(rule.getTargetCollectionName());
+                    if (targetColl == null) {
+                      if (log.isInfoEnabled()) {
+                        log.info(
+                            "Removing shard update routing rule because the 
target collection {} doesn't exist",
+                            rule.getTargetCollectionName());
+                      }
+                      removeRoutingRule(myShardId, routeKey);
+                      break;
+                    }
                     Collection<Slice> activeSlices =
                         targetColl.getRouter().getSearchSlicesSingle(id, null, 
targetColl);
                     if (activeSlices == null || activeSlices.isEmpty()) {
@@ -1031,46 +1041,8 @@ public class DistributedZkUpdateProcessor extends 
DistributedUpdateProcessor {
                 }
               }
             } else {
-              ReentrantLock ruleExpiryLock = req.getCore().getRuleExpiryLock();
-              if (!ruleExpiryLock.isLocked()) {
-                try {
-                  if (ruleExpiryLock.tryLock(10, TimeUnit.MILLISECONDS)) {
-                    log.info("Going to expire routing rule");
-                    try {
-                      Map<String, Object> map =
-                          Map.of(
-                              Overseer.QUEUE_OPERATION,
-                              OverseerAction.REMOVEROUTINGRULE.toLower(),
-                              ZkStateReader.COLLECTION_PROP,
-                              collection,
-                              ZkStateReader.SHARD_ID_PROP,
-                              myShardId,
-                              "routeKey",
-                              routeKey + "!");
-                      if 
(distributedClusterStateUpdater.isDistributedStateUpdate()) {
-                        ZkNodeProps message = new ZkNodeProps(map);
-                        distributedClusterStateUpdater.doSingleStateUpdate(
-                            
DistributedClusterStateUpdater.MutatingCommand.SliceRemoveRoutingRule,
-                            message,
-                            zkController.getOverseer().getSolrCloudManager(),
-                            zkController.getOverseer().getZkStateReader());
-                      } else {
-                        
zkController.getOverseer().offerStateUpdate(Utils.toJSON(map));
-                      }
-                    } catch (KeeperException e) {
-                      log.warn(
-                          "Exception while removing routing rule for route 
key: {}", routeKey, e);
-                    } catch (Exception e) {
-                      log.error(
-                          "Exception while removing routing rule for route 
key: {}", routeKey, e);
-                    } finally {
-                      ruleExpiryLock.unlock();
-                    }
-                  }
-                } catch (InterruptedException e) {
-                  Thread.currentThread().interrupt();
-                }
-              }
+              log.info("Removing shard update routing rule because it has 
expired");
+              removeRoutingRule(myShardId, routeKey);
             }
           }
         }
@@ -1079,6 +1051,47 @@ public class DistributedZkUpdateProcessor extends 
DistributedUpdateProcessor {
     return nodes;
   }
 
+  private void removeRoutingRule(String shardId, String routeKey) {
+    ReentrantLock ruleExpiryLock = req.getCore().getRuleExpiryLock();
+    if (ruleExpiryLock.isLocked()) {
+      return;
+    }
+    try {
+      if (ruleExpiryLock.tryLock(10, TimeUnit.MILLISECONDS)) {
+        try {
+          Map<String, Object> map =
+              Map.of(
+                  Overseer.QUEUE_OPERATION,
+                  OverseerAction.REMOVEROUTINGRULE.toLower(),
+                  ZkStateReader.COLLECTION_PROP,
+                  collection,
+                  ZkStateReader.SHARD_ID_PROP,
+                  shardId,
+                  "routeKey",
+                  routeKey + "!");
+          if (distributedClusterStateUpdater.isDistributedStateUpdate()) {
+            ZkNodeProps message = new ZkNodeProps(map);
+            distributedClusterStateUpdater.doSingleStateUpdate(
+                
DistributedClusterStateUpdater.MutatingCommand.SliceRemoveRoutingRule,
+                message,
+                zkController.getOverseer().getSolrCloudManager(),
+                zkController.getOverseer().getZkStateReader());
+          } else {
+            zkController.getOverseer().offerStateUpdate(Utils.toJSON(map));
+          }
+        } catch (KeeperException e) {
+          log.warn("Exception while removing routing rule for route key: {}", 
routeKey, e);
+        } catch (Exception e) {
+          log.error("Exception while removing routing rule for route key: {}", 
routeKey, e);
+        } finally {
+          ruleExpiryLock.unlock();
+        }
+      }
+    } catch (InterruptedException e) {
+      Thread.currentThread().interrupt();
+    }
+  }
+
   private void doDefensiveChecks(DistribPhase phase, UpdateCommand 
updateCommand) {
     boolean isReplayOrPeersync =
         (updateCommand.getFlags() & (UpdateCommand.REPLAY | 
UpdateCommand.PEER_SYNC)) != 0;
diff --git a/solr/core/src/test/org/apache/solr/cloud/MigrateRouteKeyTest.java 
b/solr/core/src/test/org/apache/solr/cloud/MigrateRouteKeyTest.java
index 723f9d9ba6a..09114574f2a 100644
--- a/solr/core/src/test/org/apache/solr/cloud/MigrateRouteKeyTest.java
+++ b/solr/core/src/test/org/apache/solr/cloud/MigrateRouteKeyTest.java
@@ -100,6 +100,59 @@ public class MigrateRouteKeyTest extends SolrCloudTestCase 
{
     assertTrue(remoteSolrException.getMessage().contains("split.key cannot be 
null or empty"));
   }
 
+  @Test
+  public void updateSucceedsAfterMigrateTargetIsDeleted() throws Exception {
+    String sourceCollection = "deletedMigrateTarget-source";
+    CollectionAdminRequest.createCollection(sourceCollection, "conf", 1, 2)
+        .process(cluster.getSolrClient());
+    String targetCollection = "deletedMigrateTarget-target";
+    CollectionAdminRequest.createCollection(targetCollection, "conf", 1, 1)
+        .process(cluster.getSolrClient());
+
+    cluster.getSolrClient().add(sourceCollection, new SolrInputDocument("id", 
"a!1"));
+    cluster.getSolrClient().commit(sourceCollection);
+
+    invokeCollectionMigration(
+        CollectionAdminRequest.migrateData(sourceCollection, targetCollection, 
"a!")
+            .setForwardTimeout(45));
+    waitForState(
+        "Expected to find routing rule for split key a",
+        sourceCollection,
+        c -> {
+          if (c == null) return false;
+          Map<String, RoutingRule> routingRules = 
c.getSlice("shard1").getRoutingRules();
+          return routingRules != null && routingRules.containsKey("a!");
+        });
+
+    
CollectionAdminRequest.deleteCollection(targetCollection).process(cluster.getSolrClient());
+    waitForState("Expected target collection deletion", targetCollection, c -> 
c == null);
+
+    cluster.getSolrClient().add(sourceCollection, new SolrInputDocument("id", 
"a!2"));
+    cluster.getSolrClient().commit(sourceCollection);
+
+    DocCollection sourceState = getCollectionState(sourceCollection);
+    assertEquals(2, sourceState.getSlice("shard1").getReplicas().size());
+    for (Replica replica : sourceState.getSlice("shard1")) {
+      try (SolrClient replicaClient = getHttpSolrClient(replica)) {
+        SolrQuery query = new SolrQuery("id:\"a!2\"");
+        query.set("distrib", false);
+        assertEquals(
+            "Document missing from replica " + replica.getName(),
+            1,
+            replicaClient.query(query).getResults().getNumFound());
+      }
+    }
+
+    waitForState(
+        "Expected dangling routing rule removal",
+        sourceCollection,
+        c -> {
+          if (c == null) return false;
+          Map<String, RoutingRule> routingRules = 
c.getSlice("shard1").getRoutingRules();
+          return routingRules == null || !routingRules.containsKey("a!");
+        });
+  }
+
   @Test
   public void multipleShardMigrateTest() throws Exception {
 

Reply via email to