I have N process to run over SQL Server 2008. If any of the processes fails I need to rollback all the others.
I was thinking to use the TPL creating a parent task and N child task. All of this enclosed with a transactionScope (IsolationLevel.ReadCommitted) but in my example below, the child2 throws an error(customers2 is not a valid table) and the child1 doesn't rolled back.
Am I assuming something wrong here? is there other way to manage this scenario?
Here is my test code:
edit I modified the code as below using the DependClone on the current transaction. I think is working.
try
{
using (TransactionScope mainTransaction = TransactionUtils.CreateTransactionScope())
{
var parentTransactionClone1 = Transaction.Current.DependentClone(DependentCloneOption.BlockCommitUntilComplete);
var parentTransactionClone2 = Transaction.Current.DependentClone(DependentCloneOption.BlockCommitUntilComplete);
var parentTask = Task.Factory.StartNew(() =>
{
var childTask1 = Task.Factory.StartNew(() =>
{
using (TransactionScope childScope1 = new TransactionScope(parentTransactionClone1))
{
SqlConnection cnn = new SqlConnection("Server=.\\sqlexpress;Database=northwind;Trusted_Connection=True;");
cnn.Open();
SqlCommand cmd = new SqlCommand("update customers set city ='valXXX' where customerID= 'ALFKI'", cnn);
cmd.ExecuteNonQuery();
cnn.Close();
childScope1.Complete();
}
parentTransactionClone1.Complete();
}, TaskCreationOptions.AttachedToParent);
var childTask2 = Task.Factory.StartNew(() =>
{
using (TransactionScope childScope2 = new TransactionScope(parentTransactionClone2))
{
SqlConnection cnn = new SqlConnection("Server=.\\sqlexpress;Database=northwind;Trusted_Connection=True;");
cnn.Open();
SqlCommand cmd = new SqlCommand("update customers2 set city ='valyyy' where customerID= 'ANATR'", cnn);
cmd.ExecuteNonQuery();
cnn.Close();
childScope2.Complete();
}
parentTransactionClone2.Complete();
}, TaskCreationOptions.AttachedToParent);
});
parentTask.Wait();
mainTransaction.Complete();
}
}
catch (Exception ex)
{
// manage ex
}
public static TransactionScope CreateTransactionScope()
{
var transactionOptions = new TransactionOptions();
transactionOptions.IsolationLevel = IsolationLevel.ReadCommitted;
transactionOptions.Timeout = TransactionManager.MaximumTimeout;
return new TransactionScope(TransactionScopeOption.Required, transactionOptions);
}
Task Parallel Library cannot figure out on its own the details of the task, and it won't be rolling back automatically, the closest you can do is from the parent task you define another task child1-rollback that gets executed only if child1 fails, and you can define this very nicely by specifying a TaskContinuationOption set to OnlyOnFailure so the task will execute only if child1 fails, same can be said about child2.
The TransactionScope class sets the ambient transaction for the current thread (see also Transaction.Current only.
You should at least assume that each task runs in a separate thread (although that is not a necessity with the TPL).
Review the "important" box in the remarks section of the relevant article - if you want to share a transaction between threads, you need to use the DependentTransaction class.
Personally, I am sure that the whole facility to share a transaction amongst multiple threads works technically, however, I have always found it easier to write up a design that uses a seperate transaction per thread.