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 a60886fb38a SOLR-18248 : Migration of Tasks V2 APIs to JAX-RS
Constructs (#4452)
a60886fb38a is described below
commit a60886fb38a919161daa5592f04bdc68eb4b4053
Author: Jalaz Kumar <[email protected]>
AuthorDate: Sun Jul 26 20:33:06 2026 +0530
SOLR-18248 : Migration of Tasks V2 APIs to JAX-RS Constructs (#4452)
Co-authored-by: Eric Pugh <[email protected]>
---
changelog/unreleased/SOLR-18248-list-tasks.yml | 8 +
.../apache/solr/client/api/endpoint/TasksApi.java | 47 ++++++
.../solr/client/api/model/ActiveTaskDetails.java | 33 +++++
.../client/api/model/ListActiveTaskResponse.java | 25 ++++
.../solr/client/api/model/TaskStatusResponse.java | 40 +++++
.../solr/handler/admin/HealthCheckHandler.java | 3 -
.../apache/solr/handler/admin/api/ActiveTask.java | 85 +++++++++++
.../solr/handler/admin/api/ListActiveTasksAPI.java | 48 ------
.../handler/component/ActiveTaskQuerySupport.java | 165 +++++++++++++++++++++
.../component/ActiveTasksListComponent.java | 128 ----------------
.../handler/component/ActiveTasksListHandler.java | 64 ++++----
.../handler/component/TaskManagementHandler.java | 3 -
.../solr/handler/admin/api/ActiveTaskTest.java | 100 +++++++++++++
.../org/apache/solr/search/TestTaskManagement.java | 116 +++++++++++++++
.../deployment-guide/pages/task-management.adoc | 62 +++++++-
15 files changed, 711 insertions(+), 216 deletions(-)
diff --git a/changelog/unreleased/SOLR-18248-list-tasks.yml
b/changelog/unreleased/SOLR-18248-list-tasks.yml
new file mode 100644
index 00000000000..3dc9716c12c
--- /dev/null
+++ b/changelog/unreleased/SOLR-18248-list-tasks.yml
@@ -0,0 +1,8 @@
+title: Migrated ListTasks API & TaskStatus API from homegrown @EndPoint to
JAX-RS
+type: added
+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
new file mode 100644
index 00000000000..3e46c1465c6
--- /dev/null
+++ b/solr/api/src/java/org/apache/solr/client/api/endpoint/TasksApi.java
@@ -0,0 +1,47 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.solr.client.api.endpoint;
+
+import static org.apache.solr.client.api.util.Constants.INDEX_PATH_PREFIX;
+
+import io.swagger.v3.oas.annotations.Operation;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.PathParam;
+import org.apache.solr.client.api.model.ListActiveTaskResponse;
+import org.apache.solr.client.api.model.TaskStatusResponse;
+import org.apache.solr.client.api.util.StoreApiParameters;
+
+@Path(INDEX_PATH_PREFIX + "/tasks")
+public interface TasksApi {
+
+ @GET
+ @StoreApiParameters
+ @Operation(
+ summary = "Lists all the active tasks",
+ tags = {"tasks"})
+ ListActiveTaskResponse listAllActiveTasks() throws Exception;
+
+ @GET
+ @Path("/{taskID}")
+ @StoreApiParameters
+ @Operation(
+ summary = "Status of a specific task",
+ tags = {"tasks"})
+ TaskStatusResponse getTaskStatus(@PathParam("taskID") String taskID) throws
Exception;
+}
diff --git
a/solr/api/src/java/org/apache/solr/client/api/model/ActiveTaskDetails.java
b/solr/api/src/java/org/apache/solr/client/api/model/ActiveTaskDetails.java
new file mode 100644
index 00000000000..b4edb2b9efb
--- /dev/null
+++ b/solr/api/src/java/org/apache/solr/client/api/model/ActiveTaskDetails.java
@@ -0,0 +1,33 @@
+/*
+ * 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;
+
+public class ActiveTaskDetails {
+
+ public ActiveTaskDetails() {}
+
+ public ActiveTaskDetails(String id, String query) {
+ this.id = id;
+ this.query = query;
+ }
+
+ @JsonProperty public String id;
+ @JsonProperty public String query;
+}
diff --git
a/solr/api/src/java/org/apache/solr/client/api/model/ListActiveTaskResponse.java
b/solr/api/src/java/org/apache/solr/client/api/model/ListActiveTaskResponse.java
new file mode 100644
index 00000000000..c7d7841216b
--- /dev/null
+++
b/solr/api/src/java/org/apache/solr/client/api/model/ListActiveTaskResponse.java
@@ -0,0 +1,25 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.solr.client.api.model;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import java.util.List;
+
+public class ListActiveTaskResponse extends SolrJerseyResponse {
+ @JsonProperty public List<ActiveTaskDetails> tasks;
+}
diff --git
a/solr/api/src/java/org/apache/solr/client/api/model/TaskStatusResponse.java
b/solr/api/src/java/org/apache/solr/client/api/model/TaskStatusResponse.java
new file mode 100644
index 00000000000..9ca07c014e4
--- /dev/null
+++ b/solr/api/src/java/org/apache/solr/client/api/model/TaskStatusResponse.java
@@ -0,0 +1,40 @@
+/*
+ * 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;
+
+public class TaskStatusResponse extends SolrJerseyResponse {
+
+ public enum TaskStatus {
+ ACTIVE("active"),
+ INACTIVE("inactive");
+
+ private final String value;
+
+ TaskStatus(String value) {
+ this.value = value;
+ }
+
+ public String getValue() {
+ return this.value;
+ }
+ }
+
+ @JsonProperty public TaskStatus status;
+}
diff --git
a/solr/core/src/java/org/apache/solr/handler/admin/HealthCheckHandler.java
b/solr/core/src/java/org/apache/solr/handler/admin/HealthCheckHandler.java
index 1dab5d1d977..8c73ac11af3 100644
--- a/solr/core/src/java/org/apache/solr/handler/admin/HealthCheckHandler.java
+++ b/solr/core/src/java/org/apache/solr/handler/admin/HealthCheckHandler.java
@@ -59,9 +59,6 @@ import org.apache.solr.security.AuthorizationContext;
* specify the acceptable generation lag follower should be with respect to
its leader using the
* <code>maxGenerationLag=<max_generation_lag></code> request parameter.
If <code>
* maxGenerationLag</code> is not provided then health check would simply
return OK.
- *
- * <p>All health-check logic lives in the v2 {@link NodeHealth}; this handler
is a thin v1 bridge
- * that extracts request parameters and delegates.
*/
public class HealthCheckHandler extends RequestHandlerBase {
diff --git
a/solr/core/src/java/org/apache/solr/handler/admin/api/ActiveTask.java
b/solr/core/src/java/org/apache/solr/handler/admin/api/ActiveTask.java
new file mode 100644
index 00000000000..6ed8fa001fd
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/handler/admin/api/ActiveTask.java
@@ -0,0 +1,85 @@
+/*
+ * 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 java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import org.apache.solr.api.JerseyResource;
+import org.apache.solr.client.api.endpoint.TasksApi;
+import org.apache.solr.client.api.model.ActiveTaskDetails;
+import org.apache.solr.client.api.model.ListActiveTaskResponse;
+import org.apache.solr.client.api.model.TaskStatusResponse;
+import org.apache.solr.handler.component.ActiveTaskQuerySupport;
+import org.apache.solr.jersey.PermissionName;
+import org.apache.solr.request.SolrQueryRequest;
+
+public class ActiveTask extends JerseyResource implements TasksApi {
+
+ private final SolrQueryRequest solrQueryRequest;
+
+ @Inject
+ public ActiveTask(SolrQueryRequest solrQueryRequest) {
+ this.solrQueryRequest = solrQueryRequest;
+ }
+
+ @Override
+ @PermissionName(READ_PERM)
+ public ListActiveTaskResponse listAllActiveTasks() throws Exception {
+ final ListActiveTaskResponse response =
instantiateJerseyResponse(ListActiveTaskResponse.class);
+ response.tasks = ActiveTaskQuerySupport.listActiveTasks(solrQueryRequest);
+ return response;
+ }
+
+ @Override
+ @PermissionName(READ_PERM)
+ public TaskStatusResponse getTaskStatus(String taskID) throws Exception {
+ final TaskStatusResponse response =
instantiateJerseyResponse(TaskStatusResponse.class);
+
+ boolean isTaskActive =
ActiveTaskQuerySupport.isTaskActive(solrQueryRequest, taskID);
+
+ response.status =
+ (isTaskActive)
+ ? TaskStatusResponse.TaskStatus.ACTIVE
+ : TaskStatusResponse.TaskStatus.INACTIVE;
+
+ return response;
+ }
+
+ public static List<ActiveTaskDetails> getActiveTasksOnThisShard(
+ SolrQueryRequest solrQueryRequest) {
+ Iterator<Map.Entry<String, String>> iterator =
+
solrQueryRequest.getCore().getCancellableQueryTracker().getActiveQueriesGenerated();
+
+ List<ActiveTaskDetails> activeTaskDetails = new ArrayList<>();
+ while (iterator.hasNext()) {
+ Map.Entry<String, String> entry = iterator.next();
+ activeTaskDetails.add(new ActiveTaskDetails(entry.getKey(),
entry.getValue()));
+ }
+
+ return activeTaskDetails;
+ }
+
+ public static boolean isTaskActiveOnThisShard(SolrQueryRequest
solrQueryRequest, String taskId) {
+ return
solrQueryRequest.getCore().getCancellableQueryTracker().isQueryIdActive(taskId);
+ }
+}
diff --git
a/solr/core/src/java/org/apache/solr/handler/admin/api/ListActiveTasksAPI.java
b/solr/core/src/java/org/apache/solr/handler/admin/api/ListActiveTasksAPI.java
deleted file mode 100644
index fba3d9d26f3..00000000000
---
a/solr/core/src/java/org/apache/solr/handler/admin/api/ListActiveTasksAPI.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.ActiveTasksListHandler;
-import org.apache.solr.request.SolrQueryRequest;
-import org.apache.solr.response.SolrQueryResponse;
-import org.apache.solr.security.PermissionNameProvider;
-
-/**
- * V2 API for listing any currently running "tasks".
- *
- * <p>This API (GET /v2/collections/collectionName/tasks/list) is analogous to
the v1
- * /solr/collectionName/tasks/list API.
- */
-public class ListActiveTasksAPI {
- private final ActiveTasksListHandler listTaskHandler;
-
- public ListActiveTasksAPI(ActiveTasksListHandler listTaskHandler) {
- this.listTaskHandler = listTaskHandler;
- }
-
- @EndPoint(
- path = {"/tasks/list"},
- method = GET,
- permission = PermissionNameProvider.Name.READ_PERM)
- public void listActiveTasks(SolrQueryRequest req, SolrQueryResponse rsp)
throws Exception {
- listTaskHandler.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
new file mode 100644
index 00000000000..e1da0f6bb44
--- /dev/null
+++
b/solr/core/src/java/org/apache/solr/handler/component/ActiveTaskQuerySupport.java
@@ -0,0 +1,165 @@
+/*
+ * 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 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.TASK_CHECK_UUID;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import org.apache.solr.client.api.model.ActiveTaskDetails;
+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.ActiveTask;
+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 ActiveTaskQuerySupport() {}
+
+ public static List<ActiveTaskDetails> listActiveTasks(SolrQueryRequest req)
throws Exception {
+ return execute(req, null).taskList;
+ }
+
+ public static boolean isTaskActive(SolrQueryRequest req, String taskId)
throws Exception {
+ return execute(req, taskId).taskActive;
+ }
+
+ private static TaskQueryResult execute(SolrQueryRequest req, String taskId)
throws Exception {
+ if (!shouldDistributed(req)) {
+ return localResult(req, taskId);
+ }
+ return distributedResult(req, taskId);
+ }
+
+ private static TaskQueryResult localResult(SolrQueryRequest req, String
taskId) {
+ if (taskId != null) {
+ return new TaskQueryResult(List.of(),
ActiveTask.isTaskActiveOnThisShard(req, taskId));
+ }
+ return new TaskQueryResult(ActiveTask.getActiveTasksOnThisShard(req),
false);
+ }
+
+ private static TaskQueryResult distributedResult(SolrQueryRequest req,
String taskId)
+ throws Exception {
+ final ShardHandler shardHandler =
+ req.getCoreContainer().getShardHandlerFactory().getShardHandler();
+ final ResponseBuilder responseBuilder =
+ TaskManagementHandler.buildResponseBuilder(req, new
SolrQueryResponse(), List.of());
+ shardHandler.prepDistributed(responseBuilder);
+
+ if (!responseBuilder.isDistrib
+ || responseBuilder.shards == null
+ || responseBuilder.shards.length == 0) {
+ return localResult(req, taskId);
+ }
+
+ final ShardRequest shardRequest = new ShardRequest();
+ shardRequest.shards = responseBuilder.shards;
+ shardRequest.actualShards = shardRequest.shards;
+ shardRequest.responses = new ArrayList<>(shardRequest.actualShards.length);
+
+ for (String shard : shardRequest.actualShards) {
+ ModifiableSolrParams params = new ModifiableSolrParams();
+ params.set(QT, ACTIVE_TASK_LIST_HANDLER_PATH);
+ if (taskId != null) {
+ params.set(TASK_CHECK_UUID, taskId);
+ }
+ ShardHandler.setShardAttributesToParams(params, shardRequest.purpose);
+ shardHandler.submit(shardRequest, shard, params);
+ }
+
+ ShardResponse shardResponse = shardHandler.takeCompletedOrError();
+ if (shardResponse != null && shardResponse.getShard() != null) {
+ if (shardResponse.getException() != null) {
+ shardHandler.cancelAll();
+ if (shardResponse.getException() instanceof SolrException) {
+ throw (SolrException) shardResponse.getException();
+ }
+ throw new SolrException(SolrException.ErrorCode.SERVER_ERROR,
shardResponse.getException());
+ }
+ }
+
+ if (taskId != null) {
+ return new TaskQueryResult(List.of(),
mergeTaskStatus(shardRequest.responses));
+ }
+ return new TaskQueryResult(mergeTaskList(shardRequest.responses), false);
+ }
+
+ private static boolean shouldDistributed(SolrQueryRequest req) {
+ CoreContainer coreContainer = req.getCoreContainer();
+ if (coreContainer == null) {
+ return false;
+ }
+ return req.getParams().getBool(DISTRIB, coreContainer.isZooKeeperAware());
+ }
+
+ private static boolean mergeTaskStatus(List<ShardResponse> responses) {
+ for (ShardResponse shardResponse : responses) {
+ Object taskStatus =
shardResponse.getSolrResponse().getResponse().get("taskStatus");
+ if (taskStatus instanceof Boolean && (Boolean) taskStatus) {
+ return true;
+ }
+
+ if (taskStatus instanceof String && ((String)
taskStatus).contains("active")) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @SuppressWarnings("unchecked")
+ private static List<ActiveTaskDetails> mergeTaskList(List<ShardResponse>
responses) {
+ Map<String, String> mergedTasks = new LinkedHashMap<>();
+ for (ShardResponse shardResponse : responses) {
+ Object taskList =
shardResponse.getSolrResponse().getResponse().get("taskList");
+ if (taskList instanceof NamedList) {
+ NamedList<Object> namedList = (NamedList<Object>) taskList;
+ for (int i = 0; i < namedList.size(); i++) {
+ String taskId = namedList.getName(i);
+ Object taskQuery = namedList.getVal(i);
+ if (taskId != null) {
+ mergedTasks.put(taskId, taskQuery == null ? null :
taskQuery.toString());
+ }
+ }
+ }
+ }
+
+ List<ActiveTaskDetails> mergedTaskList = new
ArrayList<>(mergedTasks.size());
+ for (Map.Entry<String, String> task : mergedTasks.entrySet()) {
+ mergedTaskList.add(new ActiveTaskDetails(task.getKey(),
task.getValue()));
+ }
+
+ return mergedTaskList;
+ }
+
+ private static final class TaskQueryResult {
+ private final List<ActiveTaskDetails> taskList;
+ private final boolean taskActive;
+
+ private TaskQueryResult(List<ActiveTaskDetails> taskList, boolean
taskActive) {
+ this.taskList = taskList;
+ this.taskActive = taskActive;
+ }
+ }
+}
diff --git
a/solr/core/src/java/org/apache/solr/handler/component/ActiveTasksListComponent.java
b/solr/core/src/java/org/apache/solr/handler/component/ActiveTasksListComponent.java
deleted file mode 100644
index ce8a31335c2..00000000000
---
a/solr/core/src/java/org/apache/solr/handler/component/ActiveTasksListComponent.java
+++ /dev/null
@@ -1,128 +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 java.util.Iterator;
-import java.util.LinkedHashMap;
-import java.util.Map;
-import org.apache.solr.common.MapWriter;
-import org.apache.solr.common.util.NamedList;
-
-/** List the active tasks that can be cancelled */
-public class ActiveTasksListComponent extends SearchComponent {
- public static final String COMPONENT_NAME = "activetaskslist";
-
- private boolean shouldProcess;
-
- @Override
- public void prepare(ResponseBuilder rb) throws IOException {
- if (rb.isTaskListRequest()) {
- shouldProcess = true;
- }
- }
-
- @Override
- public void process(ResponseBuilder rb) {
- if (!shouldProcess) {
- return;
- }
-
- if (rb.getTaskStatusCheckUUID() != null) {
- boolean isActiveOnThisShard =
- rb.req
- .getCore()
- .getCancellableQueryTracker()
- .isQueryIdActive(rb.getTaskStatusCheckUUID());
-
- rb.rsp.add("taskStatus", isActiveOnThisShard);
- return;
- }
-
- rb.rsp.add(
- "taskList",
- (MapWriter)
- ew -> {
- Iterator<Map.Entry<String, String>> iterator =
-
rb.req.getCore().getCancellableQueryTracker().getActiveQueriesGenerated();
-
- while (iterator.hasNext()) {
- Map.Entry<String, String> entry = iterator.next();
- ew.put(entry.getKey(), entry.getValue());
- }
- });
- }
-
- @Override
- @SuppressWarnings("unchecked")
- public void handleResponses(ResponseBuilder rb, ShardRequest sreq) {
- if (!shouldProcess) {
- return;
- }
-
- NamedList<String> resultList = new NamedList<>();
-
- for (ShardResponse r : sreq.responses) {
-
- if (rb.getTaskStatusCheckUUID() != null) {
- boolean isTaskActiveOnShard =
r.getSolrResponse().getResponse().getBooleanArg("taskStatus");
-
- if (isTaskActiveOnShard) {
- rb.rsp
- .getValues()
- .add("taskStatus", "id:" + rb.getTaskStatusCheckUUID() + ",
status: active");
- return;
- } else {
- continue;
- }
- }
-
- LinkedHashMap<String, String> result =
- (LinkedHashMap<String, String>)
r.getSolrResponse().getResponse().get("taskList");
-
- Iterator<Map.Entry<String, String>> iterator =
result.entrySet().iterator();
-
- while (iterator.hasNext()) {
- Map.Entry<String, String> entry = iterator.next();
-
- resultList.add(entry.getKey(), entry.getValue());
- }
- }
-
- if (rb.getTaskStatusCheckUUID() != null) {
- // We got here with the specific taskID check being specified -- this
means that the taskID
- // was not found in active tasks on any shard
- rb.rsp
- .getValues()
- .add("taskStatus", "id:" + rb.getTaskStatusCheckUUID() + ", status:
inactive");
- return;
- }
-
- rb.rsp.getValues().add("taskList", resultList);
- }
-
- @Override
- public String getDescription() {
- return "Responsible for listing all active cancellable tasks and also
supports checking the status of "
- + "a particular task";
- }
-
- @Override
- public Category getCategory() {
- return Category.OTHER;
- }
-}
diff --git
a/solr/core/src/java/org/apache/solr/handler/component/ActiveTasksListHandler.java
b/solr/core/src/java/org/apache/solr/handler/component/ActiveTasksListHandler.java
index f1ce12cd937..810e66d8957 100644
---
a/solr/core/src/java/org/apache/solr/handler/component/ActiveTasksListHandler.java
+++
b/solr/core/src/java/org/apache/solr/handler/component/ActiveTasksListHandler.java
@@ -19,49 +19,57 @@ package org.apache.solr.handler.component;
import static org.apache.solr.common.params.CommonParams.TASK_CHECK_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.ListActiveTasksAPI;
+import org.apache.solr.api.JerseyResource;
+import org.apache.solr.client.api.model.ActiveTaskDetails;
+import org.apache.solr.common.params.ShardParams;
+import org.apache.solr.common.util.NamedList;
+import org.apache.solr.common.util.SimpleOrderedMap;
+import org.apache.solr.handler.admin.api.ActiveTask;
import org.apache.solr.request.SolrQueryRequest;
import org.apache.solr.request.SolrRequestHandler;
import org.apache.solr.response.SolrQueryResponse;
import org.apache.solr.security.AuthorizationContext;
import org.apache.solr.security.PermissionNameProvider;
-/** Handles request for listing all active cancellable tasks */
+/** Handles request for listing all active cancellable tasks and get status
check of any taskId. */
public class ActiveTasksListHandler 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 {
- Map<String, String> extraParams = null;
- ResponseBuilder rb = buildResponseBuilder(req, rsp, getComponentsList());
-
- rb.setIsTaskListRequest(true);
-
- String taskStatusCheckUUID = req.getParams().get(TASK_CHECK_UUID, null);
-
- if (taskStatusCheckUUID != null) {
- if (rb.isDistrib) {
- extraParams = new HashMap<>();
-
- extraParams.put(TASK_CHECK_UUID, taskStatusCheckUUID);
+ String taskStatusCheckID = req.getParams().get(TASK_CHECK_UUID, null);
+ boolean isShardedRequest = req.getParams().getBool(ShardParams.IS_SHARD,
false);
+
+ if (taskStatusCheckID != null) {
+ boolean taskStatus = ActiveTaskQuerySupport.isTaskActive(req,
taskStatusCheckID);
+ if (isShardedRequest) {
+ rsp.add("taskStatus", taskStatus);
+ } else {
+ rsp.add(
+ "taskStatus",
+ "id: " + taskStatusCheckID + ", status: " + (taskStatus ? "active"
: "inactive"));
}
- rb.setTaskStatusCheckUUID(taskStatusCheckUUID);
+ } else {
+ NamedList<String> tasks = new SimpleOrderedMap<>();
+ List<ActiveTaskDetails> taskList =
ActiveTaskQuerySupport.listActiveTasks(req);
+ if (taskList != null) {
+ for (ActiveTaskDetails task : taskList) {
+ tasks.add(task.id, task.query);
+ }
+ }
+ rsp.add("taskList", tasks);
}
-
- processRequest(req, rb, extraParams);
}
+ // ////////////////////// SolrInfoMBeans methods //////////////////////
+
@Override
public String getDescription() {
- return "activetaskslist";
+ return "Active Tasks List";
}
@Override
@@ -79,7 +87,6 @@ public class ActiveTasksListHandler extends
TaskManagementHandler {
if (path.startsWith("/tasks/list")) {
return this;
}
-
return null;
}
@@ -90,14 +97,11 @@ public class ActiveTasksListHandler extends
TaskManagementHandler {
@Override
public Collection<Api> getApis() {
- return AnnotatedApi.getApis(new ListActiveTasksAPI(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(ActiveTask.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 afb7a05c586..3e9c5ec2116 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
@@ -115,9 +115,6 @@ public abstract class TaskManagementHandler extends
RequestHandlerBase
QueryCancellationComponent component = new QueryCancellationComponent();
components.add(component);
- ActiveTasksListComponent activeTasksListComponent = new
ActiveTasksListComponent();
- components.add(activeTasksListComponent);
-
return components;
}
diff --git
a/solr/core/src/test/org/apache/solr/handler/admin/api/ActiveTaskTest.java
b/solr/core/src/test/org/apache/solr/handler/admin/api/ActiveTaskTest.java
new file mode 100644
index 00000000000..1ab1e2bc489
--- /dev/null
+++ b/solr/core/src/test/org/apache/solr/handler/admin/api/ActiveTaskTest.java
@@ -0,0 +1,100 @@
+/*
+ * 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.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.client.api.model.ListActiveTaskResponse;
+import org.apache.solr.client.api.model.TaskStatusResponse;
+import org.apache.solr.core.CancellableQueryTracker;
+import org.apache.solr.core.SolrCore;
+import org.apache.solr.request.SolrQueryRequest;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+public class ActiveTaskTest extends SolrTestCaseJ4 {
+
+ private SolrQueryRequest mockQueryRequest;
+ private SolrCore solrCore;
+ private CancellableQueryTracker cancellableQueryTracker;
+
+ private ActiveTask activeTask;
+
+ @BeforeClass
+ public static void ensureWorkingMockito() {
+ assumeWorkingMockito();
+ }
+
+ @Override
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+
+ mockQueryRequest = mock(SolrQueryRequest.class);
+ solrCore = mock(SolrCore.class);
+ cancellableQueryTracker = mock(CancellableQueryTracker.class);
+
+ activeTask = new ActiveTask(mockQueryRequest);
+ }
+
+ @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();
+
+ when(mockQueryRequest.getCore()).thenReturn(solrCore);
+
when(solrCore.getCancellableQueryTracker()).thenReturn(cancellableQueryTracker);
+
when(cancellableQueryTracker.getActiveQueriesGenerated()).thenReturn(mockIterator);
+
+ ListActiveTaskResponse response = activeTask.listAllActiveTasks();
+ assertNotNull(response.tasks);
+
+ assertEquals(2, response.tasks.size());
+
+ assertEquals("taskID1", response.tasks.get(0).id);
+ assertEquals("/search?q=h&gf=text-1", response.tasks.get(0).query);
+
+ assertNull(response.error);
+ }
+
+ @Test
+ public void testGetTaskStatus() throws Exception {
+
+ when(mockQueryRequest.getCore()).thenReturn(solrCore);
+
when(solrCore.getCancellableQueryTracker()).thenReturn(cancellableQueryTracker);
+
when(cancellableQueryTracker.isQueryIdActive("taskID_running")).thenReturn(true);
+
when(cancellableQueryTracker.isQueryIdActive("taskID_stopped")).thenReturn(false);
+
+ TaskStatusResponse responseRunningTask =
activeTask.getTaskStatus("taskID_running");
+ assertEquals(TaskStatusResponse.TaskStatus.ACTIVE,
responseRunningTask.status);
+ assertNull(responseRunningTask.error);
+
+ TaskStatusResponse responseStoppedTask =
activeTask.getTaskStatus("taskID_stopped");
+ assertEquals(TaskStatusResponse.TaskStatus.INACTIVE,
responseStoppedTask.status);
+ assertNull(responseStoppedTask.error);
+ }
+}
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 13c98d87ace..f1039705765 100644
--- a/solr/core/src/test/org/apache/solr/search/TestTaskManagement.java
+++ b/solr/core/src/test/org/apache/solr/search/TestTaskManagement.java
@@ -33,15 +33,23 @@ import org.apache.lucene.util.BytesRef;
import org.apache.solr.client.solrj.SolrRequest;
import org.apache.solr.client.solrj.SolrRequest.SolrRequestType;
import org.apache.solr.client.solrj.SolrServerException;
+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.cloud.SolrCloudTestCase;
import org.apache.solr.common.SolrInputDocument;
+import org.apache.solr.common.cloud.DocCollection;
+import org.apache.solr.common.cloud.Replica;
+import org.apache.solr.common.cloud.Slice;
+import org.apache.solr.common.params.CommonParams;
import org.apache.solr.common.params.ModifiableSolrParams;
import org.apache.solr.common.params.SolrParams;
import org.apache.solr.common.util.ExecutorUtil;
import org.apache.solr.common.util.NamedList;
+import org.apache.solr.core.SolrCore;
+import org.apache.solr.embedded.JettySolrRunner;
+import org.apache.solr.request.SolrQueryRequestBase;
import org.hamcrest.Matcher;
import org.hamcrest.Matchers;
import org.junit.After;
@@ -213,6 +221,114 @@ public class TestTaskManagement extends SolrCloudTestCase
{
assertTrue(result.contains("inactive"));
}
+ @Test
+ public void testCheckSpecificQueryStatus_Active() throws Exception {
+ for (int i = 0; i < 10; i++) {
+ executeQueryAsync(Integer.toString(i));
+ }
+
+ ModifiableSolrParams params = new ModifiableSolrParams();
+ params.set("taskUUID", "5");
+
+ var request =
+ new GenericSolrRequest(
+ SolrRequest.METHOD.GET, "/tasks/list",
SolrRequest.SolrRequestType.ADMIN, params)
+ .setRequiresCollection(true);
+ NamedList<Object> queryResponse =
cluster.getSolrClient(COLLECTION_NAME).request(request);
+
+ String result = (String) queryResponse.get("taskStatus");
+
+ assertTrue(result.contains("active"));
+ }
+
+ @Test
+ public void testCheckSpecificQueryStatus_Inactive() throws Exception {
+ for (int i = 0; i < 10; i++) {
+ executeQueryAsync(Integer.toString(i));
+ }
+
+ ModifiableSolrParams params = new ModifiableSolrParams();
+ params.set("taskUUID", "15");
+
+ var request =
+ new GenericSolrRequest(
+ SolrRequest.METHOD.GET, "/tasks/list",
SolrRequest.SolrRequestType.ADMIN, params)
+ .setRequiresCollection(true);
+ NamedList<Object> queryResponse =
cluster.getSolrClient(COLLECTION_NAME).request(request);
+
+ String result = (String) queryResponse.get("taskStatus");
+
+ assertTrue(result.contains("inactive"));
+ }
+
+ /**
+ * 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.
+ */
+ @Test
+ public void testCrossShardTaskStatusVisibility() 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-visibility-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);
+ ModifiableSolrParams fakeParams = new ModifiableSolrParams();
+ fakeParams.set(CommonParams.QUERY_UUID, taskId);
+ try (var fakeReq = new SolrQueryRequestBase(shard2Core, fakeParams)) {
+ shard2Core.getCancellableQueryTracker().generateQueryID(fakeReq);
+ }
+
+ try {
+ try (var shard1Client =
+ new
HttpJettySolrClient.Builder(shard1Leader.getBaseUrl()).build()) {
+ ModifiableSolrParams params = new ModifiableSolrParams();
+ params.set(CommonParams.TASK_CHECK_UUID, taskId);
+ var statusReq =
+ new GenericSolrRequest(
+ SolrRequest.METHOD.GET, "/tasks/list",
SolrRequestType.ADMIN, params)
+ .setRequiresCollection(true);
+ NamedList<Object> response = shard1Client.request(statusReq,
COLLECTION_NAME);
+
+ Object taskStatus = response.get("taskStatus");
+ assertNotNull("taskStatus missing from response", taskStatus);
+ assertTrue(
+ "Task registered on shard 2 must be visible from shard 1 via
cross-shard fan-out. "
+ + "Got: "
+ + taskStatus
+ + " ("
+ + taskStatus.getClass().getSimpleName()
+ + ") — if this is Boolean.FALSE the distributed aggregation
was dropped.",
+ taskStatus instanceof String && ((String)
taskStatus).contains("status: active"));
+ }
+ } finally {
+ shard2Core.getCancellableQueryTracker().releaseQueryID(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 a320d9cfdf3..5cce6eb8bc9 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
@@ -57,13 +57,20 @@ V2 API::
====
[source,bash]
----
-curl -X GET "http://localhost:8983/v2/collections/collectionName/tasks/list"
+curl -X GET "http://localhost:8983/v2/collections/collectionName/tasks"
----
====
======
=== Sample Response
+The V1 and V2 APIs currently return `taskList` in different shapes.
+
+[tabs#listalltasks-response]
+======
+V1 API::
++
+====
----
{
"responseHeader":{
@@ -74,6 +81,30 @@ curl -X GET
"http://localhost:8983/v2/collections/collectionName/tasks/list"
"5","q=weight_i:[0+TO+200]&canCancel=true&queryUUID=5",
"4bcd27bb-0792-4512-a699-532fa7878bd3","q=weight_i:[0+TO+200]&canCancel=true"]}
----
+====
+
+V2 API::
++
+====
+[source,json]
+----
+{
+ "responseHeader":{
+ "status":0,
+ "QTime":16},
+ "tasks":[
+ {
+ "id":"0",
+ "query":"q=weight_i:[0+TO+200]&canCancel=true&queryUUID=0"},
+ {
+ "id":"5",
+ "query":"q=weight_i:[0+TO+200]&canCancel=true&queryUUID=5"},
+ {
+ "id":"4bcd27bb-0792-4512-a699-532fa7878bd3",
+ "query":"q=weight_i:[0+TO+200]&canCancel=true"}]}
+----
+====
+======
== Cancelling An Active Cancellable Task
To cancel an active task, please use the following syntax:
@@ -140,20 +171,43 @@ V2 API::
====
[source,bash]
----
-curl -X GET
"http://localhost:8983/v2/collections/collectionName/tasks/list?taskUUID=5"
+curl -X GET "http://localhost:8983/v2/collections/collectionName/tasks/5"
----
====
======
=== taskUUID Parameter
-`taskUUID` parameter can be used to specify a task UUID whose status can be
checked.
+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}`.
=== Sample Response
+The V1 and V2 APIs currently return `taskStatus` in different shapes.
+
+[tabs#checksingletask-response]
+======
+V1 API::
++
+====
+----
+{
+ "responseHeader":{
+ "status":0,
+ "QTime":16},
+ "taskStatus":"id: 5, status: active"}
+----
+====
+
+V2 API::
++
+====
+[source,json]
----
{
"responseHeader":{
"status":0,
"QTime":16},
- "taskStatus":"id:5, status: active"}
+ "status":"ACTIVE"}
----
+====
+======