We now have a pretty solid way to send messages, publish messages and we've got those messages flowing over a reliable transport mechanism. Sending and publishing individual messages only gets us so far. We often need a way to coordinate complex processes which involve multiple services. For this NServiceBus has the concept of sagas which some might call process managers.
- Kata 1 - Sending a message
- Kata 2 - Publishing a message
- Kata 3 - Switching transports
- Kata 4 - Long running processes
- Kata 5 - Timeouts
- Kata 6 - When things go wrong
Consider the bakery which is creating the cake for me to eat: when it starts making a new cake because I ate the last one it has a bunch of things it needs to coordinate. They need to preheat ovens, gather ingredients, mix ingredients, grease pans, fill pans, put pans in the oven, remember to take the cake out, cool it, ice it... the list goes on and on - no wonder cake is so expensive. Coordinating all these activities is complex in a distributed system, really in any system. There are a lot of corner cases that we usually fail to consider in a non-distributed system which become much more apparent when building out a process manager. What if we preheat the oven but then discover that we're all out of flour? In that monolithic system we might throw an exception and hope that somebody is monitoring for it in a log file somewhere. Realistically that's never going to happen. In the meantime nobody has shut the oven off and the bakery burns down.
A saga allows us to store the state of a process, to react to messages as they come in and send new messages. We use this to coordinate the activities of the bakery. In our example above we can probably call the process "BakeCakeSaga". When messages come in relating to the order then we need to be able to find a way to look up the state and make modifications to it. NServiceBus implements this through a method called ConfigureHowToFindSaga
. This function will provide a mapping from every message that interacts with the saga to find the saga data. For our example we'd probably use something like an order id.
Let's build out a very simple saga which responds to just a few messages in our system so we can see how it works. Saga can get pretty complex but they are quite testable so that's nice.
The Kata
Create a saga which handles the messages CakeOrderPlaced
, CakeOrderCanceled
, CakeOrderShipped
. Each of these messages will contain an OrderId
, a GUID, which will be used to identify the saga as well as whatever information might be associated with those messages. For now just write out to the console when each of these messages is received - unless you want to bake me a cake which I will accept.
The Solution
- Add some mechanism to handle the persistence of saga data. For now we'll just use the in learning persistence. In the various program.cs files add
1 | var persistence = endpointConfiguration.UsePersistence<LearningPersistence>(); |
- Create messages classes in the messages project
1 | namespace messages; |
- Create a saga class in the receiver project
1 | using NServiceBus; |
- Add a saga data class to the receiver project
1 | using NServiceBus; |
- Modify the sender project's program.cs to send the messages adding a loop which sends all the different messages involved in the saga.
1 |
|
Things to try now
- Run the applications and in the sender app try pressing some keys like
p
orc
ors
to see the messages being handled by the saga. - Try starting the applications in different orders and see how the saga handles the messages.
Things to Notice
Notice that the Saga can be started by 3 different events. Why would a saga be started by a cancel message? You can't cancel an order which hasn't even been placed yet - right? Well it turns out you can. Without some serious hoop jumping through the order of message delivery it not guaranteed. So in fact orders can be canceled or shipped before we get the message telling us the order has been placed. It is sort of mind-blowing.