Synchronizing access to CDI Conversations

If you are using CDI’s conversations you will most likely have come across the dreaded BusyConversationException. The CDI 1.0 specification states that the container must ensure that only one request at a time may be associated with the conversation by blocking or rejecting subsequent requests.

OpenWebBeans will immediately reassign and throw a BusyConversationException if the conversation is currently held by another request. Weld on the other hand uses a 1 second timeout to obtain a lock on the conversation before reassigning the request and throwing a BusyConversationException. Although this makes it less likely to occur, if you are dealing with multiple waiting requests or long running processes, you will still encounter them.

My previous efforts concentrated on controlling the requests on the client side (see my earlier posts on JSF 2 AJAX/Submit issues with @ConversationScoped Beans and Keeping focus on element with JSF 2 AJAX render). You should always try to synchronize your asynchronous requests (no pun intended) on the client, since there is no guarantee in which order they will arrive at the server, let alone processed. Still, you may wish to make your application that bit more stable by adding that extra server sided safety net. Essentially we will have to synchronize access to conversations, by queuing incoming requests until the prior request has finished processing. Just like Weld does but without the timeout.

Originally I wanted to accomplish this with a JSF PhaseListener, however at least in the case of OpenWebBeans it was not possible to force the execution of another PhaseListener prior to the WebBeansPhaseListener. As specified, the conversation is assigned in the RESTORE_VIEW phase of the JSF life cycle. Since this is the first phase, the only way to influence the execution order is to define it explicitly in the faces-config.xml. This however triggers a double invocation of the WebBeansPhaseListener causing a BusyConversationException on every call.

A ServletRequestListener is the perfect solution for this problem. Its requestInitalized method is called when the request is about to enter the application context (prior to any filters) and its requestDestroyed method when the thread is about to leave the application context. This allows me to control incoming requests and keep track of conversation processing state.

Sadly the CDI 1.0 specification does not offer any standard way to retrieve conversation state information.
This leaves two possible solutions which are to access the CDI implementation specific internals or keep track of conversation states separately. Since I prefer an application server independent generic solution, I opted for the latter.

[java]
/**
* Chuck Norris can handle requests to busy conversations.
*/
@WebListener
public class ChuckNorrisConversationWebListener
implements ServletRequestListener, Serializable {

@Inject
private ConversationLocks conversationLocks;

@Override
public void requestDestroyed(ServletRequestEvent event) {
if (hasConversationContext(event)) {
conversationLocks.get(getConversationId(event)).unlock();
}
}

@Override
public void requestInitialized(ServletRequestEvent event) {
if (hasConversationContext(event)) {
obtainConvesationLock(event);
}
}

private void obtainConvesationLock(ServletRequestEvent event) {
conversationLocks.get(getConversationId(event)).lock();
}

private boolean hasConversationContext(ServletRequestEvent event) {
return StringUtils.isNoneEmpty(getConversationId(event));
}

private String getConversationId(ServletRequestEvent event) {
return event.getServletRequest().getParameter("cid");
}
}
[/java]

Every request that is associated with a conversation must provide the conversations cid as a parameter. We use this to keep track of conversation state by assigning a Lock to every cid.
When a request is aimed at a conversation, it must obtain a lock on the conversation. If the lock is currently held by a prior request the thread will wait until it is notified. The notification is triggered by the returning thread releasing the lock when exiting through the requestDestroyed method. Please be aware that Java Locks do not offer any guarantees on execution order of waiting threads.

We would create a memory leak, if we were to use a static map inside the listener to keep track of conversation states in the listener.

The conversation locks could be held in a static map inside the listener, using a combined key of sessionId and cid. However this would mean that the conversations would be tracked indefinitely, creating a memory leak.

[java]
@SessionScoped
public class ConversationLocks
implements Serializable {

private Map<String, Lock> conversationLocks = new ConcurrentHashMap<>();

public Lock get(String cid) {
if (conversationLocks.containsKey(cid)) {
return conversationLocks.get(cid);
} else {
return initConversationLock(cid);
}
}

private synchronized Lock initConversationLock(String key) {
if (conversationLocks.containsKey(key)) {
return conversationLocks.get(key);
} else {
ReentrantLock lock = new ReentrantLock();
conversationLocks.put(key, lock);
return lock;
}
}
}
[/java]

The issue can be avoided, by keeping the conversation locks in session scope. The container will cleanup the session context automatically when the session is invalidated.

Originally I tried to keep the request listener in session scope, however some implementations don’t handle this as expected. Extracting the conversational locks to a @SessionScoped bean solves this problem.

Further details regarding Java concurrency and Servlet Listeners are beyond the scope of this post, for more details see Oracle Java Concurrency tutorial and Servlet 3.0 Specification.

7 thoughts on “Synchronizing access to CDI Conversations”

  1. Nice solution, thanks a lot.

    But, for some reason, it’s just working when I put a breaking point at this class. Without the breaking point I a still get conversation lock timeout

    1. I am guessing that you are using Weld, since you have mentioned a conversation lock timeout.
      My best guess is that the container hasn’t resolved the CDI session context yet, which application server are you using?

      If you are using Weld on Tomcat or Jetty instead of a full JEE compliant application server you might be getting this issue because the annotated @WebListener gets executed before the Weld listener. If that is the case you should get rid of the annotation @WebListener and specifiy the ChuckNorrisConversationWebListener explicitly in the web.xml after the Weld listener.

  2. Nice post!

    This and quite a few other shortcomings was the reason why Gerhard Petracek and I did came up with an alternative Conversation approach in Apache MyFaces CODI. In the meantime we also ported this over to Apache DeltaSpike.
    http://deltaspike.apache.org/documentation/jsf.html

    We not only support straight conversations but als 1-per-browser tab scopes (@WindowScoped) and ‘auto-conversations’ (@ViewAccessScoped). The later is pretty popular as it is really easy to use. The bean basically survives as long as you use it on a page. If you navigate away from it and don’t use the bean on a page then it will get destroyed. All this is tracked per browser tab of course 🙂

  3. Thank you! This solved my busyconversationLocks with my tomcat6 jboss.weld-servlet 2.1.2.Final branch.

    Is this still needed in tomcat7/tomcat8 with jboss.weld-servlet 2.4.1.Final?

    Thanks again.

  4. I implemented this solution in my web app based on Spring MVC framework, and it seemed to work until performance testing showed that ConversationLocks object (even though it was @SessionScoped) was being shared across sessions, causing significant degradation.

    The problem was that I had imported the wrong annotation @javax.faces.bean.SessionScoped in ConversationLocks. The correct one is @javax.enterprise.context.SessionScoped .
    Took me quite some time to figure out this subtle issue. So something important to keep in mind in case someone is using Spring MVC.

  5. We applied this implementation to our system and faced too many problem with the following logic.

    event.getServletRequest().getParameter(“cid”)

    After called HttpServletRequest.getParameter(“cid”), multipart request will be parsed wrong in Apache Common File Upload (1.3.1)
    Workaround here is trying to get cid from HttpServletRequest.getRequestURI() instead.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.