Search code examples
c#async-awaittransactionscope

TransactionTimeOut Exception on all DB hits made by a thread on which there was "A TransactionScope must be disposed on the same thread.." exception


In production all of the sudden we started to have Exception message was "A TransactionScope must be disposed on the same thread that it was created.". Looking into code it was clear we are having .Net Transaction within the async method and TransactionScopeAsyncFlowOption.Enabled is Not used in TransactionScope. What interesting was somehow the above aborted transaction that caused Exception remains attached to the Thread and every API request served by that Thread causes TransactionTimeOut Exception on every db interaction.

I tried to reproduce issue on local by small program. If we see breakpoint we have transaction outside transaction scope. (its not every time , its only for the threads for which exception occurs) enter image description here

My question is why transaction remains with the thread?

        static void Main(string[] args)
    {
        var loopResult = TxIssue();
        while (!loopResult.IsCompleted)
                Thread.Sleep(3000);
        Console.ReadLine();
    }
    public static ParallelLoopResult TxIssue()
    {
        return Parallel.For(0, 10, async t =>
        {
            try
            {
                if (Transaction.Current != null)
                {
                    Console.WriteLine($"threadId : {Thread.CurrentThread.ManagedThreadId}");
                    Console.WriteLine($"Transaction status = {Transaction.Current.TransactionInformation.LocalIdentifier}");
                }
                using (var ts = new TransactionScope(TransactionScopeOption.RequiresNew))
                {
                    await Task.Delay(300);
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
            }
        });
    }

Solution

  • Whenever you await an asynchronous operation, the current thread is freed up to be used by other operations. So one of your early threads might create a transaction on Thread #1, and then when it hits the await the delegate will return a Task that hasn't completed yet. The transaction scope is still associated with Thread #1, but Thread #1 is considered available for other tasks, so the Parallel.For might reuse Thread #1 to invoke another instance of the delegate.

    I haven't tested this, but it appears the fix is to specify that you want to use the async flow in an optional argument to the TransactionScope.

    using (var ts = new TransactionScope(
                       TransactionScopeOption.RequiresNew, 
                       TransactionScopeAsyncFlowOption.Enabled))
    

    That tells the TransactionScope to associate itself with the async context instead of the Thread. Then, when the await causes the async context to get disassociated from the Thread, it will also cause the TransactionScope to get removed. And when the awaited task completes, even if its continuation happens on a different thread, the TransactionScope will become associated with the thread the continuation runs on.