Skip to content
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

Merged
merged 12 commits into from
May 26, 2021

Conversation

agoallikmaa
Copy link
Contributor

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 the AsyncContext 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.

@agoallikmaa
Copy link
Contributor Author

Added fix for Liberty - response object attached as an attribute to request because on Liberty, AsyncContext#addListener(AsyncListener, ServletRequest, ServletResponse) must be called instead of AsyncContext#addListener(AsyncListener)

Copy link
Member

@trask trask left a 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

Comment on lines 30 to 31
// 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.
Copy link
Member

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?

Copy link
Contributor Author

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.

Copy link
Member

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--

Copy link
Contributor Author

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).

Comment on lines 70 to 75
// 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.
Copy link
Member

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

Copy link
Contributor Author

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.

Copy link
Member

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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rephrased the comment

@@ -31,6 +32,8 @@ public static void onEnter(

context = tracer().startServerSpan(request);
scope = context.makeCurrent();

tracer().setAsyncListenerResponse(request, response);
Copy link
Member

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?

Copy link
Contributor Author

@agoallikmaa agoallikmaa May 23, 2021

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.

Copy link
Member

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?

Copy link
Contributor Author

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.

Copy link
Member

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

Copy link
Contributor Author

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) {
Copy link
Contributor

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

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.


@Advice.OnMethodExit(suppress = Throwable.class)
public static void startAsyncExit(
@Advice.This(typing = Assigner.Typing.DYNAMIC) HttpServletRequest request) {
Copy link
Contributor

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.

Copy link
Contributor Author

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) {
Copy link
Contributor

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?

Copy link
Contributor Author

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.


// In case the request finished before adding the listener on older tomcats...
if (!servletRequest.isAsyncStarted() && responseHandled.compareAndSet(false, true)) {
if (servletRequest != null) {
Copy link
Contributor

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

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Copy link
Member

@trask trask left a 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 👍

@trask trask merged commit fd132d4 into open-telemetry:main May 26, 2021
robododge pushed a commit to robododge/opentelemetry-java-instrumentation that referenced this pull request Jun 17, 2021
…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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants