Skip to content

Commit bbad0d7

Browse files
authored
Async Servlet state management changes (#385)
1 parent f2a89b3 commit bbad0d7

File tree

6 files changed

+55
-29
lines changed

6 files changed

+55
-29
lines changed

apm-agent-api/src/main/java/co/elastic/apm/api/AbstractSpanImpl.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,22 @@
1+
/*-
2+
* #%L
3+
* Elastic APM Java agent
4+
* %%
5+
* Copyright (C) 2018 Elastic and contributors
6+
* %%
7+
* Licensed under the Apache License, Version 2.0 (the "License");
8+
* you may not use this file except in compliance with the License.
9+
* You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing, software
14+
* distributed under the License is distributed on an "AS IS" BASIS,
15+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
* See the License for the specific language governing permissions and
17+
* limitations under the License.
18+
* #L%
19+
*/
120
package co.elastic.apm.api;
221

322
import javax.annotation.Nonnull;

apm-agent-plugins/apm-servlet-plugin/src/main/java/co/elastic/apm/agent/servlet/AsyncInstrumentation.java

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@
2323
import co.elastic.apm.agent.bci.HelperClassManager;
2424
import co.elastic.apm.agent.bci.VisibleForAdvice;
2525
import co.elastic.apm.agent.impl.ElasticApmTracer;
26-
import co.elastic.apm.agent.impl.transaction.Transaction;
2726
import net.bytebuddy.asm.Advice;
2827
import net.bytebuddy.description.NamedElement;
2928
import net.bytebuddy.description.method.MethodDescription;
@@ -38,7 +37,6 @@
3837
import java.util.Arrays;
3938
import java.util.Collection;
4039

41-
import static co.elastic.apm.agent.servlet.ServletApiAdvice.TRANSACTION_ATTRIBUTE;
4240
import static net.bytebuddy.matcher.ElementMatchers.hasSuperType;
4341
import static net.bytebuddy.matcher.ElementMatchers.isInterface;
4442
import static net.bytebuddy.matcher.ElementMatchers.isPublic;
@@ -126,12 +124,8 @@ public static class StartAsyncAdvice {
126124
@Advice.OnMethodExit(suppress = Throwable.class)
127125
private static void onExitStartAsync(@Advice.This HttpServletRequest request, @Advice.Return AsyncContext asyncContext) {
128126
if (tracer != null) {
129-
final Transaction transaction = tracer.currentTransaction();
130-
if (transaction != null) {
131-
request.setAttribute(TRANSACTION_ATTRIBUTE, transaction);
132-
if (asyncHelper != null) {
133-
asyncHelper.getForClassLoaderOfClass(AsyncContext.class).onExitStartAsync(asyncContext);
134-
}
127+
if (asyncHelper != null) {
128+
asyncHelper.getForClassLoaderOfClass(AsyncContext.class).onExitStartAsync(asyncContext);
135129
}
136130
}
137131
}

apm-agent-plugins/apm-servlet-plugin/src/main/java/co/elastic/apm/agent/servlet/ServletApiAdvice.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@
4141
import java.util.Enumeration;
4242
import java.util.Map;
4343

44+
import static co.elastic.apm.agent.servlet.ServletTransactionHelper.TRANSACTION_ATTRIBUTE;
45+
4446
/**
4547
* Only the methods annotated with {@link Advice.OnMethodEnter} and {@link Advice.OnMethodExit} may contain references to
4648
* {@code javax.servlet}, as these are inlined into the matching methods.
@@ -49,8 +51,6 @@
4951
*/
5052
public class ServletApiAdvice {
5153

52-
@VisibleForAdvice
53-
public static final String TRANSACTION_ATTRIBUTE = ServletApiAdvice.class.getName() + ".transaction";
5454
@Nullable
5555
@VisibleForAdvice
5656
public static ServletTransactionHelper servletTransactionHelper;
@@ -149,8 +149,9 @@ public static void onExitServletService(@Advice.Argument(0) ServletRequest servl
149149
servletResponse instanceof HttpServletResponse) {
150150

151151
final HttpServletRequest request = (HttpServletRequest) servletRequest;
152-
if (request.isAsyncStarted()) {
153-
// the response is not ready yet; the request is handled asynchronously
152+
if (request.getAttribute(ServletTransactionHelper.ASYNC_ATTRIBUTE) != null) {
153+
// HttpServletRequest.startAsync was invoked on this request.
154+
// The transaction should be handled from now on by the other thread committing the response
154155
transaction.deactivate();
155156
} else {
156157
// this is not an async request, so we can end the transaction immediately
@@ -170,7 +171,6 @@ public static void onExitServletService(@Advice.Argument(0) ServletRequest servl
170171
} else {
171172
parameterMap = null;
172173
}
173-
request.removeAttribute(TRANSACTION_ATTRIBUTE);
174174
servletTransactionHelper.onAfter(transaction, t, response.isCommitted(), response.getStatus(), request.getMethod(),
175175
parameterMap, request.getServletPath(), request.getPathInfo(), contentTypeHeader);
176176
}

apm-agent-plugins/apm-servlet-plugin/src/main/java/co/elastic/apm/agent/servlet/ServletTransactionHelper.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,11 @@
5151
@VisibleForAdvice
5252
public class ServletTransactionHelper {
5353

54+
@VisibleForAdvice
55+
public static final String TRANSACTION_ATTRIBUTE = ServletApiAdvice.class.getName() + ".transaction";
56+
@VisibleForAdvice
57+
public static final String ASYNC_ATTRIBUTE = ServletApiAdvice.class.getName() + ".async";
58+
5459
private final Logger logger = LoggerFactory.getLogger(ServletTransactionHelper.class);
5560

5661
private final Set<String> METHODS_WITH_BODY = new HashSet<>(Arrays.asList("POST", "PUT", "PATCH", "DELETE"));

apm-agent-plugins/apm-servlet-plugin/src/main/java/co/elastic/apm/agent/servlet/helper/ApmAsyncListener.java

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,20 @@
2929
import javax.servlet.http.HttpServletRequest;
3030
import javax.servlet.http.HttpServletResponse;
3131
import java.util.Map;
32+
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
33+
34+
import static co.elastic.apm.agent.servlet.ServletTransactionHelper.TRANSACTION_ATTRIBUTE;
3235

3336
/**
3437
* Based on brave.servlet.ServletRuntime$TracingAsyncListener (under Apache license 2.0)
3538
*/
3639
public class ApmAsyncListener implements AsyncListener {
40+
private static final AtomicIntegerFieldUpdater<ApmAsyncListener> EVENT_COUNTER_UPDATER =
41+
AtomicIntegerFieldUpdater.newUpdater(ApmAsyncListener.class, "endEventCounter");
42+
3743
private final ServletTransactionHelper servletTransactionHelper;
3844
private final Transaction transaction;
39-
private volatile boolean completed = false;
45+
private volatile int endEventCounter = 0;
4046

4147
ApmAsyncListener(ServletTransactionHelper servletTransactionHelper, Transaction transaction) {
4248
this.servletTransactionHelper = servletTransactionHelper;
@@ -45,26 +51,17 @@ public class ApmAsyncListener implements AsyncListener {
4551

4652
@Override
4753
public void onComplete(AsyncEvent event) {
48-
if (!completed) {
49-
endTransaction(event);
50-
completed = true;
51-
}
54+
endTransaction(event);
5255
}
5356

5457
@Override
5558
public void onTimeout(AsyncEvent event) {
56-
if (!completed) {
57-
endTransaction(event);
58-
completed = true;
59-
}
59+
endTransaction(event);
6060
}
6161

6262
@Override
6363
public void onError(AsyncEvent event) {
64-
if (!completed) {
65-
endTransaction(event);
66-
completed = true;
67-
}
64+
endTransaction(event);
6865
}
6966

7067
/**
@@ -82,7 +79,14 @@ public void onStartAsync(AsyncEvent event) {
8279
// because only the onExitServletService method may contain references to the servlet API
8380
// (see class-level Javadoc)
8481
private void endTransaction(AsyncEvent event) {
82+
// To ensure transaction is ended only by a single event
83+
if (EVENT_COUNTER_UPDATER.getAndIncrement(this) > 0) {
84+
return;
85+
}
86+
8587
HttpServletRequest request = (HttpServletRequest) event.getSuppliedRequest();
88+
request.removeAttribute(TRANSACTION_ATTRIBUTE);
89+
8690
HttpServletResponse response = (HttpServletResponse) event.getSuppliedResponse();
8791
final Response resp = transaction.getContext().getResponse();
8892
if (transaction.isSampled() && servletTransactionHelper.isCaptureHeaders()) {

apm-agent-plugins/apm-servlet-plugin/src/main/java/co/elastic/apm/agent/servlet/helper/StartAsyncAdviceHelperImpl.java

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@
2828
import javax.servlet.AsyncContext;
2929
import javax.servlet.ServletRequest;
3030

31+
import static co.elastic.apm.agent.servlet.ServletTransactionHelper.ASYNC_ATTRIBUTE;
32+
import static co.elastic.apm.agent.servlet.ServletTransactionHelper.TRANSACTION_ATTRIBUTE;
33+
3134
public class StartAsyncAdviceHelperImpl implements AsyncInstrumentation.StartAsyncAdviceHelper<AsyncContext> {
3235

3336
private static final String ASYNC_LISTENER_ADDED = ServletApiAdvice.class.getName() + ".asyncListenerAdded";
@@ -47,9 +50,7 @@ public void onExitStartAsync(AsyncContext asyncContext) {
4750
return;
4851
}
4952
final Transaction transaction = tracer.currentTransaction();
50-
if (transaction != null &&
51-
transaction.isSampled() &&
52-
request.getAttribute(ASYNC_LISTENER_ADDED) == null) {
53+
if (transaction != null && transaction.isSampled() && request.getAttribute(ASYNC_LISTENER_ADDED) == null) {
5354
// makes sure that the listener is only added once, even if the request is wrapped
5455
// which leads to multiple invocations of startAsync for the same underlying request
5556
request.setAttribute(ASYNC_LISTENER_ADDED, Boolean.TRUE);
@@ -58,6 +59,9 @@ public void onExitStartAsync(AsyncContext asyncContext) {
5859
// however, only some application server like WebSphere actually implement it that way
5960
asyncContext.addListener(new ApmAsyncListener(servletTransactionHelper, transaction),
6061
asyncContext.getRequest(), asyncContext.getResponse());
62+
63+
request.setAttribute(ASYNC_ATTRIBUTE, Boolean.TRUE);
64+
request.setAttribute(TRANSACTION_ATTRIBUTE, transaction);
6165
}
6266
}
6367
}

0 commit comments

Comments
 (0)