Phase0 in .Net System.Transactions

In a previous post (https://blogs.msdn.com/florinlazar/archive/2006/01/29/518956.aspx) I talked about Phase0 and some useful things it allows you to do. Although not referred as Phase0, phase 0 features are available in .Net System.Transactions in two forms. Actually in three, but the third is a derivate. I will make many references here to the previous post on Phase0 and thus I recommend reading it first.

The first form where Phase 0 shows up is through EnlistmentOptions.EnlistDuringPrepareRequired that you can pass to EnlistVolatile or EnlistDurable methods. This allows you to create new enlistments during Prepare phase (and after Commit was called) and thus it allows you to create the caching resource manager (RM) discussed in the earlier post. To create a cache RM, you will first call EnlistVolatile with EnlistmentOptions.EnlistDuringPrepareRequired, and then, during Prepare you will connect to the database to write the updates. The connection to the database will usually call EnlistDurable on the transaction. Similar to Phase0 in the native DTC API, during the Prepare phase, one can enlist again passing EnlistmentOptions.EnlistDuringPrepareRequired which will issue another round of Prepare on that enlistment and so on.

The second form comes with DependentTransaction. You get a DependentTransaction by calling the DependentClone method on a transaction object. You can get the other two scenarios enabled by Phase0, “early commit protection” and “async programming” by tweaking the DependentCloneOption to RollbackIfNotComplete and BlockCommitUntilComplete respectively. Then, you signal that the work is done by calling the Complete method of this dependent transaction.

Thus you will write the DoTransfer method in .Net as follows:

int DoTransfer(int amount, Transaction tx)
{
// create the dependent clone
DependentTransaction dtx = tx.DependentClone(DependentCloneOption. RollbackIfNotComplete);
    DB1.OpenConnection(tx);
DB1.DoDebit(amount);
DB2.OpenConnection(tx);
DB2.DoCredit(amount);

    // signal work complete on the “phase0” enlistment
dtx.Complete();
}

If the DependentTransaction is not Complete-ed before a Commit is issued by the client, the transaction will be aborted.

The “third” derivate form is TransactionScope itself. TransactionScope protects you against early commits by automatically handling under the covers a DependentTransaction with RollbackIfNotComplete. That is why TransactionScope is recommended for most of the transactions scenarios. Given this you can write with the same confidence the DoTransfer method as follows:

int DoTransfer(int amount, Transaction tx)
{
// creating a TransactionScope by passing in the received transaction
// this will set tx to be the ambient transaction and create a "rollback" dependent transaction
using(TransactionScope ts = new TransactionScope(tx))
{
DB1.OpenConnection(); // note that now OpenConnection is picking the transaction from the ambient context instead of passing it as a parameter
DB1.DoDebit(amount);
DB2.OpenConnection();
DB2.DoCredit(amount);

ts.Complete();
}
}

Other Scenarios enabled and/or made easier by DependentTransaction

The “Worker Threads Scenario”
This is explained both at https://pluralsight.com/blogs/jimjohn/archive/2005/05/01/7923.aspx and https://msdn2.microsoft.com/en-US/library/ms229976(VS.80).aspx

Passing transaction ownership in a sequence of threads
Described at https://pluralsight.com/blogs/jimjohn/archive/2006/01/01/17769.aspx.

Async Programming
This is the scenario described in my previous post where the client asks asynchronously a service to do some work as part of the transaction, but doesn’t want/need to wait until the service completes the work. The code will look like the following in .Net:

Client Code
void MainFunction()
{
using(TransactionScope ts = new TransactionScope())
{
// dtx is a member of the current class of type DependentTransaction
// its lifetime needs to be at least until the ack from the service is received
this.dtx = Transaction.Current.DependentClone(DependentCloneOption.BlockCommitUntilComplete);
AsyncDoTransfer(tx, AckFromDoTransfer);
ts.Complete();
}
}
void AckFromDoTransfer()
{
this.dtx.Complete();
}

Server Code
void AsyncDoTransfer(tx, AckFromDoTransfer)
{
DependentTransaction dtx = tx.DependentClone(DependentCloneOption. BlockCommitUntilComplete);
AckFromDoTransfer(); // the client can Commit safely from now on since I have dtx created on the service side

    // the dependent clone auto-created by TransactionScope doesn’t help in this scenario because it rollbacks on early commit while we need a blocking clone
using(TransactionScope ts = new TransactionScope(tx))
{
// doing the work that takes a lot of time
DB1.OpenConnection();
DB1.DoDebit(amount);
DB2.OpenConnection();
DB2.DoCredit(amount);
ts.Complete();
}

    // work if finished on the service so we can safely let the transaction continue
dtx.Complete();
}

Comments

  • Anonymous
    April 18, 2006
    As usual, excellent post Florin. What can I say, this topic is really interesting and important IMO.<br><br>

    Now lets say SQL Server does transactions, and various transactions can deadlock each other. But SQL Server has a deadlock monitor, which ensures one party wins, and the other looses.
    <br><br>
    Dependent transactions seem to be a perfect candidate in creating a distributed deadlock. Is it upto the individual RMs to resolve those deadlocks, or is there any support in Sys.Tx (other than the default 1 minute timeout) to resolve them?
    <br>
    Sahil

  • Anonymous
    October 03, 2006
    To: Sahil Transaction timeout is the only built-in support in Sys.Tx to deal with deadlocks.

  • Anonymous
    February 10, 2007
    Remember Phase 0 that I described in my previous posts: http://blogs.msdn.com/florinlazar/archive/2006/01/29/msdtc-the-magic-of-phase-zero-phase0-or-when-using-2pc-transactions-is-not-enough.aspx