Services component transaction synchronization
We had interesting problem @work. We use serviced components as distributed transactions backend and we tried to use ReadCommited synchronization level to boost performance.
However we use some product that requires Serialized synchronization level – this is a problem because you can't run on single transaction (unless it uses serialized synchronization level).
So we thought of the following solution – run the components that requires serialized as RequiredNew (instead of Required – meaning, each instance of the services component will use a new transaction, instead of sharing existing one).
So far are you with me? Hope so.
OK so we have to different transaction but how can we control the commit / abort process if we need to run both of them using data calculated in each stage on different transactions? (see the flow below for example).
We need to write a little bit of code. But before that lets define the problems, the tools we have and than come up with a solution.
The problem: synchronizing two transactions – commit together / rollback together.
General flow
- Run code on T1
- Run code on T2
- Run code on T1
- Run code on T2
- if has error rollback T2 + T1
- else commit T2 + T1
The tools at our disposal: ContextUtil , [AutoComplete]
- ContextUtil – setting done bit and consistent bit (more info)
- [AutoComplete] – method level attribute that automatically sets the bits (on exception aborts trasaction) (more info)
The solution:
Manually use SetComplete / SetAbort in T2 context, in order to commit / rollback T2, without using AutoComplete attribute.
The Code in charge of T2 must provide commit / rollback public methods (they will use SetComplete / SetAbort inorder to run on T2 context).
Code snippet:
[Transaction(TransactionOption.Required)]
[Guid("....")]
public class Tran1 :ServicedComponent
{
[AutoComplete]
public void DoWork()
{
string data = GetDataOnTran1();
using (Tran2 tran2 = new Tran2)
{
try{
//do work with data from transaction 1 on transaction 2
//becarefull about using resources that haven't been commited yey
//such as tables with FK
string processedData = tran2.DoWork(data)
//process data returned from transaction 2 on transaction 1
string evenMoreProcessedData = DoMoreWork(processedData);
//processes data again on trasaction 2
data = tran2.DoMoreWork(evenMoreProcessedData );
//Manualy commit data on trasaction 2 (trasaction 1 not commited yet)
tran2.CommitData();
}
catch
{
//notice the manul rollback of transaction 2
tran2.RollbackData();
throw;
}
}
}
private string DoMoreWork(string someWork)
{
...
}
}
[Transaction(TransactionOption.RequiredNew)] //new trasaction
[Guid("....")]
public class Tran2 :ServicedComponent
{
//no autocomplete here
public string DoWork()
{
...
}
private string DoMoreWork(string someWork)
{
...
}
public void CommitData()
{
//notice that when using context util methods, they run in CONTEXT of the transaction, meaning on transaction 2
ContextUtil.SetComplete();
}
public void RollbackData()
{
//notice that when using context util methods, they run in CONTEXT of the transaction, meaning on transaction 2
ContextUtil.SetAbort();
}
}
Final remarks:
The solution is not bullet proof – in case of error/timeout when T2 is already committed, there's no way to perform rollback and we get inconsistent data (T1 will perform rollback). However this is better that nothing