diff --git a/changelog/unreleased/SOLR-18248-cancel-tasks.yml b/changelog/unreleased/SOLR-18248-cancel-tasks.yml
new file mode 100644
index 000000000000..d86b6429de1a
--- /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 46d80de5d9d3..d2b9c9216376 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 @@
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 @@ interface Status {
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 000000000000..41158d6e521f
--- /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 000000000000..095a981a5566
--- /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 a63adc055f42..000000000000
--- 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".
- *
- *
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 6276b419451f..b6a2fcc8b777 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 @@
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.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 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 @@ private static TaskQueryResult distributedResult(SolrQueryRequest req, String ta
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 @@ private static TaskQueryResult distributedResult(SolrQueryRequest req, String ta
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 @@ private static TaskQueryResult distributedResult(SolrQueryRequest req, String ta
}
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 @@ private static boolean mergeTaskStatus(List responses) {
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 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 mergeTaskList(List responses) {
Map mergedTasks = new LinkedHashMap<>();
@@ -156,10 +198,13 @@ private static List mergeTaskList(List respons
private static final class TaskQueryResult {
private final List taskList;
private final boolean taskActive;
+ private final boolean taskCancelled;
- private TaskQueryResult(List taskList, boolean taskActive) {
+ private TaskQueryResult(
+ List 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 9648bb312e44..000000000000
--- 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 18c99ba09c3b..a99ba2d1a41e 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 @@
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.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 components;
@Override
public void handleRequestBody(SolrQueryRequest req, SolrQueryResponse rsp) throws Exception {
- ResponseBuilder rb = buildResponseBuilder(req, rsp, getComponentsList());
- Map 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 SolrRequestHandler getSubHandler(String path) {
if (path.startsWith("/tasks/cancel")) {
return this;
}
-
return null;
}
@@ -94,14 +87,11 @@ public Boolean registerV2() {
@Override
public Collection getApis() {
- return AnnotatedApi.getApis(new CancelTaskAPI(this));
+ return List.of();
}
- private List getComponentsList() {
- if (components == null) {
- components = buildComponentsList();
- }
-
- return components;
+ @Override
+ public Collection> 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 3e9c5ec21160..ad89247223f1 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 void inform(SolrCore core) {
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 extraParams)
- throws IOException {
- ShardHandler shardHandler = shardHandlerFactory.getShardHandler();
- List 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 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 buildComponentsList() {
- List components = new ArrayList<>(2);
-
- QueryCancellationComponent component = new QueryCancellationComponent();
- components.add(component);
-
- return components;
- }
-
public static ResponseBuilder buildResponseBuilder(
SolrQueryRequest req, SolrQueryResponse rsp, List components) {
CoreContainer cc = req.getCoreContainer();
diff --git a/solr/core/src/resources/ImplicitPlugins.json b/solr/core/src/resources/ImplicitPlugins.json
index a9e8dd45ef4c..8bb2019695a7 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 9fa63f89801f..19f4f5312e02 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 void testImplicitPlugins() {
++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 000000000000..ef85bf2a2121
--- /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/CancelTaskTest.java b/solr/core/src/test/org/apache/solr/handler/admin/api/CancelTaskTest.java
new file mode 100644
index 000000000000..1fcc1a52083d
--- /dev/null
+++ b/solr/core/src/test/org/apache/solr/handler/admin/api/CancelTaskTest.java
@@ -0,0 +1,82 @@
+/*
+ * 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.SolrTestCaseJ4.assumeWorkingMockito;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+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 CancelTaskTest extends SolrTestCase {
+
+ private CancellableQueryTracker cancellableQueryTracker;
+ private CancelTask cancelTask;
+
+ @BeforeClass
+ public static void ensureWorkingMockito() {
+ assumeWorkingMockito();
+ }
+
+ @Override
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+
+ SolrQueryRequest solrQueryRequest = mock(SolrQueryRequest.class);
+ SolrCore solrCore = mock(SolrCore.class);
+ cancellableQueryTracker = mock(CancellableQueryTracker.class);
+
+ when(solrQueryRequest.getCore()).thenReturn(solrCore);
+ when(solrCore.getCancellableQueryTracker()).thenReturn(cancellableQueryTracker);
+
+ cancelTask = new CancelTask(solrQueryRequest);
+ }
+
+ @Test
+ public void testCancelRunningTask() throws Exception {
+ CancellableCollector cancellableCollector = mock(CancellableCollector.class);
+ when(cancellableQueryTracker.getCancellableTask("taskID_running"))
+ .thenReturn(cancellableCollector);
+
+ CancelTaskResponse response = cancelTask.cancelRunningTask("taskID_running");
+
+ assertEquals(CancelTaskResponse.CancellationStatus.SUCCESS, response.status);
+ verify(cancellableCollector).cancel();
+ }
+
+ @Test
+ public void testCancelNonExistentTaskReturns404() {
+ when(cancellableQueryTracker.getCancellableTask("taskID_missing")).thenReturn(null);
+
+ 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 acc4fa1d1c7b..0bf04ca29aa5 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.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 113f9dcb82a2..28a24977b8cb 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.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 f1039705765e..182ceaa637c0 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.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.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 void testCheckSpecificQueryStatus_Inactive() throws Exception {
}
/**
- * Regression test for cross-shard task visibility.
- *
- *
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.
- *
- *
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 void testCrossShardTaskStatusVisibility() throws Exception {
}
}
+ /**
+ * 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 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