SentryServletRequestListener and Async requests with Tomcat
Integration
sentry-servlet
Java Version
17
Version
7.11.0
Steps to Reproduce
When using "async" servlet requests that use the SentryServletRequestListener. I see the "http" bread crumbs from other requests.
The same thread is not used for the push and pop events for an async request.
It would take me some time but I could Probably write an example.
Note that this pattern works fine if I don't call as everything stays in the same thread ServletRequest.startAsync()
I don't see any example using the javax.servlet.AsyncContext
public class SentryServletRequestListener implements ServletRequestListener {
@Override
public void requestDestroyed(@NotNull ServletRequestEvent servletRequestEvent) {
hub.popScope();
}
@Override
public void requestInitialized(@NotNull ServletRequestEvent servletRequestEvent) {
hub.pushScope();
...
hub.addBreadcrumb(
Breadcrumb.http(httpRequest.getRequestURI(), httpRequest.getMethod()), hint);
Expected Result
Bread Crumbs don't leak and build up over time.
Actual Result
For an app that does not use async context
Sentry objects are ~ number of threads in the application.
141: 799 134232 io.sentry.Scope
178: 799 83096 io.sentry.protocol.Contexts
201: 797 57384 io.sentry.Hub
234: 799 44744 io.sentry.PropagationContext
286: 799 31960 io.sentry.CircularFifoQueue
287: 799 31960 io.sentry.Stack$StackItem
289: 798 31920 io.sentry.Baggage
322: 799 25568 io.sentry.SynchronizedQueue
325: 797 25504 io.sentry.Stack
326: 797 25504 io.sentry.TracesSampler
If I use an async method Breadcrumb start to accumulate mostly with the
65: 21468 1545696 io.sentry.Breadcrumb
109: 2714 455952 io.sentry.Scope
135: 2714 282256 io.sentry.protocol.Contexts
171: 2713 151928 io.sentry.PropagationContext
194: 2714 108560 io.sentry.CircularFifoQueue
195: 2714 108560 io.sentry.Stack$StackItem
197: 2711 108440 io.sentry.Baggage
232: 2714 86848 io.sentry.SynchronizedQueue
266: 893 64296 io.sentry.Hub
349: 1495 35880 io.sentry.servlet.SentryRequestHttpServletRequestProcessor
The http messages/bread crumbs that accumulate are always the methods that are async.
Hi @jasonculverhouse, Thanks for reaching out!
Unfortunately you are correct, Async servlets are currently not supported in Sentry. We will discuss internally and let you know!
In the meantime, I'm looking at a workaround to provide to you, but it needs some testing. Again, as soon as I have something, I will let you know.
Hi @jasonculverhouse, Sorry for the delay, but I haven't found a viable workaround yet. Just wanted to let you know, that we are still looking into this and haven't forgotten. We'll bump the prio on this and I'll hopefully be able to alot enough time the week after next to get this resolved.
Hi @jasonculverhouse, I finally got around to create and test a workaround for your issue.
You could create a class like this to wrap a runnable:
import io.sentry.IHub;
import io.sentry.Sentry;
public class SentryRunnableWrapper {
public static Runnable wrapRunnable(final Runnable runnable) {
// Clone the current hub
final IHub newHub = Sentry.getCurrentHub().clone();
return () -> {
// Save the current hub of this thread
final IHub oldState = Sentry.getCurrentHub();
// Use the new hub as the current hub in this thread
Sentry.setCurrentHub(newHub);
try {
// Execute the runnable
// Within this runnable, Sentry.getCurrentHub() will return the new hub
runnable.run();
} finally {
// Restore the original hub
Sentry.setCurrentHub(oldState);
}
};
}
}
And use it in your servlet like this:
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
AsyncContext context = req.startAsync();
// Wrap the runnable with SentryRunnableWrapper to clone and restore the hub
context.start(SentryWrapper.wrapRunnable(() -> {
try {
Thread.sleep(1000);
Sentry.addBreadcrumb("Async Thread Breadcrumb");
Sentry.captureMessage("Async Message", SentryLevel.WARNING);
context.getResponse().getWriter().println("Processing Completed");
} catch (InterruptedException | IOException e) {
throw new RuntimeException(e);
} finally {
context.complete();
}
}));
// This is needed to pop the scope that was pushed by the SentryRequestListener.requestInitialized,
// because the SentryRequestListener.requestDestroyed that usually pops the scope might be running
// on a different Thread and thus uses a different hub instance.
Sentry.getCurrentHub().popScope();
}
We are looking into a way to make this easier (at the very least so that you don't have to call Sentry.getCurrentHub().popScope(); in your servlet).
The issue you are seeing stems from this: In the Async Servlet up to three different Threads are used: Thread 1 -> RequestListener.requestInitialized Thread 1 -> doGet() in the servlet Thread 2 -> async work doGet() Thread 3 -> RequestListener.requestDestroyed
Since Sentry's Hub is stored in a Thread local the RequestListeners call to pushScope and popScope are potentially on different threads.
Please also have a look at the newest pre-release of the Java SDK V8 (currently in alpha) in combination with our otel agent which should solve your problem more elegantly. You can find it here.
Using the OpenTelemetry agent of the Java SDK 8.x.x solves this issue when using asyncContext.start(). When a Thread is started manually, the user is responsible for injecting the the correct Scopes into the Thread.
Asynchronous Servlets are not supported by the Sentry SDK without OpenTelemetry.
We're closing this issue, as it is solved using the OpenTelemetry Agent.
If you need this feature in the Java SDK without OpenTelemetry feel free to create a feature request.