4

I'm doing some stuff inside a using block for a TransactionScope object. At some point I wanted to call some async code by firing and forget (I don't want to wait for the result, and I'm not interested in what happens during that call) and I wanted that code to not be part of the transaction (by using TransactionScopeOption.Suppress option).

So initially I made something similar to the methodFails that I have commented in the code below. It got me a nice "System.InvalidOperationException: 'TransactionScope nested incorrectly'". I looked up in SO for somebody having similar problems, and found this Question where the answer by ZunTzu gave me the idea for method1 using TransactionScopeAsyncFlowOption.Enabled option, which works as I expected for methodFails but without the exception.

Then I thought of an alternative that I put in method2 that consists in putting the async code in a third method (method3) called by firing-and-forget while the TransactionScopeOption.Suppress option is kept in the non-async method2. And this approach seems to work as good as method1 in my sample program.

So my question is: which approach is better, method1 or method2, or maybe a third one that I have no thought about? I'm leaning for method1 because it sounds like "the people making the TransactionScope class put that TransactionScopeAsyncFlowOption there for a reason". But the fact that TransactionScopeAsyncFlowOption.Enabled is not the default for a TransactionScope makes me think that maybe there is a performance hit by enabling that, and fire-and-forget may be a special case where I can save that performance hit.

The sample code:

    class Program
    {
        static void Main(string[] args)
        {
            using (TransactionScope scope1 = new TransactionScope())
            {
                // Do some stuff in scope1...

                // Start calls that could execute async code
                //Task a = methodFails(); // This commented method would launch exception: System.InvalidOperationException: 'TransactionScope nested incorrectly'
                Task b = method1(); // Fire and forget
                method2();

                // Rest of stuff in scope1 ...
            }
            Console.ReadLine();
        }

        static async Task methodFails()
        {
            //Start of non-transactional section 
            using (TransactionScope scope2 = new TransactionScope(TransactionScopeOption.Suppress))
            {
                //Do non-transactional work here
                Console.WriteLine("Hello World 0.1!!");
                await Task.Delay(10000);
                Console.WriteLine("Hello World 0.2!!");
            }
            //Restores ambient transaction here
            Console.WriteLine("Hello World 0.3!!");
        }

        static async Task method1()
        {
            //Start of non-transactional section 
            using (TransactionScope scope2 = new TransactionScope(TransactionScopeOption.Suppress, TransactionScopeAsyncFlowOption.Enabled))
            {
                //Do non-transactional work here
                Console.WriteLine("Hello World 1.1!!");
                await Task.Delay(10000);
                Console.WriteLine("Hello World 1.2!!");
            }
            //Restores ambient transaction here
            Console.WriteLine("Hello World 1.3!!");
        }

        static void method2()
        {
            //Start of non-transactional section 
            using (TransactionScope scope2 = new TransactionScope(TransactionScopeOption.Suppress))
            {
                //Do non-transactional work here
                Task ignored = method3(); // Fire and forget
            }
            //Restores ambient transaction here
            Console.WriteLine("Hello World 2.2!!");
        }

        static async Task method3()
        {
            //Do non-transactional work here
            Console.WriteLine("Hello World 2.1!!");
            await Task.Delay(10000);
            Console.WriteLine("Hello World 2.3!!");
        }
    }
Nic
  • 12,220
  • 20
  • 77
  • 105
Jorge Y.
  • 1,123
  • 1
  • 9
  • 16
  • If you just want to run your code in fire-and-forget outside of the transaction scope, I'd say it's a good use-case for `Task.Run` – Kevin Gosse Feb 14 '18 at 08:03
  • @Kevin Gosse I read some time ago in Stephen Cleary's blog that Task.Run should be for CPU bound code. In my production code, the "Task.Delay" call is in fact a stored procedure call that takes several seconds to complete, so I think it fits better in the async-await scenario – Jorge Y. Feb 14 '18 at 08:11
  • That's the kind of comment that sometimes make me wonder if that blog article didn't hurt more than it helped. `Task.Run` is useful whenever you want to offload work to another thread. It rarely makes sense for I/O bound operations and it's an easy-to-make mistake, that's why Stephen made the approximation "Task.Run = CPU bound". But it doesn't mean you shouldn't use it when it helps you, as long as you understand what it does – Kevin Gosse Feb 14 '18 at 18:59
  • I interpret that article along the lines of "if it there is a (reasonable) way to get the I/O job done with async-await, go with that". In my case, I don't see any of the two options I initially posted being a bad way to solve the problem, and async-await syntax conveys a little bit better than Task.Run the nature of the work being done. But I agree with you that it's worth evaluating all the tools available. – Jorge Y. Feb 14 '18 at 19:40

2 Answers2

4

But the fact that TransactionScopeAsyncFlowOption.Enabled is not the default for a TransactionScope makes me think that maybe there is a performance hit by enabling that, and fire-and-forget may be a special case where I can save that performance hit.

TransactionScopeAsyncFlowOption.Enabled was introduced for backward compatibility purposes when they fixed a bug. Strangely, you don't benefit from the bug fix unless you "opt in" by setting this flag. They did it that way so the bug fix didn't break any existing code that relied on the buggy behavior.

In this article:

You might not know this, but the 4.5.0 version of the .NET Framework contains a serious bug regarding System.Transactions.TransactionScope and how it behaves with async/await. Because of this bug, a TransactionScope can't flow through into your asynchronous continuations. This potentially changes the threading context of the transaction, causing exceptions to be thrown when the transaction scope is disposed.

This is a big problem, as it makes writing asynchronous code involving transactions extremely error-prone.

The good news is that as part of the .NET Framework 4.5.1, Microsoft released the fix for that "asynchronous continuation" bug. The thing is that developers like us now need to explicitly opt-in to get this new behavior. Let's take a look at how to do just that.

  • A TransactionScope wrapping asynchronous code needs to specify TransactionScopeAsyncFlowOption.Enabled in its constructor.
Community
  • 1
  • 1
John Wu
  • 50,556
  • 8
  • 44
  • 80
  • Interesting article, in special the part clarifying why is not the default value. The doubt I still have is that `method2` is not async, although it calls an async method, so maybe there I'm not "wrapping asynchronous" code. Anyway, I think I will avoid premature optimizations, that maybe even are not an optimization, and go with the "Enabled" option. Thanks! – Jorge Y. Feb 14 '18 at 08:52
3

You could call your async methods within a HostingEnvironment.QueueBackgroundWorkItem call.

HostingEnvironment.QueueBackgroundWorkItem(async cancellationToken =>
{
    await LongRunningMethodAsync();
});

QueueBackgroundWorkItem is summarized as follows:

The HostingEnvironment.QueueBackgroundWorkItem method lets you schedule small background work items. ASP.NET tracks these items and prevents IIS from abruptly terminating the worker process until all background work items have completed.

Nic
  • 12,220
  • 20
  • 77
  • 105
  • 1
    Didn't know about that QueueBackgroundWorkItem, sounds good but... unfortunately for this specific problem I'm not in an ASP.NET environment. – Jorge Y. Feb 14 '18 at 08:31