TransactionScope throws TransactionAbortedException when Disposed

4.1k views Asked by At

I have the following scenario:

  • Parent process start TransactionScope, a token identifying the transaction is created using TransactionInterop.GetTransmitterPropagationToken, inserts data to database. TransactionScope completes.
  • Another process is started, using the above mentioned token it creates Transaction which is then used to create TransactionScope. This process also inserts data to database. TransactionScope completes and is disposed.
  • Parent process tries to dispose its TransactionScope at this point, and TransactionAbortedException is thrown. The exception doesn't give any reasons for not being able to commit.

Stack Trace:

at System.Transactions.TransactionStatePromotedAborted.BeginCommit(InternalTransaction tx, Boolean asyncCommit, AsyncCallback asyncCallback, Object asyncState)
at System.Transactions.CommittableTransaction.Commit()
at System.Transactions.TransactionScope.InternalDispose()
at System.Transactions.TransactionScope.Dispose()
at DistributedTransactions.Program.Main() in c:\Users\agolan.ALLSHARE\Documents\Visual Studio 2013\Projects\DistributedTransactions\DistributedTransactions\Program.cs:line 44
at System.AppDomain._nExecuteAssembly(RuntimeAssembly assembly, String[] args)
at System.AppDomain.ExecuteAssembly(String assemblyFile, Evidence assemblySecurity, String[] args)
at Microsoft.VisualStudio.HostingProcess.HostProc.RunUsersAssembly()
at System.Threading.ThreadHelper.ThreadStart_Context(Object state)
at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
at System.Threading.ThreadHelper.ThreadStart()

Here is the code of the application:

internal class Program {
    private static void Main() {
        string connString = "data source=.;initial catalog=Test;integrated security=True;persist security info=True";
        string tokenFile = @"c:\Temp\token.txt";

        Transaction transaction = null;
        bool isChild = false;
        if (File.Exists(tokenFile)) {
            isChild = true;
            string tokenString = File.ReadAllText(tokenFile);
            byte[] token = Convert.FromBase64String(tokenString);
            transaction = TransactionInterop.GetTransactionFromTransmitterPropagationToken(token);
        }
        using (var transactionScope = transaction != null ? new TransactionScope(transaction) : new TransactionScope(TransactionScopeOption.Required, new TimeSpan(0, 15, 0))) {
            var curr = Transaction.Current;
            if (!isChild) {
                byte[] transactionBytes = TransactionInterop.GetTransmitterPropagationToken(curr);

                string tokenString = Convert.ToBase64String(transactionBytes);
                File.WriteAllText(tokenFile, tokenString);
            }
            using (var conn = new SqlConnection(connString)) {
                conn.Open();
                using (SqlCommand cmd = conn.CreateCommand()) {
                    Console.WriteLine("Enter id and value");
                    cmd.CommandText = "INSERT INTO KeyValue(Id, Value) VALUES (@1, @2)";
                    cmd.Parameters.Add(new SqlParameter("@1", Console.ReadLine()));
                    cmd.Parameters.Add(new SqlParameter("@2", Console.ReadLine()));
                    cmd.ExecuteNonQuery();
                }
            }
            transactionScope.Complete();

            Console.WriteLine("Dispose");
            Console.ReadLine();
        }
    }
}

The questions:

  • Why is it not able to commit?
  • Is it possible to use TransactionScope in this scenario (I read answer Using transactions across processes but I would like to use TransactionScope)? How?
1

There are 1 answers

0
Hasani Blackwell On BEST ANSWER

I was able to replicate this locally and it happens because the other application has exited before the parent process has committed the transaction. The other applications must continue running until the parent commits or rolls back the transaction. One way of doing this is preventing the applications from exiting until the TransactionCompleted event is fired.

I revised your code to use a ManualResetEventSlim to make the child process wait for the parent transaction process to complete before exiting.

internal class Program
{
    private static void Main()
    {
        string connString = "data source=.;initial catalog=Test;integrated security=True;persist security info=True";
        string tokenFile = @"c:\Temp\token.txt";

Transaction transaction = null; bool isChild = false; if (File.Exists(tokenFile)) { isChild = true; string tokenString = File.ReadAllText(tokenFile); byte[] token = Convert.FromBase64String(tokenString); transaction = TransactionInterop.GetTransactionFromTransmitterPropagationToken(token); } using (var parentTxCompleteEvent = new ManualResetEventSlim(!isChild)) { using (var transactionScope = transaction != null ? new TransactionScope(transaction) : new TransactionScope(TransactionScopeOption.Required, new TimeSpan(0, 15, 0))) { var curr = Transaction.Current; if (!isChild) { byte[] transactionBytes = TransactionInterop.GetTransmitterPropagationToken(curr); string tokenString = Convert.ToBase64String(transactionBytes); File.WriteAllText(tokenFile, tokenString); } else { transaction.TransactionCompleted += (sender, e) => parentTxCompleteEvent.Set(); } using (var conn = new SqlConnection(connString)) { conn.Open(); using (SqlCommand cmd = conn.CreateCommand()) { Console.WriteLine("Enter id and value"); cmd.CommandText = "INSERT INTO KeyValue(Id, Value) VALUES (@1, @2)"; cmd.Parameters.Add(new SqlParameter("@1", Console.ReadLine())); cmd.Parameters.Add(new SqlParameter("@2", Console.ReadLine())); cmd.ExecuteNonQuery(); } } transactionScope.Complete(); Console.WriteLine("Dispose"); Console.ReadLine(); } parentTxCompleteEvent.Wait(); } }

}