4.1 Writing Your Own Resource Manager
It
is only reasonable to expect that because databases are the most
critical part of the architecture (at least to database developers and
administrators!), they have had fantastic transactional support for a
long time. But don’t you want your other, nondatabase operations to be
transactional as well if they also could benefit from transactional
behavior?
Let’s consider the simple operation of setting a value
to an integer and wrapping that as a part of a transaction, as shown in
the following code snippet. The first question is: How do you set a
value for an integer?
int myInt;
myInt = 10;
Unfortunately, wrapping this code in a TransactionScope won’t make it transactional. This is because System.Int32 is not smart enough to understand that it is being wrapped inside a TransactionScope and that it should auto-enlist within a running transaction. This is probably a good thing, because in the event of a rollback, to perform a graceful recovery, System.Int32
would have to maintain a previous version. You probably wouldn’t want
to pay this overhead for all your integers. So you need to write a
class that lets you maintain enough history in the event of a rollback.
This class should also be able to interact with a TM and listen for
various two-phase commit notifications. To do so, this class, or the RM you are writing, must implement the IEnlistmentNotification
interface. This interface requires you to implement certain methods
that are called at the appropriate points in time by the TC during the
two phases of a two-phase commit process.
Here are the methods that IEnlistmentNotification requires you to implement:
Commit Notifies the RM that the transaction has been committed. The RM then makes the changes permanent.
Rollback Notifies the RM that the transaction has been rolled back. The RM then reverts to the previous stable state.
Prepare Called during the first (prepare) phase of a distributed
transaction—when the TM asks the participants whether they are ready to
commit. If the TM receives a successful notification from each
participating RM, it calls the Commit methods.
InDoubt
Notifies the RMs if the TM loses contact with one or more participants
in the transaction. In this situation, the status of the transaction is
unknown, and the application logic must decide whether to revert to the
previous consistent state or remain in an inconsistent state.
Example 3
puts all of these concepts into actual code. It shows a full
implementation of a volatile RM.
Example 3. Implementing your own resource manager.
public class VolatileRM : IEnlistmentNotification
{
private string _whoAmI = "";
public VolatileRM(string whoAmI)
{
this._whoAmI = whoAmI;
}
private int _memberValue = 0;
private int _oldMemberValue = 0;
public int MemberValue
{
get
{
return this._memberValue;
}
set
{
Transaction tran = Transaction.Current;
if (tran != null)
{
Console.WriteLine(
this._whoAmI + ": MemberValue setter - EnlistVolatile");
tran.EnlistVolatile(this, EnlistmentOptions.None);
}
this._oldMemberValue = this._memberValue;
this._memberValue = value;
}
}
#region IEnlistmentNotification Members
public void Commit(Enlistment enlistment)
{
Console.WriteLine(this._whoAmI + ": Commit");
// Clear out _oldMemberValue
this._oldMemberValue = 0;
enlistment.Done();
}
public void InDoubt(Enlistment enlistment)
{
Console.WriteLine(this._whoAmI + ": InDoubt");
enlistment.Done();
}
public void Prepare(PreparingEnlistment preparingEnlistment)
{
Console.WriteLine(this._whoAmI + ": Prepare");
preparingEnlistment.Prepared();
}
public void Rollback(Enlistment enlistment)
{
Console.WriteLine(this._whoAmI + ": Rollback");
// Restore previous state
this._memberValue = this._oldMemberValue;
this._oldMemberValue = 0;
enlistment.Done();
}
#endregion
}
Let’s examine this code more closely. At the very top is a class that implements IEnlistmentNotification. This signifies that your RM will receive notifications from the current transaction manager:
public class VolatileRM : IEnlistmentNotification
The code begins with a private string variable named _whoAmI and a constructor. This will help you analyze the chain of events when more than one RM is involved.
private string _whoAmI = "";
public VolatileRM(string whoAmI)
{
this._whoAmI = whoAmI;
}
Next, the code defines two class-level variables named _memberValue and _oldMemberValue, followed by a MemberValue property. The motivation for writing this class is the fact that System.Int32 is unable to interact with an RM or maintain historical values to roll back integers. The MemberValue property’s get accessor exposes _memberValue, and its set accessor assigns a new value to _memberValue and then enlists in the currently running transaction. The _oldMemberValue variable holds the historical value that will be used in the event of a rollback.
private int _memberValue = 0;
private int _oldMemberValue = 0;
public int MemberValue
{
get { return _memberValue; }
set
{
Transaction tran = Transaction.Current;
if (tran != null)
{
Console.WriteLine(
tran._whoAmI + ": MemberValue setter - EnlistVolatile");
tran.EnlistVolatile(this, EnlistmentOptions.None);
}
this._oldMemberValue = this._memberValue;
this._memberValue = value;
}
}
As you can see, the code first attempts to find the current transaction in the Transaction.Current variable, and then uses the EnlistVolatile method to enlist in the current transaction in a volatile manner. Volatile enlistment is sufficient for this example. If you were working with a durable resource, you would call the EnlistDurable method instead. Last, the code performs the logic of assigning the new value and preserving the old value.
With
the class and its data set up, the rest of the details involve hooking
up implementation so that you can enlist in a current running
transaction with the RM and perform the appropriate actions based on
the notifications received. This functionality is implemented in the
four methods that the IEnlistmentNotification interface requires you to implement. The TM calls the appropriate methods (Commit, Rollback, Prepare, and InDoubt) for you and passes in a System.Transactions.Enlistment variable as a parameter. After successfully performing each step, you should call the enlistment.Done() method to indicate that this step has done its work.
The only exception to this rule is the Prepare method, which receives a special kind of Enlistment, a System.Transactions.PreparingEnlistment variable, as a parameter, which inherits from the System.Transactions.Enlistment class. PreparingEnlistment adds a few methods to Enlistment:
ForceRollBack() or ForceRollBack(Exception) Notifies the TM that an error has occurred and that the current participating RM wants to issue a rollback. You can specify your own exception if you want.
Prepared
Notifies the TM that this RM has successfully finished doing its part
of the transaction (the prepare phase of the two-phase commit process).
byte[] RecoveryInformation
Used to specify information to the TM in the event of reenlistment to
perform a graceful recovery (in situations such as the RM crashing).
Alternatively, you can call the base class method Done to act as an innocent bystander and observe the transaction but not really participate in it. If you call Done in the prepare phase, the TM skips notifying the RM of the second (commit) phase of a two-phase notification process.