Showing posts with label EnlistmentNotification. Show all posts
Showing posts with label EnlistmentNotification. Show all posts

Monday, March 9, 2009

Smart Clients and System.Transactions Part 5 – Fixing the Timeout Problem

This is the fifth installment of an ongoing series about using System.Transactions as the fundament for client-side infrastructure for managing changes among loosely coupled components.

Previous posts: Introduction, Timeout, Enlistments, The transaction sink


Earlier I discussed the time-out problem which was a serious setback to the plan of building a client side change gathering infrastructure based on System.Transactions. How did we fix this problem? Well, we didn't. Instead we had to resort to cheating:

var scopeFactory = IoC.GetInstance<ITransactionScopeFactory>();
using (var scope = scopeFactory.Start())
{
// Transactional code

scope.Complete();
}


 



The above code shows how we now start client side transactions. As you can see, we no longer start a System.Transactions.TransactionScope, but rather look up a factory class (ITransactionScopeFactory) through a service locator, and ask this instance to start a transaction scope. This scope implements the interface ITransactionScope which is defined as  follows:



/// <summary>
/// A scope mimicking the API of System.Transactions.TransactionScope.
/// Defines a single method Complete() used for marking the scope
/// as "successful". This interface extends IDisposable, and when
/// Dispose() is invoked, this scope will instruct the ambient
/// transaction to commit or rollback depending on whether the
/// scope has been completed or not.
/// </summary>
public interface ITransactionScope : IDisposable
{
/// <summary>
/// Marks the scope as complete, resulting in this scope
/// instructing the ambient transaction to commit when
/// Dispose() is invoked later on. If Complete() is never
/// invoked, the scope will force the ambient transaction
/// to rollback upon Dispose().
/// </summary>
void Complete();
}


 



The responsibility of the factory is to determine the type of scope to generate, and this it does by querying the ambient transaction as to whether or not a transaction already has been started. If such a transaction does not exist an instance of ClientTransactionScope is created, and if a transaction already exists, an instance of NestedClientTransactionScope is created. The difference between these two classes lie mainly in their respective constructors and in the Dispose() method:



Constructor and Dispose() of ClientTransactionScope



/// <summary>
/// Initializes a new instance of the <see cref="ClientTransactionScope"/> class.
/// The ambient transaction is automatically started as this instance constructs.
/// </summary>
public ClientTransactionScope()
{
GetClientTransaction().Begin();
}

/// <summary>
/// If Complete() has been invoked prior to this, the
/// ambient transaction will be instructed to commit
/// here, else the transaction will be rolled back
/// </summary>
public virtual void Dispose()
{
if (Completed)
{
GetClientTransaction().Commit();
}
else
{
GetClientTransaction().Rollback();
}
}


 



Constructor and Dispose() of NestedClientTransactionScope



/// <summary>
/// Initializes a new instance of the <see cref="NestedClientTransactionScope"/> class.
/// This constructor does nothing since an ambient transaction already has been
/// started if an instance
/// </summary>
public NestedClientTransactionScope()
{}

/// <summary>
/// If Complete() has been invoked prior to this, nothing happens
/// here. If Complete() has not been invoked, the ambient transaction
/// will be marked as "non commitable". This has ne immediate
/// consequence, but the transaction is doomed and it will be
/// rollback when the outermost scope is disposed regardless of
/// if this scope attempts to rollback or commit the tx.
/// </summary>
public override void Dispose()
{
if (!Completed)
{
GetClientTransaction().MarkInnerTransactionNotCompleted();
}
}


 



The comments in the code explain the distinction between these two classes.



Commit and Rollback



The actual task of finishing the transaction, either  by commit or rollback, is the responsibility of the ambient transaction.  Throughout the lifetime of the scope, enlistments that have detected changes have enlisted with the ambient transaction. The exact details of how this enlistment procedure is done is kept hidden from the enlistments, but what actually happens is that the ambient transaction maintains a dictionary in which the enlistments are added.



When the time to commit or roll back has finally arrived, a real System.Transactions.TransactionScope is started, the registered enlistments are enlisted with the transaction, and Complete() is either invoked or not on the scope depending on whether or not the transaction is meant to be committed or rolled back:



/// <summary>
/// Instructs the transaction to begin the two-phased commit procedure.
/// This will be done except if any nested inner transaction scope
/// have instructed the transaction to rollback prior to this. If this
/// is the case the transaction will roll back the transaction at this
/// point in time.
/// </summary>
public void Commit()
{
if (!canCommit)
{
Rollback();
throw new TransactionAbortedException("Inner scope not completed");
}
using (var scope = new TransactionScope())
{
EnlistAll();
scope.Complete();
}
}

/// <summary>
/// Instructs the transaction to rollback. This will happen at
/// once if the sending scope is the outer scope (fromInnerScope == true)
/// else the rollback will be postponed until when the outer scope
/// requests a commit
/// </summary>
public void Rollback()
{
using (new TransactionScope())
{
EnlistAll();
// Don't Complete the scope,
// resulting in a rollback
}
}

private void EnlistAll()
{
var tx = Transaction.Current;
tx.EnlistVolatile(this, EnlistmentOptions.None);
tx.EnlistVolatile(sink, EnlistmentOptions.None);
foreach (var notification in enlistments.Values)
{
tx.EnlistVolatile(notification, EnlistmentOptions.None);
}
}


 



Conclusion



This concludes this series which has been an attempt at showing the benefits and problems that we have seen when realizing a novel idea: using the functionality of System.Transactions as a "change gathering infrastructure". The idea has proved viable, however the "timeout problem" proved to be a serious bump in the road, and forced us to implement code so that the actual functionality of System.Transactions only comes into play in the final moments of the logical scope.

Thursday, January 29, 2009

Smart Clients and System.Transactions Part 4 – The Transaction Sink

This is the fourth installment of an ongoing series about using System.Transactions as the fundament for client-side infrastructure for managing changes among loosely coupled components.

Previous posts: Introduction, Timeout, Enlistments


This series is supposed to be about something I called "change gathering infrastructure", but up until now there has been precious little change gathering going on. But now at last we're ready to take a closer look at this.

Here's the entire definition of the IAmbientTransaction interface. I've showed it before without the ReceiveChanges signature.

public interface IAmbientTransaction
{
void Enlist(IEnlistmentNotification enlistmentNotification, EnlistmentOptions options);
void ReceiveChanges(string changeKey, object changes);
}


This is the method that the providers use in order to give notice of the changes that they have collected during the current transaction. The natural method where the providers do this is in the Prepare() method, which constitutes the first part of the two phase commit implemented by System.Transactions. So, the Prepare() method of the transactional provider typically looks like this:



public void Prepare(PreparingEnlistment preparingEnlistment)
{
ambientTransaction.ReceiveChanges(typeof(T).FullName, GetChanges());
preparingEnlistment.Prepared();
}


The ambient transaction delegates the task of receiving changes to an object implementing IAmbientTransactionSink. The work is shared between them as follows: the transaction acts as the facade (the providers don't know anything about the sink) and in addition is responsible for knowing when every provider has sent their changes, while the sink knows how to transform all received changes and transform them into a message to be sent to the server for replay.



Here's the definition of the sink:



public interface IAmbientTransactionSink : IEnlistmentNotification
{
void ReceiveChanges(string changeKey, object changes);
void WriteAllChanges();
}


How does the ambient transaction know when all changes have been received?




Originally I thought I had found a simple and elegant solution to this problem, one that didn't even burden the transaction with the additional task of knowing when every change has been received. Notice the commit coordinated by System.Transactions is two-phase; first every enlistment receives the Prepare() method, and then the Commit() method. It is recommended that all enlistments do the brunt of their work in the Prepare() method, and this is where the transactional providers send in their changes. So, I thought, if I design the sink so that it does nothing during Prepare(), it can be sure that all providers have registered their changes when it receives the Commit() message: everybody does their work during Prepare() except the sink which does its work (assembling a change request and sending it to the server for replay ) during Commit().



However, this idea is flawed, and to understand why you have to know how and when the Rollback() message is invoked on the enlistments:



When and how may a System.Transactions Rollback carried through?





There are two possible ways that the Rollback() message is distributed to the enlisted providers:



1. Complete() never sent to the TransactionScope


    a. Because the logic in the program explicitly decides not to do so


    b. Because an exception causes program execution to jump directly to the implicit


2. Prepare() not sent to preparing enlistment in the Prepare() method of a provider


    a. Because the enlistment deliberately decides not to set the flag because it for some reason wants the transaction to be aborted.


    b. Because an exception in the Prepare code of the enlistment is raised.



Notice that all of these methods for instigating a rollback happen before the Commit() phase has been reached. This means that it is impossible to start a rollback after this phase has been reached. My original idea of having the sink do its work in the Commit method therefore is a no go: if something goes wrong here (and it inevitably will) I have no way of rolling back graciously.



So what have I learned here: don't do any work in the Commit() method of your enlistments. This method should only be used for doing risk free management of internal data structures (clearing lists, resetting counters and similar).



The solution




So the solution is pretty simple. Let the ambient transaction keep track of how many enlistments there are in total, and how many that have delivered their changes. When all enlistments are done, the transaction sink can be ordered to send the change message. Below are some of the code snippets involved in this process:



public void ReceiveChanges(string changeKey, object changes)
{
enlistmentCount--;
transactionSink.ReceiveChanges(changeKey, changes);

if (enlistmentCount == 0)
transactionSink.WriteAllChanges();
}
public void Commit(Enlistment enlistment)
{
if (enlistmentCount > 0)
{
throw new TransactionException(String.Format("enlistmentCount = {0} in Commit()", enlistmentCount));
}
ExitTransaction();
enlistment.Done();
}

public void Rollback(Enlistment enlistment)
{
ExitTransaction();
enlistment.Done();
}
private void ExitTransaction()
{

enlistments.Clear();
enlistmentCount = 0;
}


Ok, this almost wraps it up for this series, however I still haven't explained how we solved the timout problem. This will be the topic of the next (an final) post in this series.