The Saga pattern on Azure with NServiceBus

The Saga Pattern

As you probably know by now, I’ve been adding support for Windows Azure to the NServiceBus framework, mostly because I believe NServiceBus has the most comprehensive set of communication pattern implementations available for doing development on the Windows Azure Platform. And one of these is the Saga pattern! Before I show you how easy it is to get Saga’s working on Windows Azure, let’s take some time to discuss the pattern itself as odds are you never heard of it before.

In essence a Saga is an object that keeps track of the state of a conversation between an object and it’s different partners. By combining multiple saga’s, representing each partner in the conversation you can fulfill the role of an orchestration that overcomes some of the challenges faced by workflow implementations in a cloud environment such as Windows Azure.

Indeed, the saga is a very viable alternative to a workflow. I’m not going into a debate on which style is better, I feel they both deserve a spot in my toolcase and can perfectly be used in conjunction in a solution. But the most important difference though is that Saga’s are more responsive in nature, they will act on incoming messages and rarely be in charge of the entire conversation. Because of this responsive and typical asynchronous nature of the communication between saga’s, they are not dependent on a distributed transaction. This is quite usefull in the cloud as distributed transactions simply don’t exist there.

Configuring saga support in NServiceBus

Alright, now how easy is it to persist saga’s to Azure’s Table Storage? As anything in NServiceBus, you just configure it by calling a few methods at configuration time. The Sagas method enables saga support, and specifying that you want to persist it to Azure Table Storage is done by means of the AzureSagaPersister method.

Configure.With()
                .Log4Net()
                .StructureMapBuilder(ObjectFactory.Container)

                .AzureConfigurationSource()
                .AzureMessageQueue().JsonSerializer()
                .AzureSubcriptionStorage()
                .Sagas().AzureSagaPersister().NHibernateUnitOfWork()

                .UnicastBus()
                     .LoadMessageHandlers()
                     .IsTransactional(true)
                .CreateBus()
                .Start();

Of course you also need to specify a connection string representing your storage account in the web.config (or service definition file if hosted in azure). Another option that you can enable is schema creation, which is recommended for azure table storage so that it is ensured that all table names exist (table storage doesn’t have any other schema information).

<AzureSagaPersisterConfig ConnectionString="UseDevelopmentStorage=true" CreateSchema="true" />

The Starbucks sample

If you want to see saga’s in action on azure, you can have a look at a azurified version of the Starbucks example. If you’ve never heard of the Starbucks example, it was originally defined by ayende on his blog and has become the default example for demonstrating the use of saga’s to implement a orchestration between humans and services. The implementation can be found in the NServiceBus trunk.

The scenario goes as follows:

I want to order a venti hot chocolate:

  • I (client) ask the cashier (external facing service) for a venti hot chocolate.
  • The cashier informs the barista (internal service) that a new order for venti hot chocolate has arrived.
  • The barista starts preparing the drink.
  • The cashier starts the payment process (credit card auth, or just counting change).
  • When the cashier finishes the payment process, he tells the barista that the payment process is done.
  • I (client) move to the outgoing coffee stand, and wait for my coffee.
  • The barista finishes preparing the drink, check that the payment for this order was done, and serve the drink.
  • I pick up my drink, and the whole thing is finished.

The implementation of the workflow consists of a saga for each the Cashier and the Barista. There is no saga for the customer as we humans tend to perform our communication orchestration ourselves. I’ll show you the code of the Cashier saga to explain how a saga is implemented, you can find the full source in the NServiceBus trunk of course.

First of all, we need to give our saga a memory, a data structure used to contain the state of the conversation. This data structure must implement the IContainSagaData interface to fulfill the identification requirements such as an Id and a representation of the originator. Furthermore we need to add some properties representing the type of drink, the amount ordered and the name of the customer to get the drink back to him or her.

public class CashierSagaData : IContainSagaData
{
    public virtual Guid Id { get; set; }
    public virtual String Originator { get; set; }
    public virtual String OriginalMessageId { get; set; }

    public virtual Double Amount { get; set; }
    public virtual String CustomerName { get; set; }
    public virtual String Drink { get; set; }
    public virtual DrinkSize DrinkSize { get; set; }
    public virtual Guid OrderId { get; set; }
}

The implementation of the saga itself requires you specify the state as the closing of the Saga generic type used as a base implementation. Furthermore you need to specify for which messages the saga will take part in the conversation. One specific message will mark the start of the conversation, in this example a new order, which is indicated by the IAmStartedByMessages interface. Other messages are handled with the traditional IHandleMessages interface.

public class CashierSaga : Saga<CashierSagaData>,
                            IAmStartedByMessages<NewOrderMessage>,
                            IHandleMessages<PaymentMessage>
{
    private readonly IStarbucksCashierView _view;

    public CashierSaga()
    {}

    public CashierSaga(IStarbucksCashierView view)
    {
        _view = view;
    }

We also need to specify when a saga instance should be loaded from persistence and play part in the conversation. Many orchestration frameworks would require you to pass the orchestration ID along with all of the messages, but NServiceBus has opted to implement this the other way around, we will map existing message information to the saga’s identifier using the ConfigureHowToFindSaga and ConfigureMapping methods.

    public override void ConfigureHowToFindSaga()
    {
        ConfigureMapping(s => s.OrderId, m => m.OrderId);
        ConfigureMapping(s => s.OrderId, m => m.OrderId);
    }

Defining the orchestration of the conversation is done by means of the individual message handlers, updating the state of the saga’s data and publishing, or replying to, other messages through the bus. The end of the conversation is marked by the MarkAsComplete method, this will in fact remove the saga from the conversation. The code below shows you how the cashier communicates with both the customer and the barista.

        
    public void Handle(NewOrderMessage message)
    {
        _view.NewOrder(new NewOrderView(message));

        Data.Drink = message.Drink;
        Data.DrinkSize = message.DrinkSize;
        Data.OrderId = message.OrderId;
        Data.CustomerName = message.CustomerName;
        Data.Amount = CalculateAmountAccordingTo(message.DrinkSize);

        Bus.Publish(new PrepareOrderMessage(Data.CustomerName, Data.Drink, Data.DrinkSize, Data.OrderId));
        Bus.Reply(new PaymentRequestMessage(Data.Amount, message.CustomerName, message.OrderId));
    }

    public void Handle(PaymentMessage message)
    {
        if(message.Amount >= Data.Amount)
        {
            var viewData = new ReceivedFullPaymentView(Data.CustomerName, Data.Drink, Data.DrinkSize);
            _view.ReceivedFullPayment(viewData);

            Bus.Publish(new PaymentCompleteMessage(Data.OrderId));
        }
        else if(message.Amount == 0)
        {
            var viewData = new CustomerRefusesToPayView(Data.CustomerName, Data.Amount, Data.Drink, Data.DrinkSize);
            _view.CustomerRefusesToPay(viewData);
        }

        MarkAsComplete();
    }

    private static Double CalculateAmountAccordingTo(DrinkSize size)
    {
        switch(size)
        {
            case DrinkSize.Tall:
                return 3.25;
            case DrinkSize.Grande:
                return 4.00;
            case DrinkSize.Venti:
                return 4.75;
            default:
                throw new InvalidOperationException(String.Format("Size '{0}' does not compute!", size));
        }
    }
}

Hope this example shows you how you can implement saga’s, as an alternative to workflow orchestrations, on the windows azure platform. But before I leave you, there is still one thing to remember:

One thing to remember

This implementation uses Azure Table storage under the hood and Azure table storage only supports a small number of data types as specified in following msdn article. It speaks for itself that you can only use compatible .Net types in your saga’s state. If you need more data types, you will have to consider SQL Azure as your data store, which I will show you how to do in a next post…

Advertisements