Question

I have a session scoped class that contains an object to manage user statistics. When a user logs in(through SSO) an application scoped method checks the table for active sessions - if any are found the session is invalidated using the session id in the table.

A row is added to a userStats table in the session scoped class:

/**
     * get all the info needed for collecting user stats, add userStats to the users session and save to userStats table
     * this happens after session is created
     * @param request
     */
    private void createUserStats(HttpServletRequest request){
        if (!this.sessionExists) {
            this.userStats = new UserStats(this.user, request.getSession(true)
                    .getId(), System.getProperty("atcots_host_name"));
            request.getSession().setAttribute("userstats", this.userStats);
            Events.instance().raiseEvent("userBoundToSession", this.userStats);
            this.sessionExists = true;
            log.info("user " + this.user + " is now logged on");

            // add this to the db
            try {
                this.saveUserStatsToDb();
            } catch (Exception e) {
                log.error("could not save " + this.userStats.getId().getPeoplesoftId() + " information to db");
                e.printStackTrace();
            }           

        }

    }

When the user's session is destroyed this row is updated with a log off time.

For reasons I can't explain nor duplicate 2 users in the last 2 weeks have logged in and locked the row. When that happens any database calls by that user are no longer possible and the application is effectively unusable for this user.

 [org.hibernate.util.JDBCExceptionReporter] (http-127.0.0.1-8180-3) SQL Error: 0, SQLState: null
2012-07-26 18:45:53,427 ERROR [org.hibernate.util.JDBCExceptionReporter] (http-127.0.0.1-8180-3) Transaction is not active: tx=TransactionImple < ac, BasicAction: -75805e7d:3300:5011c807:6a status: ActionStatus.ABORT_ONLY >; - nested throwable: (javax.resource.ResourceException: Transaction is not active: tx=TransactionImple < ac, BasicAction: -75805e7d:3300:5011c807:6a status: ActionStatus.ABORT_ONLY >)

The gathering of these stats is important, but not life and death, if I can't get the information I'd like to give up and keep it moving. But that's not happening. What is happening is that the entityManager is marking the transaction for rollback and any db call after that returns the above error. I originally saved the users stats at the application scope - so when the row locked it locked the entityManager for the ENTIRE APPLICATION (this did not go over well). When I moved the method to session scope it only locks out the offending user.

I tried setting the entityManger to a lesser scope(I tried EVENT and METHOD):
        ((EntityManager) Component.getInstance("entityManager", ScopeType.EVENT)).persist(this.userStats);
        ((EntityManager) Component.getInstance("entityManager", ScopeType.EVENT)).flush();

This doesn't make db calls at all. I've tried manually rolling back the transaction, but no joy.

When I lock a row in a table that has data that is used at the conversation scope level the results are not nearly as catastrophic - no data is saved but it recovers.

ETA:

I tried raising an AsynchronousEvent - that works locally, but deployed to our remote test server - and this is odd - I get:

    DEBUG [org.quartz.core.JobRunShell] (qtz_Worker-1) Calling execute on job DEFAULT.2d0badb3:139030aec6e:-7f34
    INFO  [com.mypkg.myapp.criteria.SessionCriteria] (qtz_Worker-1) observing predestroy for seam
    DEBUG [com.mypkg.myapp.criteria.SessionCriteria] (qtz_Worker-1) destroy destroy destroy sessionCriteria
    ERROR [org.jboss.seam.async.AsynchronousExceptionHandler] (qtz_Worker-1) Exeception thrown whilst executing asynchronous call
    java.lang.IllegalArgumentException: attempt to create create event with null entity
            at org.hibernate.event.PersistEvent.<init>(PersistEvent.java:45)
            at org.hibernate.event.PersistEvent.<init>(PersistEvent.java:38)
            at org.hibernate.impl.SessionImpl.persist(SessionImpl.java:619)
            at org.hibernate.impl.SessionImpl.persist(SessionImpl.java:623)
...

The odd bit is that it appears to be going through the Quartz handler.

ETA again:

So, not so odd, I had set Quartz as the async handler - I thought it was only for scheduling jobs. Also asynchronous methods don't have access to the session context, so I had to add a parameter to my observing method to actually have an object to persist:

@Observer("saveUserStatsEvent")
@Transactional
public void saveUserStatsToDb(UserStats userstats) throws Exception {
    if(userstats != null){
        log.debug("persisting userstats to db");
        this.getEntityManager().persist(userstats);
        this.getEntityManager().flush();
    }
}

How do I recover from this?

Was it helpful?

Solution

First of all, specifying a scope in Component.getInstance() does not have the result of creating the component in the scope specified. EntityManager instances always live in the conversation context (be it temporary or long-running). The scope parameter of getInstance() serves the sole purpose of hinting the context in which the component should be, in order to avoid an expensive search in all contexts (which is what happens if you don't specify a context or specify the wrong context).

The transaction is being marked for rollback because of the previous error. If the entityManager were to commit regardless, it would not be transactional (a transaction in fact guarantees that if an error happens nothing is persisted). If you want to isolate the login transaction from stats gathering, the simplest solution is to perform the saveUserStatsToDb method inside an asynchronous event (transactions are bound to the thread, so using a different thread guarantees that the event is handled in a separate transaction).

Something like this:

@Observer("saveUserStatsEvent")
@Transactional
public void saveUserStatsToDb(UserStats stats) {
    ((EntityManager)Component.getInstance("entityManager")).persist(stats);
}

And in your createUserStats method:

Events.instance().raiseAsynchronousEvent("saveUserStatsEvent", this.userStats);

However, this just circumvents the problem by dividing the transactions in two. What you really want to solve is the locking condition at the base of the problem.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top