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

hossman 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 5c3465e  SOLR-8889: Fixed various problems in Solr and SolrJ that 
could cause deleteById commands with "_route_" information to processed by the 
wrong shard, and/or fail when forwarded to replicas from the shard leader.
5c3465e is described below

commit 5c3465eb498551c53dc310e44ae82cb89890ffc6
Author: Chris Hostetter <[email protected]>
AuthorDate: Mon Aug 16 09:56:51 2021 -0700

    SOLR-8889: Fixed various problems in Solr and SolrJ that could cause 
deleteById commands with "_route_" information to processed by the wrong shard, 
and/or fail when forwarded to replicas from the shard leader.
    
    Portions of this bug and fixes were tracked in SOLR-12694.
---
 solr/CHANGES.txt                                   |   3 +
 .../processor/DistributedZkUpdateProcessor.java    |   2 +-
 .../solr/cloud/FullSolrCloudDistribCmdsTest.java   | 165 +++++++++++-
 .../solr/update/DeleteByIdWithRouterFieldTest.java | 291 +++++++++++++++++++++
 .../solrj/request/JavaBinUpdateRequestCodec.java   |   9 +-
 .../solr/client/solrj/request/UpdateRequest.java   |   8 +-
 6 files changed, 469 insertions(+), 9 deletions(-)

diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt
index 01c697b..7261e75 100644
--- a/solr/CHANGES.txt
+++ b/solr/CHANGES.txt
@@ -420,6 +420,9 @@ Bug Fixes
 * SOLR-15579: Allow for many values in a SQL IN clause; previously using more 
than 19 would result in
   the IN clause being ignored, now users are only limited by the 
`maxBooleanClauses` configured for a collection (Timothy Potter)
 
+* SOLR-8889: Fixed various problems in Solr and SolrJ that could cause 
deleteById commands with "_route_" information to processed
+  by the wrong shard, and/or fail when forwarded to replicas from the shard 
leader. (Dan Fox, Yuki Yano, hossman)
+
 Other Changes
 ---------------------
 * SOLR-15566: Clarify ref guide documentation about SQL queries with `SELECT 
*` requiring a `LIMIT` clause (Timothy Potter)
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 fd475ac..e031cf7 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
@@ -575,7 +575,7 @@ public class DistributedZkUpdateProcessor extends 
DistributedUpdateProcessor {
       nodes = setupRequest(acmd.getIndexedIdStr(), 
acmd.getSolrInputDocument());
     } else if (cmd instanceof DeleteUpdateCommand) {
       DeleteUpdateCommand dcmd = (DeleteUpdateCommand)cmd;
-      nodes = setupRequest(dcmd.getId(), null);
+      nodes = setupRequest(dcmd.getId(), null, (null != dcmd.getRoute() ? 
dcmd.getRoute() : req.getParams().get(ShardParams._ROUTE_)) );
     }
   }
 
diff --git 
a/solr/core/src/test/org/apache/solr/cloud/FullSolrCloudDistribCmdsTest.java 
b/solr/core/src/test/org/apache/solr/cloud/FullSolrCloudDistribCmdsTest.java
index 381cce8..65937d7 100644
--- a/solr/core/src/test/org/apache/solr/cloud/FullSolrCloudDistribCmdsTest.java
+++ b/solr/core/src/test/org/apache/solr/cloud/FullSolrCloudDistribCmdsTest.java
@@ -18,6 +18,7 @@ package org.apache.solr.cloud;
 
 import java.lang.invoke.MethodHandles;
 
+import java.util.Arrays;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
@@ -27,9 +28,10 @@ import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Future;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicInteger;
-
+  
 import org.apache.lucene.util.TestUtil;
 import org.apache.lucene.util.LuceneTestCase.Slow;
+import org.apache.solr.client.solrj.SolrClient;
 import org.apache.solr.client.solrj.SolrQuery;
 import org.apache.solr.client.solrj.cloud.SocketProxy;
 import org.apache.solr.client.solrj.embedded.JettySolrRunner;
@@ -137,6 +139,167 @@ public class FullSolrCloudDistribCmdsTest extends 
SolrCloudTestCase {
     
   }
 
+  public void testDeleteByIdImplicitRouter() throws Exception {
+    final CloudSolrClient cloudClient = cluster.getSolrClient();
+    final String name = "implicit_collection_without_routerfield_" + 
NAME_COUNTER.getAndIncrement();
+    assertEquals(RequestStatusState.COMPLETED,
+                 
CollectionAdminRequest.createCollectionWithImplicitRouter(name, "_default", 
"shard1,shard2", 2)
+                 .processAndWait(cloudClient, DEFAULT_TIMEOUT));
+    cloudClient.waitForState(name, DEFAULT_TIMEOUT, TimeUnit.SECONDS,
+                             (n, c) -> DocCollection.isFullyActive(n, c, 2, 
2));
+    cloudClient.setDefaultCollection(name);
+
+    final DocCollection docCol =  
cloudClient.getZkStateReader().getClusterState().getCollection(name);
+    try (SolrClient shard1 = 
getHttpSolrClient(docCol.getSlice("shard1").getLeader().getCoreUrl());
+         SolrClient shard2 = 
getHttpSolrClient(docCol.getSlice("shard2").getLeader().getCoreUrl())) {
+         
+      // Add three documents to shard1
+      shard1.add(sdoc("id", "1", "title", "s1 one"));
+      shard1.add(sdoc("id", "2", "title", "s1 two"));
+      shard1.add(sdoc("id", "3", "title", "s1 three"));
+      shard1.commit();
+      final AtomicInteger docCounts1 = new AtomicInteger(3);
+      
+      // Add two documents to shard2
+      shard2.add(sdoc("id", "4", "title", "s2 four"));
+      shard2.add(sdoc("id", "5", "title", "s2 five"));
+      shard2.commit();
+      final AtomicInteger docCounts2 = new AtomicInteger(2);
+
+      // A re-usable helper to verify that the expected number of documents 
can be found on each shard...
+      Runnable checkShardCounts = () -> {
+        try {
+          // including cloudClient helps us test view from other nodes that 
aren't the leaders...
+          for (SolrClient c : Arrays.asList(cloudClient, shard1, shard2)) {
+            assertEquals(docCounts1.get() + docCounts2.get(), 
c.query(params("q", "*:*")).getResults().getNumFound());
+            
+            assertEquals(docCounts1.get(), c.query(params("q", "*:*", 
"shards", "shard1")).getResults().getNumFound());
+            assertEquals(docCounts2.get(), c.query(params("q", "*:*", 
"shards", "shard2")).getResults().getNumFound());
+            
+            assertEquals(docCounts1.get() + docCounts2.get(), 
c.query(params("q", "*:*", "shards", 
"shard2,shard1")).getResults().getNumFound());
+          }
+          
+          assertEquals(docCounts1.get(), shard1.query(params("q", "*:*", 
"distrib", "false")).getResults().getNumFound());
+          assertEquals(docCounts2.get(), shard2.query(params("q", "*:*", 
"distrib", "false")).getResults().getNumFound());
+          
+        } catch (Exception sse) {
+          throw new RuntimeException(sse);
+        }
+      };
+      checkShardCounts.run();
+
+      { // Send a delete request for a doc on shard1 to core hosting shard1 
with NO routing info
+        // Should delete (implicitly) since doc is (implicitly) located on 
this shard
+        final UpdateRequest deleteRequest = new UpdateRequest();
+        deleteRequest.deleteById("1");
+        shard1.request(deleteRequest);
+        shard1.commit();
+        docCounts1.decrementAndGet();
+      }
+      checkShardCounts.run();
+      
+      { // Send a delete request to core hosting shard1 with a route param for 
a document that is actually in shard2
+        // Should delete.
+        final UpdateRequest deleteRequest = new UpdateRequest();
+        deleteRequest.deleteById("4").withRoute("shard2");
+        shard1.request(deleteRequest);
+        shard1.commit();
+        docCounts2.decrementAndGet();
+      }
+      checkShardCounts.run();
+
+      { // Send a delete request to core hosting shard1 with NO route param 
for a document that is actually in shard2
+        // Shouldn't delete, since deleteById requests are not broadcast to 
all shard leaders.
+        // (This is effictively a request to delete "5" if an only if it is on 
shard1)
+        final UpdateRequest deleteRequest = new UpdateRequest();
+        deleteRequest.deleteById("5");
+        shard1.request(deleteRequest);
+        shard1.commit();
+      }
+      checkShardCounts.run();
+      
+      { // Multiple deleteById commands for different shards in a single 
request
+        final UpdateRequest deleteRequest = new UpdateRequest();
+        deleteRequest.deleteById("2", "shard1");
+        deleteRequest.deleteById("5", "shard2");
+        shard1.request(deleteRequest);
+        shard1.commit();
+        docCounts1.decrementAndGet();
+        docCounts2.decrementAndGet();
+      }
+      checkShardCounts.run();
+    }
+    
+  }
+
+  public void testDeleteByIdCompositeRouterWithRouterField() throws Exception {
+    final CloudSolrClient cloudClient = cluster.getSolrClient();
+    final String name = "composite_collection_with_routerfield_" + 
NAME_COUNTER.getAndIncrement();
+    assertEquals(RequestStatusState.COMPLETED,
+                 CollectionAdminRequest.createCollection(name, "_default", 2, 
2)
+                 .setRouterName("compositeId")
+                 .setRouterField("routefield_s")
+                 .setShards("shard1,shard2")
+                 .processAndWait(cloudClient, DEFAULT_TIMEOUT));
+    cloudClient.waitForState(name, DEFAULT_TIMEOUT, TimeUnit.SECONDS,
+                             (n, c) -> DocCollection.isFullyActive(n, c, 2, 
2));
+    cloudClient.setDefaultCollection(name);
+    
+    final DocCollection docCol =  
cloudClient.getZkStateReader().getClusterState().getCollection(name);
+    try (SolrClient shard1 = 
getHttpSolrClient(docCol.getSlice("shard1").getLeader().getCoreUrl());
+         SolrClient shard2 = 
getHttpSolrClient(docCol.getSlice("shard2").getLeader().getCoreUrl())) {
+
+      // Add three documents w/diff routes (all sent to shard1 leader's core)
+      shard1.add(sdoc("id", "1", "routefield_s", "europe"));
+      shard1.add(sdoc("id", "3", "routefield_s", "europe"));
+      shard1.add(sdoc("id", "5", "routefield_s", "africa"));
+      shard1.commit();
+
+      // Add two documents w/diff routes (all sent to shard2 leader's core)
+      shard2.add(sdoc("id", "4", "routefield_s", "africa"));
+      shard2.add(sdoc("id", "2", "routefield_s", "europe"));
+      shard2.commit();
+      
+      final AtomicInteger docCountsEurope = new AtomicInteger(3);
+      final AtomicInteger docCountsAfrica = new AtomicInteger(2);
+
+      // A re-usable helper to verify that the expected number of documents 
can be found based on _route_ key...
+      Runnable checkShardCounts = () -> {
+        try {
+          // including cloudClient helps us test view from other nodes that 
aren't the leaders...
+          for (SolrClient c : Arrays.asList(cloudClient, shard1, shard2)) {
+            assertEquals(docCountsEurope.get() + docCountsAfrica.get(), 
c.query(params("q", "*:*")).getResults().getNumFound());
+            
+            assertEquals(docCountsEurope.get(), c.query(params("q", "*:*", 
"_route_", "europe")).getResults().getNumFound());
+            assertEquals(docCountsAfrica.get(), c.query(params("q", "*:*", 
"_route_", "africa")).getResults().getNumFound());
+          }
+        } catch (Exception sse) {
+          throw new RuntimeException(sse);
+        }
+      };
+      checkShardCounts.run();
+      
+      { // Send a delete request to core hosting shard1 with a route param for 
a document that was originally added via core on shard2
+        final UpdateRequest deleteRequest = new UpdateRequest();
+        deleteRequest.deleteById("4", "africa");
+        shard1.request(deleteRequest);
+        shard1.commit();
+        docCountsAfrica.decrementAndGet();
+      }
+      checkShardCounts.run();
+      
+      { // Multiple deleteById commands with different routes in a single 
request
+        final UpdateRequest deleteRequest = new UpdateRequest();
+        deleteRequest.deleteById("2", "europe");
+        deleteRequest.deleteById("5", "africa");
+        shard1.request(deleteRequest);
+        shard1.commit();
+        docCountsEurope.decrementAndGet();
+        docCountsAfrica.decrementAndGet();
+      }
+      checkShardCounts.run();
+    }
+  }
 
   public void testThatCantForwardToLeaderFails() throws Exception {
     final CloudSolrClient cloudClient = cluster.getSolrClient();
diff --git 
a/solr/core/src/test/org/apache/solr/update/DeleteByIdWithRouterFieldTest.java 
b/solr/core/src/test/org/apache/solr/update/DeleteByIdWithRouterFieldTest.java
new file mode 100644
index 0000000..af16af4
--- /dev/null
+++ 
b/solr/core/src/test/org/apache/solr/update/DeleteByIdWithRouterFieldTest.java
@@ -0,0 +1,291 @@
+/*
+ * 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.update;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import org.apache.lucene.util.IOUtils;
+import org.apache.lucene.util.TestUtil;
+
+import org.apache.solr.client.solrj.SolrClient;
+import org.apache.solr.client.solrj.impl.LBSolrClient;
+import org.apache.solr.client.solrj.request.CollectionAdminRequest;
+import org.apache.solr.client.solrj.request.UpdateRequest;
+import org.apache.solr.cloud.SolrCloudTestCase;
+import org.apache.solr.cloud.CloudInspectUtil;
+import org.apache.solr.common.SolrDocumentList;
+import org.apache.solr.common.SolrInputDocument;
+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.params.ShardParams;
+import org.apache.solr.common.params.SolrParams;
+
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+
+public class DeleteByIdWithRouterFieldTest extends SolrCloudTestCase {
+
+  public static final String COLL = "test";
+  public static final String ROUTE_FIELD = "field_s";
+  public static final int NUM_SHARDS = 3;
+  
+  private static final List<SolrClient> clients = new ArrayList<>(); // not 
CloudSolrClient
+
+  /** 
+   * A randomized prefix to put on every route value.
+   * This helps ensure that documents wind up on diff shards between diff test 
runs
+   */
+  private static String RVAL_PRE = null;
+  
+  @BeforeClass
+  public static void setupClusterAndCollection() throws Exception {
+    RVAL_PRE = TestUtil.randomRealisticUnicodeString(random());
+    
+    // sometimes use 2 replicas of every shard so we hit more interesting 
update code paths
+    final int numReplicas = usually() ? 1 : 2;
+    
+    configureCluster(1 + (NUM_SHARDS * numReplicas) ) // we'll have one node 
that doesn't host any replicas
+      .addConfig("conf", configset("cloud-minimal"))
+      .configure();
+
+    assertTrue(CollectionAdminRequest.createCollection(COLL, "conf", 
NUM_SHARDS, numReplicas)
+               .setRouterField(ROUTE_FIELD)
+               .process(cluster.getSolrClient())
+               .isSuccess());
+    
+    cluster.getSolrClient().setDefaultCollection(COLL);
+
+    ClusterState clusterState = 
cluster.getSolrClient().getClusterStateProvider().getClusterState();
+    for (Replica replica : clusterState.getCollection(COLL).getReplicas()) {
+      clients.add(getHttpSolrClient(replica.getCoreUrl()));
+    }
+  }
+  
+  @AfterClass
+  public static void afterClass() throws Exception {
+    IOUtils.close(clients);
+    clients.clear();
+    RVAL_PRE = null;
+  }
+
+  @After
+  public void checkShardConsistencyAndCleanUp() throws Exception {
+    checkShardsConsistentNumFound();
+    assertEquals(0,
+                 new UpdateRequest()
+                 .deleteByQuery("*:*")
+                 .setAction(UpdateRequest.ACTION.COMMIT, true, true)
+                 .process(cluster.getSolrClient())
+                 .getStatus());
+  }
+  
+  private void checkShardsConsistentNumFound() throws Exception {
+    final SolrParams params = params("q", "*:*", "distrib", "false");
+    final DocCollection collection = 
cluster.getSolrClient().getZkStateReader().getClusterState().getCollection(COLL);
+    for (Map.Entry<String,Slice> entry : 
collection.getActiveSlicesMap().entrySet()) {
+      final String shardName = entry.getKey();
+      final Slice slice = entry.getValue();
+      final Replica leader = entry.getValue().getLeader();
+      try (SolrClient leaderClient = getHttpSolrClient(leader.getCoreUrl())) {
+        final SolrDocumentList leaderResults = 
leaderClient.query(params).getResults();
+        for (Replica replica : slice) {
+          try (SolrClient replicaClient = 
getHttpSolrClient(replica.getCoreUrl())) {
+            final SolrDocumentList replicaResults = 
replicaClient.query(params).getResults();
+            assertEquals("inconsistency w/leader: shard=" + shardName + 
"core=" + replica.getCoreName(),
+                         Collections.emptySet(),
+                         CloudInspectUtil.showDiff(leaderResults, 
replicaResults,
+                                                   shardName + " leader: " + 
leader.getCoreUrl(),
+                                                   shardName + ": " + 
replica.getCoreUrl()));
+          }
+        }
+      }
+    }
+  }
+  
+  private SolrClient getRandomSolrClient() { 
+    final int index = random().nextInt(clients.size() + 1);
+    return index == clients.size() ? cluster.getSolrClient() : 
clients.get(index);
+  }
+
+  /** 
+   * 100 docs with 2 digit ids and a route field that matches the last digit 
+   * @see #del100Docs
+   */
+  private UpdateRequest add100Docs() {
+    final UpdateRequest adds = new UpdateRequest();
+    for (int x = 0; x <= 9; x++) {
+      for (int y = 0; y <= 9; y++) {
+        adds.add("id", x+""+y, ROUTE_FIELD, RVAL_PRE+y);
+      }
+    }
+    return adds;
+  }
+  
+  /** 
+   * 100 doc deletions with 2 digit ids and a route field that matches the 
last digit 
+   * @see #add100Docs
+   */
+  private UpdateRequest del100Docs() {
+    final UpdateRequest dels = new UpdateRequest();
+    for (int x = 0; x <= 9; x++) {
+      for (int y = 0; y <= 9; y++) {
+        dels.deleteById(x+""+y, RVAL_PRE+y);
+      }
+    }
+    return dels;
+  }
+
+  public void testBlocksOfDeletes() throws Exception {
+
+    assertEquals(0, add100Docs().setAction(UpdateRequest.ACTION.COMMIT, true, 
true).process(getRandomSolrClient()).getStatus());
+    assertEquals(100, getRandomSolrClient().query(params("q", 
"*:*")).getResults().getNumFound());
+
+    // sanity check a "block" of 1 delete
+    assertEquals(0,
+                 new UpdateRequest()
+                 .deleteById("06", RVAL_PRE+"6")
+                 .setAction(UpdateRequest.ACTION.COMMIT, true, true)
+                 .process(getRandomSolrClient())
+                 .getStatus());
+    assertEquals(99, getRandomSolrClient().query(params("q", 
"*:*")).getResults().getNumFound());
+
+    checkShardsConsistentNumFound();
+    
+    // block of 2 deletes w/diff routes
+    assertEquals(0,
+                 new UpdateRequest()
+                 .deleteById("17", RVAL_PRE+"7")
+                 .deleteById("18", RVAL_PRE+"8")
+                 .setAction(UpdateRequest.ACTION.COMMIT, true, true)
+                 .process(getRandomSolrClient())
+                 .getStatus());
+    assertEquals(97, getRandomSolrClient().query(params("q", 
"*:*")).getResults().getNumFound());
+    
+    checkShardsConsistentNumFound();
+
+    // block of 2 deletes using single 'withRoute()' for both
+    assertEquals(0,
+                 new UpdateRequest()
+                 .deleteById("29")
+                 .deleteById("39")
+                 .withRoute(RVAL_PRE+"9")
+                 .setAction(UpdateRequest.ACTION.COMMIT, true, true)
+                 .process(getRandomSolrClient())
+                 .getStatus());
+    assertEquals(95, getRandomSolrClient().query(params("q", 
"*:*")).getResults().getNumFound());
+    
+    checkShardsConsistentNumFound();
+    
+    { // block of 2 deletes w/ diff routes that are conditional on optimistic 
concurrency
+      final Long v48 = (Long) getRandomSolrClient().query(params("q", "id:48", 
"fl", "_version_")).getResults().get(0).get("_version_");
+      final Long v49 = (Long) getRandomSolrClient().query(params("q", "id:49", 
"fl", "_version_")).getResults().get(0).get("_version_");
+
+      assertEquals(0,
+                   new UpdateRequest()
+                   .deleteById("48", RVAL_PRE+"8", v48)
+                   .deleteById("49", RVAL_PRE+"9", v49)
+                   .setAction(UpdateRequest.ACTION.COMMIT, true, true)
+                   .process(getRandomSolrClient())
+                   .getStatus());
+    }
+    assertEquals(93, getRandomSolrClient().query(params("q", 
"*:*")).getResults().getNumFound());
+    
+    checkShardsConsistentNumFound();
+    
+    { // block of 2 deletes using single 'withRoute()' for both that are 
conditional on optimistic concurrency
+      final Long v58 = (Long) getRandomSolrClient().query(params("q", "id:58", 
"fl", "_version_")).getResults().get(0).get("_version_");
+      final Long v68 = (Long) getRandomSolrClient().query(params("q", "id:68", 
"fl", "_version_")).getResults().get(0).get("_version_");
+
+      assertEquals(0,
+                   new UpdateRequest()
+                   .deleteById("58", v58)
+                   .deleteById("68", v68)
+                   .withRoute(RVAL_PRE+"8")
+                   .setAction(UpdateRequest.ACTION.COMMIT, true, true)
+                   .process(getRandomSolrClient())
+                   .getStatus());
+    }
+    assertEquals(91, getRandomSolrClient().query(params("q", 
"*:*")).getResults().getNumFound());
+    
+    checkShardsConsistentNumFound();
+    
+    // now delete all docs, including the ones we already deleted (shouldn't 
cause any problems)
+    assertEquals(0, del100Docs().setAction(UpdateRequest.ACTION.COMMIT, true, 
true).process(getRandomSolrClient()).getStatus());
+    assertEquals(0, getRandomSolrClient().query(params("q", 
"*:*")).getResults().getNumFound());
+    
+    checkShardsConsistentNumFound();
+  }
+
+
+  /**
+   * Test that {@link UpdateRequest#getRoutesToCollection} correctly populates 
routes for all deletes
+   */
+  public void testGlassBoxUpdateRequestRoutesToShards() throws Exception {
+
+    final DocCollection docCol = 
cluster.getSolrClient().getZkStateReader().getClusterState().getCollection(COLL);
+    // we don't need "real" urls for all replicas, just something we can use 
as lookup keys for verification
+    // so we'll use the shard names as "leader urls"
+    final Map<String,List<String>> urlMap = 
docCol.getActiveSlices().stream().collect
+      (Collectors.toMap(s -> s.getName(), s -> 
Collections.singletonList(s.getName())));
+
+    // simplified rote info we'll build up with the shards for each delete 
(after sanity checking they have routing info at all)...
+    final Map<String,String> actualDelRoutes = new LinkedHashMap<>();
+    
+    final Map<String,LBSolrClient.Req> rawDelRoutes = 
del100Docs().getRoutesToCollection(docCol.getRouter(), docCol, urlMap, 
params(), ROUTE_FIELD);
+    for (LBSolrClient.Req lbreq : rawDelRoutes.values()) {
+      assertTrue(lbreq.getRequest() instanceof UpdateRequest);
+      final String shard = lbreq.getServers().get(0);
+      final UpdateRequest req = (UpdateRequest) lbreq.getRequest();
+      for (Map.Entry<String,Map<String,Object>> entry : 
req.getDeleteByIdMap().entrySet()) {
+        final String id = entry.getKey();
+        // quick sanity checks...
+        assertNotNull(id + " has null values", entry.getValue());
+        final Object route = entry.getValue().get(ShardParams._ROUTE_);
+        assertNotNull(id + " has null route value", route);
+        assertEquals("route value is wrong for id: " + id, RVAL_PRE + 
id.substring(id.length() - 1), route.toString());
+
+        actualDelRoutes.put(id, shard);
+      }
+    }
+
+    // look at the routes computed from the "adds" as the expected value for 
the routes of each "del"
+    for (SolrInputDocument doc : add100Docs().getDocuments()) {
+      final String id = doc.getFieldValue("id").toString();
+      final String actualShard = actualDelRoutes.get(id);
+      assertNotNull(id + " delete is missing?", actualShard);
+      final Slice expectedShard = docCol.getRouter().getTargetSlice(id, doc, 
doc.getFieldValue(ROUTE_FIELD).toString(), params(), docCol);
+      assertNotNull(id + " add route is null?", expectedShard);
+      assertEquals("Wrong shard for delete of id: " + id,
+                   expectedShard.getName(), actualShard);
+    }
+
+    // sanity check no one broke our test and made it a waste of time
+    assertEquals(100, add100Docs().getDocuments().size());
+    assertEquals(100, actualDelRoutes.entrySet().size());
+  }
+  
+}
+
diff --git 
a/solr/solrj/src/java/org/apache/solr/client/solrj/request/JavaBinUpdateRequestCodec.java
 
b/solr/solrj/src/java/org/apache/solr/client/solrj/request/JavaBinUpdateRequestCodec.java
index 8147cc2..8bcb1d7 100644
--- 
a/solr/solrj/src/java/org/apache/solr/client/solrj/request/JavaBinUpdateRequestCodec.java
+++ 
b/solr/solrj/src/java/org/apache/solr/client/solrj/request/JavaBinUpdateRequestCodec.java
@@ -162,10 +162,11 @@ public class JavaBinUpdateRequestCodec {
         Map<String,Object> params = entry.getValue();
         if (params != null) {
           Long version = (Long) params.get(UpdateRequest.VER);
-          if (params.containsKey(ShardParams._ROUTE_))
-            updateRequest.deleteById(entry.getKey(), (String) 
params.get(ShardParams._ROUTE_));
-          else
-          updateRequest.deleteById(entry.getKey(), version);
+          if (params.containsKey(ShardParams._ROUTE_)) {
+            updateRequest.deleteById(entry.getKey(), (String) 
params.get(ShardParams._ROUTE_), version);
+          } else {
+            updateRequest.deleteById(entry.getKey(), version);
+          }
         } else {
           updateRequest.deleteById(entry.getKey());
         }
diff --git 
a/solr/solrj/src/java/org/apache/solr/client/solrj/request/UpdateRequest.java 
b/solr/solrj/src/java/org/apache/solr/client/solrj/request/UpdateRequest.java
index 39c0d07..20bae0e 100644
--- 
a/solr/solrj/src/java/org/apache/solr/client/solrj/request/UpdateRequest.java
+++ 
b/solr/solrj/src/java/org/apache/solr/client/solrj/request/UpdateRequest.java
@@ -305,10 +305,12 @@ public class UpdateRequest extends AbstractUpdateRequest {
         String deleteId = entry.getKey();
         Map<String,Object> map = entry.getValue();
         Long version = null;
+        String route = null;
         if (map != null) {
           version = (Long) map.get(VER);
+          route = (String) map.get(_ROUTE_);
         }
-        Slice slice = router.getTargetSlice(deleteId, null, null, null, col);
+        Slice slice = router.getTargetSlice(deleteId, null, route, null, col);
         if (slice == null) {
           return null;
         }
@@ -320,11 +322,11 @@ public class UpdateRequest extends AbstractUpdateRequest {
         T request = routes.get(leaderUrl);
         if (request != null) {
           UpdateRequest urequest = (UpdateRequest) request.getRequest();
-          urequest.deleteById(deleteId, version);
+          urequest.deleteById(deleteId, route, version);
         } else {
           UpdateRequest urequest = new UpdateRequest();
           urequest.setParams(params);
-          urequest.deleteById(deleteId, version);
+          urequest.deleteById(deleteId, route, version);
           urequest.setCommitWithin(getCommitWithin());
           urequest.setBasicAuthCredentials(getBasicAuthUser(), 
getBasicAuthPassword());
           request = reqSupplier.get(urequest, urls);

Reply via email to