Мы провели несколько нагрузочных тестов на Saga с исходящим почтовым ящиком в памяти. Во время этих тестов мы моделировали различные типы сбоев: перезапуски приложений, перезапуск инфраструктуры, перезапуск брокера сообщений и т. Д. c.
Мы заметили, что некоторые экземпляры саг не заканчиваются sh, и у нас было множество ошибок. : Automatonymous.NotAcceptedStateMachineException: ... {SomeEvent}: не принято в состоянии {SomeState}
После некоторой отладки мы изолировали проблему. Я попытаюсь описать это, используя этот пример кода:
public class OrderStateMachine : MassTransitStateMachine<Order>
{
public OrderStateMachine()
{
InstanceState(x => x.CurrentState);
During(Initial,
When(Create).TransitionTo(New));
During(New,
When(AddItem)
.Then(x => x.Instance.Items.Add(x.Data.Name)),
When(Submit)
.ThenAsync(async x =>
{
// do something
await x.Publish(new SendEmail {Text = $"Order submitted. {x.Instance.Summary}"});
})
.TransitionTo(Submitted));
During(Submitted,
When(Accept)
.ThenAsync(async x =>
{
// do something
await x.Publish(new SendEmail {Text = $"Order accepted. {x.Instance.Summary}"});
})
.Finalize());
SetCompletedWhenFinalized();
}
public State New { get; private set; }
public State Submitted { get; private set; }
public Event<Create> Create { get; private set; }
public Event<AddItem> AddItem { get; private set; }
public Event<Submit> Submit { get; private set; }
public Event<Accept> Accept { get; private set; }
}
public class Order : SagaStateMachineInstance
{
public Guid CorrelationId { get; set; }
public string CurrentState { get; set; }
public IList<string> Items { get; set; } = new List<string>();
public string Summary => $"Items: {string.Join(", ", Items)}";
}
public class Create : CorrelatedBy<Guid>
{
public Guid CorrelationId { get; set; }
}
public class AddItem : CorrelatedBy<Guid>
{
public Guid CorrelationId { get; set; }
public string Name { get; set; }
}
public class Submit : CorrelatedBy<Guid>
{
public Guid CorrelationId { get; set; }
}
public class Accept : CorrelatedBy<Guid>
{
public Guid CorrelationId { get; set; }
}
public class SendEmail
{
public string Text { get; set; }
}
Вот что происходит:
- Во время Заказ в состоянии Новый , мы обрабатываем событие Submit , вносим некоторые изменения в порядок, публикуем sh SendEmail событие и переходим в Отправлено состояние.
- Состояние заказа успешно сохраняется в БД
- Сбои приложения (т. Е. Принудительный перезапуск) перед отправкой сообщения в исходящие сообщения.
- Приложение перезапускается, Подтверждение доставляется во второй раз, Заказ находится в состоянии Отправлено , и мы получаем исключение Automatonymous.NotAcceptedStateMachineException: ... Отправить : Не принято в состоянии Отправлено
Что если это произойдет в состоянии Отправлено во время Принять Обработка событий? Мое предположение:
- Во время Заказ в состоянии Отправлено , мы обрабатываем Принять событие, внести некоторые изменения в порядок, опубликовать sh Событие SendEmail и завершение Заказ
- Заказ Состояние удаляется из БД, поскольку оно завершено и настроено на CompletedWhenFinalized
- Приложение аварийно завершает работу (то есть принудительно перезапускает), прежде чем отправляет сообщение в исходящие.
- Приложение перезапускается, Принять доставляется во второй раз, Заказ больше не находится в БД, мы потеряли всю информацию об этом ... что происходит сейчас?
Какое лучшее решение для таких ситуаций? Я прочитал замечательную статью Криса о In-Memory Outbox, но не понимаю, как можно обрабатывать сообщение во время повторной доставки, когда Saga находится в состоянии, когда оно больше не обрабатывает это сообщение. Конечно, мы можем обработать доставленное событие в следующем состоянии с некоторыми хитрыми логами c, но это кажется довольно громоздким. Наша Saga намного сложнее, чем предоставленный образец.
Может быть, решением будет транзакция, которая фиксируется после отправки всех сообщений из исходящих сообщений? Можно ли как-то настроить Transaction Outbox на Saga?