System.Transactions: Implement Your Own Resource Manager

One of the biggest paradigm shifts that has occurred with the introduction of .NET 2.0 probably is the introduction of System.Transactions. I had talked briefly about System.Transactions in my previous article about SQLCLR, but why should transactions be limited to databases? Don't we need reliable code outside of databases as well? Sure we do! And that's why you cannot afford to ignore System.Transactions and the tsunami of changes it will unleash in future generations of Microsoft platforms. Your first step is to learn how System.Transactions works.

How Does System.Transactions Work?

System.Transactions offers you an easy and straightforward way to implement your operations in a transactional manner. You can use it to implement a transaction in a number of ways, but a typical implementation is to wrap transactional operations inside a TransactionScope (the variable instance you use to make a block of code transactional) as follows:

using (TransactionScope ts = new TransactionScope())
{
   // Transactional code comes here.
   ts.Complete();
}

In the space commented, "Transactional code comes here," you would use classes that hold or implement resource managers. Transactions work on resources, and each such resource is managed by a resource manager (RM). Resource managers work in cooperation with other entities, typically other processes or services called transaction managers (TMs).

A common way an RM can work in cooperation with a TM within a transaction is what is commonly referred to as a two-phase commit process. The following is the typical flow of a two-phase commit:

  1. An RM enlists in the transaction.
  2. The RM does its part, and signals the end of Phase 1 to the TM. This phase can also be referred to as the prepare phase.
  3. The TM gives the green signal to all RMs after they all have executed the prepare phase successfully.
  4. The RMs get the green signal and actually commit their work. In the event of a red signal, they roll back their work. This is the second phase, or the commit phase.
  5. In either case, the transaction coordinator coordinates with all the RMs to ensure that they all either succeed and do the requested work, or they all roll back their work together.

This entire process is provided by the System.Transactions framework, which leverages TMs such as MSDTC (Microsoft Distributed Transactions Coordinator). To use System.Transactions to manage resources participating in transactional operations, you need to either use an already existing RM or write your own.

Because the framework of System.Transactions is available but very few actual RMs are available right now, many developers likely will find it useful to write their very own RMs and use them in conjunction with the hordes of RMs that will appear in future releases of the .NET Framework or in third-party offerings. One rather common RM that does ship with .NET 2.0 is the SqlConnection class, which the example in this article will use in conjunction with a homegrown RM within a single transaction. But if you had to write your own RM, the first important question to ask is, "What does your RM do?"

What Does Your RM Do?

A critical question affects the general implementation of your RM: Does it manage a resource that is durable or volatile in nature?

In a typical two phase commit process, a durable resource refers to a resource that would require failure recovery. A good example would be transactional file copy. If you had to implement an RM that encapsulated the process of copying a file into a transaction, the file could actually be copied in Phase 1. In the event that the RM goes down, its recovery contract ensures that in the event of a rollback, the TM has enough information to restore the original state. A rollback in transactional file copy could be to delete the file or replace it with the pre-existing file. Enlisting durably in such a transaction is done by the System.Transaction.Transaction.EnlistDurable method.

Conversely, a volatile resource is one that doesn't require recovery. A good example is in-memory data structures. A RM that encapsulates such an operation could enlist in the current running transaction in a volatile manner by using the System.Transaction.Transaction.EnlistVolatile method.

Yet another method of enlistment is PSPE, or Promotable Single Phase Enlistment, where a durable RM takes the ownership of a transaction, which can later be escalated to be managed by a TM as the conditions change. Because durable enlistment is more expensive than volatile enlistment, PSPE provides an attractive way of implementing an RM with durable resources, yet getting away with the performance penalties in many scenarios.

After choosing your enlistment method, you then need to implement the IEnlistmentNotification interface to ensure that you get the necessary callbacks from the TM to ensure proper commit or abort. Examples of TMs on the Windows platform are LTM (Lightweight Transaction Manager) for volatile enlistments and MSDTC for distributed or durable transactions.

Without any further delay, the following example demonstrates the implementation of these concepts.

Implementing an RM Using Volatile Enlistment

The following is an example of an RM that encapsulates the process of writing to a member variable within a transaction:

public class VolatileRM : IEnlistmentNotification
{
   private int memberValue = 0;
   private int oldMemberValue = 0;

   public int MemberValue
   {
      get { return memberValue; }
   }

   public void SetMemberValue(int newMemberValue)
   {
      Transaction currentTx = Transaction.Current;
      if (currentTx != null)
      {
         Console.WriteLine("VolatileRM: SetMemberValue -
                            EnlistVolatile");
         currentTx.EnlistVolatile(this, EnlistmentOptions.None);
      }
      oldMemberValue = memberValue;
      memberValue = newMemberValue;
   }

   #region IEnlistmentNotification Members

   public void Commit(Enlistment enlistment)
   {
      Console.WriteLine("VolatileRM: Commit");
      // Clear out oldMemberValue
      oldMemberValue = 0;
   }

   public void InDoubt(Enlistment enlistment)
   {
      Console.WriteLine("VolatileRM: InDoubt");
   }

   public void Prepare(PreparingEnlistment preparingEnlistment)
   {
      Console.WriteLine("VolatileRM: Prepare");
      preparingEnlistment.Prepared();
   }

   public void Rollback(Enlistment enlistment)
   {
      Console.WriteLine("VolatileRM: Rollback");
      // Restore previous state
      memberValue = oldMemberValue;
      oldMemberValue = 0;
   }

   #endregion
}

As you can see, the code checks whether it is currently within a transaction in SetMemberValue. If it indeed is, it enlists itself in the volatile transaction as follows and then performs the necessary operation while keeping within itself enough information for a possible rollback:

public void SetMemberValue(int newMemberValue)
{
   Transaction currentTx = Transaction.Current;
   if (currentTx != null)
   {
      Console.WriteLine("VolatileRM: SetMemberValue -
                          EnlistVolatile");
      currentTx.EnlistVolatile(this, EnlistmentOptions.None);
   }
   oldMemberValue = memberValue;
   memberValue = newMemberValue;
}

The code implements a bunch of other methods as well. Commit and Rollback, as their names suggest, either commit or roll back the work. The TM calls the Prepare method to notify your RM that it is requesting Phase 1 of the transaction to be committed, and to determine whether or not your RM is ready to do its work. At this point, you have one of three choices:

  1. Call the preparingEnlistment.Prepare() method. This notifies the TM that you are ready to go, and you are interested in listening to further notifications when you commit your work.
  2. Call preparingEnlistment.ForceRollBack() or specify preparingEnlistment.RecoveryInformation. In the event of re-enlistment, this provides the new instance of the RM with sufficient information to perform a graceful recovery. But, don't worry about that just yet.
  3. Call preparingEnlistment.Done() to act as an innocent bystander. Merely observe the transaction and don't participate in it. By calling Done, the TM skips notifying you about Phase 2 of the two-phase commit.

System.Transactions: Implement Your Own Resource Manager

Now that you have created an RM that works with System.Transactions, you can leverage the framework to write simple and reliable code. Using the above RM is rather simple. For example, entering the following code:

VolatileRM vrm = null ;
using (TransactionScope txSc = new TransactionScope())
{
   vrm = new VolatileRM();
   vrm.SetMemberValue(3);
   txSc.Complete();
}
Console.WriteLine("Member Value:" + vrm.MemberValue);

Produces the following output:

VolatileRM: SetMemberValue - EnlistVolatile
VolatileRM: Prepare
VolatileRM: Commit
Member Value:3

As you can see, the Member Value is updated, and the RM got notifications at the Prepare and Commit stages because it enlisted in a volatile fashion.

If you wanted to force a rollback through the RM, you could simply change the RM's Prepare method to the following:

public void Prepare(PreparingEnlistment preparingEnlistment)
{
   Console.WriteLine("VolatileRM: Prepare");
   preparingEnlistment.ForceRollback();
}

By doing so, you force a rollback, which will cause an exception as shown in Figure 1.

Figure 1: An Exception from a Forced Rollback

You can in fact force a custom exception by using a suitable overload of the preparingEnlistment.RollBack method.

Yet another method for issuing a rollback is using the client program, or the consumer of the RM. Simply remove the TransactionScope.Complete statement, or enlist another RM that enlists within the same transaction and issues a rollback on the entire transaction:

VolatileRM vrm = null ;
using (TransactionScope txSc = new TransactionScope())
{
   vrm = new VolatileRM();
   vrm.SetMemberValue(3);
   // txSc.Complete();
}
Console.WriteLine("Member Value:" + vrm.MemberValue);

When you run this code, it produces the following output:

VolatileRM: SetMemberValue - EnlistVolatile
VolatileRM: Rollback
Member Value: 0

At this point, you may be wondering what the big deal is with this paradigm? Well, the big deal is that by following this paradigm now, you can effectively created an RM that will allow its consumers to easily write transactional code that may or may not include operations of other RMs. For instance, you can easily enlist your RM, which performs the non-database operation of modifying a member variable, in the same transaction as a database transaction. You can do this easily by setting up a database using the following T-SQL script:

Create Database Test
Go
Create Table Demo
(
   DemoValue varchar(5)
)

The following code then will enlist VolatileRM and two instances of SqlConnection in the same transaction:

private static string connStr =
   "Data Source=(local);Initial Catalog=Test;
    Integrated Security=True";
static void Main(string[] args)
{
   VolatileRM vrm = null ;
   using (TransactionScope txSc = new TransactionScope())
   {
      vrm = new VolatileRM();
      vrm.SetMemberValue(3);

      using (SqlConnection cn = new SqlConnection(connStr))
      {
         SqlCommand cmd = cn.CreateCommand();
         cmd.CommandText = "Insert into Demo(DemoValue)
                            Values ('XXX')";
         cn.Open();
         cmd.ExecuteNonQuery();
         cn.Close();
      }

      using (SqlConnection cn = new SqlConnection(connStr))
      {
         SqlCommand cmd = cn.CreateCommand();
         cmd.CommandText = "Insert into Demo(DemoValue)
                            Values ('YYY')";
         cn.Open();
         cmd.ExecuteNonQuery();
         cn.Close();
      }

      Console.WriteLine( "Transaction identifier:" +
         Transaction.Current.TransactionInformation.
         DistributedIdentifier);
      txSc.Complete();
   }
   Console.WriteLine("Member Value:" + vrm.MemberValue);
}

The framework is smart enough to recognize that your SqlConnection instance participates in the transaction using PSPE. As soon as a second SqlConnection instance shows up, it will automatically escalate the transaction to MSDTC when the second cn.Open is called. You can see this in the applet at Control Panel C Administrative Tools C Component Services (see Figure 2).

Figure 2: Transaction List Applet for Your Enlisted RM

Note: SqlConnection connected to SQL Server 2005 exhibits promotable enlistment, but SqlConnection connected to SQL Server 2000 or below will enlist in a durable fashion even though only a single RM exists in the current transaction scope.

When you run the program now, it produces the following output:

VolatileRM: SetMemberValue - EnlistVolatile
Transaction identifier:c40015f6-5086-4688-b565-c65db1cbc8e7
VolatileRM: Prepare
VolatileRM: Commit
Member Value:3

As you can see, you are still enlisting volatile. However, as need be, your transaction is automatically promoted to MSDTC and it gets a distributed identifier, which (surprise, surprise) matches the GUID reported by MSDTC in Figure 2. If you're an architect, this gives you the best of both worlds: transactional integrity and the best possible performance. And you get all of this by writing consumer code in a rather simplistic fashion:

Transaction
{
   Operation A ;
   Operation B ;
   ....
   ....
   Commit()
}

Implementing a System.Transactions-based Transaction

Now you know the basics of how to implement a System.Transactions-based transaction. You learned the various ways of enlisting with a current running transaction and saw how easy it is to implement your very own RM that enlists in a volatile fashion.

About the Author

Sahil Malik has worked for a number of top-notch clients in Microsoft technologies ranging from DOS to .NET. He is the author of Pro ADO.NET 2.0 and co-author of Pro ADO.NET with VB.NET 1.1. Sahil is currently also working on a multimedia series on ADO.NET 2.0 for Keystone Learning. For his community involvement, contributions, and speaking, he has also been awarded the Microsoft MVP award.



Downloads

Comments

  • There are no comments yet. Be the first to comment!

Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • Live Event Date: October 29, 2014 @ 11:00 a.m. ET / 8:00 a.m. PT Are you interested in building a cognitive application using the power of IBM Watson? Need a platform that provides speed and ease for rapidly deploying this application? Join Chris Madison, Watson Solution Architect, as he walks through the process of building a Watson powered application on IBM Bluemix. Chris will talk about the new Watson Services just released on IBM bluemix, but more importantly he will do a step by step cognitive …

  • Companies must routinely transfer files and share data to run their business, work with partners, and speed operations. However, many find the traditional approach to file transfer lacks necessary security, is too complex and difficult to manage, does not support the levels of automation needed, and breaks down when addressing the file transfer requirements of new areas like Big Data analytics and mobile applications. This QuinStreet SmartSelect discusses how the changing business environment is making the use …

Most Popular Programming Stories

More for Developers

Latest Developer Headlines

RSS Feeds