Maintaining a Consistent Application State with TPL

Maintaining a Consistent Application State with TPL

  • Comments 6

The aim of this post is to help developers writing applications in which operations may need to be performed and then later undone due to a subsequent failure. It shows a pattern for how to maintain such a consistent application state by utilizing functionality from the Task Parallel Library (TPL) in the .NET Framework 4.

For the purposes of this blog post, a program/routine is a state machine where each state is equally valid and important and should be handled correctly regardless of whether it belongs to a happy path or not. Specifically, a step that rolls back an incomplete state transition is equally important as a step that makes a forward state transition. A lot could be written about the advantages and disadvantages of exceptions vs. error codes, and such a discussion is not the goal of this post: for the purpose of this article, it is only worth mentioning that throwing and catching exceptions is focused on the happy path (it keeps the code clean and easy to comprehend), while returning and checking error codes is focused on detecting the places of the code (program states) where deviations from the happy path may occur.

In general, a routine that consists of n atomic forward steps, where each forward step may fail and then needs to be undone, could be modeled as the following state machine:


 

 

 

 

 

 

 

 

 

 

 

Assuming a step is atomic means we don't have to undo that particular step if it fails. That is why the undo sequences in the diagram start with the last successful step.

Note: It is also assumed that undoing may not fail. If failures are possible to occur during rollback, then you should strongly consider using a real transactional resource manager like a database server.

Now that we've stated the problem, what is the best way to approach it? Obviously, there are solutions through either error checking or through exception handling, but both of those come at the expense of convoluting the code. Notice that the number of state transition paths is about three times the number of forward steps. Ideally, we are looking for a solution that has a clean happy path as well as clean undo paths without any if statements or separate Exception types for each forward step. In other words, we are looking for a way to describe a program as a state machine.

That's exactly what the TPL does through tasks (as states) and continuations (as state transitions).

Let's take a concrete application as an example and write some real code. Let's say we have a system that manages the process of making a sandwich. In a terminal state our resources, bread and ham, are in the fridge. (For the sake of simplicity, we produce one sandwich at a time.) First, we take a slice of bread out of the fridge. Second, we take a slice of ham out of the fridge. Third, we assemble the bread and the ham into a sandwich. If an operation fails, we have to return whatever ingredients are currently on the line back to the fridge so they don't rot:

And here is the code of our routine:

// HAPPY PATH

Task retrieveBread = Task.Factory.StartNew(RetrieveBreadFromFridge);

Task retrieveHam = retrieveBread.ContinueWith(_ => RetrieveHamFromFridge(),

                                       TaskContinuationOptions.OnlyOnRanToCompletion);

Task assembleSandwich = retrieveHam.ContinueWith(_ => AssembleSandwich(),

                                       TaskContinuationOptions.OnlyOnRanToCompletion);

 

 

// RESET STATE

TaskCompletionSource<bool> reset = new TaskCompletionSource<bool>();

assembleSandwich.ContinueWith(_ => reset.SetCanceled(),

                              TaskContinuationOptions.OnlyOnRanToCompletion);

 

 

// UNDO PATH

Task returnBread = new Task(ReturnBreadToFridge);

returnBread.ContinueWith(_ => reset.SetResult(true),

                         TaskContinuationOptions.OnlyOnRanToCompletion);

 

Task returnHam = new Task(ReturnHamToFridge);

returnHam.ContinueWith(_ => returnBread.Start(),

                       TaskContinuationOptions.OnlyOnRanToCompletion);

 

 

// HAPPY PATH -> UNDO PATH

//      On a failure in bread retrieval - signal "reset".

retrieveBread.ContinueWith(_ => reset.SetResult(true),

                           TaskContinuationOptions.OnlyOnFaulted);

 

//      On a failure in ham retrieval - return bread

retrieveHam.ContinueWith(_ => returnBread.Start(),

                         TaskContinuationOptions.OnlyOnFaulted);

 

//      On a failure in sandwich assembly- return ham and bread

assembleSandwich.ContinueWith(_ => returnHam.Start(),

                              TaskContinuationOptions.OnlyOnFaulted);

 

 

// WAIT

// Log the execution of the HAPPY PATH tasks.

// Additionally, wait on the reset state to get signaled.

Task[] loggedTasks = new Task[]

                         {retrieveBread, retrieveHam, assembleSandwich, reset.Task};

Task.Factory.ContinueWhenAll(loggedTasks,LogErrors)

            .Wait();

As you can see, there are no if statements, nor are there any Exception types customized to provide information what step failed.

Finally, I added a logging task that effectively waits for all the state machine tasks to finish, and then traverses the given task array and logs any error messages. (Notice that I still have to wait for that task to finish.) The body of that method is straightforward:

private void LogErrors(Task[] tasks)

{

    Console.WriteLine("\n\tERRORS:");

    foreach (Task task in tasks)

    {

        if (task.IsFaulted)

        {

            // Use the InnerException since it is wrapped in an AggregateException

            Console.WriteLine("\t{0}", task.Exception.InnerException.Message);

        }

    }

}

In conclusion, TPL enables writing state-sensitive applications on the .NET platform without convoluting the application code with many if statements or customized exception types. While the code is still a little verbose, it closely resembles the state machine's diagram. The attached zip file contains the complete C# project. Feel free to download it and play with it. Your feedback is welcome.

Attachment: ConsistentState.zip
Leave a Comment
  • Please add 3 and 5 and type the answer here:
  • Post
  • Great post!

    I am excited to try this in practice and would be interested to play with the sample C# project. However I couldn't find it on this page. Did I overlook the link to download the zip file or is the link missing?

    Thank you,

    Fred

  • Fred, thank you for pointing out that the zip file was missing. I'm sorry about that. It's there now. Please download it and let me know how it works.

    Thanks,

    Zlatko

  • Thank you Zlatko. I was able to download, compile and execute the sample on my computer. I will spend more time on iy this week-end and give you a more complete feedback early next week.

    Fred

  • Hi Zlatko,

    I spent some time working on the sample. This is an exciting new way to think about and architecture applications. Handling “undo/rollback” steps as separate tasks simplify a lot the coding.

    However I found 2 concerns:

    1. Dealing with complex path

    I am not sure how this approach can be extended to deal with complex path that includes optional paths (or maybe loops). For instance let’s say that after retrieving ham, the next step will be to retrieve ketchup or (exclusively) to retrieve mayonnaise and that this is a runtime decision. How you would schedule the execution of retrieveKetchup or retrieveMayonnaise? In the sample the happy path is hardcoded in the Demo method and I couldn’t find a way to modify it at runtime. Is there a solution to this?

    2. Tasks must all be declared/created in advance

    All tasks must be declared in advance even if they are not used. For instance the undo tasks will be created each time even if the sandwich is assembled successfully. Although I understand that the tasks will not be scheduled for execution, isn’t it a problem to create all those tasks? Is the TPL designed in a way so that it has no performance impact to create additional tasks that eventually are never executed?

    Thank you,

    Fred

  • Hi Fred,

    Thanks for exploring concurrent workflow processing with TPL! This is very interesting feedback.

    If I’m interpreting your first issue correctly, you are suggesting there should be a “switch function” on the continuations that drives the control flow based on parameter other than test execution status. Correct? I think that could be done today – create a task isKetchupRequired that does nothing if a parameter dictates that ketchup must be added and throws otherwise. You can do a similar predicate for mayonnaise. You still have a static state graph.

    There is nothing wrong with declaring the entire state graph in advance. For instance, a drive-through has a hardcoded list of recipes/options they can take – they won’t do anything that’s not on their manual. That’s their static state graph. What a customer provides is parameters that drive the control flow through the state graph. You are right that although unused tasks don’t get executed, it would be better if they never get queued. That’s something we should improve in our future releases.

    Zlatko

  • Hi Zlatko,

    Thank you for your advices. I hope to be able to use all those concepts in an upcoming project that will rely heavily on the TPL. If interesting things come out of this project, I will let you know.

    Best regards,

    Fred

Page 1 of 1 (6 items)