My orignal code is using TransactionScope and Async/Await.
There is a DispatcherTimer on my WPF application and there are some code in the tick as below:
List<string> connectionString = new List<string>();
foreach (string connStr in connectionString)
{
using (TransactionScope transactionScope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled))
{
try
{
await CallSQL1(connStr);
await CallSQL2(connStr);
transactionScope.Complete();
}
catch (Exception ex)
{
Console.WriteLine("Call Sql Error");
}
}
}
public function CallSQL1 and CallSQL2 as below
public async Task<int> CallSQL1(string connStr)
{
try
{
using (MySqlConnection myConn = new MySqlConnection(connStr))
{
await myConn.OpenAsync();
string sCmd = "INSERT INTO machine_info VALUES ('CCC','099','AOT','2020-02-02 02:00:00')";
using (MySqlCommand myCmd = new MySqlCommand(sCmd, myConn))
{
int result = await myCmd.ExecuteNonQueryAsync();
return 1;
}
}
}
catch (MySqlException ex)
{
throw new Exception("Error Occur");
}
}
public async Task<int> CallSQL2(string connStr)
{
try
{
using (MySqlConnection myConn = new MySqlConnection(connStr))
{
await myConn.OpenAsync();
string sCmd = "INSERT INTO machine_status VALUES ('workpiece','10','IDLE','9','3030-03-03 03:00:00')";
using (MySqlCommand myCmd = new MySqlCommand(sCmd, myConn))
{
int result = await myCmd.ExecuteNonQueryAsync();
return 1;
}
}
}
catch (Exception ex)
{
throw new Exception("Error Occur");
}
}
List store 3 different database connection information. Every tick, I will connect to 3 database and call 2 function (CallSQL1, CallSQL2), each function will insert data to different table.
If error happened, TransactionScope will make sure it will rollback. Everything work good for now!
And then I am thinking, maybe I can improve it. To make it become parallel work by using Task.Add and Task.WhenAll. So I chagne code as below:
List<Task> disTaskList = new List<Task>();
foreach (string connStr in connectionString)
{
using (TransactionScope transactionScope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled))
{
try
{
disTaskList.Add(CallSQL1(connStr));
disTaskList.Add(CallSQL2(connStr));
transactionScope.Complete();
}
catch (Exception ex)
{
Console.WriteLine("Call Sql Error Excption = ");
}
}
await Task.WhenAll(disTaskList);
}
Now the error show at last line ‘await Task.WhenAll(disTaskList);’
System.Collections.Generic.KeyNotFoundException: ‘The given key was not present in the dictionary.’
Is there something wrong with my thinking? My idea is If there is error occur, then transactionScope will rollback disTaskList.Add for this database. After foreach 3 database I can await the rest Task in disTaskList.
For Example:
- DB1 CallSQL1 ok -> disTaskList.Add(CallSQL1(connStr)), disTaskList.count = 1.
- DB1 CallSQL2 error -> transactionScope will rollback, disTaskList.count = 0.
- DB2 CallSQL1 ok -> disTaskList.Add(CallSQL1(connStr)), disTaskList.count = 1.
- DB2 CallSQL2 ok -> disTaskList.Add(CallSQL2(connStr)), disTaskList.count = 2.
- DB3 CallSQL1 ok -> disTaskList.Add(CallSQL1(connStr)), disTaskList.count = 3.
- DB3 CallSQL2 ok -> disTaskList.Add(CallSQL2(connStr)), disTaskList.count = 4.
Can anyone give me some suggestion?
2
Answers
The likely reason is that you are disposing/completing the transaction before both SQL calls have completed.
The idea of running multiple operations in parallel, inside a single transaction, seem weird to me, but I’m not a database expert. Are you sure you should not be using separate transaction scopes for each operation?
You should at the very least move the Task.Wait inside the scope:
Or just skip the async stuff, In the version of MySql I’m using truly asynchronous operations are not supported, and they will block in practice.
Your initial approach was better. Your second solution with
Task.WhenAll
completes the transaction when the tasks will still be executing.The transaction completion may be attempted mid operation, which you really do not want.
The whole point of a transaction is to ensure that the database does not commit part of a sequence of operations that need to occur together.
If your
CallSQL1
&CallSQL2
don’t need to be committed together then you do not need a transaction at all.If you want to stick with
Task.WhenAll
, then you must await before the transaction is complete.