François,

On 3/4/25 10:32 AM, François Rajotte wrote:
Hi,

I'm looking for advice on how to properly synchronize asynchronous
servlets that use the Java servlet 3.0 async APIs.

Especially, I'm trying to avoid having the servlet experience
IllegalStateExceptions when accessing HttpServletRequest and
HttpServletResponse objects that tomcat has recycled.

The theory I'm working with is that:
The servlet is accessing the HttpServletRequest and
HttpServletResponse objects from non-container threads. For example,
we can assume that an asynchronous operation has completed on some
thread and we want to send the response.
At any time, tomcat may decide to "complete" the request and recycle
the HttpRequest and HttpResponse objects. For example, the socket
could be disconnected or timed out. Tomcat notifies the servlet that
this is happening by calling the AsyncListener.onComplete method.

The non-container thread may be accessing the HttpServletRequest and
HttpServletResponse objects while Tomcat is about to recycle the
objects. So it seems that some sort of synchronization is required at
the servlet level to avoid accessing recycled objects.
The strategy I have implemented is to acquire a lock at the servlet
level when processing the AsyncListener.onComplete callback and also
when accessing the HttpServletRequest and HttpServletResponse objects
from non-container threads. That way, we can be sure that the
non-container thread will never access recycled objects.

However, I've noticed that implementing synchronization at the servlet
level introduces deadlocks between application locks and internal
tomcat locks.

This is exactly what I'd expect, given the explanation above.

Be advised, I'm not an expert at servlet async. I'm only looking at this from an architectural perspective, here.

First, if Tomcat were still "allowed" to terminate the async request, what would you like the non-container thread to do? Continue? Stop? Something else?

Here are two stack traces taken from tomcat 9.0.97:

Non-container thread calling
HttpServletResponse.getOutputStream().close(), while holding an
application-level lock:
    java.lang.Thread.State: BLOCKED (on object monitor)
     at 
org.apache.coyote.AsyncStateMachine.asyncError(AsyncStateMachine.java:421)
     - waiting to lock <0x00000007fc5feec0> (a
org.apache.coyote.AsyncStateMachine)
     at 
org.apache.coyote.AbstractProcessor.setErrorState(AbstractProcessor.java:121)
     at 
org.apache.coyote.AbstractProcessor.handleIOException(AbstractProcessor.java:665)
     at org.apache.coyote.AbstractProcessor.action(AbstractProcessor.java:388)
     at org.apache.coyote.Response.action(Response.java:207)
     at org.apache.catalina.connector.OutputBuffer.close(OutputBuffer.java:258)
     at 
org.apache.catalina.connector.CoyoteOutputStream.close(CoyoteOutputStream.java:176)

Container thread calling AsyncListener.onComplete(), AsyncListener
implementation trying to acquire the application-level lock:
<application code, stuck trying to acquire the lock held by the
non-container thread>
         at 
org.apache.catalina.core.AsyncListenerWrapper.fireOnComplete(AsyncListenerWrapper.java:39)
         at 
org.apache.catalina.core.AsyncContextImpl.fireOnComplete(AsyncContextImpl.java:106)
         at 
org.apache.coyote.AsyncStateMachine.asyncPostProcess(AsyncStateMachine.java:284)
         - locked <0x00000007fc5feec0> (a org.apache.coyote.AsyncStateMachine)
         at 
org.apache.coyote.AbstractProcessor.asyncPostProcess(AbstractProcessor.java:196)
         at 
org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:83)
         at 
org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:937)
         at 
org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1791)
         at 
org.apache.tomcat.util.net.BwSocketProcessor.doRun(BwSocketProcessor.java:24)
         at 
org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52)

There is a deadlock between the application-level lock and the
AsyncStateMachine object monitor. They are acquired in different order
by two different threads.

What would be the recommendation to avoid this deadlock?
- Don't synchronize at the application/servlet level, and accept that
sometimes an IllegalStateException will be thrown on the non-container
thread? But then, isn't the IllegalStateException a symptom that a
recycled object is being accessed? What if the recycled object is
already being reused for another purpose? Doesn't that risk cross-talk
between requests?

In general, how can we ensure consistency when accessing the
HttpServletRequest and HttpServletResponse from a non-container thread
while running in async mode?

I would say that you should manage "synchronization" (maybe "coordination" since synchronization might imply the use of the synchronized keyword in Java?) by using some kind of helper object that handles events coming from both the Servlet world (e.g. onComplete) and your non-container-thread world (e.g. sendDataBackToClient).

If you have a class that can be shared between the two worlds, an object of that class can be notified that Tomcat is completing the request, and even notify the non-container-thread that its work will not be able to be returned to the client, and it can stop if it can.

That same object will also be able to veto and/or ignore any data sent to it from the non-container thread in a hypothetical sendDataBackToClient method.

Does that make any sense?

-chris


---------------------------------------------------------------------
To unsubscribe, e-mail: users-unsubscr...@tomcat.apache.org
For additional commands, e-mail: users-h...@tomcat.apache.org

Reply via email to