Understanding nested transaction scopes

The TransactionScope object is central to the System.Transactions model.  At its simplest, it provides a very easy mechanism for demarcating transactions in the application -- i.e.

 

using (TransactionScope s = new TransactionScope ())

{

s.Complete ();

}

 

However, hidden in that is the use of TransactionScopeOption, and the ability to nest TransactionScope regions.  This is one area that I've gotten a number of questions on, including the one that showed up in the Suggestion Box.

 

Often the first question is whether or not these are 'nested transactions', in which partial areas of the overall transaction may be rolled back, updated isolated between concurrent subtransactions, and still result in a single commit point for the overall transaction.  No, nested TransactionScopes in .Net 2.0 do not provide that.

 

That then leads into questions about what nesting TransactionScopes mean.  When we thought about this mechanism, we realized that there are two questions that an application developer has at the point that they would be creating a TransactionScope:

 

  • How does the contained code handle errors: does it require a transaction rollback, does it assume that it does the compensation itself?
  • How does the contained code relate to its caller: can it reasonably participate in a caller's transaction, or does it require its own?

 

This converts into the three TransactionScopeOption settings:

 

  • Required: the contained code depends on a transaction for atomicity, isolation, and compensation, and could be part of a broader transaction.
  • RequiresNew: the contained code depends on a transaction, but must be independently committed or rolled back.
  • Suppress: the contained code performs its own compensation, so must not be part of a transaction.

 

The first setting is the default, under the assumption that if you are creating a TransactionScope, you probably want a transaction.  In that case, the normal situation is one where the operation you're about to do can be reasonably integrated into a transaction that already active.

 

The second setting is for cases where the contained code block does require a transaction for its consistency, and provides a feature that demands that it be separate from any transaction that might already be active.  One typical example would be a function that provides activity logging to, say, a database.  It may be implemented such that it required a transaction to provide consistency, but it couldn't accept an outer rollback to undo the record of the attempted activity.

 

The final setting, Suppress, handles case where the contained code needs to be sure that it is not executing as part of any transaction.  This is fairly uncommon for a local operations -- the only real case for this would be if the contained code was designed to handle its own compensation, yet used recoverable resources, such as SQL, to do its actions.

 

On the other hand, the one case I can see it normally being useful is if the code is calling a remote operation, either through COM+ or Indigo, that accepts an incoming transaction.

 

In that case, the caller could either suppress the current transaction, or create a new one. The latter potentially has a much different performance profile, since the new transaction would still be a distributed one.  The way to avoid that performance overhead, and retain the  feature that the called remote operation executed outside the current transaction, would be to suppress any transaction around the outbound call.

 

 

While this helps explain the TransactionScope creation options, the other big question is what does Complete mean?  When we show a standalone example, it is easy to confuse Complete with Commit -- after all, since the TransactionScope instance is the only one involved, the transaction does commit when it is disposed.

 

However, the reality is slightly more subtle.  Complete means just what the name suggests -- that the code within the TransactionScope has completed (successfully) all the operations it intended to do as part of the transaction.  Note that this is also why you can't call Complete twice.  To do so would mean that at least one of the two calls wasn't the last intended operation.

 

When a TransactionScope is disposed it first looks to see if it was completed successfully.  If it was not, the transaction is immediately rolled back.  If it was, and this was the TransactionScope that created the transaction in the first place, the transaction is committed.  Finally, in both cases, the current transaction is replaced with the value that was current when the TransactionScope was created.

 

Thus we have, for success:

 

// no transaction is active here

using (TransactionScope s = new TransactionScope ())

{

    // transaction 'a' is active here

    using (TransactionScope t = new TransactionScope
                (TransactionScopeOption.RequiresNew))

    {

        // transaction 'b' is active here

 

        t.Complete ();

    } // the transaction from 's' is put into Current.

 

    // transaction 'a' is active here

    using (TransactionScope u = new TransactionScope ())

    {

        // transaction 'a' is active here

        u.Complete ();

    } // the transaction from 's' is put into Current.

 

    // transaction 'a' is active here

    s.Complete ();

}

// 'a' commits at this point

// no transaction is active here

 

For a failure case we have:

 

// no transaction is active here

using (TransactionScope s = new TransactionScope ())

{

    // transaction 'a' is active here

    using (TransactionScope t = new TransactionScope
                  TransactionScopeOption.RequiresNew))

    {

        // transaction 'b' is active here

 

        t.Complete ();

    }

 

    // transaction 'a' is active here

    using (TransactionScope u = new TransactionScope ())

    {

        // transaction 'a' is active here

        //u.Complete ();

    } // 'a' rolls back here,

      //the transaction from 's' is put into Current.

 

    // transaction 'a' is current, but aborted here

    s.Complete ();

}  // 'a' is already aborted, so is just removed at this point

// no transaction is active here

 

 

In both cases, TransactionScope t is for a completely different transaction from scopes s & u.  In the first case, the transaction behind scope u does not commit until its originating scope (scope s) completes.

 

In the second case scope u ends without issuing a Complete, so it is aborted at that point. However, when scope s is restored as the current scope, the aborted transaction is made current.  This ensures that any later activity in scope s is attempted under the correct transaction.

 


Posted Jun 18 2005, 09:14 PM by jim-johnson

Comments

Christopher Steen wrote Link Listing - June 19, 2005
on 06-19-2005 10:35 PM
Link Listing - June 19, 2005
Bobby Marinov wrote re: Understanding nested transaction scopes
on 06-21-2005 2:01 PM
The naming of the transactions in the code and comments is confusing. What is the correspondence between transaction a as specified in the comments and the name of actual the transaction instances, e.g. s?
Jim Johnson wrote re: Understanding nested transaction scopes
on 06-21-2005 2:14 PM
I think part of the confusion may arise because a TransactionScope is transacted region, rather than a transaction. This isn't overly obvious for non-nested cases, since a transaction scope that first created the transaction implicitly commits the transaction as part of completing that transaction scope. However, in the nested case the difference between the transaction lifetime and nested transaction scope lifetimes becomes much more apparent.

So, in the examples, transaction 'a' is created as part of creating TransactionScope 's'. It is suspended by the creation of TransactionScope 't' (which creates transaction 'b'). When 't' ends, transaction 'b' is committed, and TransactionScope 's' is restored, which makes transaction 'a' active again as a consequence.

When TransactionScope 'u' is created, transaction 'a' remains active because 'u' is also using it. When 'u' ends, either successfully or unsuccessfully, transaction scope 's' is restored, which again makes transaction 'a' active as a consequence -- regardless of whether or not 'a' has been aborted.

Finally, when 's', which originally created transaction 'a' completes, transaction 'a' is now no longer active -- and, in fact, will have either committed or aborted.
edwwin wrote nested transavtion model
on 06-30-2005 5:08 AM
this site is help ful
Tecnologie .NET (Dotnet) wrote Transazioni in .NET 2.0
on 09-12-2005 3:55 PM
Sanjay wrote re: Understanding nested transaction scopes
on 10-18-2005 7:48 AM
Trying this ODP and Beta 2 and complete doesn't matter, transaction is saved. Any idea what is sticky here?
Jim Johnson wrote re: Understanding nested transaction scopes
on 10-18-2005 8:40 AM
Sanjay,

I'm not sure what you're doing in your test. Can you send me your source example via mail and I'll take a look at it. That's probably the best way to figure it out.

Jim.
Rosario Carbone wrote re: Understanding nested transaction scopes
on 01-12-2006 7:18 AM
Hi Jim. Very useful this page.
But I am quite confused on the second example.
Do you mean that because the transaction from 'u' which I would call 'c' has the u.Complete (); line commented and then rolled back automatically the transaction from 's' (the external one) transaction 'a' is rolled back as well? This means that the s.Complete (); is never executed? Or that means that altough it is executed the transaction 'a' (from s) is not commited?
As far as I understood transaction 'a' (from scope 's') and 'c' (from scope 'u') will either committed (first example) or aborted (second example).
Is that correct?

Thanks

Rosario

Jim Johnson wrote re: Understanding nested transaction scopes
on 01-13-2006 10:00 AM
Rosario,

The key to the second example is that TransactionScope 'u' does not create a new transaction. It inherits and uses transaction 'a' from the outer TransactionScope (TransactionScope 's').

That means that when TransactionScope 'u' exits its using block without calling Complete, transaction 'a' is aborted (as this is the transaction that TransactionScope 'u' was handling).

Does this help?
Jim.
Rosario wrote re: Understanding nested transaction scopes
on 01-27-2006 5:30 AM
Jim,

OK thank you. I understand now what happen in the second example. But what happen in the first example if the transaction 'b' (scope 't') is not completed (aborted)? I think, as far as I understood, that because it is RequiresNew which means that the transaction independently is committed or rolled back this has no effect on the external transaction 'a', is that correct?
This page would be more complete if you explained what happens to the transaction 'c' scope 'u' if the transaction 'a' scope 's' is rolled back and also if only the transaction 'b' scope 't' is rolled back, although I think this quite obvious.

Rosario
Aown Muhammad wrote re: Understanding nested transaction scopes
on 07-14-2006 10:35 PM
Thanks for writing this helpful article
chris wrote re: Understanding nested transaction scopes
on 09-05-2007 11:56 PM
I will make a misunderstand that in the sencond example:Does it mean the transaction 'a' will abort,and the transaction 'b' (transactionscope 'u') will commit ?
chris wrote re: Understanding nested transaction scopes
on 09-06-2007 2:15 AM
I know,thanks for Jim's article,it's very helpful
peter wrote re: Understanding nested transaction scopes
on 01-09-2009 12:36 AM

Hi, the acticle is much helpful. Thanks.

But I am still confused with the second example.

will transaction 'b'(transactionscope 'u') be commited successfully?

jim-johnson wrote re: Understanding nested transaction scopes
on 01-09-2009 9:19 AM

In the second example transaction 'b' is created by TransactionScope 't', and committed at the end of that TransactionScope.  

TransactionScopes  's' & 'u' operate on transaction 'a', which aborts at the end of TransactionScope 'u'.

Jim.

Rick O'Shay wrote re: Understanding nested transaction scopes
on 04-14-2009 9:59 AM

Did you "think about" and "realize" or simply regurgitate the transaction scopes that have been around for decades, and prominently so within Java and later .NET? Corollary: skip the example if you aren't willing to bother proof reading your sloppy, confusing comments vis-a-vis names of transactions. Jeezuz.

jim-johnson wrote re: Understanding nested transaction scopes
on 05-01-2009 8:31 AM

Rick,  I assume that you're referring to the declarative transaction attributes that originated in MTS and later moved to Java and .Net?  Yes, I'm well aware of them, and have been for years.  The MTS team certainly introduced a much better assembly construct than transaction monitors had had previously.

However, while you spotted that there's a relationship to declarative transactions, I think you may have missed a couple of notable differences between those features and the TransactionScope class.

First, TransactionScope is about bringing declarative transaction expressiveness to any imperative .Net code.  If you look at MTS, .Net, or Java, the declarative transaction support exists outside of the imperative code.  It is a function of the container, and can only be expressed at entry points into the container.  If a programmer just wants to demarcate some imperative code they have to use a different model (e.g. MSDTC, XA, or some variant thereof).  And those models do not support the composition characteristics found in declarative transactions.

Second, the declarative transactions found in MTS, Java, and .Net do not represent the semantics that I called out in my post.  Specifically, they do not provide the first one ("How does the contained code handle errors: does it require a transaction rollback, does it assume that it does the compensation itself?").  This is a structural limitation --the attribution is available to the system assembler, which means that the attribution does not allow a developer to securely state the atomicity execution requirements of the code.  Instead, it allows the system assembler to state the atomicity environment that they will provide to the code.

[I do realize that in, e.g., JEE 5 you can use attributes placed in the source, but use of these just reverses the problem -- that you _do_ want the system assembler to be able to specify whether a transaction can flow into an exported interface, and just putting it all in the code removes that ability.

This, btw, is why the WCF attribution does not look like that found in MTS, Java, or .Net.  The attribution available to the system assembler is about flow, the attribution reserved solely to the developer is about the atomicity requirements of the code.  But I digress.]

Hope this helps,

Jim.

Add a Comment

(required)  
(optional)
(required)  
Remember Me?