gerlowskija commented on code in PR #1115:
URL: https://github.com/apache/solr/pull/1115#discussion_r1005806358


##########
solr/core/src/java/org/apache/solr/handler/admin/CollectionsHandler.java:
##########
@@ -1791,14 +1794,17 @@ public Map<String, Object> execute(
     REPLACENODE_OP(
         REPLACENODE,
         (req, rsp, h) -> {
-          return copy(
-              req.getParams(),
-              null,
-              "source", // legacy
-              "target", // legacy
-              WAIT_FOR_FINAL_STATE,
-              CollectionParams.SOURCE_NODE,
-              CollectionParams.TARGET_NODE);
+          final SolrParams params = req.getParams();
+          final RequiredSolrParams requiredParams = req.getParams().required();
+          final ReplaceNodeAPI.ReplaceNodeRequestBody requestBody =
+              new ReplaceNodeAPI.ReplaceNodeRequestBody();
+          requestBody.targetNodeName = params.get(TARGET_NODE);
+          requestBody.waitForFinalState = params.getBool(WAIT_FOR_FINAL_STATE, 
false);

Review Comment:
   [0] Prior to this PR, if the user didn't explicitly specify a 
waitForFinalState value, the created "message" wouldn't contain that key at all.
   
   The use of `getBool(key, defaultVal)` here means that the constructed RNRB 
will _always_ have a waitForFinalState value that gets added to the "message" 
that the v2 code builds.  This isn't a problem necessarily; the docs say that 
this parameter defaults to "false" - so this shouldn't change any behavior.  
But it is something to keep an eye on if test failures crop up later.
   
   If you wanted to be more conservative though, you could drop the default-val 
here to keep closer in line with the existing behavior.



##########
solr/core/src/java/org/apache/solr/handler/admin/api/ReplaceNodeAPI.java:
##########
@@ -0,0 +1,121 @@
+/*
+ * 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.handler.admin.api;
+
+import static 
org.apache.solr.client.solrj.impl.BinaryResponseParser.BINARY_CONTENT_TYPE_V2;
+import static org.apache.solr.cloud.Overseer.QUEUE_OPERATION;
+import static org.apache.solr.common.params.CollectionParams.SOURCE_NODE;
+import static org.apache.solr.common.params.CollectionParams.TARGET_NODE;
+import static 
org.apache.solr.common.params.CommonAdminParams.WAIT_FOR_FINAL_STATE;
+import static 
org.apache.solr.handler.admin.CollectionsHandler.DEFAULT_COLLECTION_OP_TIMEOUT;
+import static 
org.apache.solr.security.PermissionNameProvider.Name.COLL_EDIT_PERM;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.media.Schema;
+import io.swagger.v3.oas.annotations.parameters.RequestBody;
+import java.util.HashMap;
+import java.util.Map;
+import javax.inject.Inject;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import org.apache.solr.client.solrj.SolrResponse;
+import org.apache.solr.common.cloud.ZkNodeProps;
+import org.apache.solr.common.params.CollectionParams;
+import org.apache.solr.common.params.CollectionParams.CollectionAction;
+import org.apache.solr.core.CoreContainer;
+import org.apache.solr.handler.admin.CollectionsHandler;
+import org.apache.solr.jersey.JacksonReflectMapWriter;
+import org.apache.solr.jersey.PermissionName;
+import org.apache.solr.jersey.SolrJerseyResponse;
+import org.apache.solr.request.SolrQueryRequest;
+import org.apache.solr.response.SolrQueryResponse;
+
+/**
+ * V2 API for recreating replicas in one node (the source) on another node(s) 
(the target).
+ *
+ * <p>This API is analogous to the v1 /admin/collections?action=REPLACENODE 
command.
+ */
+@Path("cluster/nodes/{sourceNodeName}/commands/replace/")
+public class ReplaceNodeAPI extends AdminAPIBase {
+
+  @Inject
+  public ReplaceNodeAPI(
+      CoreContainer coreContainer,
+      SolrQueryRequest solrQueryRequest,
+      SolrQueryResponse solrQueryResponse) {
+    super(coreContainer, solrQueryRequest, solrQueryResponse);
+  }
+
+  @POST
+  @Produces({"application/json", "application/xml", BINARY_CONTENT_TYPE_V2})
+  @PermissionName(COLL_EDIT_PERM)
+  public SolrJerseyResponse replaceNode(
+      @Parameter(description = "The name of the node to be replaced.", 
required = true)
+          @PathParam("sourceNodeName")
+          String nodeName,
+      @RequestBody(description = "Contains user provided parameters", required 
= true)
+          ReplaceNodeRequestBody requestBody)
+      throws Exception {
+    final SolrJerseyResponse response = 
instantiateJerseyResponse(SolrJerseyResponse.class);
+    final CoreContainer coreContainer = 
fetchAndValidateZooKeeperAwareCoreContainer();
+    /** TODO Record node for log and tracing */
+    final ZkNodeProps remoteMessage = createRemoteMessage(nodeName, 
requestBody);
+    final SolrResponse remoteResponse =
+        CollectionsHandler.submitCollectionApiCommand(
+            coreContainer,
+            coreContainer.getDistributedCollectionCommandRunner(),
+            remoteMessage,
+            CollectionParams.CollectionAction.REPLACENODE,
+            DEFAULT_COLLECTION_OP_TIMEOUT);
+    if (remoteResponse.getException() != null) {
+      throw remoteResponse.getException();
+    }
+
+    disableResponseCaching();
+    return response;
+  }
+
+  public ZkNodeProps createRemoteMessage(String nodeName, 
ReplaceNodeRequestBody requestBody) {
+    final Map<String, Object> remoteMessage = new HashMap<>();
+    remoteMessage.put(SOURCE_NODE, nodeName);
+    remoteMessage.put(TARGET_NODE, requestBody.targetNodeName);
+    remoteMessage.put(WAIT_FOR_FINAL_STATE, requestBody.waitForFinalState);
+    remoteMessage.put(QUEUE_OPERATION, CollectionAction.REPLACENODE.toLower());
+
+    return new ZkNodeProps(remoteMessage);
+  }
+
+  public static class ReplaceNodeRequestBody implements 
JacksonReflectMapWriter {
+
+    public ReplaceNodeRequestBody() {}
+
+    public ReplaceNodeRequestBody(String targetNodeName, Boolean 
waitForFinalState) {
+      this.targetNodeName = targetNodeName;
+      this.waitForFinalState = waitForFinalState;
+    }
+
+    @Schema(description = "The name of the targetNode.")
+    @JsonProperty("targetNodeName")
+    public String targetNodeName;
+
+    @JsonProperty("waitForFinalState")

Review Comment:
   [0] The `@Schema` annotations you see on this code end up used as 
descriptions in the OpenAPI spec that Solr generates as part of its build.
   
   If you're not familiar with OpenAPI, it's really awesome! It provides 
tooling you can use to generate a "spec" - a machine-readable description of 
your API.  And then other tooling can take that spec and do all sorts of things 
with it, like generating clients for various languages.
   
   Right now, Solr's set up to generate a spec (run `./gradlew resolve` and 
take a look at `solr/core/build/generated/openapi/openapi.json`).  We haven't 
started using the spec yet, but it's on my roadmap to set us up to generate 
Solr clients for Python, golang, etc.
   
   Anyway, this is all to say: these `@Schema` annotations don't seem like they 
do much now, but they'll be super helpful to have in place once we start using 
the spec.
   
   You don't need to have them everywhere if you're not sure what to say about 
certain parameters.  But if you want to include them, you can always copy the 
snippet in the ref guide that describes the parameter.



##########
solr/core/src/java/org/apache/solr/handler/admin/api/ReplaceNodeAPI.java:
##########
@@ -0,0 +1,121 @@
+/*
+ * 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.handler.admin.api;
+
+import static 
org.apache.solr.client.solrj.impl.BinaryResponseParser.BINARY_CONTENT_TYPE_V2;
+import static org.apache.solr.cloud.Overseer.QUEUE_OPERATION;
+import static org.apache.solr.common.params.CollectionParams.SOURCE_NODE;
+import static org.apache.solr.common.params.CollectionParams.TARGET_NODE;
+import static 
org.apache.solr.common.params.CommonAdminParams.WAIT_FOR_FINAL_STATE;
+import static 
org.apache.solr.handler.admin.CollectionsHandler.DEFAULT_COLLECTION_OP_TIMEOUT;
+import static 
org.apache.solr.security.PermissionNameProvider.Name.COLL_EDIT_PERM;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.media.Schema;
+import io.swagger.v3.oas.annotations.parameters.RequestBody;
+import java.util.HashMap;
+import java.util.Map;
+import javax.inject.Inject;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import org.apache.solr.client.solrj.SolrResponse;
+import org.apache.solr.common.cloud.ZkNodeProps;
+import org.apache.solr.common.params.CollectionParams;
+import org.apache.solr.common.params.CollectionParams.CollectionAction;
+import org.apache.solr.core.CoreContainer;
+import org.apache.solr.handler.admin.CollectionsHandler;
+import org.apache.solr.jersey.JacksonReflectMapWriter;
+import org.apache.solr.jersey.PermissionName;
+import org.apache.solr.jersey.SolrJerseyResponse;
+import org.apache.solr.request.SolrQueryRequest;
+import org.apache.solr.response.SolrQueryResponse;
+
+/**
+ * V2 API for recreating replicas in one node (the source) on another node(s) 
(the target).
+ *
+ * <p>This API is analogous to the v1 /admin/collections?action=REPLACENODE 
command.
+ */
+@Path("cluster/nodes/{sourceNodeName}/commands/replace/")
+public class ReplaceNodeAPI extends AdminAPIBase {
+
+  @Inject
+  public ReplaceNodeAPI(
+      CoreContainer coreContainer,
+      SolrQueryRequest solrQueryRequest,
+      SolrQueryResponse solrQueryResponse) {
+    super(coreContainer, solrQueryRequest, solrQueryResponse);
+  }
+
+  @POST
+  @Produces({"application/json", "application/xml", BINARY_CONTENT_TYPE_V2})
+  @PermissionName(COLL_EDIT_PERM)
+  public SolrJerseyResponse replaceNode(
+      @Parameter(description = "The name of the node to be replaced.", 
required = true)
+          @PathParam("sourceNodeName")
+          String nodeName,
+      @RequestBody(description = "Contains user provided parameters", required 
= true)
+          ReplaceNodeRequestBody requestBody)
+      throws Exception {
+    final SolrJerseyResponse response = 
instantiateJerseyResponse(SolrJerseyResponse.class);
+    final CoreContainer coreContainer = 
fetchAndValidateZooKeeperAwareCoreContainer();
+    /** TODO Record node for log and tracing */
+    final ZkNodeProps remoteMessage = createRemoteMessage(nodeName, 
requestBody);
+    final SolrResponse remoteResponse =
+        CollectionsHandler.submitCollectionApiCommand(
+            coreContainer,
+            coreContainer.getDistributedCollectionCommandRunner(),
+            remoteMessage,
+            CollectionParams.CollectionAction.REPLACENODE,
+            DEFAULT_COLLECTION_OP_TIMEOUT);
+    if (remoteResponse.getException() != null) {
+      throw remoteResponse.getException();
+    }
+
+    disableResponseCaching();
+    return response;
+  }
+
+  public ZkNodeProps createRemoteMessage(String nodeName, 
ReplaceNodeRequestBody requestBody) {
+    final Map<String, Object> remoteMessage = new HashMap<>();
+    remoteMessage.put(SOURCE_NODE, nodeName);
+    remoteMessage.put(TARGET_NODE, requestBody.targetNodeName);
+    remoteMessage.put(WAIT_FOR_FINAL_STATE, requestBody.waitForFinalState);
+    remoteMessage.put(QUEUE_OPERATION, CollectionAction.REPLACENODE.toLower());
+
+    return new ZkNodeProps(remoteMessage);
+  }
+
+  public static class ReplaceNodeRequestBody implements 
JacksonReflectMapWriter {
+
+    public ReplaceNodeRequestBody() {}
+
+    public ReplaceNodeRequestBody(String targetNodeName, Boolean 
waitForFinalState) {
+      this.targetNodeName = targetNodeName;
+      this.waitForFinalState = waitForFinalState;
+    }
+

Review Comment:
   [-1] v1 REPLACENODE supports the "async" parameter, so we should probably 
add that to the RequestBody as well.
   
   (`CollectionsHandler.submitCollectionApiCommand`, which you use above, 
already has logic built around the parameter, so all we should have to do is 
add it to the RequestBody here, and then make sure it gets added to the message 
in `createRemoteMessage` above.)



##########
solr/core/src/java/org/apache/solr/handler/admin/CollectionsHandler.java:
##########
@@ -1791,14 +1794,17 @@ public Map<String, Object> execute(
     REPLACENODE_OP(
         REPLACENODE,
         (req, rsp, h) -> {
-          return copy(
-              req.getParams(),
-              null,
-              "source", // legacy

Review Comment:
   [-1] I'm fine with dropping the deprecated old parameter names in this PR, 
but if we drop them here, we should also drop the code that checks for them in 
[ReplaceNodeCmd](https://github.com/apache/solr/blob/c99af207c761ec34812ef1cc3054eb2804b7448b/solr/core/src/java/org/apache/solr/cloud/api/collections/ReplaceNodeCmd.java#L67)



##########
solr/core/src/java/org/apache/solr/handler/admin/api/ReplaceNodeAPI.java:
##########
@@ -0,0 +1,121 @@
+/*
+ * 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.handler.admin.api;
+
+import static 
org.apache.solr.client.solrj.impl.BinaryResponseParser.BINARY_CONTENT_TYPE_V2;
+import static org.apache.solr.cloud.Overseer.QUEUE_OPERATION;
+import static org.apache.solr.common.params.CollectionParams.SOURCE_NODE;
+import static org.apache.solr.common.params.CollectionParams.TARGET_NODE;
+import static 
org.apache.solr.common.params.CommonAdminParams.WAIT_FOR_FINAL_STATE;
+import static 
org.apache.solr.handler.admin.CollectionsHandler.DEFAULT_COLLECTION_OP_TIMEOUT;
+import static 
org.apache.solr.security.PermissionNameProvider.Name.COLL_EDIT_PERM;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.media.Schema;
+import io.swagger.v3.oas.annotations.parameters.RequestBody;
+import java.util.HashMap;
+import java.util.Map;
+import javax.inject.Inject;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import org.apache.solr.client.solrj.SolrResponse;
+import org.apache.solr.common.cloud.ZkNodeProps;
+import org.apache.solr.common.params.CollectionParams;
+import org.apache.solr.common.params.CollectionParams.CollectionAction;
+import org.apache.solr.core.CoreContainer;
+import org.apache.solr.handler.admin.CollectionsHandler;
+import org.apache.solr.jersey.JacksonReflectMapWriter;
+import org.apache.solr.jersey.PermissionName;
+import org.apache.solr.jersey.SolrJerseyResponse;
+import org.apache.solr.request.SolrQueryRequest;
+import org.apache.solr.response.SolrQueryResponse;
+
+/**
+ * V2 API for recreating replicas in one node (the source) on another node(s) 
(the target).
+ *
+ * <p>This API is analogous to the v1 /admin/collections?action=REPLACENODE 
command.
+ */
+@Path("cluster/nodes/{sourceNodeName}/commands/replace/")
+public class ReplaceNodeAPI extends AdminAPIBase {
+
+  @Inject
+  public ReplaceNodeAPI(
+      CoreContainer coreContainer,
+      SolrQueryRequest solrQueryRequest,
+      SolrQueryResponse solrQueryResponse) {
+    super(coreContainer, solrQueryRequest, solrQueryResponse);
+  }
+
+  @POST
+  @Produces({"application/json", "application/xml", BINARY_CONTENT_TYPE_V2})
+  @PermissionName(COLL_EDIT_PERM)
+  public SolrJerseyResponse replaceNode(
+      @Parameter(description = "The name of the node to be replaced.", 
required = true)
+          @PathParam("sourceNodeName")
+          String nodeName,
+      @RequestBody(description = "Contains user provided parameters", required 
= true)
+          ReplaceNodeRequestBody requestBody)
+      throws Exception {
+    final SolrJerseyResponse response = 
instantiateJerseyResponse(SolrJerseyResponse.class);
+    final CoreContainer coreContainer = 
fetchAndValidateZooKeeperAwareCoreContainer();
+    /** TODO Record node for log and tracing */
+    final ZkNodeProps remoteMessage = createRemoteMessage(nodeName, 
requestBody);
+    final SolrResponse remoteResponse =
+        CollectionsHandler.submitCollectionApiCommand(
+            coreContainer,
+            coreContainer.getDistributedCollectionCommandRunner(),
+            remoteMessage,
+            CollectionParams.CollectionAction.REPLACENODE,
+            DEFAULT_COLLECTION_OP_TIMEOUT);
+    if (remoteResponse.getException() != null) {
+      throw remoteResponse.getException();
+    }
+
+    disableResponseCaching();
+    return response;
+  }
+
+  public ZkNodeProps createRemoteMessage(String nodeName, 
ReplaceNodeRequestBody requestBody) {
+    final Map<String, Object> remoteMessage = new HashMap<>();
+    remoteMessage.put(SOURCE_NODE, nodeName);
+    remoteMessage.put(TARGET_NODE, requestBody.targetNodeName);

Review Comment:
   [0] If the user doesn't provide a request-body, e.g.
   
   ```
   curl -ilk -X POST 
"http://localhost:8983/api/cluster/nodes/localhost:7574_solr/commands/replace";
   ```
   
   then this line will cause a NullPointerException.
   
   I'm a little torn on this - I think it'd technically be "correct" if we 
forced users to provide an empty JSON object (`{}`) as the request body even if 
they don't want to specify any of the optional parameters.  But in practice - 
idk, it seems like it'd be a pain that we could save our users.
   
   What do you think of surrounding these lines with a `if (requestBody != 
null)` check?
   
   Also, this would be a great this to have a test for later on when you get to 
that point.



-- 
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.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to