You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@solr.apache.org by ep...@apache.org on 2026/09/27 00:08:34 UTC
(solr) branch main updated: SOLR-16458: Add node threads JAX-RS API (#4722)
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 72add954641 SOLR-16458: Add node threads JAX-RS API (#4722)
72add954641 is described below
commit 72add954641a3ceacb99ecfaa02717c81f22926e
Author: Yash Goswami <yg...@outlook.com>
AuthorDate: Sun Sep 27 05:38:29 2026 +0530
SOLR-16458: Add node threads JAX-RS API (#4722)
Co-authored-by: Eric Pugh <ep...@opensourceconnections.com>
---
changelog/unreleased/SOLR-16458.yml | 9 ++
.../solr/client/api/endpoint/NodeThreadsApi.java | 33 +++++
.../solr/client/api/model/NodeThreadsResponse.java | 145 ++++++++++++++++++
.../solr/handler/admin/ThreadDumpHandler.java | 165 +++++++--------------
.../solr/handler/admin/api/NodeThreadsAPI.java | 122 ++++++++++++---
.../solr/handler/admin/ThreadDumpHandlerTest.java | 59 ++++++++
.../solr/handler/admin/api/NodeThreadsAPITest.java | 54 +++++++
.../handler/admin/api/V2NodeAPIMappingTest.java | 31 +---
.../pages/implicit-requesthandlers.adoc | 53 ++++++-
solr/webapp/web/js/angular/controllers/threads.js | 37 +++--
solr/webapp/web/js/angular/services.js | 14 +-
11 files changed, 530 insertions(+), 192 deletions(-)
diff --git a/changelog/unreleased/SOLR-16458.yml b/changelog/unreleased/SOLR-16458.yml
new file mode 100644
index 00000000000..a899e68348d
--- /dev/null
+++ b/changelog/unreleased/SOLR-16458.yml
@@ -0,0 +1,9 @@
+title: >
+ SolrJ now offers a `SystemApi.GetThreadDump` SolrRequest class for fetching
+ thread dumps from a single Solr node through the JAX-RS `/api/node/threads` API.
+type: added
+authors:
+ - name: Yash Goswami
+links:
+ - name: SOLR-16458
+ url: https://issues.apache.org/jira/browse/SOLR-16458
diff --git a/solr/api/src/java/org/apache/solr/client/api/endpoint/NodeThreadsApi.java b/solr/api/src/java/org/apache/solr/client/api/endpoint/NodeThreadsApi.java
new file mode 100644
index 00000000000..39dcb17b588
--- /dev/null
+++ b/solr/api/src/java/org/apache/solr/client/api/endpoint/NodeThreadsApi.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.endpoint;
+
+import io.swagger.v3.oas.annotations.Operation;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+import org.apache.solr.client.api.model.NodeThreadsResponse;
+
+/** V2 API for retrieving thread information from the receiving node. */
+@Path("/node/threads")
+public interface NodeThreadsApi {
+
+ @GET
+ @Operation(
+ summary = "Retrieve thread information for the receiving node.",
+ tags = {"system"})
+ NodeThreadsResponse getThreadDump();
+}
diff --git a/solr/api/src/java/org/apache/solr/client/api/model/NodeThreadsResponse.java b/solr/api/src/java/org/apache/solr/client/api/model/NodeThreadsResponse.java
new file mode 100644
index 00000000000..ff575497b45
--- /dev/null
+++ b/solr/api/src/java/org/apache/solr/client/api/model/NodeThreadsResponse.java
@@ -0,0 +1,145 @@
+/*
+ * 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 io.swagger.v3.oas.annotations.media.Schema;
+import java.util.List;
+
+/** Response body for the {@code /api/node/threads} endpoint. */
+public class NodeThreadsResponse extends SolrJerseyResponse {
+
+ @Schema(description = "Thread information for the receiving JVM.")
+ @JsonProperty("system")
+ public SystemInfo system;
+
+ public static class SystemInfo {
+
+ @Schema(description = "Live and peak platform-thread counts.")
+ @JsonProperty("threadCount")
+ public ThreadCount threadCount;
+
+ @Schema(description = "Threads detected in a deadlock. Omitted when no deadlock is detected.")
+ @JsonProperty("deadlocks")
+ public List<ThreadEntry> deadlocks;
+
+ @Schema(description = "Snapshot of live platform threads, each wrapped in a thread property.")
+ @JsonProperty("threadDump")
+ public List<ThreadEntry> threadDump;
+ }
+
+ public static class ThreadCount {
+
+ @Schema(description = "Current number of live platform threads.")
+ @JsonProperty("current")
+ public long current;
+
+ @Schema(
+ description =
+ "Peak live platform-thread count since JVM startup or the last peak-count reset.")
+ @JsonProperty("peak")
+ public long peak;
+
+ @Schema(description = "Current number of live daemon platform threads.")
+ @JsonProperty("daemon")
+ public long daemon;
+ }
+
+ public static class ThreadEntry {
+
+ @Schema(description = "Details of one platform thread.")
+ @JsonProperty("thread")
+ public ThreadInfo thread;
+ }
+
+ public static class ThreadInfo {
+
+ @Schema(description = "JVM thread identifier.")
+ @JsonProperty("id")
+ public long id;
+
+ @Schema(description = "Thread name.")
+ @JsonProperty("name")
+ public String name;
+
+ @Schema(description = "JVM thread state, such as RUNNABLE, BLOCKED, or WAITING.")
+ @JsonProperty("state")
+ public String state;
+
+ @Schema(description = "Name of the lock the thread is waiting for, if any.")
+ @JsonProperty("lock")
+ public String lock;
+
+ @Schema(description = "Lock the thread is waiting for, if any, and its owner when known.")
+ @JsonProperty("lock-waiting")
+ public LockWaiting lockWaiting;
+
+ @Schema(description = "Ownable synchronizers held by this thread. Omitted when none are held.")
+ @JsonProperty("synchronizers-locked")
+ public List<String> synchronizersLocked;
+
+ @Schema(description = "Object monitors held by this thread. Omitted when none are held.")
+ @JsonProperty("monitors-locked")
+ public List<String> monitorsLocked;
+
+ @Schema(description = "Present and true when the thread is suspended.")
+ @JsonProperty("suspended")
+ public Boolean suspended;
+
+ @Schema(description = "Present and true when the thread is executing native code.")
+ @JsonProperty("native")
+ public Boolean nativeThread;
+
+ @Schema(
+ description =
+ "Total thread CPU time formatted in milliseconds with an ms suffix. Omitted when unsupported; a negative value indicates unavailable timing.")
+ @JsonProperty("cpuTime")
+ public String cpuTime;
+
+ @Schema(
+ description =
+ "Thread CPU time in user mode formatted in milliseconds with an ms suffix. Omitted when unsupported; a negative value indicates unavailable timing.")
+ @JsonProperty("userTime")
+ public String userTime;
+
+ @Schema(description = "Stack frames, starting with the most recent method invocation.")
+ @JsonProperty("stackTrace")
+ public List<String> stackTrace;
+ }
+
+ public static class LockWaiting {
+
+ @Schema(description = "Lock class name and identity hash code.")
+ @JsonProperty("name")
+ public String name;
+
+ @Schema(description = "Thread owning the lock. Omitted when no owner is known.")
+ @JsonProperty("owner")
+ public LockOwner owner;
+ }
+
+ public static class LockOwner {
+
+ @Schema(description = "Name of the thread owning the lock.")
+ @JsonProperty("name")
+ public String name;
+
+ @Schema(description = "JVM thread identifier.")
+ @JsonProperty("id")
+ public long id;
+ }
+}
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/ThreadDumpHandler.java b/solr/core/src/java/org/apache/solr/handler/admin/ThreadDumpHandler.java
index 5d2832cd74e..9844bdac756 100644
--- a/solr/core/src/java/org/apache/solr/handler/admin/ThreadDumpHandler.java
+++ b/solr/core/src/java/org/apache/solr/handler/admin/ThreadDumpHandler.java
@@ -20,16 +20,12 @@ import static org.apache.solr.common.params.CommonParams.ID;
import static org.apache.solr.common.params.CommonParams.NAME;
import java.io.IOException;
-import java.lang.management.LockInfo;
-import java.lang.management.ManagementFactory;
-import java.lang.management.ThreadInfo;
-import java.lang.management.ThreadMXBean;
-import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
-import java.util.Locale;
-import org.apache.solr.api.AnnotatedApi;
import org.apache.solr.api.Api;
+import org.apache.solr.api.JerseyResource;
+import org.apache.solr.client.api.model.NodeThreadsResponse.ThreadEntry;
+import org.apache.solr.client.api.model.NodeThreadsResponse.ThreadInfo;
import org.apache.solr.common.util.NamedList;
import org.apache.solr.common.util.SimpleOrderedMap;
import org.apache.solr.handler.RequestHandlerBase;
@@ -45,124 +41,71 @@ public class ThreadDumpHandler extends RequestHandlerBase {
@Override
public void handleRequestBody(SolrQueryRequest req, SolrQueryResponse rsp) throws IOException {
- SimpleOrderedMap<Object> system = new SimpleOrderedMap<>();
- rsp.add("system", system);
-
- ThreadMXBean tmbean = ManagementFactory.getThreadMXBean();
-
- // Thread Count
- SimpleOrderedMap<Object> nl = new SimpleOrderedMap<>();
- nl.add("current", tmbean.getThreadCount());
- nl.add("peak", tmbean.getPeakThreadCount());
- nl.add("daemon", tmbean.getDaemonThreadCount());
- system.add("threadCount", nl);
-
- // Deadlocks
- ThreadInfo[] tinfos;
- long[] tids = tmbean.findDeadlockedThreads();
- if (tids != null) {
- tinfos = tmbean.getThreadInfo(tids, Integer.MAX_VALUE);
- NamedList<SimpleOrderedMap<Object>> lst = new NamedList<>();
- for (ThreadInfo ti : tinfos) {
- if (ti != null) {
- lst.add("thread", getThreadInfo(ti, tmbean));
- }
- }
- system.add("deadlocks", lst);
+ final var response = new NodeThreadsAPI().getThreadDump();
+ final var system = new SimpleOrderedMap<Object>();
+ final var counts = new SimpleOrderedMap<Object>();
+ counts.add("current", Math.toIntExact(response.system.threadCount.current));
+ counts.add("peak", Math.toIntExact(response.system.threadCount.peak));
+ counts.add("daemon", Math.toIntExact(response.system.threadCount.daemon));
+ system.add("threadCount", counts);
+ if (response.system.deadlocks != null) {
+ system.add("deadlocks", toNamedList(response.system.deadlocks));
}
-
- // Now show all the threads....
-
- tinfos = tmbean.dumpAllThreads(true, true);
- NamedList<SimpleOrderedMap<Object>> lst = new NamedList<>();
- for (ThreadInfo ti : tinfos) {
- if (ti != null) {
- lst.add("thread", getThreadInfo(ti, tmbean));
- }
- }
- system.add("threadDump", lst);
+ system.add("threadDump", toNamedList(response.system.threadDump));
+ rsp.add("system", system);
rsp.setHttpCaching(false);
}
- // --------------------------------------------------------------------------------
- // --------------------------------------------------------------------------------
-
- private static SimpleOrderedMap<Object> getThreadInfo(ThreadInfo ti, ThreadMXBean tmbean) {
- SimpleOrderedMap<Object> info = new SimpleOrderedMap<>();
- long tid = ti.getThreadId();
-
- info.add(ID, tid);
- info.add(NAME, ti.getThreadName());
- info.add("state", ti.getThreadState().toString());
+ private static NamedList<SimpleOrderedMap<Object>> toNamedList(List<ThreadEntry> threads) {
+ final var result = new NamedList<SimpleOrderedMap<Object>>();
+ for (var entry : threads) {
+ result.add("thread", toNamedList(entry.thread));
+ }
+ return result;
+ }
- if (ti.getLockName() != null) {
- // TODO: this is redundent with lock-waiting below .. deprecate & remove
- // TODO: (but first needs UI change)
- info.add("lock", ti.getLockName());
+ private static SimpleOrderedMap<Object> toNamedList(ThreadInfo thread) {
+ final var info = new SimpleOrderedMap<Object>();
+ info.add(ID, thread.id);
+ info.add(NAME, thread.name);
+ info.add("state", thread.state);
+ if (thread.lock != null) {
+ info.add("lock", thread.lock);
}
- {
- final LockInfo lockInfo = ti.getLockInfo();
- if (null != lockInfo) {
- final SimpleOrderedMap<Object> lock = new SimpleOrderedMap<>();
- info.add("lock-waiting", lock);
- lock.add(NAME, lockInfo.toString());
- if (-1 == ti.getLockOwnerId() && null == ti.getLockOwnerName()) {
- lock.add("owner", null);
- } else {
- final SimpleOrderedMap<Object> owner = new SimpleOrderedMap<>();
- lock.add("owner", owner);
- owner.add(NAME, ti.getLockOwnerName());
- owner.add(ID, ti.getLockOwnerId());
- }
+ if (thread.lockWaiting != null) {
+ final var lock = new SimpleOrderedMap<Object>();
+ lock.add(NAME, thread.lockWaiting.name);
+ SimpleOrderedMap<Object> owner = null;
+ if (thread.lockWaiting.owner != null) {
+ owner = new SimpleOrderedMap<>();
+ owner.add(NAME, thread.lockWaiting.owner.name);
+ owner.add(ID, thread.lockWaiting.owner.id);
}
+ lock.add("owner", owner);
+ info.add("lock-waiting", lock);
}
- {
- final LockInfo[] synchronizers = ti.getLockedSynchronizers();
- if (0 < synchronizers.length) {
- final List<String> locks = new ArrayList<>(synchronizers.length);
- info.add("synchronizers-locked", locks);
- for (LockInfo sync : synchronizers) {
- locks.add(sync.toString());
- }
- }
+ if (thread.synchronizersLocked != null) {
+ info.add("synchronizers-locked", thread.synchronizersLocked);
}
- {
- final LockInfo[] monitors = ti.getLockedMonitors();
- if (0 < monitors.length) {
- final List<String> locks = new ArrayList<>(monitors.length);
- info.add("monitors-locked", locks);
- for (LockInfo monitor : monitors) {
- locks.add(monitor.toString());
- }
- }
+ if (thread.monitorsLocked != null) {
+ info.add("monitors-locked", thread.monitorsLocked);
}
-
- if (ti.isSuspended()) {
- info.add("suspended", true);
+ if (thread.suspended != null) {
+ info.add("suspended", thread.suspended);
}
- if (ti.isInNative()) {
- info.add("native", true);
+ if (thread.nativeThread != null) {
+ info.add("native", thread.nativeThread);
}
-
- if (tmbean.isThreadCpuTimeSupported()) {
- info.add("cpuTime", formatNanos(tmbean.getThreadCpuTime(tid)));
- info.add("userTime", formatNanos(tmbean.getThreadUserTime(tid)));
+ if (thread.cpuTime != null) {
+ info.add("cpuTime", thread.cpuTime);
}
-
- // Add the stack trace
- int i = 0;
- String[] trace = new String[ti.getStackTrace().length];
- for (StackTraceElement ste : ti.getStackTrace()) {
- trace[i++] = ste.toString();
+ if (thread.userTime != null) {
+ info.add("userTime", thread.userTime);
}
- info.add("stackTrace", trace);
+ info.add("stackTrace", thread.stackTrace.toArray(String[]::new));
return info;
}
- private static String formatNanos(long ns) {
- return String.format(Locale.ROOT, "%.4fms", ns / (double) 1000000);
- }
-
//////////////////////// SolrInfoMBeans methods //////////////////////
@Override
@@ -177,12 +120,12 @@ public class ThreadDumpHandler extends RequestHandlerBase {
@Override
public Collection<Api> getApis() {
- return AnnotatedApi.getApis(new NodeThreadsAPI(this));
+ return List.of();
}
@Override
- public Boolean registerV2() {
- return Boolean.TRUE;
+ public Collection<Class<? extends JerseyResource>> getJerseyResources() {
+ return List.of(NodeThreadsAPI.class);
}
@Override
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/api/NodeThreadsAPI.java b/solr/core/src/java/org/apache/solr/handler/admin/api/NodeThreadsAPI.java
index bea1827304c..0223010b87e 100644
--- a/solr/core/src/java/org/apache/solr/handler/admin/api/NodeThreadsAPI.java
+++ b/solr/core/src/java/org/apache/solr/handler/admin/api/NodeThreadsAPI.java
@@ -14,34 +14,114 @@
* 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 static org.apache.solr.security.PermissionNameProvider.Name.METRICS_READ_PERM;
-import org.apache.solr.api.EndPoint;
-import org.apache.solr.handler.admin.ThreadDumpHandler;
-import org.apache.solr.request.SolrQueryRequest;
-import org.apache.solr.response.SolrQueryResponse;
+import jakarta.inject.Inject;
+import java.lang.management.LockInfo;
+import java.lang.management.ManagementFactory;
+import java.lang.management.ThreadInfo;
+import java.lang.management.ThreadMXBean;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import org.apache.solr.api.JerseyResource;
+import org.apache.solr.client.api.endpoint.NodeThreadsApi;
+import org.apache.solr.client.api.model.NodeThreadsResponse;
+import org.apache.solr.client.api.model.NodeThreadsResponse.LockOwner;
+import org.apache.solr.client.api.model.NodeThreadsResponse.LockWaiting;
+import org.apache.solr.client.api.model.NodeThreadsResponse.SystemInfo;
+import org.apache.solr.client.api.model.NodeThreadsResponse.ThreadCount;
+import org.apache.solr.client.api.model.NodeThreadsResponse.ThreadEntry;
+import org.apache.solr.jersey.PermissionName;
-/**
- * V2 API for triggering a thread dump on the receiving node.
- *
- * <p>This API (GET /v2/node/threads) is analogous to the v1 /admin/info/threads.
- */
-public class NodeThreadsAPI {
- private final ThreadDumpHandler handler;
+/** Implementation of {@link NodeThreadsApi}. */
+public class NodeThreadsAPI extends JerseyResource implements NodeThreadsApi {
+
+ @Inject
+ public NodeThreadsAPI() {}
+
+ @Override
+ @PermissionName(METRICS_READ_PERM)
+ public NodeThreadsResponse getThreadDump() {
+ final var response = instantiateJerseyResponse(NodeThreadsResponse.class);
+ final var tmbean = ManagementFactory.getThreadMXBean();
+ response.system = new SystemInfo();
+ final var counts = new ThreadCount();
+ counts.current = tmbean.getThreadCount();
+ counts.peak = tmbean.getPeakThreadCount();
+ counts.daemon = tmbean.getDaemonThreadCount();
+ response.system.threadCount = counts;
+ final long[] deadlockedThreads = tmbean.findDeadlockedThreads();
+ if (deadlockedThreads != null) {
+ response.system.deadlocks =
+ toThreadEntries(tmbean.getThreadInfo(deadlockedThreads, Integer.MAX_VALUE), tmbean);
+ }
+ response.system.threadDump = toThreadEntries(tmbean.dumpAllThreads(true, true), tmbean);
+ return response;
+ }
+
+ private static List<ThreadEntry> toThreadEntries(ThreadInfo[] threads, ThreadMXBean tmbean) {
+ final List<ThreadEntry> entries = new ArrayList<>(threads.length);
+ for (var thread : threads) {
+ if (thread != null) {
+ final var entry = new ThreadEntry();
+ entry.thread = getThreadInfo(thread, tmbean);
+ entries.add(entry);
+ }
+ }
+ return entries;
+ }
+
+ private static NodeThreadsResponse.ThreadInfo getThreadInfo(ThreadInfo ti, ThreadMXBean tmbean) {
+ final var info = new NodeThreadsResponse.ThreadInfo();
+ final long tid = ti.getThreadId();
+ info.id = tid;
+ info.name = ti.getThreadName();
+ info.state = ti.getThreadState().toString();
+ info.lock = ti.getLockName();
+ final LockInfo lockInfo = ti.getLockInfo();
+ if (lockInfo != null) {
+ info.lockWaiting = new LockWaiting();
+ info.lockWaiting.name = lockInfo.toString();
+ if (ti.getLockOwnerId() != -1 || ti.getLockOwnerName() != null) {
+ info.lockWaiting.owner = new LockOwner();
+ info.lockWaiting.owner.name = ti.getLockOwnerName();
+ info.lockWaiting.owner.id = ti.getLockOwnerId();
+ }
+ }
+ info.synchronizersLocked = toLockNames(ti.getLockedSynchronizers());
+ info.monitorsLocked = toLockNames(ti.getLockedMonitors());
+ if (ti.isSuspended()) {
+ info.suspended = true;
+ }
+ if (ti.isInNative()) {
+ info.nativeThread = true;
+ }
+ if (tmbean.isThreadCpuTimeSupported()) {
+ info.cpuTime = formatNanos(tmbean.getThreadCpuTime(tid));
+ info.userTime = formatNanos(tmbean.getThreadUserTime(tid));
+ }
+ info.stackTrace = new ArrayList<>(ti.getStackTrace().length);
+ for (StackTraceElement ste : ti.getStackTrace()) {
+ info.stackTrace.add(ste.toString());
+ }
+ return info;
+ }
- public NodeThreadsAPI(ThreadDumpHandler handler) {
- this.handler = handler;
+ private static List<String> toLockNames(LockInfo[] locks) {
+ if (locks.length == 0) {
+ return null;
+ }
+ final List<String> names = new ArrayList<>(locks.length);
+ for (LockInfo lock : locks) {
+ names.add(lock.toString());
+ }
+ return names;
}
- @EndPoint(
- path = {"/node/threads"},
- method = GET,
- permission = METRICS_READ_PERM)
- public void triggerThreadDump(SolrQueryRequest req, SolrQueryResponse rsp) throws Exception {
- handler.handleRequestBody(req, rsp);
+ private static String formatNanos(long ns) {
+ return String.format(Locale.ROOT, "%.4fms", ns / (double) 1000000);
}
}
diff --git a/solr/core/src/test/org/apache/solr/handler/admin/ThreadDumpHandlerTest.java b/solr/core/src/test/org/apache/solr/handler/admin/ThreadDumpHandlerTest.java
index 14ee85e3a3d..fb9ab708133 100644
--- a/solr/core/src/test/org/apache/solr/handler/admin/ThreadDumpHandlerTest.java
+++ b/solr/core/src/test/org/apache/solr/handler/admin/ThreadDumpHandlerTest.java
@@ -33,6 +33,10 @@ import org.apache.solr.client.solrj.embedded.EmbeddedSolrServer;
import org.apache.solr.client.solrj.request.GenericSolrRequest;
import org.apache.solr.common.params.ModifiableSolrParams;
import org.apache.solr.common.util.NamedList;
+import org.apache.solr.common.util.SimpleOrderedMap;
+import org.apache.solr.handler.admin.api.NodeThreadsAPI;
+import org.apache.solr.jersey.SolrJacksonMapper;
+import org.apache.solr.response.SolrQueryResponse;
import org.junit.BeforeClass;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -51,6 +55,61 @@ public class ThreadDumpHandlerTest extends SolrTestCaseJ4 {
initCore("solrconfig.xml", "schema.xml");
}
+ public void testV1ResponseStructure() throws Exception {
+ final var response = new SolrQueryResponse();
+ try (var handler = new ThreadDumpHandler()) {
+ handler.handleRequestBody(null, response);
+ }
+ assertFalse(response.isHttpCaching());
+ assertTrue(response.getValues().get("system") instanceof SimpleOrderedMap<?>);
+ final var system = (NamedList<?>) response.getValues().get("system");
+ assertTrue(system.get("threadCount") instanceof SimpleOrderedMap<?>);
+ final var counts = (NamedList<?>) system.get("threadCount");
+ assertTrue(counts.get("current") instanceof Integer);
+ assertTrue(counts.get("peak") instanceof Integer);
+ assertTrue(counts.get("daemon") instanceof Integer);
+ final var threads = (NamedList<?>) system.get("threadDump");
+ assertTrue(threads.size() > 1);
+ for (var entry : threads) {
+ assertEquals("thread", entry.getKey());
+ assertTrue(entry.getValue() instanceof SimpleOrderedMap<?>);
+ final var thread = (NamedList<?>) entry.getValue();
+ assertTrue(thread.get("id") instanceof Long);
+ assertTrue(thread.get("stackTrace") instanceof String[]);
+ if (thread.get("lock-waiting") != null) {
+ assertTrue(thread.get("lock-waiting") instanceof SimpleOrderedMap<?>);
+ final var lock = (NamedList<?>) thread.get("lock-waiting");
+ if (lock.get("owner") != null) {
+ assertTrue(lock.get("owner") instanceof SimpleOrderedMap<?>);
+ }
+ }
+ }
+ }
+
+ public void testV2ResponseStructure() throws Exception {
+ final var response = new NodeThreadsAPI().getThreadDump();
+ assertTrue(response.system.threadCount.current > 0);
+ assertTrue(response.system.threadDump.size() > 1);
+ final var currentThread =
+ response.system.threadDump.stream()
+ .map(entry -> entry.thread)
+ .filter(thread -> thread.id == Thread.currentThread().threadId())
+ .findFirst()
+ .orElseThrow();
+ assertEquals(Thread.currentThread().getName(), currentThread.name);
+ assertEquals("RUNNABLE", currentThread.state);
+ assertFalse(currentThread.stackTrace.isEmpty());
+ final var mapper = SolrJacksonMapper.getObjectMapper();
+ final var json = mapper.readTree(mapper.writeValueAsString(response));
+ final var threads = json.path("system").path("threadDump");
+ assertTrue(threads.isArray());
+ assertEquals(response.system.threadDump.size(), threads.size());
+ for (var entry : threads) {
+ assertTrue(entry.path("thread").isObject());
+ assertTrue(entry.path("thread").path("stackTrace").isArray());
+ }
+ }
+
public void testMonitor() throws Exception {
doTestMonitor(false);
}
diff --git a/solr/core/src/test/org/apache/solr/handler/admin/api/NodeThreadsAPITest.java b/solr/core/src/test/org/apache/solr/handler/admin/api/NodeThreadsAPITest.java
new file mode 100644
index 00000000000..8663928c9e3
--- /dev/null
+++ b/solr/core/src/test/org/apache/solr/handler/admin/api/NodeThreadsAPITest.java
@@ -0,0 +1,54 @@
+/*
+ * 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 org.apache.solr.SolrTestCase;
+import org.apache.solr.client.solrj.request.SystemApi;
+import org.apache.solr.util.SolrJettyTestRule;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Test;
+
+/** HTTP coverage for the node thread dump through the generated SolrJ client. */
+public class NodeThreadsAPITest extends SolrTestCase {
+
+ @ClassRule public static final SolrJettyTestRule solrTestRule = new SolrJettyTestRule();
+
+ @BeforeClass
+ public static void setupSolr() throws Exception {
+ solrTestRule.startSolr(createTempDir());
+ }
+
+ @Test
+ public void testThreadDump() throws Exception {
+ final var response = new SystemApi.GetThreadDump().process(solrTestRule.getAdminClient());
+
+ assertNotNull(response.responseHeader);
+ assertEquals(0, response.responseHeader.status);
+ assertNull(response.error);
+ assertTrue(response.system.threadCount.current > 0);
+ assertFalse(response.system.threadDump.isEmpty());
+ final var currentThread =
+ response.system.threadDump.stream()
+ .map(entry -> entry.thread)
+ .filter(thread -> thread.id == Thread.currentThread().threadId())
+ .findFirst()
+ .orElseThrow();
+ assertEquals(Thread.currentThread().getName(), currentThread.name);
+ assertFalse(currentThread.stackTrace.isEmpty());
+ }
+}
diff --git a/solr/core/src/test/org/apache/solr/handler/admin/api/V2NodeAPIMappingTest.java b/solr/core/src/test/org/apache/solr/handler/admin/api/V2NodeAPIMappingTest.java
index ac2b660bc88..0d08741bab0 100644
--- a/solr/core/src/test/org/apache/solr/handler/admin/api/V2NodeAPIMappingTest.java
+++ b/solr/core/src/test/org/apache/solr/handler/admin/api/V2NodeAPIMappingTest.java
@@ -21,7 +21,6 @@ import static org.apache.solr.common.params.CommonParams.ACTION;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
import java.util.HashMap;
import java.util.List;
@@ -34,8 +33,6 @@ import org.apache.solr.common.util.CommandOperation;
import org.apache.solr.common.util.ContentStreamBase;
import org.apache.solr.handler.RequestHandlerBase;
import org.apache.solr.handler.admin.CoreAdminHandler;
-import org.apache.solr.handler.admin.InfoHandler;
-import org.apache.solr.handler.admin.ThreadDumpHandler;
import org.apache.solr.request.SolrQueryRequest;
import org.apache.solr.request.SolrQueryRequestBase;
import org.apache.solr.response.SolrQueryResponse;
@@ -49,8 +46,6 @@ public class V2NodeAPIMappingTest extends SolrTestCaseJ4 {
private ApiBag apiBag;
private ArgumentCaptor<SolrQueryRequest> queryRequestCaptor;
private CoreAdminHandler mockCoresHandler;
- private InfoHandler infoHandler;
- private ThreadDumpHandler mockThreadDumpHandler;
@BeforeClass
public static void ensureWorkingMockito() {
@@ -60,14 +55,10 @@ public class V2NodeAPIMappingTest extends SolrTestCaseJ4 {
@Before
public void setupApiBag() {
mockCoresHandler = mock(CoreAdminHandler.class);
- infoHandler = mock(InfoHandler.class);
- mockThreadDumpHandler = mock(ThreadDumpHandler.class);
queryRequestCaptor = ArgumentCaptor.forClass(SolrQueryRequest.class);
- when(infoHandler.getThreadDumpHandler()).thenReturn(mockThreadDumpHandler);
-
apiBag = new ApiBag(false);
- registerAllNodeApis(apiBag, mockCoresHandler, infoHandler);
+ registerAllNodeApis(apiBag, mockCoresHandler);
}
@Test
@@ -108,28 +99,12 @@ public class V2NodeAPIMappingTest extends SolrTestCaseJ4 {
assertEquals("true", v1Params.get("rejoinAtHead"));
}
- @Test
- public void testThreadDumpApiAllProperties() throws Exception {
- final ModifiableSolrParams solrParams = new ModifiableSolrParams();
- solrParams.add("anyParamName", "anyParamValue");
- final SolrParams v1Params =
- captureConvertedThreadDumpV1Params("/node/threads", "GET", solrParams);
-
- // All parameters are passed through to v1 API as-is
- assertEquals("anyParamValue", v1Params.get("anyParamName"));
- }
-
private SolrParams captureConvertedCoreV1Params(String path, String method, String v2RequestBody)
throws Exception {
return doCaptureParams(
path, method, new ModifiableSolrParams(), v2RequestBody, mockCoresHandler);
}
- private SolrParams captureConvertedThreadDumpV1Params(
- String path, String method, SolrParams inputParams) throws Exception {
- return doCaptureParams(path, method, inputParams, null, mockThreadDumpHandler);
- }
-
private SolrParams doCaptureParams(
String path,
String method,
@@ -161,10 +136,8 @@ public class V2NodeAPIMappingTest extends SolrTestCaseJ4 {
return queryRequestCaptor.getValue().getParams();
}
- private static void registerAllNodeApis(
- ApiBag apiBag, CoreAdminHandler coreHandler, InfoHandler infoHandler) {
+ private static void registerAllNodeApis(ApiBag apiBag, CoreAdminHandler coreHandler) {
apiBag.registerObject(new OverseerOperationAPI(coreHandler));
apiBag.registerObject(new RejoinLeaderElectionAPI(coreHandler));
- apiBag.registerObject(new NodeThreadsAPI(infoHandler.getThreadDumpHandler()));
}
}
diff --git a/solr/solr-ref-guide/modules/configuration-guide/pages/implicit-requesthandlers.adoc b/solr/solr-ref-guide/modules/configuration-guide/pages/implicit-requesthandlers.adoc
index 6adff4771b7..c99fb342feb 100644
--- a/solr/solr-ref-guide/modules/configuration-guide/pages/implicit-requesthandlers.adoc
+++ b/solr/solr-ref-guide/modules/configuration-guide/pages/implicit-requesthandlers.adoc
@@ -130,12 +130,55 @@ Core Info:: Return core information.
Threads:: Return info on all JVM threads.
+
-[cols="3*.",frame=none,grid=cols,options="header"]
-|===
-|API Endpoints |Class & Javadocs |Paramset
-|v1: `solr/admin/info/threads`
+[tabs#thread-dump-api]
+======
+V1 API::
++
+====
+`solr/admin/info/threads`
+
+Abbreviated JSON response with the default `json.nl=flat` format.
+The thread dump alternates `"thread"` keys and thread objects:
+
+[source,json]
+----
+{
+ "system": {
+ "threadDump": [
+ "thread", {"id": 1, "name": "main", "state": "RUNNABLE"},
+ "thread", {"id": 2, "name": "worker", "state": "WAITING"}
+ ]
+ }
+}
+----
+====
-v2: `api/node/threads` |{solr-javadocs}/core/org/apache/solr/handler/admin/ThreadDumpHandler.html[ThreadDumpHandler] |`_ADMIN_THREADS`
+V2 API::
++
+====
+`api/node/threads`
+
+Abbreviated JSON response.
+The thread dump is an array of objects, each containing a `"thread"` property:
+
+[source,json]
+----
+{
+ "system": {
+ "threadDump": [
+ {"thread": {"id": 1, "name": "main", "state": "RUNNABLE"}},
+ {"thread": {"id": 2, "name": "worker", "state": "WAITING"}}
+ ]
+ }
+}
+----
+====
+======
++
+[cols="2*.",frame=none,grid=cols,options="header"]
+|===
+|Class & Javadocs |Paramset
+|{solr-javadocs}/core/org/apache/solr/handler/admin/ThreadDumpHandler.html[ThreadDumpHandler] |`_ADMIN_THREADS`
|===
=== Analysis Handlers
diff --git a/solr/webapp/web/js/angular/controllers/threads.js b/solr/webapp/web/js/angular/controllers/threads.js
index 22a90029593..123a42d6382 100644
--- a/solr/webapp/web/js/angular/controllers/threads.js
+++ b/solr/webapp/web/js/angular/controllers/threads.js
@@ -16,25 +16,32 @@
*/
solrAdminApp.controller('ThreadsController',
- function($scope, Threads, Constants){
+ function($scope, $timeout, SystemV2, Constants, ApiErrorHandler){
$scope.resetMenu("threads", Constants.IS_ROOT_PAGE);
$scope.refresh = function() {
- Threads.get(function(data) {
- var threadDump = data.system.threadDump;
- var threads = [];
- for (var i=1; i<threadDump.length; i+=2) {
- var thread = threadDump[i];
- if (!!thread.stackTrace) {
- var stackTrace = [];
- for (var j=0; j<thread.stackTrace.length; j++) {
- var trace = thread.stackTrace[j].replace("(", "\u200B("); // allow wrapping to happen, \u200B is a zero-width space
- stackTrace.push({id:thread.id + ":" + j, trace: trace});
+ SystemV2.getThreadDump(function(error, data, response) {
+ $timeout(function() {
+ if (error) { ApiErrorHandler.handle(response); return; }
+ if (!data || !data.system) {
+ $scope.threads = [];
+ return;
+ }
+ var threadDump = data.system.threadDump;
+ var threads = [];
+ for (var i=0; i<threadDump.length; i++) {
+ var thread = threadDump[i].thread;
+ if (!!thread.stackTrace) {
+ var stackTrace = [];
+ for (var j=0; j<thread.stackTrace.length; j++) {
+ var trace = thread.stackTrace[j].replace("(", "\u200B("); // allow wrapping to happen, \u200B is a zero-width space
+ stackTrace.push({id:thread.id + ":" + j, trace: trace});
+ }
+ thread.stackTrace = stackTrace;
}
- thread.stackTrace = stackTrace;
+ threads.push(thread);
}
- threads.push(thread);
- }
- $scope.threads = threads;
+ $scope.threads = threads;
+ });
});
};
$scope.toggleStacktrace = function(thread) {
diff --git a/solr/webapp/web/js/angular/services.js b/solr/webapp/web/js/angular/services.js
index 2119d3bd174..6e10997f743 100644
--- a/solr/webapp/web/js/angular/services.js
+++ b/solr/webapp/web/js/angular/services.js
@@ -165,7 +165,7 @@ solrAdminServices.factory('Metrics',
// v2 ClusterAPI (/api/cluster) delegates straight through to the same v1 CollectionsHandler
// that v1's CLUSTERSTATUS action used, so the response shape is byte-identical -- no
// generated solrApi client class exists for it (old-style @EndPoint API, predates the
- // OpenAPI-based v2 framework), so this stays a plain $resource, like Threads/ParamSet.
+ // OpenAPI-based v2 framework), so this stays a plain $resource, like ParamSet.
return $resource('/api/cluster', {'wt':'json', '_':Date.now()}, {
"status": {}
});
@@ -242,14 +242,6 @@ solrAdminServices.factory('Metrics',
delete solrApi.ApiClient.instance.defaultHeaders["User-Agent"];
return new solrApi.NodeApi();
})
-.factory('Threads',
- ['$resource', function($resource) {
- // v2 NodeThreadsAPI (/api/node/threads) still just delegates straight through to the same v1
- // ThreadDumpHandler, so the response shape is byte-identical -- no generated solrApi client
- // class exists for it (it predates the OpenAPI-based v2 API framework), so this stays a plain
- // $resource, like Security and (partially) SchemaDesigner.
- return $resource('/api/node/threads', {'wt':'json', '_':Date.now()});
- }])
.factory('Replication',
['$resource', function($resource) {
return $resource(':core/replication', {'wt':'json', core: "@core", '_':Date.now()}, {
@@ -276,7 +268,7 @@ solrAdminServices.factory('Metrics',
// v2 GetConfigAPI/ModifyParamSetAPI (/api/(cores|collections)/:core/config/params) still
// delegate straight through to the same v1 SolrConfigHandler, so the response shape is
// byte-identical -- no generated solrApi client class exists for it (old-style @EndPoint API,
- // predates the OpenAPI-based v2 framework), so this stays a plain $resource, like Threads.
+ // predates the OpenAPI-based v2 framework), so this stays a plain $resource, like Collections.
// NB: unlike v1's flexible routing, the v2 API requires knowing up front whether ":core" is a
// collection name (SolrCloud) or an actual core name (standalone/user-managed) --
// /api/collections/... 500s in standalone mode (it tries to resolve aliases, which needs ZK),
@@ -401,7 +393,7 @@ solrAdminServices.factory('Metrics',
// body (the server deliberately reads the raw content stream, dispatched by Content-Type,
// rather than a formal parameter) and query() takes no query params at all (the server
// forwards arbitrary SolrParams straight through). Both stay on this plain $resource, like
- // Threads/Collections/ParamSet. Every other Schema Designer endpoint uses SchemaDesignerV2.
+ // Collections/ParamSet. Every other Schema Designer endpoint uses SchemaDesignerV2.
return $resource('/api/schema-designer/:configSet/:path', {wt: 'json', path: '@path', configSet: '@configSet', filePath: '@filePath', _:Date.now()}, {
get: {method: "GET"},
post: {method: "POST", timeout: 90000},