NServiceBus: Timeout gets handled by multiple Sagas

433 views Asked by At

We currently have a NServiceBus 5 system, which contains two recurring Sagas. Since they act as dispatcher to periodically pull multiple sorts of data from an external system, we're using the Timeouts to trigger this: We created a generic and empty class called ExecuteTask, which is used by the Saga to handle the timeout.

public class ScheduleSaga1 : Saga<SchedulerSagaData>,
    IAmStartedByMessages<StartScheduleSaga1>,
    IHandleMessages<StopSchedulingSaga>,
    IHandleTimeouts<ExecuteTask>

And the other Saga is almost identically defined:

public class ScheduleSaga2: Saga<SchedulerSagaData>,
    IAmStartedByMessages<StartScheduleSaga2>,
    IHandleMessages<StopSchedulingSaga>,
    IHandleTimeouts<ExecuteTask>

The timeout is handled equally in both Sagas:

    public void Handle(StartScheduleSaga1 message)
    {
        if (_schedulingService.IsDisabled())
        {
            _logger.Info($"Task '{message.TaskName}' is disabled!");
        }
        else
        {
            Debugger.DoDebug($"Scheduling '{message.TaskName}' started!");
            Data.TaskName = message.TaskName;

            // Check to avoid that if the saga is already started, don't initiate any more tasks
            // as those timeout messages will arrive when the specified time is up.
            if (!Data.IsTaskAlreadyScheduled)
            {
                // Setup a timeout for the specified interval for the task to be executed.
                Data.IsTaskAlreadyScheduled = true;

                // Send the first Message Immediately!
                SendMessage();

                // Set the timeout
                var timeout = _schedulingService.GetTimeout();
                RequestTimeout<ExecuteTask>(timeout);
            }
        }
    }

    public void Timeout(ExecuteTask state)
    {
        if (_schedulingService.IsDisabled())
        {
            _logger.Info($"Task '{Data.TaskName}' is disabled!");
        }
        else
        {
            SendMessage();

            // Action that gets executed when the specified time is up
            var timeout = _schedulingService.GetTimeout();
            Debugger.DoDebug($"Request timeout for Task '{Data.TaskName}' set to {timeout}!");
            RequestTimeout<ExecuteTask>(timeout);
        }
    }


    private void SendMessage()
    {
        // Send the Message to the bus so that the handler can handle it
        Bus.Send(EndpointConfig.EndpointName, Activator.CreateInstance(typeof(PullData1Request)));
    }

Now the problem: Since both Sagas are requesting Timeouts for ExecuteTask, it gets dispatched to both Sagas! Even worse, it seems like the stateful Data in the Sagas gets messed up, since both Sagas are sending both message.

Therefore, it seems like the Timeouts are getting sent to all the Saga Instances which are requesting it. But looking at the example https://docs.particular.net/samples/saga/simple/ there is no special logic regarding multiple Saga instances and their state.

Is my assumption correct? If this is the case, what are the best practices to have multiple Sagas requesting and receiving Timeouts?

1

There are 1 answers

0
Dennis van der Stelt On

The only reason I can think of when this is happening is that they share the same identifier to uniquely identify the saga instance.

Both ScheduleSaga1 and ScheduleSaga2 are using the same SchedulerSagaData for storing state. NServiceBus sees an incoming message and tries to retrieve the state, based on the unique identifier in the incoming message. If both StartScheduleSaga1 and StartScheduleSaga2 come in with identifier 1 for example, NServiceBus will search for saga state in the table SchedulerSagaData with unique identifier 1.

Both ScheduleSaga1 and ScheduleSaga2 will then share the same row!!!

Timeouts are based on SagaId in the TimeoutEntity table. Because both sagas share the same SagaId, it's logical they are both executed once the timeout arrives.

At the minimum you should not reuse the identifier to schedule tasks. It's probably better to not share the same class for storing saga state. Also easier to debug.