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

epugh 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 6efa7ebc46b SOLR-18248: Migration of CancelTask V2 API to JAX-RS 
construct (#4828)
6efa7ebc46b is described below

commit 6efa7ebc46bcb6938521165420d4c10f490581d2
Author: Jalaz Kumar <[email protected]>
AuthorDate: Sun Sep 20 07:51:47 2026 +0530

    SOLR-18248: Migration of CancelTask V2 API to JAX-RS construct (#4828)
    
    Co-authored-by: Eric Pugh <[email protected]>
---
 changelog/unreleased/SOLR-18248-cancel-tasks.yml   |   9 ++
 .../apache/solr/client/api/endpoint/TasksApi.java  |  12 +++
 .../solr/client/api/model/CancelTaskResponse.java  |  31 ++++++
 .../apache/solr/handler/admin/api/CancelTask.java  |  68 ++++++++++++
 .../solr/handler/admin/api/CancelTaskAPI.java      |  48 ---------
 .../handler/component/ActiveTaskQuerySupport.java  |  77 ++++++++++---
 .../component/QueryCancellationComponent.java      | 108 -------------------
 ...onHandler.java => TaskCancellationHandler.java} |  52 ++++-----
 .../handler/component/TaskManagementHandler.java   |  81 --------------
 solr/core/src/resources/ImplicitPlugins.json       |  14 +--
 .../test/org/apache/solr/core/SolrCoreTest.java    |   2 +-
 .../solr/handler/admin/api/CancelTaskAPITest.java  |  80 ++++++++++++++
 ...istActiveTasksTest.java => CancelTaskTest.java} |  58 +++++-----
 .../solr/handler/admin/api/GetTaskStatusTest.java  |   5 +-
 .../handler/admin/api/ListActiveTasksTest.java     |   5 +-
 .../org/apache/solr/search/TestTaskManagement.java | 119 +++++++++++++++++++--
 .../deployment-guide/pages/task-management.adoc    |  82 +++++++++++---
 17 files changed, 497 insertions(+), 354 deletions(-)

diff --git a/changelog/unreleased/SOLR-18248-cancel-tasks.yml 
b/changelog/unreleased/SOLR-18248-cancel-tasks.yml
new file mode 100644
index 00000000000..d86b6429de1
--- /dev/null
+++ b/changelog/unreleased/SOLR-18248-cancel-tasks.yml
@@ -0,0 +1,9 @@
+title:
+  Migration of CancelTask V2 API to JAX-RS construct
+type: changed
+authors:
+  - name: Jalaz Kumar
+  - name: Eric Pugh
+links:
+  - name: SOLR-18248
+    url: https://issues.apache.org/jira/browse/SOLR-18248
diff --git 
a/solr/api/src/java/org/apache/solr/client/api/endpoint/TasksApi.java 
b/solr/api/src/java/org/apache/solr/client/api/endpoint/TasksApi.java
index 46d80de5d9d..d2b9c921637 100644
--- a/solr/api/src/java/org/apache/solr/client/api/endpoint/TasksApi.java
+++ b/solr/api/src/java/org/apache/solr/client/api/endpoint/TasksApi.java
@@ -20,9 +20,11 @@ package org.apache.solr.client.api.endpoint;
 import static org.apache.solr.client.api.util.Constants.INDEX_PATH_PREFIX;
 
 import io.swagger.v3.oas.annotations.Operation;
+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.CancelTaskResponse;
 import org.apache.solr.client.api.model.ListActiveTaskResponse;
 import org.apache.solr.client.api.model.TaskStatusResponse;
 import org.apache.solr.client.api.util.StoreApiParameters;
@@ -48,4 +50,14 @@ public interface TasksApi {
         tags = {"tasks"})
     TaskStatusResponse getTaskStatus(@PathParam("taskID") String taskID) 
throws Exception;
   }
+
+  @Path(INDEX_PATH_PREFIX + "/tasks/{taskID}")
+  interface Cancel {
+    @DELETE
+    @StoreApiParameters
+    @Operation(
+        summary = "Cancel any specific task",
+        tags = {"tasks"})
+    CancelTaskResponse cancelRunningTask(@PathParam("taskID") String taskID) 
throws Exception;
+  }
 }
diff --git 
a/solr/api/src/java/org/apache/solr/client/api/model/CancelTaskResponse.java 
b/solr/api/src/java/org/apache/solr/client/api/model/CancelTaskResponse.java
new file mode 100644
index 00000000000..41158d6e521
--- /dev/null
+++ b/solr/api/src/java/org/apache/solr/client/api/model/CancelTaskResponse.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;
+
+/** Response body returned after a task cancellation request. */
+public class CancelTaskResponse extends SolrJerseyResponse {
+
+  public enum CancellationStatus {
+    SUCCESS,
+    NOT_FOUND
+  }
+
+  @JsonProperty public CancelTaskResponse.CancellationStatus status;
+}
diff --git 
a/solr/core/src/java/org/apache/solr/handler/admin/api/CancelTask.java 
b/solr/core/src/java/org/apache/solr/handler/admin/api/CancelTask.java
new file mode 100644
index 00000000000..095a981a556
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/handler/admin/api/CancelTask.java
@@ -0,0 +1,68 @@
+/*
+ * 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.security.PermissionNameProvider.Name.READ_PERM;
+
+import jakarta.inject.Inject;
+import org.apache.solr.api.JerseyResource;
+import org.apache.solr.client.api.endpoint.TasksApi;
+import org.apache.solr.client.api.model.CancelTaskResponse;
+import org.apache.solr.common.SolrException;
+import org.apache.solr.handler.component.ActiveTaskQuerySupport;
+import org.apache.solr.jersey.PermissionName;
+import org.apache.solr.request.SolrQueryRequest;
+import org.apache.solr.search.CancellableCollector;
+
+/** V2 API implementation for cancelling an active task. */
+public class CancelTask extends JerseyResource implements TasksApi.Cancel {
+
+  private final SolrQueryRequest solrQueryRequest;
+
+  @Inject
+  public CancelTask(SolrQueryRequest solrQueryRequest) {
+    this.solrQueryRequest = solrQueryRequest;
+  }
+
+  @Override
+  @PermissionName(READ_PERM)
+  public CancelTaskResponse cancelRunningTask(String taskID) throws Exception {
+    final CancelTaskResponse response = 
instantiateJerseyResponse(CancelTaskResponse.class);
+
+    boolean isTaskCancelled = 
ActiveTaskQuerySupport.cancelTask(solrQueryRequest, taskID);
+
+    if (isTaskCancelled) {
+      response.status = CancelTaskResponse.CancellationStatus.SUCCESS;
+      return response;
+    }
+    response.status = CancelTaskResponse.CancellationStatus.NOT_FOUND;
+    throw new SolrException(
+        SolrException.ErrorCode.NOT_FOUND, "Task with ID '" + taskID + "' not 
found");
+  }
+
+  public static boolean cancelTaskActiveOnThisShard(
+      SolrQueryRequest solrQueryRequest, String taskId) {
+    CancellableCollector cancellableTask =
+        
solrQueryRequest.getCore().getCancellableQueryTracker().getCancellableTask(taskId);
+    if (cancellableTask != null) {
+      cancellableTask.cancel();
+      return true;
+    }
+    return false;
+  }
+}
diff --git 
a/solr/core/src/java/org/apache/solr/handler/admin/api/CancelTaskAPI.java 
b/solr/core/src/java/org/apache/solr/handler/admin/api/CancelTaskAPI.java
deleted file mode 100644
index a63adc055f4..00000000000
--- a/solr/core/src/java/org/apache/solr/handler/admin/api/CancelTaskAPI.java
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * 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.SolrRequest.METHOD.GET;
-
-import org.apache.solr.api.EndPoint;
-import org.apache.solr.handler.component.QueryCancellationHandler;
-import org.apache.solr.request.SolrQueryRequest;
-import org.apache.solr.response.SolrQueryResponse;
-import org.apache.solr.security.PermissionNameProvider;
-
-/**
- * V2 API for cancelling a currently running "task".
- *
- * <p>This API (GET /v2/collections/collectionName/tasks/cancel) is analogous 
to the v1
- * /solr/collectionName/tasks/cancel API.
- */
-public class CancelTaskAPI {
-  private final QueryCancellationHandler cancellationHandler;
-
-  public CancelTaskAPI(QueryCancellationHandler cancellationHandler) {
-    this.cancellationHandler = cancellationHandler;
-  }
-
-  @EndPoint(
-      path = {"/tasks/cancel"},
-      method = GET,
-      permission = PermissionNameProvider.Name.READ_PERM)
-  public void cancelActiveTask(SolrQueryRequest req, SolrQueryResponse rsp) 
throws Exception {
-    cancellationHandler.handleRequestBody(req, rsp);
-  }
-}
diff --git 
a/solr/core/src/java/org/apache/solr/handler/component/ActiveTaskQuerySupport.java
 
b/solr/core/src/java/org/apache/solr/handler/component/ActiveTaskQuerySupport.java
index 6276b419451..b6a2fcc8b77 100644
--- 
a/solr/core/src/java/org/apache/solr/handler/component/ActiveTaskQuerySupport.java
+++ 
b/solr/core/src/java/org/apache/solr/handler/component/ActiveTaskQuerySupport.java
@@ -18,6 +18,7 @@ package org.apache.solr.handler.component;
 
 import static org.apache.solr.common.params.CommonParams.DISTRIB;
 import static org.apache.solr.common.params.CommonParams.QT;
+import static org.apache.solr.common.params.CommonParams.QUERY_UUID;
 import static org.apache.solr.common.params.CommonParams.TASK_CHECK_UUID;
 
 import java.util.ArrayList;
@@ -29,39 +30,51 @@ import org.apache.solr.common.SolrException;
 import org.apache.solr.common.params.ModifiableSolrParams;
 import org.apache.solr.common.util.NamedList;
 import org.apache.solr.core.CoreContainer;
+import org.apache.solr.handler.admin.api.CancelTask;
 import org.apache.solr.handler.admin.api.ListActiveTasks;
 import org.apache.solr.request.SolrQueryRequest;
 import org.apache.solr.response.SolrQueryResponse;
 
 public class ActiveTaskQuerySupport {
   private static final String ACTIVE_TASK_LIST_HANDLER_PATH = "/tasks/list";
+  private static final String CANCEL_TASK_HANDLER_PATH = "/tasks/cancel";
 
   private ActiveTaskQuerySupport() {}
 
   public static List<ActiveTaskDetails> listActiveTasks(SolrQueryRequest req) 
throws Exception {
-    return execute(req, null).taskList;
+    return execute(req, null, false).taskList;
   }
 
   public static boolean isTaskActive(SolrQueryRequest req, String taskId) 
throws Exception {
-    return execute(req, taskId).taskActive;
+    return execute(req, taskId, false).taskActive;
   }
 
-  private static TaskQueryResult execute(SolrQueryRequest req, String taskId) 
throws Exception {
+  public static boolean cancelTask(SolrQueryRequest req, String taskId) throws 
Exception {
+    return execute(req, taskId, true).taskCancelled;
+  }
+
+  private static TaskQueryResult execute(
+      SolrQueryRequest req, String taskId, boolean isCancellationRequest) 
throws Exception {
     if (!shouldDistributed(req)) {
-      return localResult(req, taskId);
+      return localResult(req, taskId, isCancellationRequest);
     }
-    return distributedResult(req, taskId);
+    return distributedResult(req, taskId, isCancellationRequest);
   }
 
-  private static TaskQueryResult localResult(SolrQueryRequest req, String 
taskId) {
+  private static TaskQueryResult localResult(
+      SolrQueryRequest req, String taskId, boolean isCancellationRequest) {
     if (taskId != null) {
-      return new TaskQueryResult(List.of(), 
ListActiveTasks.isTaskActiveOnThisShard(req, taskId));
+      return (isCancellationRequest)
+          ? new TaskQueryResult(
+              List.of(), false, CancelTask.cancelTaskActiveOnThisShard(req, 
taskId))
+          : new TaskQueryResult(
+              List.of(), ListActiveTasks.isTaskActiveOnThisShard(req, taskId), 
false);
     }
-    return new TaskQueryResult(ListActiveTasks.getActiveTasksOnThisShard(req), 
false);
+    return new TaskQueryResult(ListActiveTasks.getActiveTasksOnThisShard(req), 
false, false);
   }
 
-  private static TaskQueryResult distributedResult(SolrQueryRequest req, 
String taskId)
-      throws Exception {
+  private static TaskQueryResult distributedResult(
+      SolrQueryRequest req, String taskId, boolean isCancellationRequest) {
     final ShardHandler shardHandler =
         req.getCoreContainer().getShardHandlerFactory().getShardHandler();
     final ResponseBuilder responseBuilder =
@@ -71,7 +84,7 @@ public class ActiveTaskQuerySupport {
     if (!responseBuilder.isDistrib
         || responseBuilder.shards == null
         || responseBuilder.shards.length == 0) {
-      return localResult(req, taskId);
+      return localResult(req, taskId, isCancellationRequest);
     }
 
     final ShardRequest shardRequest = new ShardRequest();
@@ -81,9 +94,17 @@ public class ActiveTaskQuerySupport {
 
     for (String shard : shardRequest.actualShards) {
       ModifiableSolrParams params = new ModifiableSolrParams();
-      params.set(QT, ACTIVE_TASK_LIST_HANDLER_PATH);
+      if (isCancellationRequest) {
+        params.set(QT, CANCEL_TASK_HANDLER_PATH);
+      } else {
+        params.set(QT, ACTIVE_TASK_LIST_HANDLER_PATH);
+      }
       if (taskId != null) {
-        params.set(TASK_CHECK_UUID, taskId);
+        if (isCancellationRequest) {
+          params.set(QUERY_UUID, taskId);
+        } else {
+          params.set(TASK_CHECK_UUID, taskId);
+        }
       }
       ShardHandler.setShardAttributesToParams(params, shardRequest.purpose);
       shardHandler.submit(shardRequest, shard, params);
@@ -101,9 +122,11 @@ public class ActiveTaskQuerySupport {
     }
 
     if (taskId != null) {
-      return new TaskQueryResult(List.of(), 
mergeTaskStatus(shardRequest.responses));
+      return (isCancellationRequest)
+          ? new TaskQueryResult(List.of(), false, 
mergeCancellationStatus(shardRequest.responses))
+          : new TaskQueryResult(List.of(), 
mergeTaskStatus(shardRequest.responses), false);
     }
-    return new TaskQueryResult(mergeTaskList(shardRequest.responses), false);
+    return new TaskQueryResult(mergeTaskList(shardRequest.responses), false, 
false);
   }
 
   private static boolean shouldDistributed(SolrQueryRequest req) {
@@ -128,6 +151,25 @@ public class ActiveTaskQuerySupport {
     return false;
   }
 
+  // FRAGILE: matches TaskCancellationHandler's human-readable "status" 
message by substring, for
+  // both V1 and V2 (CancelTask calls this same method). Kept as-is since 
changing the V1 wire
+  // format is out of scope; see the matching FRAGILE note in
+  // TaskCancellationHandler.handleRequestBody().
+  private static boolean mergeCancellationStatus(List<ShardResponse> 
responses) {
+    for (ShardResponse shardResponse : responses) {
+      Object cancellationStatus = 
shardResponse.getSolrResponse().getResponse().get("status");
+      if (cancellationStatus instanceof Boolean && (Boolean) 
cancellationStatus) {
+        return true;
+      }
+
+      if (cancellationStatus instanceof String
+          && ((String) cancellationStatus).contains("cancelled successfully")) 
{
+        return true;
+      }
+    }
+    return false;
+  }
+
   @SuppressWarnings("unchecked")
   private static List<ActiveTaskDetails> mergeTaskList(List<ShardResponse> 
responses) {
     Map<String, String> mergedTasks = new LinkedHashMap<>();
@@ -156,10 +198,13 @@ public class ActiveTaskQuerySupport {
   private static final class TaskQueryResult {
     private final List<ActiveTaskDetails> taskList;
     private final boolean taskActive;
+    private final boolean taskCancelled;
 
-    private TaskQueryResult(List<ActiveTaskDetails> taskList, boolean 
taskActive) {
+    private TaskQueryResult(
+        List<ActiveTaskDetails> taskList, boolean taskActive, boolean 
taskCancelled) {
       this.taskList = taskList;
       this.taskActive = taskActive;
+      this.taskCancelled = taskCancelled;
     }
   }
 }
diff --git 
a/solr/core/src/java/org/apache/solr/handler/component/QueryCancellationComponent.java
 
b/solr/core/src/java/org/apache/solr/handler/component/QueryCancellationComponent.java
deleted file mode 100644
index 9648bb312e4..00000000000
--- 
a/solr/core/src/java/org/apache/solr/handler/component/QueryCancellationComponent.java
+++ /dev/null
@@ -1,108 +0,0 @@
-/*
- * 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.component;
-
-import java.io.IOException;
-import org.apache.solr.search.CancellableCollector;
-
-/** Responsible for handling query cancellation requests */
-public class QueryCancellationComponent extends SearchComponent {
-  public static final String COMPONENT_NAME = "querycancellation";
-
-  private boolean shouldProcess;
-
-  @Override
-  public void prepare(ResponseBuilder rb) throws IOException {
-    if (rb.isCancellation()) {
-      shouldProcess = true;
-    }
-  }
-
-  @Override
-  public void process(ResponseBuilder rb) {
-    if (!shouldProcess) {
-      return;
-    }
-
-    String cancellationUUID = rb.getCancellationUUID();
-
-    if (cancellationUUID == null) {
-      throw new RuntimeException("Null query UUID seen");
-    }
-
-    CancellableCollector cancellableTask =
-        
rb.req.getCore().getCancellableQueryTracker().getCancellableTask(cancellationUUID);
-
-    if (cancellableTask != null) {
-      cancellableTask.cancel();
-      rb.rsp.add("cancellationResult", "success");
-    } else {
-      rb.rsp.add("cancellationResult", "not found");
-    }
-  }
-
-  @Override
-  @SuppressWarnings("unchecked")
-  public void handleResponses(ResponseBuilder rb, ShardRequest sreq) {
-    if (!shouldProcess) {
-      return;
-    }
-
-    boolean queryFound = false;
-
-    for (ShardResponse r : sreq.responses) {
-
-      String cancellationResult =
-          (String) r.getSolrResponse().getResponse().get("cancellationResult");
-
-      if (cancellationResult.equalsIgnoreCase("success")) {
-        queryFound = true;
-
-        break;
-      }
-    }
-
-    // If any shard sees the query as present, then we mark the query as 
successfully cancelled. If
-    // no shard found the query, then that can denote that the query was not 
found. This is
-    // important since the query cancellation request is broadcast to all 
shards, and the query
-    // might have completed on some shards but not on others
-
-    if (queryFound) {
-      rb.rsp
-          .getValues()
-          .add(
-              "status",
-              "Query with queryID " + rb.getCancellationUUID() + " cancelled 
successfully");
-      rb.rsp.getValues().add("responseCode", 200 /* HTTP OK */);
-    } else {
-      rb.rsp
-          .getValues()
-          .add("status", "Query with queryID " + rb.getCancellationUUID() + " 
not found");
-      rb.rsp.getValues().add("responseCode", 404 /* HTTP NOT FOUND */);
-    }
-  }
-
-  @Override
-  public String getDescription() {
-    return "Supports cancellation of queries which are cancellable";
-  }
-
-  @Override
-  public Category getCategory() {
-    return Category.OTHER;
-  }
-}
diff --git 
a/solr/core/src/java/org/apache/solr/handler/component/QueryCancellationHandler.java
 
b/solr/core/src/java/org/apache/solr/handler/component/TaskCancellationHandler.java
similarity index 67%
rename from 
solr/core/src/java/org/apache/solr/handler/component/QueryCancellationHandler.java
rename to 
solr/core/src/java/org/apache/solr/handler/component/TaskCancellationHandler.java
index 18c99ba09c3..a99ba2d1a41 100644
--- 
a/solr/core/src/java/org/apache/solr/handler/component/QueryCancellationHandler.java
+++ 
b/solr/core/src/java/org/apache/solr/handler/component/TaskCancellationHandler.java
@@ -19,12 +19,10 @@ package org.apache.solr.handler.component;
 import static org.apache.solr.common.params.CommonParams.QUERY_UUID;
 
 import java.util.Collection;
-import java.util.HashMap;
 import java.util.List;
-import java.util.Map;
-import org.apache.solr.api.AnnotatedApi;
 import org.apache.solr.api.Api;
-import org.apache.solr.handler.admin.api.CancelTaskAPI;
+import org.apache.solr.api.JerseyResource;
+import org.apache.solr.handler.admin.api.CancelTask;
 import org.apache.solr.request.SolrQueryRequest;
 import org.apache.solr.request.SolrRequestHandler;
 import org.apache.solr.response.SolrQueryResponse;
@@ -32,40 +30,36 @@ import org.apache.solr.security.AuthorizationContext;
 import org.apache.solr.security.PermissionNameProvider;
 
 /** Handles requests for query cancellation for cancellable queries */
-public class QueryCancellationHandler extends TaskManagementHandler {
+public class TaskCancellationHandler extends TaskManagementHandler {
   // This can be a parent level member but we keep it here to allow future 
handlers to have
   // a custom list of components
-  private List<SearchComponent> components;
 
   @Override
   public void handleRequestBody(SolrQueryRequest req, SolrQueryResponse rsp) 
throws Exception {
-    ResponseBuilder rb = buildResponseBuilder(req, rsp, getComponentsList());
-    Map<String, String> extraParams = null;
+    String taskCancellationID = req.getParams().get(QUERY_UUID, null);
 
-    rb.setCancellation(true);
-
-    String cancellationUUID = req.getParams().get(QUERY_UUID, null);
-
-    if (cancellationUUID == null) {
+    if (taskCancellationID == null) {
       throw new IllegalArgumentException(
           "Query cancellation was requested but no query UUID for cancellation 
was given");
     }
 
-    if (rb.isDistrib) {
-      extraParams = new HashMap<>();
-
-      extraParams.put(QUERY_UUID, cancellationUUID);
+    boolean isTaskCancelled = ActiveTaskQuerySupport.cancelTask(req, 
taskCancellationID);
+
+    // FRAGILE: "cancelled successfully" is matched by
+    // ActiveTaskQuerySupport.mergeCancellationStatus() for cross-shard 
aggregation. Don't reword
+    // without checking there and 
TestTaskManagement.testCrossShardTaskCancellationVisibility*.
+    if (isTaskCancelled) {
+      rsp.add("status", "Query with queryID " + taskCancellationID + " 
cancelled successfully");
+      rsp.add("responseCode", 200);
+    } else {
+      rsp.add("status", "Query with queryID " + taskCancellationID + " not 
found");
+      rsp.add("responseCode", 404);
     }
-
-    // Let this be visible to handleResponses in the handling component
-    rb.setCancellationUUID(cancellationUUID);
-
-    processRequest(req, rb, extraParams);
   }
 
   @Override
   public String getDescription() {
-    return "Cancel queries";
+    return "Cancel active tasks";
   }
 
   @Override
@@ -83,7 +77,6 @@ public class QueryCancellationHandler extends 
TaskManagementHandler {
     if (path.startsWith("/tasks/cancel")) {
       return this;
     }
-
     return null;
   }
 
@@ -94,14 +87,11 @@ public class QueryCancellationHandler extends 
TaskManagementHandler {
 
   @Override
   public Collection<Api> getApis() {
-    return AnnotatedApi.getApis(new CancelTaskAPI(this));
+    return List.of();
   }
 
-  private List<SearchComponent> getComponentsList() {
-    if (components == null) {
-      components = buildComponentsList();
-    }
-
-    return components;
+  @Override
+  public Collection<Class<? extends JerseyResource>> getJerseyResources() {
+    return List.of(CancelTask.class);
   }
 }
diff --git 
a/solr/core/src/java/org/apache/solr/handler/component/TaskManagementHandler.java
 
b/solr/core/src/java/org/apache/solr/handler/component/TaskManagementHandler.java
index 3e9c5ec2116..ad89247223f 100644
--- 
a/solr/core/src/java/org/apache/solr/handler/component/TaskManagementHandler.java
+++ 
b/solr/core/src/java/org/apache/solr/handler/component/TaskManagementHandler.java
@@ -17,15 +17,8 @@
 package org.apache.solr.handler.component;
 
 import static org.apache.solr.common.params.CommonParams.DISTRIB;
-import static org.apache.solr.common.params.CommonParams.PATH;
 
-import java.io.IOException;
-import java.util.ArrayList;
 import java.util.List;
-import java.util.Map;
-import org.apache.solr.common.SolrException;
-import org.apache.solr.common.params.CommonParams;
-import org.apache.solr.common.params.ModifiableSolrParams;
 import org.apache.solr.core.CoreContainer;
 import org.apache.solr.core.SolrCore;
 import org.apache.solr.handler.RequestHandlerBase;
@@ -44,80 +37,6 @@ public abstract class TaskManagementHandler extends 
RequestHandlerBase
     this.shardHandlerFactory = 
core.getCoreContainer().getShardHandlerFactory();
   }
 
-  /**
-   * Process the actual request. extraParams is required for allowing sub 
handlers to pass in custom
-   * parameters to be put in the outgoing shard request
-   */
-  protected void processRequest(
-      SolrQueryRequest req, ResponseBuilder rb, Map<String, String> 
extraParams)
-      throws IOException {
-    ShardHandler shardHandler = shardHandlerFactory.getShardHandler();
-    List<SearchComponent> components = rb.components;
-
-    shardHandler.prepDistributed(rb);
-
-    for (SearchComponent c : components) {
-      c.prepare(rb);
-    }
-
-    if (!rb.isDistrib) {
-      for (SearchComponent component : components) {
-        component.process(rb);
-      }
-    } else {
-      ShardRequest sreq = new ShardRequest();
-
-      // Distribute to all shards
-      sreq.shards = rb.shards;
-      sreq.actualShards = sreq.shards;
-
-      sreq.responses = new ArrayList<>(sreq.actualShards.length);
-      rb.finished = new ArrayList<>();
-
-      for (String shard : sreq.actualShards) {
-        ModifiableSolrParams params = new ModifiableSolrParams(sreq.params);
-        String reqPath = (String) req.getContext().get(PATH);
-
-        params.set(CommonParams.QT, reqPath);
-        ShardHandler.setShardAttributesToParams(params, sreq.purpose);
-
-        if (extraParams != null) {
-          for (Map.Entry<String, String> entry : extraParams.entrySet()) {
-            params.set(entry.getKey(), entry.getValue());
-          }
-        }
-
-        shardHandler.submit(sreq, shard, params);
-      }
-
-      ShardResponse srsp = shardHandler.takeCompletedOrError();
-
-      if (srsp.getException() != null) {
-        shardHandler.cancelAll();
-        if (srsp.getException() instanceof SolrException) {
-          throw (SolrException) srsp.getException();
-        } else {
-          throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, 
srsp.getException());
-        }
-      }
-
-      rb.finished.add(srsp.getShardRequest());
-
-      for (SearchComponent c : components) {
-        c.handleResponses(rb, srsp.getShardRequest());
-      }
-    }
-  }
-
-  public static List<SearchComponent> buildComponentsList() {
-    List<SearchComponent> components = new ArrayList<>(2);
-
-    QueryCancellationComponent component = new QueryCancellationComponent();
-    components.add(component);
-
-    return components;
-  }
-
   public static ResponseBuilder buildResponseBuilder(
       SolrQueryRequest req, SolrQueryResponse rsp, List<SearchComponent> 
components) {
     CoreContainer cc = req.getCoreContainer();
diff --git a/solr/core/src/resources/ImplicitPlugins.json 
b/solr/core/src/resources/ImplicitPlugins.json
index a9e8dd45ef4..8bb2019695a 100644
--- a/solr/core/src/resources/ImplicitPlugins.json
+++ b/solr/core/src/resources/ImplicitPlugins.json
@@ -144,21 +144,15 @@
       }
     },
     "/tasks/cancel": {
-      "class": "solr.QueryCancellationHandler",
-      "useParams":"_TASK_CANCELLATION",
-      "components": [
-        "querycancellation"
-      ]
+      "class": "solr.TaskCancellationHandler",
+      "useParams":"_TASK_CANCELLATION"
     },
     "/tasks/list": {
       "class": "solr.ActiveTasksListHandler",
-      "useParams":"_LIST_TASKS",
-      "components": [
-        "activetaskslist"
-      ]
+      "useParams":"_LIST_TASKS"
     }
   },
-  "queryResponseWriter": {  
+  "queryResponseWriter": {
     "geojson": {
       "class": "solr.GeoJSONResponseWriter"
     },
diff --git a/solr/core/src/test/org/apache/solr/core/SolrCoreTest.java 
b/solr/core/src/test/org/apache/solr/core/SolrCoreTest.java
index 9fa63f89801..19f4f5312e0 100644
--- a/solr/core/src/test/org/apache/solr/core/SolrCoreTest.java
+++ b/solr/core/src/test/org/apache/solr/core/SolrCoreTest.java
@@ -139,7 +139,7 @@ public class SolrCoreTest extends SolrTestCaseJ4 {
       ++ihCount;
       assertEquals(pathToClassMap.get("update"), 
"solr.V2UpdateRequestHandler");
       ++ihCount;
-      assertEquals(pathToClassMap.get("/tasks/cancel"), 
"solr.QueryCancellationHandler");
+      assertEquals(pathToClassMap.get("/tasks/cancel"), 
"solr.TaskCancellationHandler");
       ++ihCount;
       assertEquals(pathToClassMap.get("/tasks/list"), 
"solr.ActiveTasksListHandler");
     }
diff --git 
a/solr/core/src/test/org/apache/solr/handler/admin/api/CancelTaskAPITest.java 
b/solr/core/src/test/org/apache/solr/handler/admin/api/CancelTaskAPITest.java
new file mode 100644
index 00000000000..ef85bf2a212
--- /dev/null
+++ 
b/solr/core/src/test/org/apache/solr/handler/admin/api/CancelTaskAPITest.java
@@ -0,0 +1,80 @@
+/*
+ * 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.core.CoreContainer.ALLOW_PATHS_SYSPROP;
+
+import org.apache.lucene.search.TotalHitCountCollector;
+import org.apache.solr.SolrTestCase;
+import org.apache.solr.client.api.model.CancelTaskResponse;
+import org.apache.solr.client.api.model.IndexType;
+import org.apache.solr.client.solrj.RemoteSolrException;
+import org.apache.solr.client.solrj.request.TasksApi;
+import org.apache.solr.core.SolrCore;
+import org.apache.solr.search.CancellableCollector;
+import org.apache.solr.util.ExternalPaths;
+import org.apache.solr.util.SolrJettyTestRule;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Test;
+
+/**
+ * HTTP-level test for the {@link CancelTask} JAX-RS endpoint -- exercises 
real Jersey route
+ * registration, DELETE dispatch, response serialization, and HTTP 404 
mapping, none of which {@link
+ * CancelTaskTest} (a direct in-process method call) can catch.
+ */
+public class CancelTaskAPITest extends SolrTestCase {
+
+  private static final String CORE_NAME = "cancelTaskApiTestCore";
+
+  @ClassRule public static SolrJettyTestRule solrTestRule = new 
SolrJettyTestRule();
+
+  @BeforeClass
+  public static void beforeClass() throws Exception {
+    System.setProperty(ALLOW_PATHS_SYSPROP, 
ExternalPaths.SERVER_HOME.toAbsolutePath().toString());
+    solrTestRule.startSolr(createTempDir());
+    
solrTestRule.newCollection(CORE_NAME).withConfigSet(ExternalPaths.DEFAULT_CONFIGSET).create();
+  }
+
+  @Test
+  public void testCancelRunningTaskHttp() throws Exception {
+    final String taskId = "cancel-task-api-test";
+    try (SolrCore core = 
solrTestRule.getJetty().getCoreContainer().getCore(CORE_NAME)) {
+      core.getCancellableQueryTracker()
+          .addShardLevelActiveQuery(taskId, new CancellableCollector(new 
TotalHitCountCollector()));
+
+      var req = new TasksApi.CancelRunningTask(IndexType.CORE, CORE_NAME, 
taskId);
+      CancelTaskResponse response = 
req.process(solrTestRule.getSolrClient(null));
+
+      assertEquals(0, response.responseHeader.status);
+      assertEquals(CancelTaskResponse.CancellationStatus.SUCCESS, 
response.status);
+    }
+  }
+
+  @Test
+  public void testCancelNonExistentTaskHttpReturns404() {
+    var req = new TasksApi.CancelRunningTask(IndexType.CORE, CORE_NAME, 
"does-not-exist");
+
+    RemoteSolrException ex =
+        expectThrows(
+            RemoteSolrException.class, () -> 
req.process(solrTestRule.getSolrClient(null)));
+    assertEquals("Expected 404 for non-existent task", 404, ex.code());
+    assertTrue(
+        "Expected error message to identify the missing task: " + 
ex.getMessage(),
+        ex.getMessage().contains("does-not-exist"));
+  }
+}
diff --git 
a/solr/core/src/test/org/apache/solr/handler/admin/api/ListActiveTasksTest.java 
b/solr/core/src/test/org/apache/solr/handler/admin/api/CancelTaskTest.java
similarity index 51%
copy from 
solr/core/src/test/org/apache/solr/handler/admin/api/ListActiveTasksTest.java
copy to solr/core/src/test/org/apache/solr/handler/admin/api/CancelTaskTest.java
index 113f9dcb82a..1fcc1a52083 100644
--- 
a/solr/core/src/test/org/apache/solr/handler/admin/api/ListActiveTasksTest.java
+++ b/solr/core/src/test/org/apache/solr/handler/admin/api/CancelTaskTest.java
@@ -17,28 +17,26 @@
 
 package org.apache.solr.handler.admin.api;
 
+import static org.apache.solr.SolrTestCaseJ4.assumeWorkingMockito;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
-import java.util.Iterator;
-import java.util.LinkedHashMap;
-import java.util.Map;
-import org.apache.solr.SolrTestCaseJ4;
-import org.apache.solr.client.api.model.ListActiveTaskResponse;
+import org.apache.solr.SolrTestCase;
+import org.apache.solr.client.api.model.CancelTaskResponse;
+import org.apache.solr.common.SolrException;
 import org.apache.solr.core.CancellableQueryTracker;
 import org.apache.solr.core.SolrCore;
 import org.apache.solr.request.SolrQueryRequest;
+import org.apache.solr.search.CancellableCollector;
 import org.junit.Before;
 import org.junit.BeforeClass;
 import org.junit.Test;
 
-public class ListActiveTasksTest extends SolrTestCaseJ4 {
+public class CancelTaskTest extends SolrTestCase {
 
-  private SolrQueryRequest mockQueryRequest;
-  private SolrCore solrCore;
   private CancellableQueryTracker cancellableQueryTracker;
-
-  private ListActiveTasks listActiveTasks;
+  private CancelTask cancelTask;
 
   @BeforeClass
   public static void ensureWorkingMockito() {
@@ -50,33 +48,35 @@ public class ListActiveTasksTest extends SolrTestCaseJ4 {
   public void setUp() throws Exception {
     super.setUp();
 
-    mockQueryRequest = mock(SolrQueryRequest.class);
-    solrCore = mock(SolrCore.class);
+    SolrQueryRequest solrQueryRequest = mock(SolrQueryRequest.class);
+    SolrCore solrCore = mock(SolrCore.class);
     cancellableQueryTracker = mock(CancellableQueryTracker.class);
 
-    listActiveTasks = new ListActiveTasks(mockQueryRequest);
+    when(solrQueryRequest.getCore()).thenReturn(solrCore);
+    
when(solrCore.getCancellableQueryTracker()).thenReturn(cancellableQueryTracker);
+
+    cancelTask = new CancelTask(solrQueryRequest);
   }
 
   @Test
-  public void testGetActiveTasks() throws Exception {
-
-    Map<String, String> myMap = new LinkedHashMap<>();
-    myMap.put("taskID1", "/search?q=h&gf=text-1");
-    myMap.put("taskID2", "/search?q=h&gf=text-2");
-    Iterator<Map.Entry<String, String>> mockIterator = 
myMap.entrySet().iterator();
+  public void testCancelRunningTask() throws Exception {
+    CancellableCollector cancellableCollector = 
mock(CancellableCollector.class);
+    when(cancellableQueryTracker.getCancellableTask("taskID_running"))
+        .thenReturn(cancellableCollector);
 
-    when(mockQueryRequest.getCore()).thenReturn(solrCore);
-    
when(solrCore.getCancellableQueryTracker()).thenReturn(cancellableQueryTracker);
-    
when(cancellableQueryTracker.getActiveQueriesGenerated()).thenReturn(mockIterator);
-
-    ListActiveTaskResponse response = listActiveTasks.listAllActiveTasks();
-    assertNotNull(response.tasks);
+    CancelTaskResponse response = 
cancelTask.cancelRunningTask("taskID_running");
 
-    assertEquals(2, response.tasks.size());
+    assertEquals(CancelTaskResponse.CancellationStatus.SUCCESS, 
response.status);
+    verify(cancellableCollector).cancel();
+  }
 
-    assertEquals("taskID1", response.tasks.get(0).id);
-    assertEquals("/search?q=h&gf=text-1", response.tasks.get(0).query);
+  @Test
+  public void testCancelNonExistentTaskReturns404() {
+    
when(cancellableQueryTracker.getCancellableTask("taskID_missing")).thenReturn(null);
 
-    assertNull(response.error);
+    SolrException exception =
+        expectThrows(SolrException.class, () -> 
cancelTask.cancelRunningTask("taskID_missing"));
+    assertEquals(SolrException.ErrorCode.NOT_FOUND.code, exception.code());
+    assertTrue(exception.getMessage().contains("taskID_missing"));
   }
 }
diff --git 
a/solr/core/src/test/org/apache/solr/handler/admin/api/GetTaskStatusTest.java 
b/solr/core/src/test/org/apache/solr/handler/admin/api/GetTaskStatusTest.java
index acc4fa1d1c7..0bf04ca29aa 100644
--- 
a/solr/core/src/test/org/apache/solr/handler/admin/api/GetTaskStatusTest.java
+++ 
b/solr/core/src/test/org/apache/solr/handler/admin/api/GetTaskStatusTest.java
@@ -17,10 +17,11 @@
 
 package org.apache.solr.handler.admin.api;
 
+import static org.apache.solr.SolrTestCaseJ4.assumeWorkingMockito;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
-import org.apache.solr.SolrTestCaseJ4;
+import org.apache.solr.SolrTestCase;
 import org.apache.solr.client.api.model.TaskStatusResponse;
 import org.apache.solr.core.CancellableQueryTracker;
 import org.apache.solr.core.SolrCore;
@@ -29,7 +30,7 @@ import org.junit.Before;
 import org.junit.BeforeClass;
 import org.junit.Test;
 
-public class GetTaskStatusTest extends SolrTestCaseJ4 {
+public class GetTaskStatusTest extends SolrTestCase {
 
   private SolrQueryRequest mockQueryRequest;
   private SolrCore solrCore;
diff --git 
a/solr/core/src/test/org/apache/solr/handler/admin/api/ListActiveTasksTest.java 
b/solr/core/src/test/org/apache/solr/handler/admin/api/ListActiveTasksTest.java
index 113f9dcb82a..28a24977b8c 100644
--- 
a/solr/core/src/test/org/apache/solr/handler/admin/api/ListActiveTasksTest.java
+++ 
b/solr/core/src/test/org/apache/solr/handler/admin/api/ListActiveTasksTest.java
@@ -17,13 +17,14 @@
 
 package org.apache.solr.handler.admin.api;
 
+import static org.apache.solr.SolrTestCaseJ4.assumeWorkingMockito;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
 import java.util.Iterator;
 import java.util.LinkedHashMap;
 import java.util.Map;
-import org.apache.solr.SolrTestCaseJ4;
+import org.apache.solr.SolrTestCase;
 import org.apache.solr.client.api.model.ListActiveTaskResponse;
 import org.apache.solr.core.CancellableQueryTracker;
 import org.apache.solr.core.SolrCore;
@@ -32,7 +33,7 @@ import org.junit.Before;
 import org.junit.BeforeClass;
 import org.junit.Test;
 
-public class ListActiveTasksTest extends SolrTestCaseJ4 {
+public class ListActiveTasksTest extends SolrTestCase {
 
   private SolrQueryRequest mockQueryRequest;
   private SolrCore solrCore;
diff --git a/solr/core/src/test/org/apache/solr/search/TestTaskManagement.java 
b/solr/core/src/test/org/apache/solr/search/TestTaskManagement.java
index f1039705765..182ceaa637c 100644
--- a/solr/core/src/test/org/apache/solr/search/TestTaskManagement.java
+++ b/solr/core/src/test/org/apache/solr/search/TestTaskManagement.java
@@ -29,7 +29,10 @@ import java.util.concurrent.CompletionException;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.TimeUnit;
+import org.apache.lucene.search.TotalHitCountCollector;
 import org.apache.lucene.util.BytesRef;
+import org.apache.solr.client.api.model.CancelTaskResponse;
+import org.apache.solr.client.api.model.IndexType;
 import org.apache.solr.client.solrj.SolrRequest;
 import org.apache.solr.client.solrj.SolrRequest.SolrRequestType;
 import org.apache.solr.client.solrj.SolrServerException;
@@ -37,6 +40,7 @@ import org.apache.solr.client.solrj.jetty.HttpJettySolrClient;
 import org.apache.solr.client.solrj.request.CollectionAdminRequest;
 import org.apache.solr.client.solrj.request.GenericSolrRequest;
 import org.apache.solr.client.solrj.request.QueryRequest;
+import org.apache.solr.client.solrj.request.TasksApi;
 import org.apache.solr.cloud.SolrCloudTestCase;
 import org.apache.solr.common.SolrInputDocument;
 import org.apache.solr.common.cloud.DocCollection;
@@ -262,17 +266,8 @@ public class TestTaskManagement extends SolrCloudTestCase {
   }
 
   /**
-   * Regression test for cross-shard task visibility.
-   *
-   * <p>On {@code main}, {@code ActiveTasksListHandler.handleRequestBody()} 
delegates to {@code
-   * processRequest()}, which fans the request out to every shard via the 
distributed query
-   * pipeline. {@code ActiveTasksListComponent.handleResponses()} then 
aggregates the per-shard
-   * results, so a task running on shard 2 is visible when the status-check 
request lands on shard
-   * 1.
-   *
-   * <p>If this handler is migrated to JAX-RS and the fan-out is replaced with 
a direct call to the
-   * handler node's own {@code CancellableQueryTracker}, a task registered 
only on shard 2 becomes
-   * invisible to a request handled by shard 1, causing a false "inactive" 
response.
+   * Regression test for cross-shard task visibility -- guards against a task 
on shard 2 becoming
+   * invisible to a status-check request handled by shard 1.
    */
   @Test
   public void testCrossShardTaskStatusVisibility() throws Exception {
@@ -329,6 +324,108 @@ public class TestTaskManagement extends SolrCloudTestCase 
{
     }
   }
 
+  /**
+   * Regression test for cross-shard task cancellation. The current V1 
approach is dependent on
+   * string matching and that makes it brittle, so this helps us make sure no 
issues creep in.
+   */
+  @Test
+  public void testCrossShardTaskCancellationVisibility() throws Exception {
+    DocCollection docCollection =
+        
cluster.getSolrClient().getClusterState().getCollection(COLLECTION_NAME);
+    List<Slice> slices = new ArrayList<>(docCollection.getSlices());
+    assertEquals("test requires exactly 2 shards", 2, slices.size());
+    Replica shard1Leader = slices.get(0).getLeader();
+    Replica shard2Leader = slices.get(1).getLeader();
+    assumeFalse(
+        "Both shard leaders landed on the same node — cross-shard scenario 
cannot be tested",
+        shard1Leader.getNodeName().equals(shard2Leader.getNodeName()));
+
+    final String taskId = "cross-shard-cancellation-test";
+
+    JettySolrRunner shard2Jetty =
+        cluster.getJettySolrRunners().stream()
+            .filter(j -> j.getNodeName().equals(shard2Leader.getNodeName()))
+            .findFirst()
+            .orElseThrow(() -> new AssertionError("No Jetty found for shard 2 
leader"));
+    try (SolrCore shard2Core = 
shard2Jetty.getCoreContainer().getCore(shard2Leader.getCoreName())) {
+      assertNotNull("Could not open shard 2 core", shard2Core);
+      shard2Core
+          .getCancellableQueryTracker()
+          .addShardLevelActiveQuery(taskId, new CancellableCollector(new 
TotalHitCountCollector()));
+
+      try {
+        try (var shard1Client =
+            new 
HttpJettySolrClient.Builder(shard1Leader.getBaseUrl()).build()) {
+          ModifiableSolrParams params = new ModifiableSolrParams();
+          params.set(CommonParams.QUERY_UUID, taskId);
+          var cancelReq =
+              new GenericSolrRequest(
+                      SolrRequest.METHOD.POST, "/tasks/cancel", 
SolrRequestType.ADMIN, params)
+                  .setRequiresCollection(true);
+          NamedList<Object> response = shard1Client.request(cancelReq, 
COLLECTION_NAME);
+
+          assertEquals(
+              "Task registered only on shard 2 must be cancellable via 
cross-shard fan-out from "
+                  + "shard 1. Got: "
+                  + response.get("status"),
+              200,
+              response.get("responseCode"));
+        }
+      } finally {
+        shard2Core.getCancellableQueryTracker().removeCancellableQuery(taskId);
+      }
+    }
+  }
+
+  /**
+   * Same as {@link #testCrossShardTaskCancellationVisibility}, but via the V2 
API -- confirms V2
+   * shares the same cross-shard string-matching fragility as V1.
+   */
+  @Test
+  public void testCrossShardTaskCancellationVisibilityV2() throws Exception {
+    DocCollection docCollection =
+        
cluster.getSolrClient().getClusterState().getCollection(COLLECTION_NAME);
+    List<Slice> slices = new ArrayList<>(docCollection.getSlices());
+    assertEquals("test requires exactly 2 shards", 2, slices.size());
+    Replica shard1Leader = slices.get(0).getLeader();
+    Replica shard2Leader = slices.get(1).getLeader();
+    assumeFalse(
+        "Both shard leaders landed on the same node — cross-shard scenario 
cannot be tested",
+        shard1Leader.getNodeName().equals(shard2Leader.getNodeName()));
+
+    final String taskId = "cross-shard-cancellation-test-v2";
+
+    JettySolrRunner shard2Jetty =
+        cluster.getJettySolrRunners().stream()
+            .filter(j -> j.getNodeName().equals(shard2Leader.getNodeName()))
+            .findFirst()
+            .orElseThrow(() -> new AssertionError("No Jetty found for shard 2 
leader"));
+    try (SolrCore shard2Core = 
shard2Jetty.getCoreContainer().getCore(shard2Leader.getCoreName())) {
+      assertNotNull("Could not open shard 2 core", shard2Core);
+      shard2Core
+          .getCancellableQueryTracker()
+          .addShardLevelActiveQuery(taskId, new CancellableCollector(new 
TotalHitCountCollector()));
+
+      try {
+        try (var shard1Client =
+            new 
HttpJettySolrClient.Builder(shard1Leader.getBaseUrl()).build()) {
+          var cancelReq =
+              new TasksApi.CancelRunningTask(IndexType.COLLECTION, 
COLLECTION_NAME, taskId);
+          CancelTaskResponse response = cancelReq.process(shard1Client, 
COLLECTION_NAME);
+
+          assertEquals(
+              "Task registered only on shard 2 must be cancellable via the V2 
API's cross-shard "
+                  + "fan-out from shard 1. Got: "
+                  + response.status,
+              CancelTaskResponse.CancellationStatus.SUCCESS,
+              response.status);
+        }
+      } finally {
+        shard2Core.getCancellableQueryTracker().removeCancellableQuery(taskId);
+      }
+    }
+  }
+
   private CompletableFuture<Void> cancelQuery(
       final String queryID, Set<Integer> cancelledQueryIdsSet, Set<Integer> 
notFoundQueryIdSet) {
     return CompletableFuture.runAsync(
diff --git 
a/solr/solr-ref-guide/modules/deployment-guide/pages/task-management.adoc 
b/solr/solr-ref-guide/modules/deployment-guide/pages/task-management.adoc
index 5cce6eb8bc9..96033d67c6d 100644
--- a/solr/solr-ref-guide/modules/deployment-guide/pages/task-management.adoc
+++ b/solr/solr-ref-guide/modules/deployment-guide/pages/task-management.adoc
@@ -17,22 +17,23 @@
 // specific language governing permissions and limitations
 // under the License.
 
-Solr offers a task management framework, that allows users to monitor (and 
even cancel) certain types of long-running tasks.
+Solr offers a task management framework that allows users to monitor (and even 
cancel) certain types of long-running tasks.
 Queries are the only type of "task" currently supported, but additional types 
may be added in the future.
 
 == Registering Tasks for Task Management
 
-Task-tracking and management is an opt-in feature: tracking must be explicitly 
enabled on each individual task.
-For queries (the only type of "task" currently supported), this is done by 
specifying the `canCancel` boolean flag as a query-parameter.
-A value of `true` enables task-management; `false` (the default) leaves it 
disabled.
+Task tracking and management is an opt-in feature: tracking must be explicitly 
enabled on each individual task.
+For queries (the only type of "task" currently supported), this is done by 
specifying the 
xref:query-guide:common-query-parameters.adoc#cancancel-parameter[`canCancel`] 
boolean flag as a query parameter.
+A value of `true` enables task management; `false` (the default) leaves it 
disabled.
 
-Solr will assign each task a UUID for tracking purposes.
-Users may override this if desired with an arbitrary string of their choice 
using the `queryUUID` query-parameter.
+Solr will assign each task an ID for tracking purposes.
+Users may override this, if desired, with an arbitrary string of their choice, 
using the `queryUUID` query parameter when submitting the original query.
 (Users are responsible for ensuring that any `queryUUID` values they provide 
are unique and don't conflict with other running tasks.)
-This UUID, whether generated or provided by the user, can then be used to 
track or cancel the task.
+This ID, whether generated or provided by the user, can then be used to track 
or cancel the task: as a query parameter with the V1 API, or as part of the URL 
path with the V2 API.
+See the sections below for the specifics of each operation.
 
 == Task Management Operations
-Task management interface supports the following types of operations:
+The task management interface supports the following types of operations:
 
 . List all currently running cancellable tasks.
 . Cancel a specific task.
@@ -77,7 +78,7 @@ V1 API::
     "status":0,
     "QTime":16},
   "taskList":[
-    "0,"q=weight_i:[0+TO+200]&canCancel=true&queryUUID=0",
+    "0","q=weight_i:[0+TO+200]&canCancel=true&queryUUID=0",
     "5","q=weight_i:[0+TO+200]&canCancel=true&queryUUID=5",
     
"4bcd27bb-0792-4512-a699-532fa7878bd3","q=weight_i:[0+TO+200]&canCancel=true"]}
 ----
@@ -106,7 +107,7 @@ V2 API::
 ====
 ======
 
-== Cancelling An Active Cancellable Task
+== Cancelling an Active Cancellable Task
 To cancel an active task, please use the following syntax:
 
 [tabs#cancelalltasks]
@@ -124,14 +125,22 @@ V2 API::
 ====
 [source,bash]
 ----
-curl -X GET 
"http://localhost:8983/v2/collections/collectionName/tasks/cancel?queryUUID=5";
+curl -X DELETE "http://localhost:8983/v2/collections/collectionName/tasks/5";
 ----
 ====
 ======
 
 === Sample Response
-==== If the task UUID was found and successfully cancelled:
 
+The V1 and V2 APIs currently return the cancellation result in different 
shapes.
+
+==== If the task was found and successfully cancelled:
+
+[tabs#cancelalltasks-response-found]
+======
+V1 API::
++
+====
 ----
 {
   "responseHeader":{
@@ -140,9 +149,32 @@ curl -X GET 
"http://localhost:8983/v2/collections/collectionName/tasks/cancel?qu
   "status":"Query with queryID 5 cancelled successfully",
   "responseCode":200}
 ----
+====
+
+V2 API::
++
+====
+[source,json]
+----
+{
+  "responseHeader":{
+    "status":0,
+    "QTime":26},
+  "status":"SUCCESS"}
+----
+====
+======
+
+==== If the task was not found:
 
-==== If the task UUID was not found
+With the V1 API, a task that isn't found is still a `200` response, with 
`responseCode` in the body indicating the failure.
+With the V2 API, a task that isn't found returns an actual HTTP `404`, with 
the standard v2 error envelope.
 
+[tabs#cancelalltasks-response-notfound]
+======
+V1 API::
++
+====
 ----
 {
   "responseHeader":{
@@ -151,6 +183,26 @@ curl -X GET 
"http://localhost:8983/v2/collections/collectionName/tasks/cancel?qu
   "status":"Query with queryID 5 not found",
   "responseCode":404}
 ----
+====
+
+V2 API::
++
+====
+[source,json]
+----
+{
+  "responseHeader":{
+    "status":404,
+    "QTime":24},
+  "error":{
+    "metadata":{
+      "error-class":"org.apache.solr.common.SolrException",
+      "root-error-class":"org.apache.solr.common.SolrException"},
+    "code":404,
+    "msg":"NOT_FOUND"}}
+----
+====
+======
 
 == Check Status of a Specific Task
 To check the status of a specific task, please use the following syntax:
@@ -177,8 +229,8 @@ curl -X GET 
"http://localhost:8983/v2/collections/collectionName/tasks/5";
 ======
 
 === taskUUID Parameter
-With the V1 API, the `taskUUID` request parameter is used to specify a task 
UUID whose status can be checked.
-With the V2 API, the task ID is instead supplied as part of the path, e.g. 
`/v2/collections/collectionName/tasks/{taskID}`.
+With the V1 API, the `taskUUID` request parameter specifies which task's 
status to check.
+With the V2 API, the task ID is instead supplied as part of the path, e.g. 
`/v2/collections/collectionName/tasks/\{taskID}`.
 
 === Sample Response
 

Reply via email to