Using transactions across processes
Asked Answered
R

1

3

I'm trying to use System.Transactions (TransactionScope) to coordinate a set of processes, each of which does some database work. Ultimately all processes need to commit or be rolled back atomically via one parent process. Unfortunately, nothing I've tried so far works.

My basic strategy is to TransactionScope in the parent process, save it to a file, and invoke a child process, which loads the file, uses the transaction inside its own TransactionScope, and returns to the parent.

But this doesn't work for me. When I get back from calling the first child, I am sometimes seeing that the parent transaction has been marked as Aborted. Attempting to clone it then throws TransactionAbortedException.

I have also seen exceptions when the second child attempts to deserialize the Transaction, I get a TransactionException with code 0x8004d00e.

I am trying to do what is described in TransactionScope across AppDomains and processes , and http://blogs.microsoft.co.il/blogs/sasha/archive/2010/04/30/propagating-a-transaction-across-appdomains.aspx . Regardless, no luck.

Here are some things I have tried, without success:

  1. Creating a DependentTransaction via DependentClone() in the child process from the loaded transaction
  2. Creating a DependentTransaction via DependentClone() in the parent process, before saving the transaction to a file
  3. Creating a Clone() in the parent process, before saving the transaction to a file
  4. Saving the transaction using serialization
  5. Saving the transaction using TransactionInterop.GetTransactionFromTransmitterPropagationToken()
  6. Explicitly opening the connection before the parent's TransactionScope
  7. Explicitly enlisting the transaction inside the parent
  8. Explicitly enlisting the transaction inside the child
  9. Completing/not completing the scope on the parent
  10. Completing/not completing the scope on the child
  11. Explicitly creating a CommittableTransaction in the parent

Here's one exception message:

System.Transactions.TransactionException: The transaction has already been implicitly or explicitly committed or aborted. ---> System.Runtime.InteropServices.COMException: The transaction has already been implicitly or explicitly committed or aborted (Exception from HRESULT: 0x8004D00E)

And the other (when using DependentClone()):

System.Transactions.TransactionAbortedException: The transaction has aborted. at System.Transactions.TransactionStatePromotedAborted.CreateBlockingClone(In ternalTransaction tx) at System.Transactions.DependentTransaction..ctor(IsolationLevel isoLevel, In ternalTransaction internalTransaction, Boolean blocking) at System.Transactions.Transaction.DependentClone(DependentCloneOption cloneO ption)

Any ideas what I'm doing wrong? I've tried a lot of permutations of this without any luck.

Here is some code (which doesn't attempt to demonstrate all the variants described above):

        // one variant I have tried is to create a CommittableTransaction
        // and pass that in the scope below

        using (TransactionScope scope = new TransactionScope())
        {
            // optionally, do some parent-level EF work

            // invoke child operations in other processes
            DoChildOperation_OutOfProc(1, Transaction.Current);
            DoChildOperation_OutOfProc(2, Transaction.Current);

            scope.Complete();
        }

        // in the variant where I created a CommittableTransaction,
        // I committed it here

    ...

    private static void DoChildOperation_OutOfProc(int id, Transaction transaction)
    {
        string tranFile = string.Format("ChildTran_{0}.txt", id);
        SaveTransactionToFile(transaction, tranFile);

        Process process = new Process();
        process.StartInfo = new ProcessStartInfo(Process.GetCurrentProcess().MainModule.FileName.Replace(".vshost", string.Empty),
            string.Format("-CHILDID={0} -TRANFILE={1}", id, tranFile));
        process.StartInfo.UseShellExecute = false;
        process.Start();
        process.WaitForExit();
    }

    private static void SaveTransactionToFile(Transaction transaction, string tranFile)
    {
        byte[] transactionBytes =
            TransactionInterop.GetTransmitterPropagationToken(transaction);

        string tranFileContents = Convert.ToBase64String(transactionBytes);

        File.WriteAllText(tranFile, tranFileContents);
    }

    private static Transaction LoadTransactionFromFile(string tranFile)
    {
        string tranFileContents = File.ReadAllText(tranFile);
        File.Delete(tranFile);

        byte[] tranBytes = Convert.FromBase64String(tranFileContents);

        Transaction tran = 
            TransactionInterop.GetTransactionFromTransmitterPropagationToken(tranBytes);
        return tran;
    }

    // the child instance of the app runs this after decoding the arguments
    // from DoChildOperation_OutOfProc() and loading the transaction out of the file

    private static void DoChildOperation(int id, Transaction childTransaction)
    {
        // in one variant, I call 
        // childTransaction.DependentClone(DependentCloneOption.BlockCommitUntilComplete)
        // and then use that inside the TransactionScope

        using (TransactionScope scope = new TransactionScope(childTransaction))
        {
            // do EF work and call SaveChanges()

            scope.Complete();
        }

        // if I created a dependent clone, call Complete() here on it
Ritualize answered 26/11, 2012 at 22:28 Comment(5)
that's really nice that you have explained the issues / errors that you are experiencing.. but can you please show the full code block where the Transaction code is being executed..?Procrastinate
I've added some code, a little sanitized but hopefully it makes it more clear.Ritualize
This looks rather hairy. Any reason why you cannot split the work into multiple transactions and then use compensation to roll back if necessary>Raseta
The end goal is to be able to invoke other services to distribute work. Each service should only know about its work, based on information given to it (which may or may not include a transaction). If data gets partially committed then the database will be in a corrupt state, even if I could implement compensation logic. Moreover, compensation logic would likely add additional state, and definitely a lot of extra complexity. The information at the links I referenced makes it appear that you can pass transactions around as I have done, so it's baffling that this doesn't just work for me.Ritualize
I had a similar problem, but it was because I was unloading app domains. I had to keep the app domains alive until the parent transaction was commited, then I unloaded the app domains. --> social.msdn.microsoft.com/Forums/en-US/…Irrefutable
R
1

Okay, the key seems to be that you can use TransactionScope in the parent, but not in the child. For the child, I open the EF connection, call connection.EnlistTransaction() with the passed transaction, and do EF SaveChanges() or transaction.Rollback() but not commit (the Transaction class does not offer this). Doing it this way, it appears that I get the desired behavior.

The gap in my understanding was really whether the transaction was getting nested (as you can do in SQL Server) or not. It appears that it really isn't; it's the same transaction. Note: Even if you create a DependentTransaction using Transaction.DependentClone() in the child, you will still fail if you put it into a TransactionScope.

This proves to be a little unfortunate, because it means that if your process is the parent, you can use TransactionScope, but if it is a child, you cannot.

Ritualize answered 27/11, 2012 at 22:14 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.