sentry-java icon indicating copy to clipboard operation
sentry-java copied to clipboard

SentryServletRequestListener and Async requests with Tomcat

Open jasonculverhouse opened this issue 1 year ago • 3 comments

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.

image

jasonculverhouse avatar Jul 11 '24 19:07 jasonculverhouse

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.

lbloder avatar Jul 16 '24 13:07 lbloder

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.

lbloder avatar Aug 23 '24 14:08 lbloder

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.

lbloder avatar Sep 17 '24 13:09 lbloder

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.

lbloder avatar Mar 10 '25 14:03 lbloder