Understanding how transaction.complete function? - c#

Hi All we have started to use transaction scope and below is the code snippet. What we need to understand is based on our understading after each of the using for connection the particular connection will be disposed/closed right? So since its closed how does the transaction.complete works?
using (TransactionScope transScope = new TransactionScope())
{
try
{
string myConnStringLocal = "User Id=***;Password=****;Host=" + globalSettings.settingLocalIP + ";Database=" + globalSettings.settingLocalDB;
using (MySqlConnection connectionLocal = new MySqlConnection(myConnStringLocal))
{
try{
connectionLocal.Open();
}
Catch(Exception e)
{
}
finally{
connectionLocal.Close();
}
}
string myConnStringCentral = "User Id=***;Password=*****;Host=" + globalSettings.settingCentralIP + ";Database=" + globalSettings.settingCentralDB;
using (MySqlConnection connectionCentral = new MySqlConnection(myConnStringCentral))
{
try{
connectionCentral.Open();
}
Catch(Exception e)
{
}
finally{
connectionCentral.Close();
}
}
string myConnStringCentralCopy = "User Id=*****;Password=*****;Host=" + globalSettings.settingCentralCopyIP + ";Database=" + globalSettings.settingCentralCopyDB;
using (MySqlConnection connectionCentralCopy = new MySqlConnection(myConnStringCentralCopy))
{
try{
connectionCentralCopy.Open();
}
Catch(Exception e)
{
}
finally{
connectionCentralCopy.Close();
}
}
transScope.Complete();
Console.WriteLine("Transaction is completed");
}
catch (Exception)
{
Console.WriteLine("Transaction is rolled back");
}
}

TransactionScope.Complete tells all of the Transaction Managers that they are good to commit this transaction. It is not a guarantee that everything will actually commit. After the Complete method is called all the transaction managers initiate a commit individually and if all of the transaction Managers succeed then the transaction is considered as completed successfully.
You may refer to this link for further details

When you connect to multiple databases in a single TransactionScope, the transaction is escalated to a distributed transaction and coordinated by MSDTC using 2-phase commit.
Regarding connection close - this is special case when connection is closed inside a TransactionScope, internally it is managed by System.Transactions infrastructure, so db session may still be open even though connection closed from your end.
See this note on MSDN:
Pending transactions started using Transact-SQL or BeginTransaction are automatically rolled back when the connection is reset if connection pooling is enabled. If connection pooling is off, the transaction is rolled back after SqlConnection.Close is called. Transactions started through System.Transactions are controlled through the System.Transactions infrastructure, and are not affected by SqlConnection.Close
EDIT Based on your comments, here is what you can do:
try
{
using (TransactionScope transScope = new TransactionScope())
{
string myConnStringLocal = ...;
using (var connectionLocal = new MySqlConnection(myConnStringLocal))
{
connectionLocal.Open();
// do your DB update
} //no need to close connection explicitly, the using() {..} statement does that for you
string myConnStringCentral = ...;
using (var connectionCentral = new MySqlConnection(myConnStringCentral))
{
connectionCentral.Open();
// do your DB update
} //no need to close connection explicitly, the using() {..} statement does that for you
string myConnStringCentralCopy = ...;
using (var connectionCentralCopy = new MySqlConnection(myConnStringCentralCopy))
{
connectionCentralCopy.Open();
// do your DB update
} //no need to close connection explicitly, the using() {..} statement does that for you
transScope.Complete();
Console.WriteLine("Transaction is completed");
} //no need to dispose transactionScope explicitly, the using() {..} statement does that for you
}
catch (Exception)
{
// If any exception occurs in the try block above transScope.Complete() line will be caught here
// and will automatically cause the transaction to rollback.
Console.WriteLine("Transaction is rolled back");
}
// You can then start new TransactionScope if you want to further update more than one DB in a transactional manner.
try
{
using (TransactionScope transScope = new TransactionScope())
{
//...
}
}
catch (Exception)
{
//...
}

When the Complete method is called, then everything in the scope will be committed, if no Exception is thrown. If the code gets out of scope without Complete, then no commit will occur. In short, if you call the Complete method, then, in case there are no Exceptions thrown, your transaction(s) in the scope of the given TransactionScope will be committed.
Also, I must add, that there might also be a hierarchy, a tree of TransactionScopes. You can also set the behavior of a TransactionScope for the case when a sub-scope of the TransactionScope rolled back.

Related

Transient Fault Retry logic best practices

Friends, I have a question about implementing a simple retry policy around the execution of the SQL command.
My question is: should the retry loop encapsulate the construction of the connection and transaction, or should it live inside the connection.
For example:
private void RetryLogSave(DynamicParameters parameters, int retries = 3)
{
int tries = 0;
using (var connection = new SqlConnection(_connectionString))
{
connection.Open();
using (var transaction = connection.BeginTransaction())
{
var logItemCommand = new CommandDefinition(commandText: Constants.InsertLogItem,
parameters: parameters, transaction: transaction, commandType: System.Data.CommandType.Text);
do
{
try
{
tries++;
connection.Execute(logItemCommand);
transaction.Commit();
break;
}
catch (Exception exc)
{
if (tries == retries)
{
transaction.Rollback();
throw exc;
}
Task.Delay(100 * tries).Wait();
}
}
while (true);
}
}
}
Is what I've done here appropriate and acceptable, or should the retry logic live on the outside of the SqlConnection construction?
Formalizing my comments as an answer.
should the retry logic live on the outside of the SqlConnection
construction?
Yes. If doing retry logic with keeping connection opened you're wasting resources. Someone else may use it while you're waiting N seconds for re-try. Opening/closing connections is usually (for most ODBC drivers) implemented on top of Connection Pooling mechanism. You do not actually close it - you allow connection to go back in pool to be reused by someone else. Keeping connections opened during re-try will force system to create more and more new physical connections (because they are not returned to the pool) and eventually your SQL Server will be exhausted.
Regarding re-try mechanism - to not reinvent the wheel, I usually use Polly library.
You can define somewhere static class with list of your polices:
public static class MyPolices
{
// Retry, waiting a specified duration between each retry
public static Policy RetryPolicy = Policy
.Handle<Exception>() // can be more specific like SqlException
.WaitAndRetry(new[]
{
TimeSpan.FromSeconds(1),
TimeSpan.FromSeconds(2),
TimeSpan.FromSeconds(3)
});
}
Then, simplify your method to
private void LogSave(DynamicParameters parameters)
{
using (var connection = new SqlConnection(_connectionString))
{
connection.Open();
using (var transaction = connection.BeginTransaction())
{
// make sure to not forget to dispose your command
var logItemCommand = new CommandDefinition(commandText: Constants.InsertLogItem,
parameters: parameters, transaction: transaction, commandType: System.Data.CommandType.Text);
try
{
// not sure if conn.Execute is your extension method?
connection.Execute(logItemCommand);
transaction.Commit();
}
catch (Exception exc)
{
transaction.Rollback();
throw;
}
}
}
}
and call it like this
MyPolices.RetryPolicy.Execute(() => LogSave(parameters));
This approach will make your code more SOLID keeping retry logic in isolation.

How to rollback a transaction using dapper

I have this:
using (var con= new SqlConnection(ConfigurationManager.ConnectionStrings["sqlcon"].ConnectionString))
{
try
{
// many transactions
}
catch (Exception e)
{
con.BeginTransaction().Rollback();
}
}
Will this work is my question.. I know another method is to make a transaction then open it then rollback.
You could use a TransactionScope variable in a using block at the same level of the using block of the SqlConnection
using (TransactionScope scope = new TransactionScope())
using (var con= new SqlConnection(ConfigurationManager.ConnectionStrings["sqlcon"].ConnectionString))
{
try
{
// many transactions
scope.Complete();
}
catch (Exception e)
{
// Not needed any rollback, if you don't call Complete
// a rollback is automatic exiting from the using block
// con.BeginTransaction().Rollback();
}
}

The operation is not valid for the state of the transaction

I have a TransactionScope() block. It always gets stuck in an insert statement. It appears in the Activity Monitor as a Blocking Task, so it blocks the SQL server, and after the timeout, I get this error:
The operation is not valid for the state of the transaction.
What’s going wrong?
const TransactionScopeOption opt = new TransactionScopeOption();
TimeSpan span = new TimeSpan(0, 0, 1, 30);
try
{
using (TransactionScope scope01 = new TransactionScope(opt, span))
{
using (var sqlcon = new SqlConnection(sSqlCon))
{
//select,insert , update statements
}
}
}
catch (Exception ex)
{
}
It probably means it aborted. Did you call transaction complete within transaction bracket?
try
{
using (TransactionScope scope01 = new TransactionScope(opt, span))
{
using (var sqlcon = new SqlConnection(sSqlCon))
{
//select,insert , update statements
}
scope01.Complete();
}
}
If it doesn't call the Complete, it will automatically rollback.
The transaction might have been timed out. check the maching.config for the default time out
<configuration>
<system.transactions>
<machinesettings maxtimeout="00:30:00" />
</system.transactions>
</configuration>

SQLTransaction has completed error

I got following error once in my application.
This SQLTransaction has completed; it is no longer usable
Stack Trace is attached below – It says about Zombie Check and Rollback.
What is the mistake in the code?
Note: This error came only once.
UPDATE
From MSDN - SqlTransaction.Rollback Method
A Rollback generates an InvalidOperationException if the connection is terminated or if the transaction has already been rolled back on the server.
From Zombie check on Transaction - Error
One of the most frequent reasons I have seen this error showing up in various applications is, sharing SqlConnection across our application.
CODE
public int SaveUserLogOnInfo(int empID)
{
int? sessionID = null;
using (SqlConnection connection = new SqlConnection(connectionString))
{
connection.Open();
SqlTransaction transaction = null;
try
{
transaction = connection.BeginTransaction();
sessionID = GetSessionIDForAssociate(connection, empID, transaction);
//Other Code
//Commit
transaction.Commit();
}
catch
{
//Rollback
if (transaction != null)
{
transaction.Rollback();
transaction.Dispose();
transaction = null;
}
//Throw exception
throw;
}
finally
{
if (transaction != null)
{
transaction.Dispose();
}
}
}
return Convert.ToInt32(sessionID,CultureInfo.InvariantCulture);
}
Stack Trace
REFERENCE:
What is zombie transaction?
Zombie check on Transaction - Error
SqlTransaction has completed
http://forums.asp.net/t/1579684.aspx/1
"This SqlTransaction has completed; it is no longer usable."... configuration error?
dotnet.sys-con.com - SqlClient Connection Pooling Exposed
Thread abort leaves zombie transactions and broken SqlConnection
You should leave some of the work to compiler, to wrap that in a try/catch/finally for you.
Also, you should expect that Rollback can occasionally throw an exception, if a problem occurs in Commit stage, or if a connection to server breaks. For that reason you should wrap it in a try/catch.
try
{
transaction.Rollback();
}
catch (Exception ex2)
{
// This catch block will handle any errors that may have occurred
// on the server that would cause the rollback to fail, such as
// a closed connection.
Console.WriteLine("Rollback Exception Type: {0}", ex2.GetType());
Console.WriteLine(" Message: {0}", ex2.Message);
}
This is copied exactly from MSDN documentation page for Rollback method.
I see that you're worried that you have a zombie transaction. In case you pasted, it doesn't sound like you have a problem. You're transaction has been completed, and you should no longer have anything to do with it. Remove references to it if you hold them, and forget about it.
From MSDN - SqlTransaction.Rollback Method
A Rollback generates an InvalidOperationException if the connection is terminated or if the transaction has already been rolled back on the server.
Rethrow a new exception to tell user that data may not have been saved, and ask her to refresh and review
Note: This error came only once.
then it is very hard to say much; it could be simply that the // Other Code etc simply took to long, and the entire thing got killed. Maybe your connection died, or an admin deliberately killed it because you were blocking.
What is the mistake in the code?
over-complicating it; it can be much simpler:
using (var connection = new SqlConnection(connectionString))
{
connection.Open();
using(var transaction = connection.BeginTransaction())
{
try
{
sessionID = GetSessionIDForAssociate(connection, empID, transaction);
//Other Code
transaction.Commit();
}
catch
{
transaction.Rollback();
throw;
}
}
}
much less code to get wrong.
I use code below can reproduce this error, I use 1000 tasks to execute Sql, after about 300 tasks Successfully Completed, lots of exception about timeout error start to occur on ExecuteNonQuery(),
then next error This SqlTransaction has completed will occur on transaction.RollBack(); and its call stack also contains ZombieCheck().
(If single program with 1000 tasks pressure not enough, you can execute multiple compiled exe file at the same time, or even use multi computers execute to one DataBase.)
So I guess one of the reason cause this error can be something wrong in Connection, then cause the transaction error happens as well.
Task[] tasks = new Task[1000];
for (int i = 0; i < 1000; i++)
{
int j = i;
tasks[i] = new Task(() =>
ExecuteSqlTransaction("YourConnectionString", j)
);
}
foreach (Task task in tasks)
{
task.Start();
}
/////////////
public void ExecuteSqlTransaction(string connectionString, int exeSqlCou)
{
using (SqlConnection connection = new SqlConnection(connectionString))
{
connection.Open();
SqlCommand command = connection.CreateCommand();
SqlTransaction transaction;
// Start a local transaction.
transaction = connection.BeginTransaction();
// Must assign both transaction object and connection
// to Command object for a pending local transaction
command.Connection = connection;
command.Transaction = transaction;
try
{
command.CommandText =
"select * from Employee";
command.ExecuteNonQuery();
// Attempt to commit the transaction.
transaction.Commit();
Console.WriteLine("Execute Sql to database."
+ exeSqlCou);
}
catch (Exception ex)
{
Console.WriteLine("Commit Exception Type: {0}", ex.GetType());
Console.WriteLine(" Message: {0}", ex.Message);
// Attempt to roll back the transaction.
try
{
transaction.Rollback();
}
catch (Exception ex2)
{
// This catch block will handle any errors that may have occurred
// on the server that would cause the rollback to fail, such as
// a closed connection.
Console.WriteLine("Rollback Exception Type: {0}", ex2.GetType());
Console.WriteLine(" Message: {0}", ex2.Message);
}
}
}
}
Besides I find if I commit twice sequentailly will invoke this exception as well.
transaction.Commit();
transaction.Commit();
Or if Connection Closed before commit also invoke this error.
connection.Close();
transaction.Commit();
Update:
I find it strange that I create another new table and insert 500 thousand data to it,
then use 100000 tasks with select * from newtable sql, running 5 programs at the same time, this time the Timeout Error occur, but when transaction.Rollback() it didn't invoke the SQLTransaction has completed error.
but if the Timeout Error occur, jump into the catch block, and in the catch block do transaction.Commit() again, the SQLTransaction has completed error will happen.
I have experienced this error once and i was stuck and unable to know what is going wrong. Actually i was deleting a record and in the Stored procedure i was not deleting its child and specially the delete statement in Stored Procedure was inside the Transaction boundary. I removed that transaction code from stored procedure and got rid of getting this Error of “This SqlTransaction has completed; it is no longer usable.”
This message is simply because that you wrote code that throws an exception after the transaction has been already committed successfully.Please try to check the code you wrote after the Commit method or you can handle it by using Try..Catch and finally Blocks :) .

Entity Framework: How to put multiple stored procedures in a transaction?

I did a lot search already but couldn't find a straight anwser.
I have two stored procedures and they both were function imported to the DBContext object
InsertA()
InsertB()
I want to put them in a transaction. (i.e. if InsertB() failed, rolled back InsertA())
How do I do that? Can I just declare a TransactionScope object and wrap around the two stored procedures?
Thanks
You need to enlist your operations in a transaction scope, as follows:
using(TransactionScope tranScope = new TransactionScope())
{
InsertA();
InsertB();
tranScope.Complete();
}
On error, the transaction scope will automatically be rolled back. Of course, you still need to handle exceptions and do whatever your exception handling design dictates (log, etc). But unless you manually call Complete(), the transaction is rolled back when the using scope ends.
The transaction scope will not be promoted to a distributed transaction unless you open other database connections in the same transaction scope (see here).
This is important to know because otherwise you would need to configure MSDTC on all your servers involved in this operation (web, middle tier eventually, sql server). So, as long as the transaction isn't promoted to a distributed one, you'll be fine.
Note:
In order to fine-tune your transaction options, such as timeouts and isolation levels, have a look at this TransactionScope constructor. Default isolation level is serializable.
Additional sample: here.
You can use the TransactionScope object or you can use the SqlConnection.BeginTransaction Method. Be careful using TransactionScope, transactions can be esculated to distributed transactions when calling stored procedures in a different database. Distributed
transactions can be resource intensive.
How to use sqlConnection.BeginTransaction...(http://msdn.microsoft.com/en-us/library/86773566.aspx)
private static void ExecuteSqlTransaction(string connectionString)
{
using (SqlConnection connection = new SqlConnection(connectionString))
{
connection.Open();
SqlCommand command = connection.CreateCommand();
SqlTransaction transaction;
// Start a local transaction.
transaction = connection.BeginTransaction("SampleTransaction");
// Must assign both transaction object and connection
// to Command object for a pending local transaction
command.Connection = connection;
command.Transaction = transaction;
try
{
command.CommandText =
"Insert into Region (RegionID, RegionDescription) VALUES (100, 'Description')";
command.ExecuteNonQuery();
command.CommandText =
"Insert into Region (RegionID, RegionDescription) VALUES (101, 'Description')";
command.ExecuteNonQuery();
// Attempt to commit the transaction.
transaction.Commit();
Console.WriteLine("Both records are written to database.");
}
catch (Exception ex)
{
Console.WriteLine("Commit Exception Type: {0}", ex.GetType());
Console.WriteLine(" Message: {0}", ex.Message);
// Attempt to roll back the transaction.
try
{
transaction.Rollback();
}
catch (Exception ex2)
{
// This catch block will handle any errors that may have occurred
// on the server that would cause the rollback to fail, such as
// a closed connection.
Console.WriteLine("Rollback Exception Type: {0}", ex2.GetType());
Console.WriteLine(" Message: {0}", ex2.Message);
}
}
}
}
How to use TransactionScope...(http://msdn.microsoft.com/en-us/library/system.transactions.transactionscope.aspx)
// This function takes arguments for 2 connection strings and commands to create a transaction
// involving two SQL Servers. It returns a value > 0 if the transaction is committed, 0 if the
// transaction is rolled back. To test this code, you can connect to two different databases
// on the same server by altering the connection string, or to another 3rd party RDBMS by
// altering the code in the connection2 code block.
static public int CreateTransactionScope(
string connectString1, string connectString2,
string commandText1, string commandText2)
{
// Initialize the return value to zero and create a StringWriter to display results.
int returnValue = 0;
System.IO.StringWriter writer = new System.IO.StringWriter();
try
{
// Create the TransactionScope to execute the commands, guaranteeing
// that both commands can commit or roll back as a single unit of work.
using (TransactionScope scope = new TransactionScope())
{
using (SqlConnection connection1 = new SqlConnection(connectString1))
{
// Opening the connection automatically enlists it in the
// TransactionScope as a lightweight transaction.
connection1.Open();
// Create the SqlCommand object and execute the first command.
SqlCommand command1 = new SqlCommand(commandText1, connection1);
returnValue = command1.ExecuteNonQuery();
writer.WriteLine("Rows to be affected by command1: {0}", returnValue);
// If you get here, this means that command1 succeeded. By nesting
// the using block for connection2 inside that of connection1, you
// conserve server and network resources as connection2 is opened
// only when there is a chance that the transaction can commit.
using (SqlConnection connection2 = new SqlConnection(connectString2))
{
// The transaction is escalated to a full distributed
// transaction when connection2 is opened.
connection2.Open();
// Execute the second command in the second database.
returnValue = 0;
SqlCommand command2 = new SqlCommand(commandText2, connection2);
returnValue = command2.ExecuteNonQuery();
writer.WriteLine("Rows to be affected by command2: {0}", returnValue);
}
}
// The Complete method commits the transaction. If an exception has been thrown,
// Complete is not called and the transaction is rolled back.
scope.Complete();
}
}
catch (TransactionAbortedException ex)
{
writer.WriteLine("TransactionAbortedException Message: {0}", ex.Message);
}
catch (ApplicationException ex)
{
writer.WriteLine("ApplicationException Message: {0}", ex.Message);
}
// Display messages.
Console.WriteLine(writer.ToString());
return returnValue;
}

Categories