diff --git a/changelog/unreleased/SOLR-16390-cluster-command-status.yml b/changelog/unreleased/SOLR-16390-cluster-command-status.yml
new file mode 100644
index 00000000000..775b6364937
--- /dev/null
+++ b/changelog/unreleased/SOLR-16390-cluster-command-status.yml
@@ -0,0 +1,8 @@
+title: "v2 cluster command-status APIs are now more REST-ful at GET/DELETE /api/cluster/commands/{id} and DELETE /api/cluster/commands (the previous /api/cluster/command-status paths are gone). SolrJ provides ClusterApi.GetClusterCommandStatus, ClusterApi.DeleteClusterCommandStatus, and ClusterApi.DeleteAllClusterCommandStatuses."
+type: changed
+authors:
+ - name: Prithvi S
+ nick: iprithv
+links:
+ - name: SOLR-16390
+ url: https://issues.apache.org/jira/browse/SOLR-16390
diff --git a/dev-docs/v2-api-conventions.adoc b/dev-docs/v2-api-conventions.adoc
index d40d7dd4a48..08a9d01fdba 100644
--- a/dev-docs/v2-api-conventions.adoc
+++ b/dev-docs/v2-api-conventions.adoc
@@ -23,6 +23,8 @@ Following these guidelines has given us the following (non-exhaustive) list of v
* `/api/backups/specificBackupName/versions/specificVersion`
* `/api/cluster/nodes/specificNodeName/roles`
* `/api/cluster/nodes/specificNodeName/roles/specificRoleName`
+* `/api/cluster/commands`
+* `/api/cluster/commands/specificCommandId`
* `/api/cluster/properties`
* `/api/cluster/properties/specificPropertyName`
* `/api/collections`
diff --git a/solr/api/src/java/org/apache/solr/client/api/endpoint/ClusterCommandsApi.java b/solr/api/src/java/org/apache/solr/client/api/endpoint/ClusterCommandsApi.java
new file mode 100644
index 00000000000..bfd3c19eda8
--- /dev/null
+++ b/solr/api/src/java/org/apache/solr/client/api/endpoint/ClusterCommandsApi.java
@@ -0,0 +1,71 @@
+/*
+ * 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.client.api.endpoint;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import jakarta.ws.rs.DELETE;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.PathParam;
+import org.apache.solr.client.api.model.DeleteClusterCommandStatusResponse;
+import org.apache.solr.client.api.model.GetClusterCommandStatusResponse;
+
+/**
+ * V2 API definitions for cluster-level asynchronous Collection API command status.
+ *
+ *
These APIs are analogous to the v1 {@code /admin/collections?action=REQUESTSTATUS} and {@code
+ * /admin/collections?action=DELETESTATUS} commands. They are not to be confused with the node-local
+ * {@link GetNodeCommandStatusApi} under {@code /api/node/commands}.
+ */
+@Path("/cluster/commands")
+public interface ClusterCommandsApi {
+
+ @GET
+ @Path("/{requestId}")
+ @Operation(
+ summary = "Request the status of an already submitted asynchronous Collection API call.",
+ tags = {"cluster"})
+ GetClusterCommandStatusResponse getClusterCommandStatus(
+ @Parameter(
+ description = "The user defined request-id for the asynchronous request.",
+ required = true)
+ @PathParam("requestId")
+ String requestId)
+ throws Exception;
+
+ @DELETE
+ @Path("/{requestId}")
+ @Operation(
+ summary =
+ "Delete the stored status of a completed or failed asynchronous Collection API call.",
+ tags = {"cluster"})
+ DeleteClusterCommandStatusResponse deleteClusterCommandStatus(
+ @Parameter(
+ description = "The user defined request-id whose stored response should be cleared.",
+ required = true)
+ @PathParam("requestId")
+ String requestId)
+ throws Exception;
+
+ @DELETE
+ @Operation(
+ summary =
+ "Delete the stored status of all completed and failed asynchronous Collection API calls.",
+ tags = {"cluster"})
+ DeleteClusterCommandStatusResponse deleteAllClusterCommandStatuses() throws Exception;
+}
diff --git a/solr/api/src/java/org/apache/solr/client/api/endpoint/GetNodeCommandStatusApi.java b/solr/api/src/java/org/apache/solr/client/api/endpoint/GetNodeCommandStatusApi.java
index a2b5bcc5986..cc807074d59 100644
--- a/solr/api/src/java/org/apache/solr/client/api/endpoint/GetNodeCommandStatusApi.java
+++ b/solr/api/src/java/org/apache/solr/client/api/endpoint/GetNodeCommandStatusApi.java
@@ -28,7 +28,7 @@
*
*
This API is analogous to the v1 /admin/cores?action=REQUESTSTATUS command. It is not to be
* confused with the more robust asynchronous command support offered under the v2
- * `/cluster/command-status` path (or the corresponding v1 path
+ * `/cluster/commands` path (or the corresponding v1 path
* `/solr/admin/collections?action=REQUESTSTATUS`). Async support at the core level differs in that
* command IDs are local to individual Solr nodes and are not persisted across restarts.
*
diff --git a/solr/api/src/java/org/apache/solr/client/api/model/DeleteClusterCommandStatusResponse.java b/solr/api/src/java/org/apache/solr/client/api/model/DeleteClusterCommandStatusResponse.java
new file mode 100644
index 00000000000..4571ec19f26
--- /dev/null
+++ b/solr/api/src/java/org/apache/solr/client/api/model/DeleteClusterCommandStatusResponse.java
@@ -0,0 +1,31 @@
+/*
+ * 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.client.api.model;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import io.swagger.v3.oas.annotations.media.Schema;
+
+/**
+ * Response body for {@code DELETE /api/cluster/commands} and {@code DELETE
+ * /api/cluster/commands/{requestId}}.
+ */
+public class DeleteClusterCommandStatusResponse extends SolrJerseyResponse {
+
+ @JsonProperty("status")
+ @Schema(description = "A message describing the result of the delete.")
+ public String status;
+}
diff --git a/solr/api/src/java/org/apache/solr/client/api/model/GetClusterCommandStatusResponse.java b/solr/api/src/java/org/apache/solr/client/api/model/GetClusterCommandStatusResponse.java
new file mode 100644
index 00000000000..6b96427cacd
--- /dev/null
+++ b/solr/api/src/java/org/apache/solr/client/api/model/GetClusterCommandStatusResponse.java
@@ -0,0 +1,97 @@
+/*
+ * 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.client.api.model;
+
+import com.fasterxml.jackson.annotation.JsonAnyGetter;
+import com.fasterxml.jackson.annotation.JsonAnySetter;
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonValue;
+import io.swagger.v3.oas.annotations.media.Schema;
+import java.util.HashMap;
+import java.util.Map;
+import org.apache.solr.client.api.util.ReflectWritable;
+
+/**
+ * Response body for {@code GET /api/cluster/commands/{requestId}}.
+ *
+ *
Completed and failed commands also flatten the original command response (for example {@code
+ * success} / {@code failure} sub-responses) into this object as additional properties.
+ */
+public class GetClusterCommandStatusResponse extends SolrJerseyResponse {
+
+ @JsonProperty("status")
+ @Schema(description = "The current state of the asynchronous request and a descriptive message.")
+ public CommandStatus status;
+
+ private Map unknownFields = new HashMap<>();
+
+ @JsonAnyGetter
+ public Map unknownProperties() {
+ return unknownFields;
+ }
+
+ @JsonAnySetter
+ public void setUnknownProperty(String field, Object value) {
+ unknownFields.put(field, value);
+ }
+
+ /** Nested {@code status} object returned by REQUESTSTATUS. */
+ public static class CommandStatus implements ReflectWritable {
+ @JsonProperty("state")
+ @Schema(description = "Request state: submitted, running, completed, failed, or notfound.")
+ public State state;
+
+ @JsonProperty("msg")
+ @Schema(description = "A message describing where the request was found, if at all.")
+ public String msg;
+
+ /**
+ * The state of an asynchronous request. Mirrors {@code
+ * org.apache.solr.client.solrj.response.RequestStatusState}'s constants and wire keys; kept as
+ * a separate type here since this module (solr:api) cannot depend on solrj.
+ */
+ public enum State {
+ SUBMITTED("submitted"),
+ RUNNING("running"),
+ COMPLETED("completed"),
+ FAILED("failed"),
+ NOT_FOUND("notfound");
+
+ private final String key;
+
+ State(String key) {
+ this.key = key;
+ }
+
+ @JsonValue
+ public String getKey() {
+ return key;
+ }
+
+ @JsonCreator
+ public static State fromKey(String key) {
+ for (State state : values()) {
+ if (state.key.equalsIgnoreCase(key)) {
+ return state;
+ }
+ }
+ throw new IllegalArgumentException("Unknown request status state: " + key);
+ }
+ }
+ }
+}
diff --git a/solr/core/src/java/org/apache/solr/handler/ClusterAPI.java b/solr/core/src/java/org/apache/solr/handler/ClusterAPI.java
index 36ee024f7f2..4e672348292 100644
--- a/solr/core/src/java/org/apache/solr/handler/ClusterAPI.java
+++ b/solr/core/src/java/org/apache/solr/handler/ClusterAPI.java
@@ -17,16 +17,11 @@
package org.apache.solr.handler;
-import static org.apache.solr.client.solrj.SolrRequest.METHOD.DELETE;
import static org.apache.solr.client.solrj.SolrRequest.METHOD.GET;
import static org.apache.solr.client.solrj.SolrRequest.METHOD.POST;
-import static org.apache.solr.cloud.api.collections.CollectionHandlingUtils.REQUESTID;
-import static org.apache.solr.common.params.CollectionParams.ACTION;
import static org.apache.solr.common.params.CollectionParams.CollectionAction.ADDROLE;
-import static org.apache.solr.common.params.CollectionParams.CollectionAction.DELETESTATUS;
import static org.apache.solr.common.params.CollectionParams.CollectionAction.OVERSEERSTATUS;
import static org.apache.solr.common.params.CollectionParams.CollectionAction.REMOVEROLE;
-import static org.apache.solr.common.params.CollectionParams.CollectionAction.REQUESTSTATUS;
import static org.apache.solr.core.RateLimiterConfig.RL_CONFIG_KEY;
import static org.apache.solr.security.PermissionNameProvider.Name.COLL_EDIT_PERM;
import static org.apache.solr.security.PermissionNameProvider.Name.COLL_READ_PERM;
@@ -199,18 +194,6 @@ public void getOverseerStatus(SolrQueryRequest req, SolrQueryResponse rsp) throw
collectionsHandler.handleRequestBody(wrapParams(req, "action", OVERSEERSTATUS.lowerName), rsp);
}
- @EndPoint(method = DELETE, path = "/cluster/command-status/{id}", permission = COLL_EDIT_PERM)
- public void deleteCommandStatus(SolrQueryRequest req, SolrQueryResponse rsp) throws Exception {
- final Map v1Params =
- Map.of(ACTION, DELETESTATUS.lowerName, REQUESTID, req.getPathTemplateValues().get("id"));
- collectionsHandler.handleRequestBody(wrapParams(req, v1Params), rsp);
- }
-
- @EndPoint(method = DELETE, path = "/cluster/command-status", permission = COLL_EDIT_PERM)
- public void flushCommandStatus(SolrQueryRequest req, SolrQueryResponse rsp) throws Exception {
- CollectionsHandler.CollectionOperation.DELETESTATUS_OP.execute(req, rsp, collectionsHandler);
- }
-
public static SolrQueryRequest wrapParams(SolrQueryRequest req, Object... def) {
Map m = Utils.makeMap(def);
return wrapParams(req, m);
@@ -232,13 +215,6 @@ public static SolrQueryRequest wrapParams(SolrQueryRequest req, Map v1Params =
- Map.of(ACTION, REQUESTSTATUS.lowerName, REQUESTID, req.getPathTemplateValues().get("id"));
- collectionsHandler.handleRequestBody(wrapParams(req, v1Params), rsp);
- }
-
@EndPoint(method = GET, path = "/cluster/nodes", permission = COLL_READ_PERM)
public void getNodes(SolrQueryRequest req, SolrQueryResponse rsp) {
rsp.add("nodes", getCoreContainer().getZkController().getClusterState().getLiveNodes());
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 270afc24906..77ec443e38f 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
@@ -16,16 +16,10 @@
*/
package org.apache.solr.handler.admin;
-import static org.apache.solr.client.solrj.response.RequestStatusState.COMPLETED;
-import static org.apache.solr.client.solrj.response.RequestStatusState.FAILED;
-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.CollectionHandlingUtils.CREATE_NODE_SET;
import static org.apache.solr.cloud.api.collections.CollectionHandlingUtils.CREATE_NODE_SET_SHUFFLE;
import static org.apache.solr.cloud.api.collections.CollectionHandlingUtils.NUM_SLICES;
-import static org.apache.solr.cloud.api.collections.CollectionHandlingUtils.REQUESTID;
import static org.apache.solr.cloud.api.collections.CollectionHandlingUtils.SHARD_UNIQUE;
import static org.apache.solr.common.SolrException.ErrorCode.BAD_REQUEST;
import static org.apache.solr.common.cloud.ZkStateReader.COLLECTION_PROP;
@@ -133,10 +127,8 @@
import org.apache.solr.client.api.model.UpdateCollectionPropertyRequestBody;
import org.apache.solr.client.solrj.SolrResponse;
import org.apache.solr.client.solrj.request.CollectionAdminRequest;
-import org.apache.solr.client.solrj.response.RequestStatusState;
import org.apache.solr.cloud.OverseerSolrResponse;
import org.apache.solr.cloud.OverseerSolrResponseSerializer;
-import org.apache.solr.cloud.OverseerTaskQueue;
import org.apache.solr.cloud.OverseerTaskQueue.QueueEvent;
import org.apache.solr.cloud.ZkController;
import org.apache.solr.cloud.ZkController.NotInClusterStateException;
@@ -162,8 +154,6 @@
import org.apache.solr.common.params.RequiredSolrParams;
import org.apache.solr.common.params.SolrParams;
import org.apache.solr.common.util.NamedList;
-import org.apache.solr.common.util.Pair;
-import org.apache.solr.common.util.SimpleOrderedMap;
import org.apache.solr.common.util.StrUtils;
import org.apache.solr.common.util.Utils;
import org.apache.solr.core.CloudConfig;
@@ -176,6 +166,7 @@
import org.apache.solr.handler.admin.api.AliasProperty;
import org.apache.solr.handler.admin.api.BalanceReplicas;
import org.apache.solr.handler.admin.api.BalanceShardUnique;
+import org.apache.solr.handler.admin.api.ClusterCommands;
import org.apache.solr.handler.admin.api.ClusterProperty;
import org.apache.solr.handler.admin.api.CollectionProperty;
import org.apache.solr.handler.admin.api.CollectionStatus;
@@ -437,13 +428,6 @@ public SolrResponse submitCollectionApiCommand(
return submitCollectionApiCommand(coreContainer.getZkController(), adminCmdContext, m, timeout);
}
- private boolean overseerCollectionQueueContains(String asyncId)
- throws KeeperException, InterruptedException {
- OverseerTaskQueue collectionQueue =
- coreContainer.getZkController().getOverseerCollectionQueue();
- return collectionQueue.containsTaskWithRequestId(ASYNC, asyncId);
- }
-
/**
* Copy prefixed params into a map. There must only be one value for these parameters.
*
@@ -490,14 +474,6 @@ public Category getCategory() {
return Category.ADMIN;
}
- private static void addStatusToResponse(
- NamedList