Question

I'm getting a pretty bizarre error using NHibernate. When I make a call to open session, I'm getting this error and stack trace.

The operation is not valid for the state of the transaction.-at System.Transactions.TransactionState.EnlistVolatile(InternalTransaction tx, IEnlistmentNotification enlistmentNotification, EnlistmentOptions enlistmentOptions, Transaction atomicTransaction) 
at System.Transactions.Transaction.EnlistVolatile(IEnlistmentNotification enlistmentNotification, EnlistmentOptions enlistmentOptions) 
at NHibernate.Transaction.AdoNetWithDistributedTransactionFactory.EnlistInDistributedTransactionIfNeeded(ISessionImplementor session) 
at NHibernate.Impl.AbstractSessionImpl.CheckAndUpdateSessionStatus() 
at NHibernate.Impl.SessionImpl..ctor(IDbConnection connection, SessionFactoryImpl factory, Boolean autoclose, Int64 timestamp, IInterceptor interceptor, EntityMode entityMode, Boolean flushBeforeCompletionEnabled, Boolean autoCloseSessionEnabled, ConnectionReleaseMode connectionReleaseMode) 
at NHibernate.Impl.SessionFactoryImpl.OpenSession(IDbConnection connection, Boolean autoClose, Int64 timestamp, IInterceptor sessionLocalInterceptor) 
at NHibernate.Impl.SessionFactoryImpl.OpenSession(IInterceptor sessionLocalInterceptor)

This is only happening at one place in my application, and even then not consistently. In specific, this is code running inside a SharePoint application. SharePoint fires off my code whenever it receives email at a specific address. I only bring this up because to note that as a result, that each time the code is called its running on a separate thread, and there are no existing NHibernate transactions or sessions on that thread.

I cracked open the NHibernate source code and looked at the method throwing the error. As noted in the stack trace, it's the "EnlistInDistributedTransactionIfNeeded" method. This is the code for that method

if (session.TransactionContext != null)
            return;

        if (System.Transactions.Transaction.Current == null)
            return;

        var transactionContext = new DistributedTransactionContext(session,
                                                                   System.Transactions.Transaction.Current);
        session.TransactionContext = transactionContext;
        logger.DebugFormat("enlisted into DTC transaction: {0}",
                           transactionContext.AmbientTransation.IsolationLevel);
        session.AfterTransactionBegin(null);
        transactionContext.AmbientTransation.TransactionCompleted +=
            delegate(object sender, TransactionEventArgs e)
                {
                    using (new SessionIdLoggingContext(session.SessionId))
                    {
                        ((DistributedTransactionContext)session.TransactionContext).IsInActiveTransaction = false;

                        bool wasSuccessful = false;
                        try
                        {
                            wasSuccessful = e.Transaction.TransactionInformation.Status
                                            == TransactionStatus.Committed;
                        }
                        catch (ObjectDisposedException ode)
                        {
                            logger.Warn("Completed transaction was disposed, assuming transaction rollback", ode);
                        }
                        session.AfterTransactionCompletion(wasSuccessful, null);
                        if (transactionContext.ShouldCloseSessionOnDistributedTransactionCompleted)
                        {
                            session.CloseSessionFromDistributedTransaction();
                        }
                        session.TransactionContext = null;
                    }
                };
        transactionContext.AmbientTransation.EnlistVolatile(transactionContext,
                                                            EnlistmentOptions.EnlistDuringPrepareRequired);

As you can see this method only really does anything if System.Transactions.Transaction.Current is not null. In my case, I can't understand any reason why it wouldn't be null, since the method trying to open the session doesn't open any other sessions or transactions, but I am no expert on distributed transactions.

A few other details that might be relevant

  1. My session factory is managed by a static object that exists for the lifetime of the webapplication/service.
  2. The sharepoint process that invokes my method is a windows service called OWSTimer. From what I understand (but haven't confirmed) it generates a separate thread for each incoming email, and then invokes my code on that thread.
  3. I have no need for distributed transactions, so if I can force NHibernate never to enlist my sessions in a distributed transaction, that's fine too.
  4. I have a number of event listeners that do interact with the session. In all cases I'm getting a session instance in the event listener with a call to var session = @event.Session.GetSession(EntityMode.Poco); Per the documentation I do not close these instances of the session. However, in a few cases I do call flush, simply because I missed that part of the documentation.

UPDATE: Here's the static method that handles creating a session and is called by my other code.

public static ISession CreateAuditableSession(string siteUrl, ISharePointDataContext context)
    {
        var factory = Instance(siteUrl);
        var session = factory.OpenSession();            
        var imp = session.GetSessionImplementation();

        imp.Listeners.PreUpdateEventListeners = new IPreUpdateEventListener[] { new AuditUpdateListener(context) };
        imp.Listeners.PostInsertEventListeners = new IPostInsertEventListener[] { new AuditUpdateListener(context) };
        imp.Listeners.FlushEventListeners = new IFlushEventListener[] { new FixedDefaultFlushEventListener() };
        imp.Listeners.PreInsertEventListeners = new IPreInsertEventListener[] { new PGUserDisplayNameRetrieverListener(context) };
        imp.Listeners.PreDeleteEventListeners = new IPreDeleteEventListener[] { new AuditUpdateListener(context) };
        imp.Listeners.PostCollectionUpdateEventListeners = new IPostCollectionUpdateEventListener[] { new AuditUpdateListener(context), new SupplierChangeEventListener() };
        imp.Listeners.PostCollectionRecreateEventListeners = new IPostCollectionRecreateEventListener[] { new AuditUpdateListener(context), new SupplierChangeEventListener() };
        imp.Listeners.PostLoadEventListeners = new IPostLoadEventListener[] { new PostLoadSubscriptionAndInjectionEventListener(context) };

        return session;       
    }
Was it helpful?

Solution

I think I've identified a workaround and have a theory on cause. As far as I can tell, this error only happens if the property System.Transactions.Transaction.Current is not null and the Current Transaction is aborted. Looking through Nhibernate's code, there's nothing that interactions with System.Transactions.Transaction.Current, or the TransactionScope class in such a way that any of my code could be causing Nhibernate to create this scenario. My own code also does not use System.Transactions directly in anyway, so it's unlikely anything I am doing could result in a leaking aborted transaction.

However, after testing I discovered that most OWSTimer code related to email processing seem to run on a single thread. As a result I suspect any other custom code related to processing incoming emails deployed in our environment is running on the same thread as my code. It's possible that a bug in some other component is leaking this transaction and screwing up subsequent calls to NHibernate.

After speaking with our production admin's I found that about the time this issue started we upgraded a third party component (Newsgator) that does alot with incoming emails. As a result I'm thinking that there may be a bug on their end, resulting in a leaking transaction.

To defend against it, I'm modifying my session management code to check if System.Transactions.Transaction.Current contains and aborted transaction before opening a new session. If it does, then I'm disposing and nulling that transaction out myself.

OTHER TIPS

This is either something strange in your code causing this situation, a bug in NHibernate (so that it can't handle some Sharepoint weirdness), or a bug in Sharepoint.

In either case, disabling this code feels like a workaround rather than fixing the true problem. It is however possible to do this. Look in the NHibernate source code, there is another transaction factory you can use. Look in NHibernate.Cfg.Environment, and you will find the configuration parameter to set it up.

(About to run out, so cannot look up the details right now.)

I have managed to solve this exact same scenario with NHibernate.ISessionFactory.Evict(System.Type persistentClass, object id);

Note that Session.Dispose() alone (without the above Evict call) didn't help, and therefore the more drastic approach of NHibernate.ISessionFactory.Evict is used and happily helped me.

Hope it could help others.

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