-
Notifications
You must be signed in to change notification settings - Fork 909
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Change Async Servlet span end logic to fix race condition on Undertow #2992
Conversation
1a2572f
to
93638d8
Compare
Added fix for Liberty - response object attached as an attribute to request because on Liberty, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The logic in service method exit itself was reduced to simply checking if an async listener has already been attached to this request
👍 this looks like a solid approach
...t-common/library/src/main/java/io/opentelemetry/instrumentation/servlet/ServletAccessor.java
Show resolved
Hide resolved
...t-common/library/src/main/java/io/opentelemetry/instrumentation/servlet/ServletAccessor.java
Outdated
Show resolved
Hide resolved
...telemetry/javaagent/instrumentation/servlet/common/service/ServletAndFilterAdviceHelper.java
Show resolved
Hide resolved
...ain/java/io/opentelemetry/javaagent/instrumentation/servlet/v5_0/async/AsyncStartAdvice.java
Show resolved
Hide resolved
...n/java/io/opentelemetry/javaagent/instrumentation/servlet/v3_0/Servlet3AsyncStartAdvice.java
Show resolved
Hide resolved
// For startAsync instrumentation to work on Liberty, the listener needs to be added with a | ||
// response object included, which is not available in that instrumentation. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it seems like we should always be passing request/response to AsyncContext.addListener(), since our listener calls AsyncEvent.getSuppliedRequest() and AsyncEvent.getSuppliedResponse().
-- or --
maybe our listener should call event.getAsyncContext().getRequest()
and event.getAsyncContext().getResponse()
instead, and then maybe we don't ever need to pass request/response?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
event.getAsyncContext().getResponse()
does not seem to solve the current issue with Liberty as it is not available there either (and the spec allows it to be missing). Passing the response would mean storing the response as a request attribute for all servlet engines - I am not sure if we want to do that preemptively for all of them.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we don't pass request/response to AsyncContext.addListener(), won't AsyncEvent.getSuppliedResponse() (which we call in our listener) always return null?
If the AsyncListener was added via AsyncContext.addListener(AsyncListener), this method must return null.
https://docs.oracle.com/javaee/7/api/javax/servlet/AsyncEvent.html#getSuppliedResponse--
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
According to documentation, yes, but apparently that was not the case in practice...
I've attached the servlet response as an attribute on all servlet engines now (in generic servlet instrumentation, and Tomcat and Jetty specific instrumentations).
// 2) asynchronously handled request, but startAsync instrumentation is broken on this server | ||
// (should be caught by tests for the specific server). | ||
// In case of broken startAsync instrumentation, no fallback handling for asynchronous requests | ||
// should be provided as handling it in the handler/service method is prone to race conditions | ||
// (seen happening on Undertow) that may make some tests pass which should fail due to the | ||
// possible race. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i'm hoping we can remove this part about broken startAsync instrumentation if the above comment turns out to be fruitful
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I actually added this comment before I encountered the issue with Liberty - so its purpose is to be a hint for debugging and to make it clear to the reader why we even need a separate instrumentation in the first place.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
since Liberty was following spec (and not broken), can we remove (or modify) this comment?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rephrased the comment
.../library/src/main/java/io/opentelemetry/instrumentation/servlet/ServletHttpServerTracer.java
Outdated
Show resolved
Hide resolved
14c69c4
to
622f291
Compare
@@ -31,6 +32,8 @@ public static void onEnter( | |||
|
|||
context = tracer().startServerSpan(request); | |||
scope = context.makeCurrent(); | |||
|
|||
tracer().setAsyncListenerResponse(request, response); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do we need to add calls to setAsyncListenerResponse
in each app server instrumentation, or can we do it once in servlet instrumentation?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, since it is possible for requests to be handled entirely within app server handlers without servlet advices ever triggering.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can those requests have servlet async listeners?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Jetty handlers and Tomcat valves both expose the ServletRequest
and are allowed to generate the response without a servlet ever being invoked. I have not tested using startAsync
in them, but the documentation does not say that either of them would only support synchronous responses.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can you add comments to the code explaining this? otherwise worried these lines could be removed thinking they aren't really needed, and I don't think we have any tests that it would break
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added those comments for all the appservers.
private Context context; | ||
private Scope scope; | ||
private boolean started; | ||
|
||
private ThreadLocalContext(HttpServletRequest req) { | ||
private ThreadLocalContext(HttpServletRequest req, HttpServletResponse response) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
could you rename req
-> request
in this class to make it consistent with our style of using verbose names
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
3314dca
to
512dc97
Compare
|
||
@Advice.OnMethodExit(suppress = Throwable.class) | ||
public static void startAsyncExit( | ||
@Advice.This(typing = Assigner.Typing.DYNAMIC) HttpServletRequest request) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that instead of dynamic typing to HttpServletRequest
it would be better to use @Advice.This ServletRequest request
and use instanceof+cast to get HttpServletRequest
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
return; | ||
} | ||
|
||
if (request != null) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can request really be null here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Replaced with instanceof
due to the typing change.
...telemetry/javaagent/instrumentation/servlet/common/service/ServletAndFilterAdviceHelper.java
Show resolved
Hide resolved
|
||
// In case the request finished before adding the listener on older tomcats... | ||
if (!servletRequest.isAsyncStarted() && responseHandled.compareAndSet(false, true)) { | ||
if (servletRequest != null) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
could combine these if statements
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@agoallikmaa thanks for your patience, and great find btw 👍
…open-telemetry#2992) * Attach servlet async listener with asyncStart instrumentation * Exclude Spring packages containing servlet request classes from global ignores * Exclude Tapestry HSR proxy with global ignore * Improve comments. * Fix for Liberty - request response when adding async listener * Removed unused methods * Explicit response to async listeners on all servlet engines * Attach response to request on Jetty * Fix broken build due to rebase, improved a comment * Address PR comments * Added a comment. * Addressed PR comments
After enabling concurrency test to RestEasy HTTP server tests, it turned out that test intermittently fails when it uses Undertow as the servlet engine. This was because of a race condition in Async Servlet instrumentations - the async listener events were fired before the method exit instrumentation added the listener, yet the
isAsyncStarted
method did not yet revert to returning false.There does not seem to be a straightforward way to avoid races like this properly in the servlet service method exit instrumentation, therefore I moved adding the async listener to the instrumentation of the
startAsync
method itself, which should guarantee that that the listener is added to theAsyncContext
before it is possible for them to be invoked.The logic in service method exit itself was reduced to simply checking if an async listener has already been attached to this request - this information is saved in the request attributes at the time when the listener is added. In case the listener is not present, assume it is a synchronous request and end it immediately.