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

dsmiley pushed a commit to branch branch_10x
in repository https://gitbox.apache.org/repos/asf/solr.git

commit 7e636dc2c10dc71b5122fb3db279a0e18b261ff5
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]>
    (cherry picked from commit a60886fb38a919161daa5592f04bdc68eb4b4053)
---
 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=&lt;max_generation_lag&gt;</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"}
 ----
+====
+======

Reply via email to