Disposal of TransactionScope with async data access

Disposal of TransactionScope with async data access

Old forum URL: forums.lhotka.net/forums/t/11868.aspx


ngm posted on Wednesday, March 06, 2013

Having async data portal method doing pretty straightforward ADO.NET data access marked as transactional seemed to be anti-pattern for TransactionalDataPortal and TransactionScope in particular.

Since TransactionScope has thread affinity, it has to be disposed on the very same thread it was created on. Therefore the moment data access continuation is invoked and when TransactionScope gets disposed the following exception occurs:

 "A TransactionScope must be disposed on the same thread that it was created."

My fallback was to at least obtain TransactionManager and get onto local transaction, but it didn't play well with async data access too. The reason being that TransactionManager uses TLS to store the created instance. Since there's no guarantee that disposal of TransactionManager will be done on the same thread where it was created, it usually never gets removed from TLS. If the next request reuses the uncleaned thread's TLS, it gets old TransactionManager and its transaction which is either already committed or rolled back.

- ngm

 

RockfordLhotka replied on Wednesday, March 06, 2013

I wonder how (or if) CSLA can help with this scenario.

The only thing that comes to mind would be to create a synchronization context along with the TransactionScope object, and then change the await calls so they use the sync context (right now they ignore any sync context on the server).

RockfordLhotka replied on Wednesday, March 06, 2013

Or I could do the easy thing, which is to through a NotSupportedException if you use transactionscope _and_ have an async root data portal method :)

ngm replied on Wednesday, March 06, 2013

RockfordLhotka

Or I could do the easy thing, which is to through a NotSupportedException if you use transactionscope _and_ have an async root data portal method :)

Hehe, that's what TransactionScope does, we can do better in CSLA.

With regards to ConnectionManager and TransactionManager, I like the concepts so much that I would hate to see it unusable with async methods.

I've just tried quick and dirty replacement of all calls from ApplicationContext.LocalContext to CallContext:

 

 

mgr = (ConnectionManager<C>)CallContext.LogicalGetData(ctxName);

if (mgr == null)

{

mgr = new ConnectionManager<C>(database, label);

CallContext.LogicalSetData(ctxName, mgr);

}

and disposal:

 

 

_connection.Dispose();

CallContext.FreeNamedDataSlot(GetContextName(_connectionString, _label));

 

 

 

It seems like it's working pretty well when threads are switching within using block. However, I didn't test some advanced scenarios such as nesting.

 

I think that CSLA should get rid of TLS dependencies everywhere it can. It would be nice to harden CSLA for async world what doesn't seem like too far away.

 

- ngm

 

RockfordLhotka replied on Wednesday, March 06, 2013

I should point out a couple things.

First, in 4.5.20 all the server-side await statements are now free-threaded (ignoring any sync context)

https://github.com/MarimerLLC/csla/issues/30

Second, in a future release I do intend on switching the default ApplicationContext context manager away from TLS and HttpContext.

https://github.com/MarimerLLC/csla/issues/29

The complexity of this is with WinRT, where it sounds like ExecutionContext either doesn't exist, or the API isn't public. So WinRT will either need to continue to use the existing context manager (I think it uses a static cache), or I'll need to develop my own threadsafe cache. I was meeting with the Microsoft guys responsible for the TPL and async/await a couple weeks ago and it is clear that ExecutionContext would probably be ideal, but the custom cache might be the smartest option to be consistent across all platforms.

ngm replied on Wednesday, March 06, 2013

RockfordLhotka

I should point out a couple things.

First, in 4.5.20 all the server-side await statements are now free-threaded (ignoring any sync context)

https://github.com/MarimerLLC/csla/issues/30

I see.

Regarding proposed solution above for TransactionScope's thread affinity, still it's critical to clear the context prior to cascading the call. Even though TransactionalPortal doesn't capture context, calls within data access might be capturing it and thus deadlocking. This will probably occur in the simple ASP.NET setup where AspNetSynchronizationContext will be captured by anything trivial like await sqlConnection.OpenAsync().

I don't know for you guys at Magenic, but for us inability to have async data access wrapped in global transaction is huge issue

RockfordLhotka

Second, in a future release I do intend on switching the default ApplicationContext context manager away from TLS and HttpContext.

https://github.com/MarimerLLC/csla/issues/29

The complexity of this is with WinRT, where it sounds like ExecutionContext either doesn't exist, or the API isn't public. So WinRT will either need to continue to use the existing context manager (I think it uses a static cache), or I'll need to develop my own threadsafe cache. I was meeting with the Microsoft guys responsible for the TPL and async/await a couple weeks ago and it is clear that ExecutionContext would probably be ideal, but the custom cache might be the smartest option to be consistent across all platforms.

.

Yeah, WinRT breaks the chain, but that might be good thing since it's probably going to prioritize replacement of this very old infrastructure which now becomes pretty vital.

Consolidating it by building one from scratch might be a good thing for CSLA. 

Still WinRT is not server technology and it seems that server-side interaction in CSLA is most affected (client and global context along with culture not flowing). It seems to me that smart use of logical call context might temporary solve the issues on the server-side Data Portal. When saying smart, I mean it's easy to have call context "overflowing" such as across app domains or to wrong threads but on the other hand it doesn't lock you as TLS does. Also, it's pretty heavy object to be used for everything.

- ngm

 

ngm replied on Wednesday, March 06, 2013

RockfordLhotka

I wonder how (or if) CSLA can help with this scenario.

The only thing that comes to mind would be to create a synchronization context along with the TransactionScope object, and then change the await calls so they use the sync context (right now they ignore any sync context on the server).

Sure it can help, CSLA can always help ;)

Well, I don't think that awaits on Data Portal ignore synchronization context, quite contrary. The only question is what synchronization context is there in place, but they capture it, if there's one set.

Back to the issue, TransactionScope seems like it came from CSLA kitchen ;) i.e. it relies on TLS. That's the reason it throws exception when disposed on different thread.

What CSLA can do is to make sure that TransactionalDataPortal disposes TransactionScope only on the thread serving the request - the one that created TransactionScope.

It can be achieved by creating your own Synchronization Context that would allow getting back to request thread but I think it's overkill for this particular scenario, unless it turns out that we need to rescue some other execution paths as well.

What I would do first, is to have TransactionalDataPortal block on cascaded call for result, so instead of:

 

result = await portal.Update(obj, context, isSync);

it would invoke:

result = portal.Update(obj, context, isSync).Result;

 

The most important part here is to clear current Synchronization Context so that async completions lower in the stack do not try to get back on the context and thus end up deadlocking with this invocation above:

 

var currentCtx = SynchronizationContext.Current;

 

SynchronizationContext.SetSynchronizationContext(null);

result = portal.Update(obj, context, isSync).Result;

SynchronizationContext.SetSynchronizationContext(currentCtx);

 

The drawback to this approach is that the Synchronization Context won't be available lower in the stack, but data access or service interaction here would rarely need it anyway.

- ngm

 

Copyright (c) Marimer LLC