Hangfire с Entity Framework - проблемы параллелизма DbContext

У меня есть служба Hangfire, отвечающая за две работы:

  • Создание объектов электронной почты с соответствующими вложениями (длительный процесс)
  • Обработка и отправка Pending писем.

Я использую ASP.NET MVC (5.2.3) с StructureMap (4.5.1) для создания контейнера IoC для внедрения зависимостей вместе с Hangfire ( 1.6.19)

Поток:

  • The email will be triggered to be created from the front-end, where a Fire and Forget hangfire job is created, pushing it into the database with a Pending status.
    • This process uses a few repository objects to create a report of the client in a PDF format (These repositories uses the DbContext to retrieve information from the database)
  • Еще одно повторяющееся задание, собирает все Pending электронные письма и составляет электронное письмо для каждого из них, используя System.Net.Mail.

Оба эти задания работают нормально, за исключением случаев, когда есть несколько заданий, создающих электронные письма одновременно.

Например: если 10 заданий запускаются одновременно из внешнего интерфейса для создания электронных писем, задание завершается ошибкой:

Вторая операция началась в этом контексте до завершения предыдущей асинхронной операции. Используйте await, чтобы убедиться, что все асинхронные операции завершены, прежде чем вызывать другой метод в этом контексте. Потокобезопасность любых членов экземпляра не гарантируется.

Строка, которая вызывает эту ошибку, использует await, но я думаю, что это потому, что разные потоки одновременно обращаются к контексту:

var ids = await context.Objects
    .Where(x => x.id == clientId && !x.IsDeleted)
    .Select(x => x.id)
    .ToListAsync();

Я настроил DbContext, пробуя разные области видимости Transient и AlwaysUnique, но ошибка не исчезла.

Кроме того, это не всегда происходит в одной и той же строке, практически везде, где контекст используется в задействованных репозиториях - если оба задания одновременно попадают в одну и ту же строку.

Задания повторяются, и электронные письма отправляются примерно раз в минуту, что может не сработать, если есть сотни писем, которые нужно обработать и отправить (что является проблемой в моем сценарии).


person Johan Aspeling    schedule 11.03.2019    source источник
comment
Так что, если вы установите класс вызывающего абонента на Transient и AlwaysUnique :-)   -  person Laurent Lequenne    schedule 11.03.2019
comment
Я попытался установить dbContext в контейнере IoC из класса вызывающего абонента как на Transient, так и на AlwaysUnique, и у меня возникла та же проблема с обоими. Когда я удаляю await и меняю .ToListAsync() на ToList(), похоже, он работает, как ожидалось ... Я не уверен, как это повлияет на этот вызов другими обычными вызовами api   -  person Johan Aspeling    schedule 11.03.2019


Ответы (1)


Проблема была решена, когда я использовал ContainerScoped LifeCycle.


Документы StructureMap:

ContainerScoped в этом случае означает, что регистрация будет построена один раз для каждого контейнера, так что корневой контейнер, любой дочерний контейнер или контейнер профиля и каждый отдельный вложенный контейнер будут создавать свой собственный экземпляр объекта.

Это означает, что есть новый экземпляр DataContext для каждого задания, запущенного из Hangfire (если я правильно понимаю, я могу говорить с поправкой)

Специально для Datacontext мне пришлось включить MARS (Multiple Active Result Sets), который позволяет SQL Server разрешать выполнение нескольких пакетов в одном соединении.

person Johan Aspeling    schedule 12.03.2019