diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/asciidoc/endpoints/threaddump.adoc b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/asciidoc/endpoints/threaddump.adoc index 997d3f36ec..1c8d4328e6 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/asciidoc/endpoints/threaddump.adoc +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/asciidoc/endpoints/threaddump.adoc @@ -5,25 +5,39 @@ The `threaddump` endpoint provides a thread dump from the application's JVM. -[[threaddump-retrieving]] -== Retrieving the Thread Dump +[[threaddump-retrieving-json]] +== Retrieving the Thread Dump as JSON -To retrieve the thread dump, make a `GET` request to `/actuator/threaddump`, as shown -in the following curl-based example: +To retrieve the thread dump as JSON, make a `GET` request to `/actuator/threaddump` with +an appropriate `Accept` header, as shown in the following curl-based example: -include::{snippets}threaddump/curl-request.adoc[] +include::{snippets}threaddump/json/curl-request.adoc[] The resulting response is similar to the following: -include::{snippets}threaddump/http-response.adoc[] +include::{snippets}threaddump/json/http-response.adoc[] -[[threaddump-retrieving-response-structure]] +[[threaddump-retrieving-json-response-structure]] === Response Structure The response contains details of the JVM's threads. The following table describes the structure of the response: [cols="3,1,2"] -include::{snippets}threaddump/response-fields.adoc[] +include::{snippets}threaddump/json/response-fields.adoc[] + + + +[[threaddump-retrieving-text]] +== Retrieving the Thread Dump as Text + +To retrieve the thread dump as text, make a `GET` request to `/actuator/threaddump` that +accepts `text/plain`, as shown in the following curl-based example: + +include::{snippets}threaddump/text/curl-request.adoc[] + +The resulting response is similar to the following: + +include::{snippets}threaddump/text/http-response.adoc[] diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/ThreadDumpEndpointDocumentationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/ThreadDumpEndpointDocumentationTests.java index d986a546e2..a52d4dce96 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/ThreadDumpEndpointDocumentationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/ThreadDumpEndpointDocumentationTests.java @@ -16,6 +16,7 @@ package org.springframework.boot.actuate.autoconfigure.endpoint.web.documentation; +import java.nio.charset.StandardCharsets; import java.util.concurrent.CountDownLatch; import java.util.concurrent.locks.ReentrantLock; @@ -25,7 +26,9 @@ import org.springframework.boot.actuate.management.ThreadDumpEndpoint; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; +import org.springframework.http.MediaType; import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation; +import org.springframework.restdocs.operation.preprocess.ContentModifyingOperationPreprocessor; import org.springframework.restdocs.payload.JsonFieldType; import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessResponse; @@ -43,7 +46,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. class ThreadDumpEndpointDocumentationTests extends MockMvcEndpointDocumentationTests { @Test - void threadDump() throws Exception { + void jsonThreadDump() throws Exception { ReentrantLock lock = new ReentrantLock(); CountDownLatch latch = new CountDownLatch(1); new Thread(() -> { @@ -60,106 +63,137 @@ class ThreadDumpEndpointDocumentationTests extends MockMvcEndpointDocumentationT Thread.currentThread().interrupt(); } }).start(); - this.mockMvc.perform(get("/actuator/threaddump")).andExpect(status().isOk()) - .andDo(MockMvcRestDocumentation.document("threaddump", preprocessResponse(limit("threads")), - responseFields(fieldWithPath("threads").description("JVM's threads."), - fieldWithPath("threads.[].blockedCount") - .description("Total number of times that the thread has been " + "blocked."), - fieldWithPath("threads.[].blockedTime") - .description("Time in milliseconds that the thread has spent " - + "blocked. -1 if thread contention " + "monitoring is disabled."), - fieldWithPath("threads.[].daemon") - .description("Whether the thread is a daemon " - + "thread. Only available on Java 9 or " + "later.") - .optional().type(JsonFieldType.BOOLEAN), - fieldWithPath("threads.[].inNative") - .description("Whether the thread is executing native code."), - fieldWithPath("threads.[].lockName").description( - "Description of the object on which the " + "thread is blocked, if any.") - .optional().type(JsonFieldType.STRING), - fieldWithPath("threads.[].lockInfo") - .description("Object for which the thread is blocked " + "waiting.").optional() - .type(JsonFieldType.OBJECT), - fieldWithPath("threads.[].lockInfo.className") - .description("Fully qualified class name of the lock" + " object.").optional() - .type(JsonFieldType.STRING), - fieldWithPath("threads.[].lockInfo.identityHashCode") - .description("Identity hash code of the lock object.").optional() - .type(JsonFieldType.NUMBER), - fieldWithPath("threads.[].lockedMonitors") - .description("Monitors locked by this thread, if any"), - fieldWithPath("threads.[].lockedMonitors.[].className") - .description("Class name of the lock object.").optional() - .type(JsonFieldType.STRING), - fieldWithPath("threads.[].lockedMonitors.[].identityHashCode") - .description("Identity hash code of the lock " + "object.").optional() - .type(JsonFieldType.NUMBER), - fieldWithPath("threads.[].lockedMonitors.[].lockedStackDepth") - .description("Stack depth where the monitor " + "was locked.").optional() - .type(JsonFieldType.NUMBER), - subsectionWithPath("threads.[].lockedMonitors.[].lockedStackFrame") - .description("Stack frame that locked the " + "monitor.").optional() - .type(JsonFieldType.OBJECT), - fieldWithPath("threads.[].lockedSynchronizers") - .description("Synchronizers locked by this thread."), - fieldWithPath("threads.[].lockedSynchronizers.[].className").description( - "Class name of the locked " + "synchronizer.").optional() - .type(JsonFieldType.STRING), - fieldWithPath("threads.[].lockedSynchronizers.[].identityHashCode").description( - "Identity hash code of the locked " + "synchronizer.").optional() - .type(JsonFieldType.NUMBER), - fieldWithPath("threads.[].lockOwnerId").description( - "ID of the thread that owns the object on which " - + "the thread is blocked. `-1` if the " + "thread is not blocked."), - fieldWithPath("threads.[].lockOwnerName") - .description("Name of the thread that owns the " - + "object on which the thread is " + "blocked, if any.") - .optional().type(JsonFieldType.STRING), - fieldWithPath("threads.[].priority") - .description("Priority of the thread. Only " + "available on Java 9 or later.") - .optional().type(JsonFieldType.NUMBER), - fieldWithPath("threads.[].stackTrace").description("Stack trace of the thread."), - fieldWithPath("threads.[].stackTrace.[].classLoaderName").description( - "Name of the class loader of the " + "class that contains the execution " - + "point identified by this entry, if " - + "any. Only available on Java 9 or " + "later.") - .optional().type(JsonFieldType.STRING), - fieldWithPath("threads.[].stackTrace.[].className").description( - "Name of the class that contains the " + "execution point identified " - + "by this entry."), - fieldWithPath("threads.[].stackTrace.[].fileName") - .description("Name of the source file that " + "contains the execution point " - + "identified by this entry, if any.") - .optional().type(JsonFieldType.STRING), - fieldWithPath("threads.[].stackTrace.[].lineNumber") - .description("Line number of the execution " - + "point identified by this entry. " + "Negative if unknown."), - fieldWithPath("threads.[].stackTrace.[].methodName").description("Name of the method."), - fieldWithPath("threads.[].stackTrace.[].moduleName") - .description("Name of the module that contains " - + "the execution point identified by " - + "this entry, if any. Only available " + "on Java 9 or later.") - .optional().type(JsonFieldType.STRING), - fieldWithPath("threads.[].stackTrace.[].moduleVersion") - .description("Version of the module that " + "contains the execution point " - + "identified by this entry, if any. " - + "Only available on Java 9 or later.") - .optional().type(JsonFieldType.STRING), - fieldWithPath("threads.[].stackTrace.[].nativeMethod") - .description("Whether the execution point is a native " + "method."), - fieldWithPath("threads.[].suspended").description("Whether the thread is suspended."), - fieldWithPath("threads.[].threadId").description("ID of the thread."), - fieldWithPath("threads.[].threadName").description("Name of the thread."), - fieldWithPath("threads.[].threadState").description( - "State of the thread (" + describeEnumValues(Thread.State.class) + ")."), - fieldWithPath("threads.[].waitedCount").description( - "Total number of times that the thread has waited" + " for notification."), - fieldWithPath("threads.[].waitedTime") - .description("Time in milliseconds that the thread has spent " - + "waiting. -1 if thread contention " + "monitoring is disabled")))); + this.mockMvc.perform(get("/actuator/threaddump").accept(MediaType.APPLICATION_JSON)).andExpect( + status().isOk()).andDo( + MockMvcRestDocumentation + .document("threaddump/json", preprocessResponse(limit("threads")), + responseFields(fieldWithPath("threads").description("JVM's threads."), + fieldWithPath("threads.[].blockedCount").description( + "Total number of times that the thread has been " + "blocked."), + fieldWithPath("threads.[].blockedTime").description( + "Time in milliseconds that the thread has spent " + + "blocked. -1 if thread contention " + + "monitoring is disabled."), + fieldWithPath("threads.[].daemon") + .description("Whether the thread is a daemon " + + "thread. Only available on Java 9 or " + "later.") + .optional().type(JsonFieldType.BOOLEAN), + fieldWithPath("threads.[].inNative") + .description("Whether the thread is executing native code."), + fieldWithPath("threads.[].lockName") + .description("Description of the object on which the " + + "thread is blocked, if any.") + .optional().type(JsonFieldType.STRING), + fieldWithPath("threads.[].lockInfo") + .description( + "Object for which the thread is blocked " + "waiting.") + .optional().type(JsonFieldType.OBJECT), + fieldWithPath("threads.[].lockInfo.className") + .description( + "Fully qualified class name of the lock" + " object.") + .optional().type(JsonFieldType.STRING), + fieldWithPath("threads.[].lockInfo.identityHashCode") + .description("Identity hash code of the lock object.") + .optional().type(JsonFieldType.NUMBER), + fieldWithPath("threads.[].lockedMonitors") + .description("Monitors locked by this thread, if any"), + fieldWithPath("threads.[].lockedMonitors.[].className") + .description("Class name of the lock object.").optional() + .type(JsonFieldType.STRING), + fieldWithPath("threads.[].lockedMonitors.[].identityHashCode") + .description("Identity hash code of the lock " + "object.") + .optional().type(JsonFieldType.NUMBER), + fieldWithPath("threads.[].lockedMonitors.[].lockedStackDepth") + .description("Stack depth where the monitor " + "was locked.") + .optional().type(JsonFieldType.NUMBER), + subsectionWithPath("threads.[].lockedMonitors.[].lockedStackFrame") + .description("Stack frame that locked the " + "monitor.") + .optional().type(JsonFieldType.OBJECT), + fieldWithPath("threads.[].lockedSynchronizers") + .description("Synchronizers locked by this thread."), + fieldWithPath("threads.[].lockedSynchronizers.[].className") + .description("Class name of the locked " + "synchronizer.") + .optional().type(JsonFieldType.STRING), + fieldWithPath("threads.[].lockedSynchronizers.[].identityHashCode") + .description( + "Identity hash code of the locked " + "synchronizer.") + .optional().type(JsonFieldType.NUMBER), + fieldWithPath("threads.[].lockOwnerId") + .description("ID of the thread that owns the object on which " + + "the thread is blocked. `-1` if the " + + "thread is not blocked."), + fieldWithPath("threads.[].lockOwnerName") + .description("Name of the thread that owns the " + + "object on which the thread is " + "blocked, if any.") + .optional().type(JsonFieldType.STRING), + fieldWithPath("threads.[].priority") + .description("Priority of the thread. Only " + + "available on Java 9 or later.") + .optional().type(JsonFieldType.NUMBER), + fieldWithPath("threads.[].stackTrace") + .description("Stack trace of the thread."), + fieldWithPath("threads.[].stackTrace.[].classLoaderName") + .description("Name of the class loader of the " + + "class that contains the execution " + + "point identified by this entry, if " + + "any. Only available on Java 9 or " + "later.") + .optional().type(JsonFieldType.STRING), + fieldWithPath("threads.[].stackTrace.[].className") + .description("Name of the class that contains the " + + "execution point identified " + "by this entry."), + fieldWithPath("threads.[].stackTrace.[].fileName") + .description("Name of the source file that " + + "contains the execution point " + + "identified by this entry, if any.") + .optional().type(JsonFieldType.STRING), + fieldWithPath("threads.[].stackTrace.[].lineNumber") + .description("Line number of the execution " + + "point identified by this entry. " + + "Negative if unknown."), + fieldWithPath("threads.[].stackTrace.[].methodName") + .description("Name of the method."), + fieldWithPath("threads.[].stackTrace.[].moduleName") + .description("Name of the module that contains " + + "the execution point identified by " + + "this entry, if any. Only available " + + "on Java 9 or later.") + .optional().type(JsonFieldType.STRING), + fieldWithPath("threads.[].stackTrace.[].moduleVersion") + .description("Version of the module that " + + "contains the execution point " + + "identified by this entry, if any. " + + "Only available on Java 9 or later.") + .optional().type(JsonFieldType.STRING), + fieldWithPath("threads.[].stackTrace.[].nativeMethod").description( + "Whether the execution point is a native " + "method."), + fieldWithPath("threads.[].suspended") + .description("Whether the thread is suspended."), + fieldWithPath("threads.[].threadId").description("ID of the thread."), + fieldWithPath("threads.[].threadName") + .description("Name of the thread."), + fieldWithPath("threads.[].threadState") + .description("State of the thread (" + + describeEnumValues(Thread.State.class) + ")."), + fieldWithPath("threads.[].waitedCount") + .description("Total number of times that the thread has waited" + + " for notification."), + fieldWithPath("threads.[].waitedTime") + .description("Time in milliseconds that the thread has spent " + + "waiting. -1 if thread contention " + + "monitoring is disabled")))); latch.countDown(); } + @Test + void textThreadDump() throws Exception { + this.mockMvc.perform(get("/actuator/threaddump").accept(MediaType.TEXT_PLAIN)).andExpect(status().isOk()) + .andDo(MockMvcRestDocumentation.document("threaddump/text", + preprocessResponse(new ContentModifyingOperationPreprocessor((bytes, mediaType) -> { + String content = new String(bytes, StandardCharsets.UTF_8); + return content.substring(0, content.indexOf("\"main\" - Thread")).getBytes(); + })))); + } + @Configuration(proxyBeanMethods = false) @Import(BaseDocumentationConfiguration.class) static class TestConfiguration { diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/management/PlainTextThreadDumpFormatter.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/management/PlainTextThreadDumpFormatter.java new file mode 100644 index 0000000000..96c64b4e18 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/management/PlainTextThreadDumpFormatter.java @@ -0,0 +1,127 @@ +/* + * Copyright 2012-2019 the original author or authors. + * + * Licensed 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 + * + * https://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.springframework.boot.actuate.management; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.lang.management.LockInfo; +import java.lang.management.ManagementFactory; +import java.lang.management.MonitorInfo; +import java.lang.management.RuntimeMXBean; +import java.lang.management.ThreadInfo; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * Formats a thread dump as plain text. + * + * @author Andy Wilkinson + */ +class PlainTextThreadDumpFormatter { + + String format(ThreadInfo[] threads) { + StringWriter dump = new StringWriter(); + PrintWriter writer = new PrintWriter(dump); + writePreamble(writer); + for (ThreadInfo info : threads) { + writeThread(writer, info); + } + return dump.toString(); + } + + private void writePreamble(PrintWriter writer) { + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + writer.println(dateFormat.format(new Date())); + RuntimeMXBean runtime = ManagementFactory.getRuntimeMXBean(); + writer.printf("Full thread dump %s (%s %s):%n", runtime.getVmName(), runtime.getVmVersion(), + System.getProperty("java.vm.info")); + writer.println(); + } + + private void writeThread(PrintWriter writer, ThreadInfo info) { + writer.printf("\"%s\" - Thread t@%d%n", info.getThreadName(), info.getThreadId()); + writer.printf(" %s: %s%n", Thread.State.class.getCanonicalName(), info.getThreadState()); + writeStackTrace(writer, info, info.getLockedMonitors()); + writer.println(); + writeLockedOwnableSynchronizers(writer, info); + writer.println(); + } + + private void writeStackTrace(PrintWriter writer, ThreadInfo info, MonitorInfo[] lockedMonitors) { + int depth = 0; + for (StackTraceElement element : info.getStackTrace()) { + writeStackTraceElement(writer, element, info, lockedMonitorsForDepth(lockedMonitors, depth), depth == 0); + depth++; + } + } + + private List lockedMonitorsForDepth(MonitorInfo[] lockedMonitors, int depth) { + return Stream.of(lockedMonitors).filter((lockedMonitor) -> lockedMonitor.getLockedStackDepth() == depth) + .collect(Collectors.toList()); + } + + private void writeStackTraceElement(PrintWriter writer, StackTraceElement element, ThreadInfo info, + List lockedMonitors, boolean firstElement) { + writer.printf("\tat %s%n", element.toString()); + LockInfo lockInfo = info.getLockInfo(); + if (firstElement && lockInfo != null) { + if (element.getClassName().equals(Object.class.getName()) && element.getMethodName().equals("wait")) { + if (lockInfo != null) { + writer.printf("\t- waiting on %s%n", format(lockInfo)); + } + } + else { + String lockOwner = info.getLockOwnerName(); + if (lockOwner != null) { + writer.printf("\t- waiting to lock %s owned by \"%s\" t@%d%n", format(lockInfo), lockOwner, + info.getLockOwnerId()); + } + else { + writer.printf("\t- parking to wait for %s%n", format(lockInfo)); + } + } + } + writeMonitors(writer, lockedMonitors); + } + + private String format(LockInfo lockInfo) { + return String.format("<%x> (a %s)", lockInfo.getIdentityHashCode(), lockInfo.getClassName()); + } + + private void writeMonitors(PrintWriter writer, List lockedMonitorsAtCurrentDepth) { + for (MonitorInfo lockedMonitor : lockedMonitorsAtCurrentDepth) { + writer.printf("\t- locked %s%n", format(lockedMonitor)); + } + } + + private void writeLockedOwnableSynchronizers(PrintWriter writer, ThreadInfo info) { + writer.println(" Locked ownable synchronizers:"); + LockInfo[] lockedSynchronizers = info.getLockedSynchronizers(); + if (lockedSynchronizers == null || lockedSynchronizers.length == 0) { + writer.println("\t- None"); + } + else { + for (LockInfo lockedSynchronizer : lockedSynchronizers) { + writer.printf("\t- Locked %s%n", format(lockedSynchronizer)); + } + } + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/management/ThreadDumpEndpoint.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/management/ThreadDumpEndpoint.java index 4e65ccd1cf..831c528af1 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/management/ThreadDumpEndpoint.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/management/ThreadDumpEndpoint.java @@ -20,6 +20,7 @@ import java.lang.management.ManagementFactory; import java.lang.management.ThreadInfo; import java.util.Arrays; import java.util.List; +import java.util.function.Function; import org.springframework.boot.actuate.endpoint.annotation.Endpoint; import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; @@ -34,9 +35,20 @@ import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; @Endpoint(id = "threaddump") public class ThreadDumpEndpoint { + private final PlainTextThreadDumpFormatter plainTextFormatter = new PlainTextThreadDumpFormatter(); + @ReadOperation public ThreadDumpDescriptor threadDump() { - return new ThreadDumpDescriptor(Arrays.asList(ManagementFactory.getThreadMXBean().dumpAllThreads(true, true))); + return getFormattedThreadDump(ThreadDumpDescriptor::new); + } + + @ReadOperation(produces = "text/plain;charset=UTF-8") + public String textThreadDump() { + return getFormattedThreadDump(this.plainTextFormatter::format); + } + + private T getFormattedThreadDump(Function formatter) { + return formatter.apply(ManagementFactory.getThreadMXBean().dumpAllThreads(true, true)); } /** @@ -46,8 +58,8 @@ public class ThreadDumpEndpoint { private final List threads; - private ThreadDumpDescriptor(List threads) { - this.threads = threads; + private ThreadDumpDescriptor(ThreadInfo[] threads) { + this.threads = Arrays.asList(threads); } public List getThreads() { diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/management/ThreadDumpEndpointTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/management/ThreadDumpEndpointTests.java index 9ba758d32d..e1d4d23570 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/management/ThreadDumpEndpointTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/management/ThreadDumpEndpointTests.java @@ -16,6 +16,12 @@ package org.springframework.boot.actuate.management; +import java.lang.Thread.State; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; + import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; @@ -33,4 +39,87 @@ class ThreadDumpEndpointTests { assertThat(new ThreadDumpEndpoint().threadDump().getThreads().size()).isGreaterThan(0); } + @Test + void dumpThreadsAsText() throws InterruptedException { + Object contendedMonitor = new Object(); + Object monitor = new Object(); + CountDownLatch latch = new CountDownLatch(1); + Thread awaitCountDownLatchThread = new Thread(() -> { + try { + latch.await(); + } + catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + }, "Awaiting CountDownLatch"); + awaitCountDownLatchThread.start(); + Thread contendedMonitorThread = new Thread(() -> { + synchronized (contendedMonitor) { + // Intentionally empty + } + }, "Waiting for monitor"); + Thread waitOnMonitorThread = new Thread(() -> { + synchronized (monitor) { + try { + monitor.wait(); + } + catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + } + }, "Waiting on monitor"); + waitOnMonitorThread.start(); + ReadWriteLock readWriteLock = new ReentrantReadWriteLock(); + Lock writeLock = readWriteLock.writeLock(); + new Thread(() -> { + writeLock.lock(); + try { + latch.await(); + } + catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + finally { + writeLock.unlock(); + } + }, "Holding write lock").start(); + while (writeLock.tryLock()) { + writeLock.unlock(); + } + awaitState(waitOnMonitorThread, State.WAITING); + awaitState(awaitCountDownLatchThread, State.WAITING); + String threadDump; + synchronized (contendedMonitor) { + contendedMonitorThread.start(); + awaitState(contendedMonitorThread, State.BLOCKED); + threadDump = new ThreadDumpEndpoint().textThreadDump(); + } + latch.countDown(); + synchronized (monitor) { + monitor.notifyAll(); + } + System.out.println(threadDump); + assertThat(threadDump) + .containsPattern(String.format("\t- parking to wait for <[0-9a-z]+> \\(a %s\\$Sync\\)", + CountDownLatch.class.getName().replace(".", "\\."))) + .contains(String.format("\t- locked <%s> (a java.lang.Object)", hexIdentityHashCode(contendedMonitor))) + .contains(String.format("\t- waiting to lock <%s> (a java.lang.Object) owned by \"%s\" t@%d", + hexIdentityHashCode(contendedMonitor), Thread.currentThread().getName(), + Thread.currentThread().getId())) + .contains(String.format("\t- waiting on <%s> (a java.lang.Object)", hexIdentityHashCode(monitor))) + .containsPattern( + String.format("Locked ownable synchronizers:%n\t- Locked <[0-9a-z]+> \\(a %s\\$NonfairSync\\)", + ReentrantReadWriteLock.class.getName().replace(".", "\\."))); + } + + private String hexIdentityHashCode(Object object) { + return Integer.toHexString(System.identityHashCode(object)); + } + + private void awaitState(Thread thread, State state) throws InterruptedException { + while (thread.getState() != state) { + Thread.sleep(50); + } + } + } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/management/ThreadDumpEndpointWebIntegrationTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/management/ThreadDumpEndpointWebIntegrationTests.java new file mode 100644 index 0000000000..077aa0418c --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/management/ThreadDumpEndpointWebIntegrationTests.java @@ -0,0 +1,59 @@ +/* + * Copyright 2012-2019 the original author or authors. + * + * Licensed 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 + * + * https://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.springframework.boot.actuate.management; + +import org.springframework.boot.actuate.endpoint.web.test.WebEndpointTest; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.MediaType; +import org.springframework.test.web.reactive.server.WebTestClient; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for {@link ThreadDumpEndpoint} exposed by Jersey, Spring MVC, and + * WebFlux. + * + * @author Andy Wilkinson + */ +class ThreadDumpEndpointWebIntegrationTests { + + @WebEndpointTest + void getRequestWithJsonAcceptHeaderShouldProduceJsonThreadDumpResponse(WebTestClient client) throws Exception { + client.get().uri("/actuator/threaddump").accept(MediaType.APPLICATION_JSON).exchange().expectStatus().isOk() + .expectHeader().contentType(MediaType.APPLICATION_JSON); + } + + @WebEndpointTest + void getRequestWithTextPlainAcceptHeaderShouldProduceTextPlainResponse(WebTestClient client) throws Exception { + String response = client.get().uri("/actuator/threaddump").accept(MediaType.TEXT_PLAIN).exchange() + .expectStatus().isOk().expectHeader().contentType("text/plain;charset=UTF-8").expectBody(String.class) + .returnResult().getResponseBody(); + assertThat(response).contains("Full thread dump"); + } + + @Configuration(proxyBeanMethods = false) + public static class TestConfiguration { + + @Bean + public ThreadDumpEndpoint endpoint() { + return new ThreadDumpEndpoint(); + } + + } + +}