/*
* Copyright 2011-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.servlet;
import java.io.IOException;
import java.util.Iterator;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import javax.servlet.AsyncContext;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.ning.http.client.AsyncHttpClient;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
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 AsyncServletIT {
private static final String PLUGIN_ID = "servlet";
private static Container container;
@BeforeClass
public static void setUp() throws Exception {
// async servlet test relies on executor plugin, which only works under javaagent
container = Containers.createJavaagent();
}
@AfterClass
public static void tearDown() throws Exception {
container.close();
}
@After
public void afterEachTest() throws Exception {
container.checkAndReset();
}
@Test
public void testAsyncServlet() throws Exception {
testAsyncServlet("", InvokeAsync.class);
}
@Test
public void testAsyncServletWithContextPath() throws Exception {
testAsyncServlet("/zzz", InvokeAsyncWithContextPath.class);
}
@Test
public void testAsyncServlet2() throws Exception {
testAsyncServlet2("", InvokeAsync2.class);
}
@Test
public void testAsyncServlet2WithContextPath() throws Exception {
testAsyncServlet2("/zzz", InvokeAsync2WithContextPath.class);
}
@Test
public void testAsyncServletWithDispatch() throws Exception {
testAsyncServletWithDispatch("", InvokeAsyncWithDispatch.class);
}
@Test
public void testAsyncServletWithDispatchWithContextPath() throws Exception {
testAsyncServletWithDispatch("/zzz", InvokeAsyncWithDispatchWithContextPath.class);
}
private void testAsyncServlet(String contextPath,
Class<? extends AppUnderTest> appUnderTestClass) throws Exception {
// given
container.getConfigService().setPluginProperty(PLUGIN_ID, "captureSessionAttributes", "*");
// when
Trace trace = container.execute(appUnderTestClass);
// then
Trace.Header header = trace.getHeader();
assertThat(header.getAsync()).isTrue();
assertThat(header.getHeadline()).isEqualTo(contextPath + "/async");
assertThat(header.getTransactionName()).isEqualTo(contextPath + "/async");
// check session attributes set across async boundary
assertThat(SessionAttributeIT.getSessionAttributes(trace)).isNull();
assertThat(SessionAttributeIT.getInitialSessionAttributes(trace)).isNull();
assertThat(SessionAttributeIT.getUpdatedSessionAttributes(trace).get("sync"))
.isEqualTo("a");
assertThat(SessionAttributeIT.getUpdatedSessionAttributes(trace).get("async"))
.isEqualTo("b");
Iterator<Trace.Entry> i = trace.getEntryList().iterator();
Trace.Entry entry = i.next();
assertThat(entry.getDepth()).isEqualTo(0);
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");
assertThat(i.hasNext()).isFalse();
}
private void testAsyncServlet2(String contextPath,
Class<? extends AppUnderTest> appUnderTestClass) throws Exception {
// given
container.getConfigService().setPluginProperty(PLUGIN_ID, "captureSessionAttributes", "*");
// when
Trace trace = container.execute(appUnderTestClass);
// then
Trace.Header header = trace.getHeader();
assertThat(header.getAsync()).isTrue();
assertThat(header.getHeadline()).isEqualTo(contextPath + "/async2");
assertThat(header.getTransactionName()).isEqualTo(contextPath + "/async2");
// check session attributes set across async boundary
assertThat(SessionAttributeIT.getSessionAttributes(trace)).isNull();
assertThat(SessionAttributeIT.getInitialSessionAttributes(trace)).isNull();
assertThat(SessionAttributeIT.getUpdatedSessionAttributes(trace).get("sync"))
.isEqualTo("a");
assertThat(SessionAttributeIT.getUpdatedSessionAttributes(trace).get("async"))
.isEqualTo("b");
Iterator<Trace.Entry> i = trace.getEntryList().iterator();
Trace.Entry entry = i.next();
assertThat(entry.getDepth()).isEqualTo(0);
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");
assertThat(i.hasNext()).isFalse();
}
private void testAsyncServletWithDispatch(String contextPath,
Class<? extends AppUnderTest> appUnderTestClass) throws Exception {
// given
container.getConfigService().setPluginProperty(PLUGIN_ID, "captureSessionAttributes", "*");
// when
Trace trace = container.execute(appUnderTestClass);
Thread.sleep(1000);
// then
Trace.Header header = trace.getHeader();
assertThat(header.getAsync()).isTrue();
assertThat(header.getHeadline()).isEqualTo(contextPath + "/async3");
assertThat(header.getTransactionName()).isEqualTo(contextPath + "/async3");
// and check session attributes set across async and dispatch boundary
assertThat(SessionAttributeIT.getSessionAttributes(trace)).isNull();
assertThat(SessionAttributeIT.getInitialSessionAttributes(trace)).isNull();
assertThat(SessionAttributeIT.getUpdatedSessionAttributes(trace).get("sync"))
.isEqualTo("a");
assertThat(SessionAttributeIT.getUpdatedSessionAttributes(trace).get("async"))
.isEqualTo("b");
assertThat(SessionAttributeIT.getUpdatedSessionAttributes(trace).get("async-dispatch"))
.isEqualTo("c");
Iterator<Trace.Entry> i = trace.getEntryList().iterator();
Trace.Entry entry = i.next();
assertThat(entry.getDepth()).isEqualTo(0);
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");
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
// (AsyncServletWithDispatch$1) calls javax.servlet.AsyncContext.dispatch(), and
// sporadically dispatch() can process and returns the response before
// AsyncServletWithDispatch$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(1);
assertThat(entry.getMessage()).isEqualTo(
"this auxiliary thread was still running when the transaction ended");
}
assertThat(i.hasNext()).isFalse();
}
public static class InvokeAsync extends InvokeAsyncBase {
public InvokeAsync() {
super("");
}
}
public static class InvokeAsyncWithContextPath extends InvokeAsyncBase {
public InvokeAsyncWithContextPath() {
super("/zzz");
}
}
private static class InvokeAsyncBase extends InvokeServletInTomcat {
private InvokeAsyncBase(String contextPath) {
super(contextPath);
}
@Override
protected void doTest(int port) throws Exception {
AsyncHttpClient asyncHttpClient = new AsyncHttpClient();
int statusCode =
asyncHttpClient.prepareGet("http://localhost:" + port + contextPath + "/async")
.execute().get().getStatusCode();
asyncHttpClient.close();
if (statusCode != 200) {
throw new IllegalStateException("Unexpected status code: " + statusCode);
}
}
}
public static class InvokeAsync2 extends InvokeAsync2Base {
public InvokeAsync2() {
super("");
}
}
public static class InvokeAsync2WithContextPath extends InvokeAsync2Base {
public InvokeAsync2WithContextPath() {
super("/zzz");
}
}
private static class InvokeAsync2Base extends InvokeServletInTomcat {
private InvokeAsync2Base(String contextPath) {
super(contextPath);
}
@Override
protected void doTest(int port) throws Exception {
AsyncHttpClient asyncHttpClient = new AsyncHttpClient();
int statusCode =
asyncHttpClient.prepareGet("http://localhost:" + port + contextPath + "/async2")
.execute().get().getStatusCode();
asyncHttpClient.close();
if (statusCode != 200) {
throw new IllegalStateException("Unexpected status code: " + statusCode);
}
}
}
public static class InvokeAsyncWithDispatch extends InvokeAsyncWithDispatchBase {
public InvokeAsyncWithDispatch() {
super("");
}
}
public static class InvokeAsyncWithDispatchWithContextPath extends InvokeAsyncWithDispatchBase {
public InvokeAsyncWithDispatchWithContextPath() {
super("/zzz");
}
}
private static class InvokeAsyncWithDispatchBase extends InvokeServletInTomcat {
private InvokeAsyncWithDispatchBase(String contextPath) {
super(contextPath);
}
@Override
protected void doTest(int port) throws Exception {
AsyncHttpClient asyncHttpClient = new AsyncHttpClient();
// send initial to trigger servlet init methods so they don't end up in trace
int statusCode =
asyncHttpClient.prepareGet("http://localhost:" + port + contextPath + "/async3")
.execute().get().getStatusCode();
if (statusCode != 200) {
asyncHttpClient.close();
throw new IllegalStateException("Unexpected status code: " + statusCode);
}
statusCode =
asyncHttpClient.prepareGet("http://localhost:" + port + contextPath + "/async3")
.execute().get().getStatusCode();
asyncHttpClient.close();
if (statusCode != 200) {
throw new IllegalStateException("Unexpected status code: " + statusCode);
}
}
}
@WebServlet(value = "/async", asyncSupported = true)
@SuppressWarnings("serial")
public static class AsyncServlet extends HttpServlet {
private final ExecutorService executor = Executors.newSingleThreadExecutor();
@Override
public void destroy() {
executor.shutdown();
}
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) {
request.getSession().setAttribute("sync", "a");
new CreateTraceEntry().traceEntryMarker();
final AsyncContext asyncContext = request.startAsync();
asyncContext.start(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(200);
((HttpServletRequest) asyncContext.getRequest()).getSession()
.setAttribute("async", "b");
new CreateTraceEntry().traceEntryMarker();
asyncContext.getResponse().getWriter().println("async response");
asyncContext.complete();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
});
}
}
@WebServlet(value = "/async2", asyncSupported = true)
@SuppressWarnings("serial")
public static class AsyncServlet2 extends HttpServlet {
private final ExecutorService executor = Executors.newSingleThreadExecutor();
@Override
public void destroy() {
executor.shutdown();
}
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) {
request.getSession().setAttribute("sync", "a");
new CreateTraceEntry().traceEntryMarker();
final AsyncContext asyncContext = request.startAsync(request, response);
asyncContext.start(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(200);
((HttpServletRequest) asyncContext.getRequest()).getSession()
.setAttribute("async", "b");
new CreateTraceEntry().traceEntryMarker();
asyncContext.getResponse().getWriter().println("async response");
asyncContext.complete();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
});
}
}
@WebServlet(value = "/async3", asyncSupported = true)
@SuppressWarnings("serial")
public static class AsyncServletWithDispatch extends HttpServlet {
private final ExecutorService executor = Executors.newSingleThreadExecutor();
@Override
public void destroy() {
executor.shutdown();
}
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) {
request.getSession().setAttribute("sync", "a");
new CreateTraceEntry().traceEntryMarker();
final AsyncContext asyncContext = request.startAsync();
asyncContext.start(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(200);
((HttpServletRequest) asyncContext.getRequest()).getSession()
.setAttribute("async", "b");
new CreateTraceEntry().traceEntryMarker();
asyncContext.dispatch("/async-forward");
} catch (Exception e) {
throw new RuntimeException(e);
}
}
});
}
}
@WebServlet(value = "/async-forward")
@SuppressWarnings("serial")
public static class SimpleServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws IOException {
request.getSession().setAttribute("async-dispatch", "c");
new CreateTraceEntry().traceEntryMarker();
response.getWriter().println("the response");
}
}
private static class CreateTraceEntry implements TraceEntryMarker {
@Override
public void traceEntryMarker() {}
}
}