Udostępnij za pośrednictwem


(WF4, Transactions) Implementation of Transactions in WF4, and Implications

Although I haven’t used this feature in my regular work, I’ve known for a while that WF4.0 ships with a palette of activities designed to support distributed transactions. A distributed transaction is when you have a transaction, which is an atomic commit of a set of operations with a consistency guarantee, implemented in a multi-party scenario, so that all parties know the transaction either completes or fails atomically. This idea has some obvious implications in banking and e-commerce applications.

In WF4, more specifically all of these transactional activities are implemented based on Microsoft’s .Net DTC model, using the features of System.Transactions namespace.

And we’re going to get even one step more specific. There are two programming models offered by the System.Transactions namespace, an explicit model based on the Transaction class, and an implicit model based on the System.Transactions.TransactionScope class. And WF4 models transactions using an implicit model based on the System.Transactions.TransactionScope concept.

The WF4/.Net Transaction model in a nutshell

Link - Here is the MSDN overview of Transactions in WF4. To try to save you switching web pages, I’ll reproduce some selected text here:

“WF provides support for participating in System.Transactions transactions by using the TransactionScope activity to scope a transacted unit of work. While the System.Transactions.TransactionScope must be explicitly completed the System.Activities.Statements.TransactionScope activity implicitly calls complete on the transaction upon successful completion. Any activities that are contained in the Body of the TransactionScope activity participate in the transaction.”

This text is trying to draw an analogy between System.Activities.Statements.TransactionScope and System.Transactions.TransactionScope. Since the two classes have the same name and long qualifying namespaces, let’s give them some nicknames, SAS-TransactionScope and ST-TransactionScope. The analogy doesn’t really stand on its own without knowing what a canonical ST-TransactionScope sample app would look like, so... what would it look like?

From ‘Implementing an implicit transaction using TransactionScope (MSDN)’:

  void SomeMethod()

  {

  using (TransactionScope scope = new TransactionScope())

  {

  /* Perform transactional work here */

  scope.Complete();

  }

  }

Uh. WHAT? This sample is still useless! Because we don’t know what is legitimate transactional work! Is ‘i++’, which increments a variable transactional work? Probably not… so what can actually go inside here?

Well, it turns out that one example of ‘transactional work’ is integrated support for System.Transactions in ADO.Net. And there we will find a real sample showing what using TransactionScope looks like (MSDN Sample:

 

    // This function takes arguments for the 2 connection strings and commands in order

    // to create a transaction involving two SQL Servers. It returns a value > 0 if the

    // transaction committed, 0 if the transaction 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 RDBMS such as Oracle 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();

 

        // Create the TransactionScope in which to execute the commands, guaranteeing

        // that both commands will commit or roll back as a single unit of work.

        using (TransactionScope scope = new TransactionScope())

        {

            using (SqlConnection connection1 = new SqlConnection(connectString1))

            {

                try

                {

                    // 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 by opening connection2

                    // only when there is a chance that the transaction can commit.

                    using (SqlConnection connection2 = new SqlConnection(connectString2))

                        try

                        {

                            // The transaction is promoted 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);

                        }

                        catch (Exception ex)

                        {

                            // Display information that command2 failed.

                            writer.WriteLine("returnValue for command2: {0}", returnValue);

                            writer.WriteLine("Exception Message2: {0}", ex.Message);

                        }

                }

                catch (Exception ex)

                {

                    // Display information that command1 failed.

                    writer.WriteLine("returnValue for command1: {0}", returnValue);

                    writer.WriteLine("Exception Message1: {0}", ex.Message);

                }

            }

 

            // If an exception has been thrown, Complete will not

            // be called and the transaction is rolled back.

      scope.Complete();

        }

 

        // The returnValue is greater than 0 if the transaction committed.

        if (returnValue > 0)

        {

            writer.WriteLine("Transaction was committed.");

        }

        else

        {

            // You could write additional business logic here, notify the caller by

            // throwing a TransactionAbortedException, or log the failure.

            writer.WriteLine("Transaction rolled back.");

        }

 

        // Display messages.

        Console.WriteLine(writer.ToString());

 

        return returnValue;

    }

 

)

By the way, it also turns out that the way ADO.Net does this, and also the way that you can define your own custom transactional work if you want to, is by implementing a (System.Transactions) ResourceManager. What does a ResourceManager basically do? A ResourceManager basically handles all the nitty-gritty implementation details of making your custom action really transactional, like making sure your custom transactional work can engage in a two-phase commit . Let’s stop going deeper there for now.

Returning to that quote explaining SAS-TransactionScope, things should now be starting to make sense, but to check, here is my own statement of what SAS-Transaction scope does:

“SAS-TransactionScope sets up a ST-TransactionScope on the current logical workflow thread so that when you execute any other activity, such as a custom CodeActivity, System.Transactions.TransactionScope.Current can be resolved. And therefore calls to TransactionScope aware frameworks can work inside Code Activities, just as if they are written inside of regular C# TransactionScope statements.”

The current logical workflow thread

This bit now deserves its own section. The WF4 threading model is a little interesting, and there are some good reasons.

Reason # 1: Workflows should be able to be resumed on a different thread than the one they were started on. In fact, it can be a different thread, in a different process than the one it was started on. This is to enable the core features of WF4 - persistence, long-runningness.

Reason # 2: Workflows doing asynchronous work should be able to share OS threads efficiently. In fact, you should be able to have thousands of e.g. AsyncCodeActivities, all in mid-execution using a single OS thread. This parsimonious use of OS thread resources is a decision enabling good performance and scalability characteristics.

What this means is that workflows need to know how to share OS threads with each other without conflicting on their own notion of thread-local state. And actually, thread-local state crops up all the time in the .Net framework! And System.Transactions.TransactionScope.Current is one of those times – it is actually a thread-static property!

So… how does one TransactionScope activity avoid conflicting with other TransactionScope activities that are doing asynchronous work on the same OS thread? The answer is Execution Properties, and especially an execution property of type RuntimeTransactionHandle which executes IExecutionProperty.

The basic flow is

1) TransactionScope creates the RuntimeTransactionHandle and sets it as an execution property
2) the workflow runtime continues running scheduled child activities
3) at some point, the workflow becomes idle waiting for asynchronous work to complete or bookmarks to resume
4) the workflow runtime calls IExecutionProperty.CleanupWorkflowThread() on all active execution properties including the RuntimeTransactionHandle
5) the workflow runtime continues running other workflow instances
6) some workflow runtime (could be a new one, could be the same one as before) reloads our workflow, and calls IExecutionProperty.SetupWorkflowThread() on all its execution properties including the RuntimeTransactionHandle, which restores current thread’s ST-TransactionScope.Current.
7) child activities thereby continue to run in the original ST-TransationScope

Consequences of the above

You can use ST-TransactionScope.Current inside AsyncCodeActivity BeginExecute() and EndExecute(). However, any asynchronous work you schedule is going to run on a different .Net thread than the main workflow dispatch thread (which is the thread calling BeginExecute()/EndExecute()), and you can’t automatically use ST-TransactionScope.Current from that other thread*.

You should, however, be able to capture the value of ST-TransactionScope.Current during BeginExecute(), and use that captured value to update ST-TransactionScope.Current, or flow the transaction across to another thread while you are doing work on that other thread. (It’s also good to be tidy and unflow it when you are done - you wouldn’t want random code executing on the transaction scope.)

Basic example problem scenario (Disclaimer! This is untested and ‘how-not-to-do-it’ code. Do not use any part of this code as basis for anything. You have been hereby been warned.)

 

public sealed class ProblemTransactionAsyncActivity : AsyncCodeActivity*

{

    // Define an activity input argument of type string

    public InArgument<string> Text { get; set; }

 

    protected override IAsyncResult BeginExecute(AsyncCodeActivityContext context, AsyncCallback callback, object state)

    {

        Task<object> DoAsyncWork = new TaskFactory<object>().StartNew(() =>

            {

                // do transactional work... uh oh! Where's ST-TransactionScope.Current?

            });

 

        //... whatever

    }

 

    protected override void EndExecute(AsyncCodeActivityContext context, IAsyncResult result)

    {

        //... whatever;

    }

}

 

(*you can of course, also encounter this issue if you try to use Task<TResult> or any other off-thread operation inside a regular CodeActivity too. I mention this just in case.)