/*
* Copyright 2016-2017 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
*
* 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.glowroot.agent.plugin.spring;
import java.util.Iterator;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.context.request.async.DeferredResult;
import org.glowroot.agent.it.harness.AppUnderTest;
import org.glowroot.agent.it.harness.Container;
import org.glowroot.agent.it.harness.Containers;
import org.glowroot.agent.it.harness.TraceEntryMarker;
import org.glowroot.wire.api.model.TraceOuterClass.Trace;
import static org.assertj.core.api.Assertions.assertThat;
public class AsyncControllerIT {
private static Container container;
@BeforeClass
public static void setUp() throws Exception {
// javaagent is required for Executor.execute() weaving
container = Containers.createJavaagent();
}
@AfterClass
public static void tearDown() throws Exception {
container.close();
}
@After
public void afterEachTest() throws Exception {
container.checkAndReset();
}
@Test
public void shouldCaptureCallableAsyncController() throws Exception {
shouldCaptureCallableAsyncController("", CallableAsyncServlet.class);
}
@Test
public void shouldCaptureDeferredResultAsyncController() throws Exception {
shouldCaptureDeferredResultAsyncController("", DeferredResultAsyncServlet.class);
}
@Test
public void shouldCaptureCallableAsyncControllerWithContextPath() throws Exception {
shouldCaptureCallableAsyncController("/zzz", CallableAsyncServletWithContextPath.class);
}
@Test
public void shouldCaptureDeferredResultAsyncControllerWithContextPath() throws Exception {
shouldCaptureDeferredResultAsyncController("/zzz",
DeferredResultAsyncServletWithContextPath.class);
}
private void shouldCaptureCallableAsyncController(String contextPath,
Class<? extends AppUnderTest> appUnderTestClass) throws Exception {
// when
Trace trace = container.execute(appUnderTestClass);
// then
assertThat(trace.getHeader().getAsync()).isTrue();
assertThat(trace.getHeader().getTransactionName()).isEqualTo(contextPath + "/async");
Iterator<Trace.Entry> i = trace.getEntryList().iterator();
Trace.Entry entry = i.next();
assertThat(entry.getDepth()).isEqualTo(0);
assertThat(entry.getMessage())
.isEqualTo("spring controller: org.glowroot.agent.plugin.spring.AsyncControllerIT"
+ "$CallableAsyncController.test()");
entry = i.next();
assertThat(entry.getDepth()).isEqualTo(1);
assertThat(entry.getMessage()).isEqualTo("trace entry marker / CreateTraceEntry");
entry = i.next();
assertThat(entry.getDepth()).isEqualTo(0);
assertThat(entry.getMessage()).isEqualTo("auxiliary thread");
entry = i.next();
assertThat(entry.getDepth()).isEqualTo(1);
assertThat(entry.getMessage()).isEqualTo("trace entry marker / CreateTraceEntry");
if (i.hasNext()) {
// this happens sporadically on travis ci because the auxiliary thread is demarcated by
// WebAsyncManager$4.run(), which calls both CallableAsyncController$1.call() and
// calls javax.servlet.AsyncContext.dispatch(), and sporadically dispatch() can process
// and returns the response before WebAsyncManager$4.run() completes, leading to
// glowroot adding a trace entry under the auxiliary thread to note that
// "this auxiliary thread was still running when the transaction ended"
// this is the stack trace for the call to CallableAsyncController$1.call():
// org.glowroot.agent.plugin.spring.AsyncControllerIT$CallableAsyncController$1.call()
// org.springframework.web.context.request.async.WebAsyncManager$4.run(WebAsyncManager.java:316)
// java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:471)
// java.util.concurrent.FutureTask.run(FutureTask.java:262)
// this is the stack trace for the call to javax.servlet.AsyncContext.dispatch():
// org.apache.catalina.core.AsyncContextImpl.dispatch(AsyncContextImpl.java)
// org.springframework.web.context.request.async.StandardServletAsyncWebRequest.dispatch(StandardServletAsyncWebRequest.java:123)
// org.springframework.web.context.request.async.WebAsyncManager.setConcurrentResultAndDispatch(WebAsyncManager.java:353)
// org.springframework.web.context.request.async.WebAsyncManager.access$200(WebAsyncManager.java:58)
// org.springframework.web.context.request.async.WebAsyncManager$4.run(WebAsyncManager.java:324)
// java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:471)
// java.util.concurrent.FutureTask.run(FutureTask.java:262)
// see similar issue in org.glowroot.agent.plugin.servlet.AsyncServletIT
entry = i.next();
assertThat(entry.getDepth()).isEqualTo(1);
assertThat(entry.getMessage()).isEqualTo(
"this auxiliary thread was still running when the transaction ended");
}
assertThat(i.hasNext()).isFalse();
}
private void shouldCaptureDeferredResultAsyncController(String contextPath,
Class<? extends AppUnderTest> appUnderTestClass) throws Exception {
// when
Trace trace = container.execute(appUnderTestClass);
// then
assertThat(trace.getHeader().getAsync()).isTrue();
assertThat(trace.getHeader().getTransactionName()).isEqualTo(contextPath + "/async2");
Iterator<Trace.Entry> i = trace.getEntryList().iterator();
Trace.Entry entry = i.next();
assertThat(entry.getDepth()).isEqualTo(0);
assertThat(entry.getMessage())
.isEqualTo("spring controller: org.glowroot.agent.plugin.spring.AsyncControllerIT"
+ "$DeferredResultAsyncController.test()");
entry = i.next();
assertThat(entry.getDepth()).isEqualTo(1);
assertThat(entry.getMessage()).isEqualTo("trace entry marker / CreateTraceEntry");
entry = i.next();
assertThat(entry.getDepth()).isEqualTo(1);
assertThat(entry.getMessage()).isEqualTo("auxiliary thread");
entry = i.next();
assertThat(entry.getDepth()).isEqualTo(2);
assertThat(entry.getMessage()).isEqualTo("trace entry marker / CreateTraceEntry");
if (i.hasNext()) {
// this happens sporadically on travis ci because the auxiliary thread
// (DeferredResultAsyncController$1) calls javax.servlet.AsyncContext.dispatch(), and
// sporadically dispatch() can process and returns the response before
// DeferredResultAsyncController$1.run() completes, leading to glowroot adding a trace
// entry under the auxiliary thread to note that "this auxiliary thread was still
// running when the transaction ended"
// see similar issue in org.glowroot.agent.plugin.spring.AsyncControllerIT
entry = i.next();
assertThat(entry.getDepth()).isEqualTo(2);
assertThat(entry.getMessage()).isEqualTo(
"this auxiliary thread was still running when the transaction ended");
}
assertThat(i.hasNext()).isFalse();
}
public static class CallableAsyncServlet extends InvokeSpringControllerInTomcat {
@Override
public void executeApp() throws Exception {
executeApp("webapp1", "", "/async");
}
}
public static class DeferredResultAsyncServlet extends InvokeSpringControllerInTomcat {
@Override
public void executeApp() throws Exception {
executeApp("webapp1", "", "/async2");
}
}
public static class CallableAsyncServletWithContextPath extends InvokeSpringControllerInTomcat {
@Override
public void executeApp() throws Exception {
executeApp("webapp1", "/zzz", "/async");
}
}
public static class DeferredResultAsyncServletWithContextPath
extends InvokeSpringControllerInTomcat {
@Override
public void executeApp() throws Exception {
executeApp("webapp1", "/zzz", "/async2");
}
}
@Controller
public static class CallableAsyncController {
@RequestMapping(value = "async")
public @ResponseBody Callable<String> test() throws InterruptedException {
new CreateTraceEntry().traceEntryMarker();
return new Callable<String>() {
@Override
public String call() throws Exception {
new CreateTraceEntry().traceEntryMarker();
return "async world";
}
};
}
}
@Controller
public static class DeferredResultAsyncController {
@RequestMapping(value = "async2")
public @ResponseBody DeferredResult<String> test() throws InterruptedException {
new CreateTraceEntry().traceEntryMarker();
final DeferredResult<String> result = new DeferredResult<String>();
final ExecutorService executor = Executors.newCachedThreadPool();
executor.execute(new Runnable() {
@Override
public void run() {
new CreateTraceEntry().traceEntryMarker();
result.setResult("async2 world");
executor.shutdown();
}
});
return result;
}
}
private static class CreateTraceEntry implements TraceEntryMarker {
@Override
public void traceEntryMarker() {}
}
}