This post is part of a series on NServiceBus. In the previous article, we created our first saga. In this article, we will introduce the concept of timeouts and how to gracefully handle sagas that have not completed within a certain time frame. All the code for this series can be found in the NServiceBusTutorial repository on GitHub.
Introduction 👀
In NServiceBus, a timeout is a message that is sent to a saga after a specified period of time. This message makes use of NServiceBus’s delayed delivery options to enable the ability to send messages at a later time. This can be an effective tool in implementing business processes that should be completed within a duration of time or for scenarios where a reminder notification should be sent if a process is not completed in a timely manner. It can also help eliminate batch jobs and scheduled tasks that are often used to check for time-based durations. In our example, we will use timeouts to handle the scenario where a contributor has not completed verification within 24 hours.
Registering Timeouts ⏳
In order to use timeouts in our saga, we need to register a timeout in a saga message handler. In our example, we will register a timeout when the StartContributorVerificationCommand
is handled. This timeout will trigger after 24 hours if the ContributorVerificationCompleted
event has not been received. Let’s revisit some code from the previous blog post:
public async Task Handle(StartContributorVerificationCommand message, IMessageHandlerContext context)
{
var command = new VerifyContributorCommand { ContributorId = message.ContributorId };
await context.Send(command);
var timeout = new ContributorVerificationSagaTimeout { ContributorId = message.ContributorId };
await RequestTimeout(context, DateTime.UtcNow.AddHours(24), timeout);
}
In this code snippet, we are processing a StartContributorVerificationCommand
by first publishing a VerifyContributorCommand
and then registering a ContributorVerificationSagaTimeout
. Since we had already covered the VerifyContributorCommand
and its flow in the other post, we’ll focus on the second half of the method where the RequestTimeout
method is called. The RequestTimeout
method is provided by the Saga<T>
base class and is used to register a timeout message. The first parameter is the IMessageHandlerContext
and the second parameter is the time at which the timeout should trigger. The third parameter is the timeout message that will be sent to the saga when the timeout triggers.
In this sequence, if a contributor does not confirm their phone number within 24 hours, the saga will process the timeout registered when it began.
Handling Timeouts ⌛
Timeouts can be used to trigger a message to be sent to the saga after a specified period of time. This can be useful for scenarios where we want to remind a user to complete an action or to escalate an issue if it is not resolved in a timely manner. In our example, when a contributor has not completed verification within 24 hours, we should expect the saga to timeout and send a command to mark the contributor’s VerificationStatus
as NotVerified
.
public async Task Timeout(ContributorVerificationSagaTimeout state, IMessageHandlerContext context)
{
await context.Send(new NotVerifyContributorCommand { ContributorId = state.ContributorId });
MarkAsComplete();
}
When the timeout is triggered, the Timeout
method is called. In this case, we are sending a NotVerifyContributorCommand
which the Worker application will process in order to mark the contributor as NotVerified
. We are also calling the MarkAsComplete
method to indicate that the saga is complete. Lastly, we need to tell the saga class that it can handle the ContributorVerificationSagaTimeout
message. This is done in a similar manner to handling messages as we will add the IHandleTimeouts<ContributorVerificationSagaTimeout>
interface to the saga class. Here is the final saga class definition:
public class ContributorVerificationSaga : Saga<ContributorVerificationSagaData>,
IAmStartedByMessages<StartContributorVerificationCommand>,
IHandleMessages<ContributorVerifiedEvent>,
IHandleTimeouts<ContributorVerificationSagaTimeout>
{
// ... mappings, handlers and other code abbreviated
}
With these changes, we have successfully added a timeout to our saga. We can see the ContributorVerificationSaga
has implemented all of the necessary interfaces to handle the messages and timeouts.
Conclusion 🧠
In this article, we introduced timeouts to our saga. Our saga is now able to verify a contributor if they respond to a notification or mark them as not verified if they do not respond within 24 hours. This is a powerful feature of NServiceBus and allows us to handle time-based scenarios in a more elegant and efficient manner. In the next article, we will cover how to write tests for our saga and even simulate the passing of time 🔮. Stay tuned! 👋