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