Getting Started
Architecture
NServiceBus
Transports
Persistence
ServiceInsight
ServicePulse
ServiceControl
Monitoring
Samples

Saga Timeouts

Component: NServiceBus
NuGet Package: NServiceBus (9.x)

Assumptions can not be made in a message-driven environment regarding the order of received messages and exactly when they will arrive. While the connection-less nature of messaging prevents a system from consuming resources while waiting, there is usually an upper limit to a waiting period that the business dictates.

The upper wait time is modeled in NServiceBus as a Timeout:

public class MySaga :
    Saga<MySagaData>,
    IAmStartedByMessages<Message1>,
    IHandleMessages<Message2>,
    IHandleTimeouts<MyCustomTimeout>
{
    protected override void ConfigureHowToFindSaga(SagaPropertyMapper<MySagaData> mapper)
    {
        mapper.MapSaga(sagaData => sagaData.SomeId)
            .ToMessage<Message1>(message => message.SomeId);
    }

    public Task Handle(Message1 message, IMessageHandlerContext context)
    {
        return RequestTimeout<MyCustomTimeout>(context, TimeSpan.FromHours(1));
    }

    public Task Handle(Message2 message, IMessageHandlerContext context)
    {
        Data.Message2Arrived = true;
        var almostDoneMessage = new AlmostDoneMessage
        {
            SomeId = Data.SomeId
        };
        return ReplyToOriginator(context, almostDoneMessage);
    }

    public Task Timeout(MyCustomTimeout state, IMessageHandlerContext context)
    {
        if (!Data.Message2Arrived)
        {
            return ReplyToOriginator(context, new TiredOfWaitingForMessage2());
        }
        return Task.CompletedTask;
    }
}

RequestTimeout<T> sends a timeout message which is delivered after the specified delay or at the specified time.

If a saga does not request a timeout then its corresponding timeout method will never be invoked.

Timezones and Daylight Saving Time (DST)

A timeout may be requested specifying either a DateTime or TimeSpan. When specifying a DateTime, the Kind property must be set. If the timeout specifies a time of day, the calculation must take into account any change to or from DST. Timezone and DST conversion may be done using TimeZoneInfo.ConvertTime.

Requesting multiple timeouts

Multiple timeouts can be requested when processing a message. The individual timeouts can be different types and different timeout durations.

public async Task Handle(Message1 message, IMessageHandlerContext context)
{
    await RequestTimeout<MyCustomTimeout>(context, TimeSpan.FromHours(1));
    await RequestTimeout<MyCustomTimeout>(context, TimeSpan.FromDays(1));
    await RequestTimeout<MyOtherCustomTimeout>(context, TimeSpan.FromSeconds(10));
    await RequestTimeout<MyOtherCustomTimeout>(context, TimeSpan.FromMinutes(30));
}

Changing or revoking timeouts

After a timeout has been requested, it cannot be changed (i.e. rescheduled) or revoked (i.e. deleted or cancelled). Requesting a timeout with the same state again, but with a different duration or timestamp, will not revoke or ignore the original timeout. The original timeout and the subsequent timeout will both be processed by the saga.

When a saga handles a timeout, it may choose to ignore it, depending on how the saga state has changed since the timeout was requested.

Completed Sagas

It is possible for a timeout to be queued after its saga has been completed. Because a timeout is tied to a specific saga instance it will be ignored once the saga instance is completed.

Timeout state

The state parameter provides a way to pass state to the Sagas timeout handle method. This is useful when many timeouts of the same "type" will be active at the same time. One example of this would be to pass in some ID that uniquely identifies the timeout eg: .RequestTimeout(new OrderNoLongerEligibleForBonus{OrderId = "xyz"}). With this state passed to the timeout handler it can now decrement the bonus correctly by looking up the order value from the saga state using the provided id.

Using the incoming message as a timeout state

As a shortcut, an incoming saga message can be re-used as a timeout state by passing it to the RequestTimeout method and making the saga implement IHandleTimeouts<TIncomingMessageType>.

Persistence

Some form of Persistence is required to store the timestamp and the state of a timeout.

In order to learn how delayed delivery works in more detail, refer to the Delayed Delivery - How it works section.

Samples

Related Articles

  • Sagas
    Maintain statefulness in distributed systems with the saga pattern and NServiceBus' event-driven architecture with built-in fault-tolerance and scalability.