Question

EDITED It seems that when using distributed transactions (EnterpriseServicesInteropOption.Full) and durable subscribers, the TransactionScope.Dispose method does not wait for all the commits to have completed, but just spawns the method calls and the TransactionCompleted event to background threads.

This is not conform the documentation that clearly states:

This method is synchronous and blocks until the transaction has been committed or aborted.

What's worse is that there seems to be no way way to determine when all commits have been processed. This is problematic because in console applications, the main thread can quit after the dispose and this effectively kills all background threads. Remote participants in the distributed transaction will never be notified of this, resulting in locks that remain open, timeouts and other ugly things...

Another issue with this is that when a new TransactionScope is created, participants can still be associated with the old transaction when they are expected to be registered within the new one.

The (simplified) code below demonstrates this problem.

My question: does anybody have an idea how to determine whether it is safe to start a new loop or not (yet)? I don't have access to the Worker's code, so I can't change anything in there... Adding a Thread.Sleep(1000) solves the problem, but that kills the performance...

EDITED

internal class TransactionScopeTest
{
    [STAThread]
    public static void Main()
    {
        var transactionOptions = new TransactionOptions { Timeout = TransactionManager.DefaultTimeout };
        var worker = new Worker();
        var transactionCompletedEvent = new AutoResetEvent(true); // true to start a first loop

        while (true)
        {
            transactionCompletedEvent.WaitOne(); // wait for previous transaction to finish

            Log("Before TransactionScope");
            using (var tx = new TransactionScope(TransactionScopeOption.Required, transactionOptions, EnterpriseServicesInteropOption.Full))
            {
                Log("Inside TransactionScope");
                Transaction.Current.TransactionCompleted += delegate
                {
                    transactionCompletedEvent.Set(); // allow a next loop to start
                    Log("TransactionCompleted event");
                };
                worker.DoWork();
                Log("Before commit");
                tx.Complete();
                Log("Before dispose");
            }
            Log("After dispose");
        }
    }

    private static void Log(string message)
    {
        Console.WriteLine("{0} ({1})", message, Thread.CurrentThread.ManagedThreadId);
    }


    public class Worker : IEnlistmentNotification
    {
        private Transaction _transaction;
        private readonly Guid _id = Guid.NewGuid();

        public void Prepare(PreparingEnlistment preparingEnlistment)
        {
            Log("Preparing");
            preparingEnlistment.Prepared();
        }

        public void Commit(Enlistment enlistment)
        {
            Log("Committing");
            _transaction = null;
            enlistment.Done();
        }

        public void Rollback(Enlistment enlistment)
        {
            Log("Rolling back");
            _transaction = null;
            enlistment.Done();
        }

        public void InDoubt(Enlistment enlistment)
        {
            Console.WriteLine(Thread.CurrentThread.ManagedThreadId + "Doubting");
            _transaction = null;
            enlistment.Done();
        }

        public void DoWork()
        {
            Enlist();
            Log("Doing my thing...");
        }

        private void Enlist()
        {
            if (_transaction == null) //Not yet enlisted
            {
                Log("Enlisting in transaction");
                _transaction = Transaction.Current;
                _transaction.EnlistDurable(_id,this, EnlistmentOptions.EnlistDuringPrepareRequired);
                return;
            }
            if (_transaction == Transaction.Current) //Already enlisted in current transaction
            {
                return;
            }
            throw new InvalidOperationException("Already enlisted in other transaction");
        }
    }
}

Output:

Before commit (1)
Before dispose (1)
Preparing (6)
After dispose (1)
Committing (6)
TransactionCompleted event (7)
Before TransactionScope (1)
Inside TransactionScope (1)
Enlisting in transaction (1)
Doing my thing... (1)
Before commit (1)
Before dispose (1)
Preparing (7)
After dispose (1)
Before TransactionScope (1)
TransactionCompleted event (7)
Inside TransactionScope (1)
Committing (6)

Unhandled Exception: System.InvalidOperationException: Already enlisted in other transaction
Was it helpful?

Solution 2

The only solution that more or less works is to wait until all background threads are stopped before exiting the console application.

I implemented this by calling the following code, just before exiting the application:

public static class ThreadTools
{ 
    /// <summary>
    /// Wait until all worker threads have finished their job.
    /// </summary>
    public static void WaitForBackgroundThreads()
    { 
        int workerThreads = 0;
        int completionPortThreads = 0;
        int maxWorkerThreads;
        int maxCompletionPortThreads;
        ThreadPool.GetMaxThreads(out maxWorkerThreads, out maxCompletionPortThreads); 
        while(workerThreads != maxWorkerThreads || completionPortThreads != maxCompletionPortThreads)
        { 
            Thread.Sleep(100);
            ThreadPool.GetAvailableThreads(out workerThreads, out completionPortThreads);
        }
    }
}

I realize this is only a hack, but until someone gives me a better solution, this is best answer I could come up with.

OTHER TIPS

Transaction.Current.TransactionCompleted is always executed after worker.Commit notification. Add an AutoResetEvent to track TransactionCompleted and wait for it before staring a new loop:

var transactionCompletedEvent = new AutoResetEvent(true); // true to start a first loop

while (true)
{
    transactionCompletedEvent.WaitOne(); // wait for previous transaction to finish

    Log("Before TransactionScope");
    using (var tx = new TransactionScope(TransactionScopeOption.Required, transactionOptions, EnterpriseServicesInteropOption.Full))
    {
        Log("Inside TransactionScope");
        Transaction.Current.TransactionCompleted += delegate 
        {
            transactionCompletedEvent.Set(); // allow a next loop to start
            Log("TransactionCompleted event"); 
        };
        worker.DoWork();
        Log("Before commit");
        tx.Complete();
        Log("Before dispose");
    }
    Log("After dispose");
}
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top