Using transactions across processes

2019-07-10 17:30发布

问题:

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

回答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.