如何确定TransactionScope已调用所有提交?

时间:2021-07-08 00:40:51

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.

编辑似乎在使用分布式事务(EnterpriseServicesInteropOption.Full)和持久订阅者时,TransactionScope.Dispose方法不会等待所有提交完成,而只是将方法调用和TransactionCompleted事件生成到后台线程。

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...

更糟糕的是,似乎没有办法确定何时处理了所有提交。这是有问题的,因为在控制台应用程序中,主线程可以在dispose之后退出,这有效地杀死所有后台线程。分布式事务中的远程参与者将永远不会收到通知,导致锁定仍然打开,超时和其他丑陋的事情......

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.

另一个问题是,当创建新的TransactionScope时,参与者仍然可以在旧事务中注册时与旧事务相关联。

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...

我的问题:是否有人知道如何确定开始新循环是否安全(尚未)?我无法访问Worker的代码,因此我无法更改其中的任何内容...添加Thread.Sleep(1000)可以解决问题,但这会导致性能下降...

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

2 个解决方案

#1


1  

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:

Transaction.Current.TransactionCompleted总是在worker.Commit通知之后执行。添加AutoResetEvent以跟踪TransactionCompleted并在开始新循环之前等待它:

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");
}

#2


-1  

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.

我意识到这只是一个黑客,但直到有人给我一个更好的解决方案,这是我能想到的最佳答案。

#1


1  

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:

Transaction.Current.TransactionCompleted总是在worker.Commit通知之后执行。添加AutoResetEvent以跟踪TransactionCompleted并在开始新循环之前等待它:

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");
}

#2


-1  

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.

我意识到这只是一个黑客,但直到有人给我一个更好的解决方案,这是我能想到的最佳答案。