Home » .Net FrameworkRSS

How to be sure that all IEnlistmentNotification.Commits are called?

I'm writing a console application that enlists some work in a transaction, so I can either commit it or do a rollback.  The problem is that the TransactionScope.Complete method causes the commits to be executed on a background thread, after the TransactionScope has been disposed.

As my application exits directly after the dispose of the TransactionScope, most of the commits never get called.

The following code demonstrates the problem:

#region Usings

using System;
using System.Threading;
using System.Transactions;

#endregionnamespace TransactionScopeTest
{
	internalclass Program
	{
		privatestaticvoid Main(string[] args)
		{
			Guid newGuid = Guid.NewGuid();
			using(TransactionScope tx = new TransactionScope(TransactionScopeOption.RequiresNew, new TransactionOptions { Timeout = new TimeSpan(0, 0, 1, 0) }, EnterpriseServicesInteropOption.Full))
			{
				Transaction.Current.TransactionCompleted += delegate { Console.WriteLine("TransactionCompleted event"); };
				for(int i = 0;i < 30;i++)
				{
					Transaction.Current.EnlistDurable(newGuid, new EnlistmentTracking(), EnlistmentOptions.EnlistDuringPrepareRequired);
				}
				Console.WriteLine("Before commit");
				tx.Complete();
				Console.WriteLine("Before dispose");
			}
			Console.WriteLine("After dispose");
			Console.WriteLine(Thread.VolatileRead(ref EnlistmentTracking.EnlistmentCounts) + " commits still open...");
		}

		publicclass EnlistmentTracking : IEnlistmentNotification
		{
			publicstaticint EnlistmentCounts;

			public EnlistmentTracking()
			{
				Interlocked.Increment(ref EnlistmentCounts);
				Console.WriteLine("Enlistment " + EnlistmentCounts);
			}

			publicvoid Prepare(PreparingEnlistment preparingEnlistment)
			{
				Console.WriteLine("Perpare");
				preparingEnlistment.Prepared();
			}

			publicvoid Commit(Enlistment enlistment)
			{
				Console.WriteLine("Commit " + EnlistmentCounts);
				Interlocked.Decrement(ref EnlistmentCounts);
				enlistment.Done();
			}

			publicvoid Rollback(Enlistment enlistment)
			{
				Console.WriteLine("Rollback " + EnlistmentCounts);
				Interlocked.Decrement(ref EnlistmentCounts);
				enlistment.Done();
			}

			publicvoid InDoubt(Enlistment enlistment)
			{
				Interlocked.Decrement(ref EnlistmentCounts);
				enlistment.Done();
			}
		}

	}
}

 

5 Answers Found

 

Answer 1

This is essentially by design.  There's no way for the transaction  manager to know when all enlistments and RMs have finished their work, synchronous or asynchronous.

Transactionally, it doesn't matter to the TransactionScope whether or not all Enlistments have completed.  What matters is that they've responded in phase 1 that they will commit  or abort, and the transaction manager has reached a consensus.  If a resource manager dies after saying it will commit, but before it actually finisheds work, it must be able to recover and eventually fulfill that commit.

In the example you give, the TM and the RM reside in the same process, so that process must stick around until all Enlistments honor their phase 1 commit.  If you were to split it out, with the TransactionScope in one process, and all RMs (each of which had an enlistment in the transaction) in their own processes, it would behave as you expect - while the process containing the TransactionScope would close, the rest would continue to complete.

There's no good way to ensure that the process sticks around until the second phase of all commits finishes.  You could sleep, but that's not deterministic.

 

Answer 2

Thank you for your answer, but that doesn't really help me with my problem.

The example above is just an example to indicate the problem.  I used two-phased commits in my real application  because I do both file manipulations ( moving files between servers) and database manipulations, and I have to make sure that both actions (moving a file and updating the database) can be completed.

That is why I invented some sort of transactional file move.  The prepare phase copies the file to its new location under a temporary filename, the commit  phase deletes the file at its source location and renames the file to its original filename at the destination location.

This mechanism works really good, except sometimes, the final step is not executed, because the application ends before the commit phase is called.

The solution I have found so far is to wait until all workerthreads have completed their work.  But this requires some coding in the program  itself.  I would rather find a way to implement this logic in our framework, so that the user can't forget to add that code.

The solution you are suggesting: putting the file manipulation logic into separate processes sounds interesting.  Can you point me to some information on how to do that?

thanks

 

Answer 3

Sounds like you want to use DependentTransaction.

ThreadPool.QueueUserWorkItem(
  new WaitCallback(WorkerThread),
  Transaction.Current.DependentClone(
   DependentCloneOption.BlockCommitUntilComplete));
In the worker thread, the dependent transaction  can be passed to the transaction scope, then the completion is effectively communicated back. 

 

Answer 4

I have the same issue as Marc, only not for a console  application. But I have tried to use the "DependentCloneOption.BlockCommitUntilComplete" fix as stated above, but with no succes.

I still see transactions  that are not committed after the dispose  :-/ Below is my sample code. Busy waiting for threads to complete  is not an option in our application.

Can you spot any errors in my sample? It still seems that the documentation on "TransactionScope.Dispose" is not correct in saying "This method  is synchronous and blocks until the transaction  has been committed or aborted. "

using System;
using System.Diagnostics;
using System.Threading;
using System.Transactions;

namespace DependentTransactionTest
{
	publicclass WorkerThread
	{
		readonly Guid newGuid = Guid.NewGuid();

		publicvoid DoWork(DependentTransaction dependentTransaction)
		{
			Thread thread  = new Thread(ThreadMethod);
			thread.Start(dependentTransaction);
		}

		publicvoid ThreadMethod(object transaction)
		{
			DependentTransaction dependentTransaction = transaction as DependentTransaction;
			Debug.Assert(dependentTransaction != null);
			try
			{
				using (TransactionScope ts = new TransactionScope(dependentTransaction))
				{
					Transaction.Current.EnlistDurable(newGuid, new EnlistmentTracking(), EnlistmentOptions.None);
					Transaction.Current.EnlistDurable(newGuid, new EnlistmentTracking(), EnlistmentOptions.None);					
					ts.Complete();
				}
			}
			finally
			{
				dependentTransaction.Complete();
				dependentTransaction.Dispose();
			}
		}
	}
  
	publicclass EnlistmentTracking : IEnlistmentNotification
	{
		publicstaticint EnlistmentCounts;

		public EnlistmentTracking()
		{
			Interlocked.Increment(ref EnlistmentCounts);
		}

    publicvoid Prepare(PreparingEnlistment preparingEnlistment)
    {
      preparingEnlistment.Prepared();
      Console.WriteLine("Prepare: " + EnlistmentCounts);
    }

		publicvoid Commit(Enlistment enlistment)
		{
			Interlocked.Decrement(ref EnlistmentCounts);
			enlistment.Done();
      Console.WriteLine("Commit: " + EnlistmentCounts);
    }

		publicvoid Rollback(Enlistment enlistment)
		{
			Interlocked.Decrement(ref EnlistmentCounts);
			enlistment.Done();
      Console.WriteLine("Rollback: " + EnlistmentCounts);
    }

		publicvoid InDoubt(Enlistment enlistment)
		{
			Interlocked.Decrement(ref EnlistmentCounts);
			enlistment.Done();
		}
	}  

	class Program
	{
		staticvoid Main(string[] args)
		{
			
			for (int i = 0; i < 100; i++)
			{
				using (var scope = new TransactionScope())
				{

					Transaction currentTransaction = Transaction.Current;
					DependentTransaction dependentTransaction = currentTransaction.DependentClone(DependentCloneOption.BlockCommitUntilComplete);
					WorkerThread workerThread = new WorkerThread();
					workerThread.DoWork(dependentTransaction);
					scope.Complete();
				}

				Console.WriteLine(Thread.VolatileRead(ref EnlistmentTracking.EnlistmentCounts) + " transaction still open.");
			}

      Console.WriteLine("Main finished: " + Thread.VolatileRead(ref EnlistmentTracking.EnlistmentCounts) + " transaction still open.");
      Console.ReadLine();
      Console.WriteLine(Thread.VolatileRead(ref EnlistmentTracking.EnlistmentCounts) + " transaction still open.");
    }
	}
}

 

Answer 5

The documentation is correct. From the perspetive of the application and the transaction manager the transaction has in fact been committed or aborted. This outcome may not have propagated to all transaction participants yet, but the decision is durably recorded and will be transactionally consistent. 

The issue with a simple console app that contains in process enlistments is that you might exit the app before the commit notification has been processed by your enlistment. Typically the enlistment is out of proc and the client applicaiton is some long running service so you wouldn't run into this. However, if you need this application to work as is, you'll need to do your own synchronization (e.g. put a WaitHandle in the Commit method of the Enlistment and then wait on all of them before exiting the app.)

-Clark

 
 
 

<< Previous      Next >>


Microsoft   |   Windows   |   Visual Studio   |   Follow us on Twitter