Search code examples
c#nservicebusmsmqnservicebus-sagas

Nservicebus saga timeout


I have a saga which checks on the status of an API calls every 30 seconds if the status returned back from the call is successful the saga is ended, if not the saga waits 30 seconds and attempts again. If the API call has not returned a successful response within 60 minutes, the saga is timed out and ended.

I am having problems getting my 60 minute timeout to fire. The code I have is

public class MonitorSubmissionFeedSagaData: IContainSagaData
{
    public Guid Id { get; set; }

    public string Originator { get; set; }

    public string OriginalMessageId { get; set; }

    public bool TimeoutSet { get; set; }

    [Unique]
    public string JobId { get; set; }
}

public class MonitorSubmissionFeedSaga : Saga<MonitorSubmissionFeedSagaData>,
    IAmStartedByMessages<MonitorFeedSubmissonCommand>,
    IHandleMessages<StartCheckSubmissionCommand>,
    IHandleTimeouts<MonitorSubmissionFeedSagaTimeout>
{
    public const int SagaTimeoutInMinutes = 60;

    public IEmpathyBrokerClientApi PostFileService { get; set; }

    protected override void ConfigureHowToFindSaga(SagaPropertyMapper<MonitorSubmissionFeedSagaData> mapper)
    {
        mapper.ConfigureMapping<MonitorFeedSubmissonCommand>(x => x.JobId).ToSaga(saga => saga.JobId);

    }

    public void Handle(MonitorFeedSubmissonCommand message)
    {
        Data.JobId = message.JobId;

        CheckTimeout();

        Bus.Send(new StartCheckSubmissionCommand
        {
            JobId = Data.JobId
        });
    }

    public void Handle(StartCheckSubmissionCommand message)
    {
        Log.Info("Saga with JobId {0} received", Data.JobId);

        bool isCompleted = GetJobStatus(message.JobId);

        while (isCompleted)
        {
            Thread.Sleep(30000);
            isCompleted = GetJobStatus(message.JobId);
        }

        MarkAsComplete();
    }

    public void CheckTimeout()
    {
        RequestTimeout<MonitorSubmissionFeedSagaTimeout>(TimeSpan.FromMinutes(SagaTimeoutInMinutes));
    }

    public void Timeout(MonitorSubmissionFeedSagaTimeout state)
    {
        MarkAsComplete();
    }

    bool GetJobStatus(string jobId)
    {
        return false;
        var status = PostFileService.GetJobIdStatus(jobId);
        if (status.state == "FAILURE" || status.state == "DISCARDED")
        {
            return false;
        }
        return true;
    }

}

Can anyone see where I am going wrong?

thanks


Solution

  • Your Saga should go idle. You're keeping it alive with a while loop. The timeout message arrives at some point and then you should check what should happen. Either another checkout or MarkAsComplete.

    I wrote this in Notepad, so it might not compile. But it's to get an idea.

    public class MonitorSubmissionFeedSagaData: IContainSagaData
    {
    public Guid Id { get; set; }
    public string Originator { get; set; }
    public string OriginalMessageId { get; set; }
    
    [Unique]
    public string JobId { get; set; }
    public DateTime SagaStartTimeUtc { get; set; }
    }
    
    public class MonitorSubmissionFeedSaga : Saga<MonitorSubmissionFeedSagaData>,
        IAmStartedByMessages<MonitorFeedSubmissonCommand>,
        IHandleTimeouts<VerifyApiTimeOut>
    {
    public IEmpathyBrokerClientApi PostFileService { get; set; }
    
    public void Handle(MonitorFeedSubmissonCommand message)
    {
        Data.JobId = message.JobId;
        Data.SagaStartTimeUtc = DateTime.NowUtc;
    
        CreateTimeoutRequest();
    }
    
    public void CreateTimeoutRequest()
    {
        RequestTimeout<VerifyApiTimeOut>(TimeSpan.FromSeconds(30));
    }
    
    public void Timeout(VerifyApiTimeOut state)
    {
        if (!GetJobStatus(Data.JobId) && DateTime.NowUtc < Data.SagaStartTimeUtc.AddMinutes(60))
        {
          CreateTimeoutRequest();
        }
    
        MarkAsComplete();
    }
    
    bool GetJobStatus(string jobId)
    {
        return false;
        var status = PostFileService.GetJobIdStatus(jobId);
        if (status.state == "FAILURE" || status.state == "DISCARDED")
        {
            return false;
        }
        return true;
    }
    
    }
    

    Another comment could be that the Saga itself should not call out to external services. Preferably not even to some database. Delegate this to another service. Every 30 seconds, send out a message to another handler. This handler should call the WebService/WebAPI. When it can confirm that everything is correct, reply to the original Saga. When it's not correct, just let it be. The Saga will send out messages every 30 seconds to retry.

    After 60 minutes, the Saga should stop sending messages and MarkAsComplete.