We have several issues relating to TransactionScope documented in Jira, and
there has been, over the years, multiple, sometimes heated, discussions on
this in the mailing lists. The following is an attempt to summarize the
abilities and possibilities.

https://nhibernate.jira.com/issues/?jql=labels%20%3D%20TransactionScope%20AND%20status%20in%20%28Open%2C%20%22In%20Progress%22%2C%20Reopened%2C%20Unconfirmed%29


SUMMARY OF WHAT System.Transactions PROVIDES:
Based on official documentation, blogs, and experiments.

IEnlistmentNotification.Prepare()
    May be called on a thread different from the one calling
Transaction.Dispose().
    Transaction.Dispose() will not return until all enlisted Prepare() have
completed.
    Can be used to enlist more resource managers, but it cannot touch
already
    enlisted resource managers, since those may have already been prepared
and
    could now be locked for further changes in waiting for the second phase.

IEnlistmentNotification.Commit()
    May be called on a thread different from the one calling
Transaction.Dispose().
    Might not run until after Transaction.Dispose() has returned.

IEnlistmentNotification.Rollback()
    May be called on a thread different from the one calling
Transaction.Dispose().
    I suspect it might not run until after Transaction.Dispose() has
returned.
    Will not run at all if our own Prepare() initiated the rollback.

Transaction.TransactionCompleted
    May be called on a thread different from the one calling
Transaction.Dispose().
    Might not run until after Transaction.Dispose() has returned.
    Called for both successful and unsuccessful transactions.
    Is documented as an alternative to a volatile enlistment:
        "You can register for this event instead of using a volatile
          enlistment to get outcome information for transactions." /MSDN
    The same MSDN docs also notes that using this event has a negative
    performance impact.



CURRENT CODE STATE
In NHibernate 3.3 (AdoNetWithDistributedTransactionFactory):

AdoNetWithDistributedTransactionFactory adds a handler for the
TransactionCompleted event and also implements IEnlistmentNotification.

A session opened inside a TransactionScope will automatically enlist. It
will not flush on dispose, but when the TransactionScope is disposed, a
flush will be triggered from the Prepare() phase in
AdoNetWithDistributedTransactionFactory. Surprisingly, this sometimes works.

Depending on in what order things are done, whether or not the transaction
was lightweight or distributed from the start, or if promotion have
occurred etc., the code in Prepare() will sometimes hang for 20 seconds
when trying to flush (waiting on a call to ADO.Net) and then abort
(NH-2238). I believe this is because the ADO.Net connection is also a
resource manager, enlisted with the transaction, and in some circumstances
the transaction manager might have called ADO.Net's Prepare() before
NHibernate's. Further attempts to use that connection then blocks while the
connection is waiting for its Commit() or Rollback() notification.
Generally speaking, I would say that while it is allowed to enlist more
resource managers during the prepare phase, it is _not_ allowed to touch
other already enlisted resource managers, because there is no ordering
guarantee.

The current code is written with the assumption that TransactionCompleted
and Commit()/Rollback() is triggered before Transaction.Dispose() returns.
They call ISessionImplementor.AfterTransactionCompletion() and
CloseSessionFromDistributedTransaction(). This causes race conditions in
the session if the user keeps working with it (NH-2176). It is also a
problem for the ADO.Net connection, since that is also not thread safe and
so while CloseSessionFromDistributedTransaction() is trying to close the
connection, the transaction manager may have used a different thread to
issue the commit phase on the connection.

Since they may in fact be called later, this causes multi-thread issues
with both the ISession and the ADO.Net connection. This is because this
separate thread will call ISessionImplementor.AfterTransactionCompletion()
and CloseSessionFromDistributedTransaction() which will touch the session
(that the application itself may already use for something else (NH-2176))
and also try to close the connection, which might cause connection pool
corruption (NH-3023).


CONCLUSIONS
* It is redundant to both implement IEnlistmentNotification and listen for
TransactionCompleted.

* Flushing changes to the ADO.Net connection in
IEnlistmentNotification.Prepare(), as we currently do, IS SIMPLY NOT
SUPPORTED, because we cannot touch the database connection since that is
itself a resource manager already enlisted.

* We might need to still enlist with the transaction to release second
level cache locks.


CODE SCENARIO 1: SESSIONS INSIDE TRANSACTION

using (transactionscope)
{
    // WITH EXPLICIT NH TRANSACTION.
    using (new session)
    using (session.BeginTransaction)
    {
        DoEntityChanges();

        // This will flush the above changes, but leave the underlying db
        // connection and transaction open.
        tx.Commit();
    }

    // WITHOUT EXPLICIT NH TRANSACTION.
    using (new session)
    {
        DoEntityChanges();

        // This will flush the above changes, but leave the underlying db
        // connection and transaction open.
        session.Flush();
    }

    // Because of the current flush during the prepare phase, this change
might
    // be committed despite being performed outside a transaction. It might
also
    // cause a deadlock. Without this change, the flush-in-prepare will
have nothing
    // to do, and so will not cause a problem.
    entity.AdditionalChange()

    transactionscope.Complete();
}


(Improved) Support for sessions inside TransactionScope is subject to the
following limitations:
* Changes must be flushed explicitly before reaching
TransactionScope.Dispose().

* We need to fix the code so that we also never touch the ADO.Net
connection after
  TransactionScope.Dispose() has been reached. To achieve this, the session
should
  release the connection no later than its own Dispose(). NH-2928. This
would also
  reduce the need for transaction promotion when two non-overlapping
sessions are
  used within the same TransactionScope.

* If desired, it should be possible to have the session auto-flush from its
Dispose()
  when used inside a TransactionScope (NH-2181). Drawback is that the
session
  would behave very differently compared to not using TransactionScope
(increased
  complexity).

* Entity changes performed outside an active transaction would not be
committed,
  just as when not using TransactionScope. Unless the entity is reattached
to a
  new session.



CODE SCENARIO 2: TRANSACTIONS INSIDE SESSION

using (new session)
{
    using (new transactionscope)
    {
        DoEntityChanges();

        transactionscope.Complete();
    }

    // Unknown amount of time before the transaction commit phase is done
with the session.

    using (new transactionscope)
    {
        DoEntityChanges();

        transactionscope.Complete();
    }
}


(Improved) Support for TransactionScope inside sessions is subject to the
following limitations:
* We simply cannot rely on the TransactionScope to tell us when to flush,
so again this can
  only work with explicit flush (either Flush() or Commit() on NH's
transaction).

* To support consecutive TransactionScopes (NH-2176), or really any use of
the session after we have reached
  Dispose() on the first TransactionScope, the application must be able to
reliable detect when the
  out-of-band Commit()/Rollback in IEnlistmentNotification have completed.



COMPARISON WITH OTHER SOFTWARE
It has been claimed that other software "works with System.Transactions
automatically and doesn't require the use of something like NH's
ITransaction". These statements seem to be both correct and wrong. Looking
at eg. Linq2SQL and Entity Framework, it's true that they have no
counterpart to NH's ITransaction. However, they do require explicit calls
to e.g. SaveChanges() before TransactionScope.Complete() and Dispose() is
called. So they too have no automatic flush-on-TransactionScope.Dispose()
behaviour.

Within the context of a TransactionScope, one can perceive the
ITransaction.Commit() as one possibly way to trigger the explicit flush of
changes. The other one is of course session.Flush(). So it's really not
that different.



IDEAS FOR PROPOSED SOLUTION
* Use only IEnlistmentNotification, not TransactionCompleted.

* Remove the flushing from IEnlistmentNotification.Prepare(). The
application must flush earlier or loose the changes.

* If we cannot move all code from the Commit/Rollback/TransactionCompleted
stage into Prepare(),
  we must instead grab some sort of lock on the session at the start of
Prepare(). The lock would be
  released at the end of the Commit or Rollback phase. In the meantime, all
other attempts to use
  the session should block waiting for the lock to be released. (I suspect
this is what happens when use of
  the ADO.Net connection blocks when flushing from Prepare().)

* Document that IInterceptor.AfterTransactionCompletion() and similar may
be called
  asynchronously on a different thread.

* Optionally, we can also add automatic flush in the session's Dispose()
method if an ambient
  transaction is detected.


I'm still a bit unclear on the interaction with the second level cache.


/Oskar

-- 

--- 
You received this message because you are subscribed to the Google Groups 
"nhibernate-development" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to nhibernate-development+unsubscr...@googlegroups.com.
For more options, visit https://groups.google.com/groups/opt_out.

Reply via email to