You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@lucene.apache.org by no...@apache.org on 2018/01/31 10:49:40 UTC

lucene-solr:master: SOLR-11067: REPLACENODE should identify appropriate nodes if targetNode is not provided

Repository: lucene-solr
Updated Branches:
  refs/heads/master b310514be -> 3ad61d2f9


SOLR-11067: REPLACENODE should identify appropriate nodes if targetNode is not provided


Project: http://git-wip-us.apache.org/repos/asf/lucene-solr/repo
Commit: http://git-wip-us.apache.org/repos/asf/lucene-solr/commit/3ad61d2f
Tree: http://git-wip-us.apache.org/repos/asf/lucene-solr/tree/3ad61d2f
Diff: http://git-wip-us.apache.org/repos/asf/lucene-solr/diff/3ad61d2f

Branch: refs/heads/master
Commit: 3ad61d2f9ce62ac12ab6021ca3f3a1085d9c2d75
Parents: b310514
Author: Noble Paul <no...@apache.org>
Authored: Wed Jan 31 21:48:08 2018 +1100
Committer: Noble Paul <no...@apache.org>
Committed: Wed Jan 31 21:49:02 2018 +1100

----------------------------------------------------------------------
 solr/CHANGES.txt                                |   2 +
 .../cloud/api/collections/AddReplicaCmd.java    |   6 +-
 .../cloud/api/collections/ReplaceNodeCmd.java   | 125 +++++++++++--------
 .../solr/handler/admin/CollectionsHandler.java  |  24 ++--
 .../solr/cloud/ReplaceNodeNoTargetTest.java     |  98 +++++++++++++++
 .../org/apache/solr/cloud/ReplaceNodeTest.java  |   4 +-
 .../solrj/request/CollectionAdminRequest.java   |   2 +-
 7 files changed, 189 insertions(+), 72 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/3ad61d2f/solr/CHANGES.txt
----------------------------------------------------------------------
diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt
index 1ce9e88..539bf24 100644
--- a/solr/CHANGES.txt
+++ b/solr/CHANGES.txt
@@ -242,6 +242,8 @@ Other Changes
 
 * SOLR-11480: Remove unused "Admin Extra" files and mentions. (Eric Pugh, Christine Poerschke)
 
+* SOLR-11067: REPLACENODE should identify appropriate nodes if targetNode is not provided (noble)
+
 ==================  7.2.1 ==================
 
 Consult the LUCENE_CHANGES.txt file for additional, low level, changes in this release.

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/3ad61d2f/solr/core/src/java/org/apache/solr/cloud/api/collections/AddReplicaCmd.java
----------------------------------------------------------------------
diff --git a/solr/core/src/java/org/apache/solr/cloud/api/collections/AddReplicaCmd.java b/solr/core/src/java/org/apache/solr/cloud/api/collections/AddReplicaCmd.java
index 6b4e427..dba27f6 100644
--- a/solr/core/src/java/org/apache/solr/cloud/api/collections/AddReplicaCmd.java
+++ b/solr/core/src/java/org/apache/solr/cloud/api/collections/AddReplicaCmd.java
@@ -242,9 +242,9 @@ public class AddReplicaCmd implements OverseerCollectionMessageHandler.Cmd {
               collection,
               message,
               Collections.singletonList(shard),
-              replicaType == Replica.Type.NRT ? 0 : 1,
-              replicaType == Replica.Type.TLOG ? 0 : 1,
-              replicaType == Replica.Type.PULL ? 0 : 1
+              replicaType == Replica.Type.NRT ? 1 : 0,
+              replicaType == Replica.Type.TLOG ? 1 : 0,
+              replicaType == Replica.Type.PULL ? 1 : 0
           ).get(0).node;
           sessionWrapper.set(PolicyHelper.getLastSessionWrapper(true));
         }

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/3ad61d2f/solr/core/src/java/org/apache/solr/cloud/api/collections/ReplaceNodeCmd.java
----------------------------------------------------------------------
diff --git a/solr/core/src/java/org/apache/solr/cloud/api/collections/ReplaceNodeCmd.java b/solr/core/src/java/org/apache/solr/cloud/api/collections/ReplaceNodeCmd.java
index 35d2379..d08b519 100644
--- a/solr/core/src/java/org/apache/solr/cloud/api/collections/ReplaceNodeCmd.java
+++ b/solr/core/src/java/org/apache/solr/cloud/api/collections/ReplaceNodeCmd.java
@@ -27,7 +27,9 @@ import java.util.Locale;
 import java.util.Map;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
 
+import org.apache.solr.client.solrj.cloud.autoscaling.PolicyHelper;
 import org.apache.solr.cloud.ActiveReplicaWatcher;
 import org.apache.solr.common.SolrCloseableLatch;
 import org.apache.solr.common.SolrException;
@@ -65,8 +67,8 @@ public class ReplaceNodeCmd implements OverseerCollectionMessageHandler.Cmd {
     String source = message.getStr(CollectionParams.SOURCE_NODE, message.getStr("source"));
     String target = message.getStr(CollectionParams.TARGET_NODE, message.getStr("target"));
     boolean waitForFinalState = message.getBool(CommonAdminParams.WAIT_FOR_FINAL_STATE, false);
-    if (source == null || target == null) {
-      throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "sourceNode and targetNode are required params" );
+    if (source == null) {
+      throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "sourceNode is a required param");
     }
     String async = message.getStr("async");
     int timeout = message.getInt("timeout", 10 * 60); // 10 minutes
@@ -76,7 +78,7 @@ public class ReplaceNodeCmd implements OverseerCollectionMessageHandler.Cmd {
     if (!clusterState.liveNodesContain(source)) {
       throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Source Node: " + source + " is not live");
     }
-    if (!clusterState.liveNodesContain(target)) {
+    if (target != null && !clusterState.liveNodesContain(target)) {
       throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Target Node: " + target + " is not live");
     }
     List<ZkNodeProps> sourceReplicas = getReplicasOfNode(source, clusterState);
@@ -98,64 +100,83 @@ public class ReplaceNodeCmd implements OverseerCollectionMessageHandler.Cmd {
     SolrCloseableLatch countDownLatch = new SolrCloseableLatch(sourceReplicas.size(), ocmh);
 
     SolrCloseableLatch replicasToRecover = new SolrCloseableLatch(numLeaders, ocmh);
-
-    for (ZkNodeProps sourceReplica : sourceReplicas) {
-      NamedList nl = new NamedList();
-      log.info("Going to create replica for collection={} shard={} on node={}", sourceReplica.getStr(COLLECTION_PROP), sourceReplica.getStr(SHARD_ID_PROP), target);
-      ZkNodeProps msg = sourceReplica.plus("parallel", String.valueOf(parallel)).plus(CoreAdminParams.NODE, target);
-      if(async!=null) msg.getProperties().put(ASYNC, async);
-      final ZkNodeProps addedReplica = ocmh.addReplica(clusterState,
-          msg, nl, () -> {
-            countDownLatch.countDown();
-            if (nl.get("failure") != null) {
-              String errorString = String.format(Locale.ROOT, "Failed to create replica for collection=%s shard=%s" +
-                  " on node=%s", sourceReplica.getStr(COLLECTION_PROP), sourceReplica.getStr(SHARD_ID_PROP), target);
-              log.warn(errorString);
-              // one replica creation failed. Make the best attempt to
-              // delete all the replicas created so far in the target
-              // and exit
-              synchronized (results) {
-                results.add("failure", errorString);
-                anyOneFailed.set(true);
+    AtomicReference<PolicyHelper.SessionWrapper> sessionWrapperRef = new AtomicReference<>();
+    try {
+      for (ZkNodeProps sourceReplica : sourceReplicas) {
+        NamedList nl = new NamedList();
+        log.info("Going to create replica for collection={} shard={} on node={}", sourceReplica.getStr(COLLECTION_PROP), sourceReplica.getStr(SHARD_ID_PROP), target);
+        String targetNode = target;
+        if (targetNode == null) {
+          Replica.Type replicaType = Replica.Type.get(sourceReplica.getStr(ZkStateReader.REPLICA_TYPE));
+          targetNode = Assign.identifyNodes(ocmh.cloudManager,
+              clusterState,
+              new ArrayList<>(ocmh.cloudManager.getClusterStateProvider().getLiveNodes()),
+              sourceReplica.getStr(COLLECTION_PROP),
+              message,
+              Collections.singletonList(sourceReplica.getStr(SHARD_ID_PROP)),
+              replicaType == Replica.Type.NRT ? 1: 0,
+              replicaType == Replica.Type.TLOG ? 1 : 0,
+              replicaType == Replica.Type.PULL ? 1 : 0
+          ).get(0).node;
+          sessionWrapperRef.set(PolicyHelper.getLastSessionWrapper(true));
+        }
+        ZkNodeProps msg = sourceReplica.plus("parallel", String.valueOf(parallel)).plus(CoreAdminParams.NODE, targetNode);
+        if (async != null) msg.getProperties().put(ASYNC, async);
+        final ZkNodeProps addedReplica = ocmh.addReplica(clusterState,
+            msg, nl, () -> {
+              countDownLatch.countDown();
+              if (nl.get("failure") != null) {
+                String errorString = String.format(Locale.ROOT, "Failed to create replica for collection=%s shard=%s" +
+                    " on node=%s", sourceReplica.getStr(COLLECTION_PROP), sourceReplica.getStr(SHARD_ID_PROP), target);
+                log.warn(errorString);
+                // one replica creation failed. Make the best attempt to
+                // delete all the replicas created so far in the target
+                // and exit
+                synchronized (results) {
+                  results.add("failure", errorString);
+                  anyOneFailed.set(true);
+                }
+              } else {
+                log.debug("Successfully created replica for collection={} shard={} on node={}",
+                    sourceReplica.getStr(COLLECTION_PROP), sourceReplica.getStr(SHARD_ID_PROP), target);
               }
+            });
+
+        if (addedReplica != null) {
+          createdReplicas.add(addedReplica);
+          if (sourceReplica.getBool(ZkStateReader.LEADER_PROP, false) || waitForFinalState) {
+            String shardName = sourceReplica.getStr(SHARD_ID_PROP);
+            String replicaName = sourceReplica.getStr(ZkStateReader.REPLICA_PROP);
+            String collectionName = sourceReplica.getStr(COLLECTION_PROP);
+            String key = collectionName + "_" + replicaName;
+            CollectionStateWatcher watcher;
+            if (waitForFinalState) {
+              watcher = new ActiveReplicaWatcher(collectionName, null,
+                  Collections.singletonList(addedReplica.getStr(ZkStateReader.CORE_NAME_PROP)), replicasToRecover);
             } else {
-              log.debug("Successfully created replica for collection={} shard={} on node={}",
-                  sourceReplica.getStr(COLLECTION_PROP), sourceReplica.getStr(SHARD_ID_PROP), target);
+              watcher = new LeaderRecoveryWatcher(collectionName, shardName, replicaName,
+                  addedReplica.getStr(ZkStateReader.CORE_NAME_PROP), replicasToRecover);
             }
-          });
-
-      if (addedReplica != null) {
-        createdReplicas.add(addedReplica);
-        if (sourceReplica.getBool(ZkStateReader.LEADER_PROP, false) || waitForFinalState) {
-          String shardName = sourceReplica.getStr(SHARD_ID_PROP);
-          String replicaName = sourceReplica.getStr(ZkStateReader.REPLICA_PROP);
-          String collectionName = sourceReplica.getStr(COLLECTION_PROP);
-          String key = collectionName + "_" + replicaName;
-          CollectionStateWatcher watcher;
-          if (waitForFinalState) {
-            watcher = new ActiveReplicaWatcher(collectionName, null,
-                Collections.singletonList(addedReplica.getStr(ZkStateReader.CORE_NAME_PROP)), replicasToRecover);
+            watchers.put(key, watcher);
+            log.debug("--- adding " + key + ", " + watcher);
+            zkStateReader.registerCollectionStateWatcher(collectionName, watcher);
           } else {
-            watcher = new LeaderRecoveryWatcher(collectionName, shardName, replicaName,
-                addedReplica.getStr(ZkStateReader.CORE_NAME_PROP), replicasToRecover);
+            log.debug("--- not waiting for " + addedReplica);
           }
-          watchers.put(key, watcher);
-          log.debug("--- adding " + key + ", " + watcher);
-          zkStateReader.registerCollectionStateWatcher(collectionName, watcher);
-        } else {
-          log.debug("--- not waiting for " + addedReplica);
         }
       }
-    }
 
-    log.debug("Waiting for replicas to be added");
-    if (!countDownLatch.await(timeout, TimeUnit.SECONDS)) {
-      log.info("Timed out waiting for replicas to be added");
-      anyOneFailed.set(true);
-    } else {
-      log.debug("Finished waiting for replicas to be added");
+      log.debug("Waiting for replicas to be added");
+      if (!countDownLatch.await(timeout, TimeUnit.SECONDS)) {
+        log.info("Timed out waiting for replicas to be added");
+        anyOneFailed.set(true);
+      } else {
+        log.debug("Finished waiting for replicas to be added");
+      }
+    } finally {
+      PolicyHelper.SessionWrapper sw = sessionWrapperRef.get();
+      if (sw != null) sw.release();
     }
-
     // now wait for leader replicas to recover
     log.debug("Waiting for " + numLeaders + " leader replicas to recover");
     if (!replicasToRecover.await(timeout, TimeUnit.SECONDS)) {

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/3ad61d2f/solr/core/src/java/org/apache/solr/handler/admin/CollectionsHandler.java
----------------------------------------------------------------------
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/CollectionsHandler.java b/solr/core/src/java/org/apache/solr/handler/admin/CollectionsHandler.java
index dcc3de6..4e01700 100644
--- a/solr/core/src/java/org/apache/solr/handler/admin/CollectionsHandler.java
+++ b/solr/core/src/java/org/apache/solr/handler/admin/CollectionsHandler.java
@@ -31,8 +31,8 @@ import java.util.Optional;
 import java.util.OptionalLong;
 import java.util.Set;
 import java.util.concurrent.TimeUnit;
-import java.util.stream.Collectors;
 import java.util.function.BiConsumer;
+import java.util.stream.Collectors;
 
 import com.google.common.collect.ImmutableSet;
 import org.apache.commons.io.IOUtils;
@@ -105,9 +105,6 @@ import static org.apache.solr.client.solrj.response.RequestStatusState.NOT_FOUND
 import static org.apache.solr.client.solrj.response.RequestStatusState.RUNNING;
 import static org.apache.solr.client.solrj.response.RequestStatusState.SUBMITTED;
 import static org.apache.solr.cloud.Overseer.QUEUE_OPERATION;
-import static org.apache.solr.cloud.api.collections.TimeRoutedAlias.CREATE_COLLECTION_PREFIX;
-import static org.apache.solr.cloud.api.collections.TimeRoutedAlias.OPTIONAL_ROUTER_PARAMS;
-import static org.apache.solr.cloud.api.collections.TimeRoutedAlias.REQUIRED_ROUTER_PARAMS;
 import static org.apache.solr.cloud.api.collections.OverseerCollectionMessageHandler.COLL_CONF;
 import static org.apache.solr.cloud.api.collections.OverseerCollectionMessageHandler.COLL_PROP_PREFIX;
 import static org.apache.solr.cloud.api.collections.OverseerCollectionMessageHandler.CREATE_NODE_SET;
@@ -119,6 +116,9 @@ import static org.apache.solr.cloud.api.collections.OverseerCollectionMessageHan
 import static org.apache.solr.cloud.api.collections.OverseerCollectionMessageHandler.REQUESTID;
 import static org.apache.solr.cloud.api.collections.OverseerCollectionMessageHandler.SHARDS_PROP;
 import static org.apache.solr.cloud.api.collections.OverseerCollectionMessageHandler.SHARD_UNIQUE;
+import static org.apache.solr.cloud.api.collections.TimeRoutedAlias.CREATE_COLLECTION_PREFIX;
+import static org.apache.solr.cloud.api.collections.TimeRoutedAlias.OPTIONAL_ROUTER_PARAMS;
+import static org.apache.solr.cloud.api.collections.TimeRoutedAlias.REQUIRED_ROUTER_PARAMS;
 import static org.apache.solr.common.SolrException.ErrorCode.BAD_REQUEST;
 import static org.apache.solr.common.cloud.DocCollection.DOC_ROUTER;
 import static org.apache.solr.common.cloud.DocCollection.RULE;
@@ -987,16 +987,12 @@ public class CollectionsHandler extends RequestHandlerBase implements Permission
       return null;
     }),
     REPLACENODE_OP(REPLACENODE, (req, rsp, h) -> {
-      SolrParams params = req.getParams();
-      String sourceNode = params.get(CollectionParams.SOURCE_NODE, params.get("source"));
-      if (sourceNode == null) {
-        throw new SolrException(ErrorCode.BAD_REQUEST, CollectionParams.SOURCE_NODE + " is a require parameter");
-      }
-      String targetNode = params.get(CollectionParams.TARGET_NODE, params.get("target"));
-      if (targetNode == null) {
-        throw new SolrException(ErrorCode.BAD_REQUEST, CollectionParams.TARGET_NODE + " is a require parameter");
-      }
-      return params.getAll(null, "source", "target", WAIT_FOR_FINAL_STATE, CollectionParams.SOURCE_NODE, CollectionParams.TARGET_NODE);
+      return req.getParams().getAll(null,
+          "source", //legacy
+          "target",//legacy
+          WAIT_FOR_FINAL_STATE,
+          CollectionParams.SOURCE_NODE,
+          CollectionParams.TARGET_NODE);
     }),
     MOVEREPLICA_OP(MOVEREPLICA, (req, rsp, h) -> {
       Map<String, Object> map = req.getParams().required().getAll(null,

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/3ad61d2f/solr/core/src/test/org/apache/solr/cloud/ReplaceNodeNoTargetTest.java
----------------------------------------------------------------------
diff --git a/solr/core/src/test/org/apache/solr/cloud/ReplaceNodeNoTargetTest.java b/solr/core/src/test/org/apache/solr/cloud/ReplaceNodeNoTargetTest.java
new file mode 100644
index 0000000..769ddce
--- /dev/null
+++ b/solr/core/src/test/org/apache/solr/cloud/ReplaceNodeNoTargetTest.java
@@ -0,0 +1,98 @@
+/*
+ * 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.cloud;
+
+
+import java.lang.invoke.MethodHandles;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Set;
+
+import org.apache.solr.client.solrj.SolrRequest;
+import org.apache.solr.client.solrj.impl.CloudSolrClient;
+import org.apache.solr.client.solrj.impl.HttpSolrClient;
+import org.apache.solr.client.solrj.request.CollectionAdminRequest;
+import org.apache.solr.client.solrj.request.CoreAdminRequest;
+import org.apache.solr.client.solrj.response.CoreAdminResponse;
+import org.apache.solr.client.solrj.response.RequestStatusState;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import static org.apache.solr.cloud.ReplaceNodeTest.createReplaceNodeRequest;
+import static org.apache.solr.cloud.autoscaling.AutoScalingHandlerTest.createAutoScalingRequest;
+
+public class ReplaceNodeNoTargetTest extends SolrCloudTestCase {
+  private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+  @BeforeClass
+  public static void setupCluster() throws Exception {
+    configureCluster(6)
+        .addConfig("conf1", TEST_PATH().resolve("configsets").resolve("cloud-dynamic").resolve("conf"))
+        .configure();
+  }
+
+  protected String getSolrXml() {
+    return "solr.xml";
+  }
+
+
+  @Test
+  public void test() throws Exception {
+    cluster.waitForAllNodes(5000);
+    String coll = "replacenodetest_coll_notarget";
+    log.info("total_jettys: " + cluster.getJettySolrRunners().size());
+
+    CloudSolrClient cloudClient = cluster.getSolrClient();
+    Set<String> liveNodes = cloudClient.getZkStateReader().getClusterState().getLiveNodes();
+    ArrayList<String> l = new ArrayList<>(liveNodes);
+    Collections.shuffle(l, random());
+    String node2bdecommissioned = l.get(0);
+    CloudSolrClient solrClient = cluster.getSolrClient();
+    String setClusterPolicyCommand = "{" +
+        " 'set-cluster-policy': [" +
+        "      {'replica':'<5', 'shard': '#EACH', 'node': '#ANY'}]}";
+    SolrRequest req = createAutoScalingRequest(SolrRequest.METHOD.POST, setClusterPolicyCommand);
+    solrClient.request(req);
+
+    CollectionAdminRequest.Create create = CollectionAdminRequest.createCollection(coll, "conf1", 5, 2, 0, 0);
+
+    cloudClient.request(create);
+    createReplaceNodeRequest(node2bdecommissioned, null, null).processAsync("001", cloudClient);
+    CollectionAdminRequest.RequestStatus requestStatus = CollectionAdminRequest.requestStatus("001");
+    boolean success = false;
+    for (int i = 0; i < 300; i++) {
+      CollectionAdminRequest.RequestStatusResponse rsp = requestStatus.process(cloudClient);
+      if (rsp.getRequestStatus() == RequestStatusState.COMPLETED) {
+        success = true;
+        break;
+      }
+      assertFalse(rsp.getRequestStatus() == RequestStatusState.FAILED);
+      Thread.sleep(50);
+    }
+    assertTrue(success);
+    try (HttpSolrClient coreclient = getHttpSolrClient(cloudClient.getZkStateReader().getBaseUrlForNodeName(node2bdecommissioned))) {
+      CoreAdminResponse status = CoreAdminRequest.getStatus(null, coreclient);
+      assertTrue(status.getCoreStatus().size() == 0);
+    }
+
+  }
+
+
+}

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/3ad61d2f/solr/core/src/test/org/apache/solr/cloud/ReplaceNodeTest.java
----------------------------------------------------------------------
diff --git a/solr/core/src/test/org/apache/solr/cloud/ReplaceNodeTest.java b/solr/core/src/test/org/apache/solr/cloud/ReplaceNodeTest.java
index f5ed310..fbee9de 100644
--- a/solr/core/src/test/org/apache/solr/cloud/ReplaceNodeTest.java
+++ b/solr/core/src/test/org/apache/solr/cloud/ReplaceNodeTest.java
@@ -166,7 +166,7 @@ public class ReplaceNodeTest extends SolrCloudTestCase {
     }
   }
 
-  private CollectionAdminRequest.AsyncCollectionAdminRequest createReplaceNodeRequest(String sourceNode, String targetNode, Boolean parallel) {
+  public static  CollectionAdminRequest.AsyncCollectionAdminRequest createReplaceNodeRequest(String sourceNode, String targetNode, Boolean parallel) {
     if (random().nextBoolean()) {
       return new CollectionAdminRequest.ReplaceNode(sourceNode, targetNode).setParallel(parallel);
     } else  {
@@ -177,7 +177,7 @@ public class ReplaceNodeTest extends SolrCloudTestCase {
         public SolrParams getParams() {
           ModifiableSolrParams params = (ModifiableSolrParams) super.getParams();
           params.set("source", sourceNode);
-          params.set("target", targetNode);
+          params.setNonNull("target", targetNode);
           if (parallel != null) params.set("parallel", parallel.toString());
           return params;
         }

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/3ad61d2f/solr/solrj/src/java/org/apache/solr/client/solrj/request/CollectionAdminRequest.java
----------------------------------------------------------------------
diff --git a/solr/solrj/src/java/org/apache/solr/client/solrj/request/CollectionAdminRequest.java b/solr/solrj/src/java/org/apache/solr/client/solrj/request/CollectionAdminRequest.java
index 8953f2e..1738bb0 100644
--- a/solr/solrj/src/java/org/apache/solr/client/solrj/request/CollectionAdminRequest.java
+++ b/solr/solrj/src/java/org/apache/solr/client/solrj/request/CollectionAdminRequest.java
@@ -584,7 +584,7 @@ public abstract class CollectionAdminRequest<T extends CollectionAdminResponse>
     public ReplaceNode(String source, String target) {
       super(CollectionAction.REPLACENODE);
       this.sourceNode = checkNotNull(CollectionParams.SOURCE_NODE, source);
-      this.targetNode = checkNotNull(CollectionParams.TARGET_NODE, target);
+      this.targetNode = target;
     }
 
     public ReplaceNode setParallel(Boolean flag) {