Краткий туториал, как организовать обработку фоновых задач с приоритетами для условного SaaS продукта с разделением клиентских данных.
Постановка задачи
Это одна из регулярно встречающихся задач, когда вам необходимо спроектировать некий SaaS продукт с подпиской для клиентов компании и фоновой обработкой задач. В таком случае необходимо озаботиться как разделением данных в БД, так и разделением фоновых обработчиков, чтобы клиенты на забивали общую очередь и не мешали друг другу. Опционально требуется внедрить приоритеты в рамках каждого клиента.
Если взять более конкретный пример - представим систему, аналог 1С, только в виде SaaS в облаке. Эта система подключена к интернет-магазинам, скачивает заказы, формирует отгрузки, которые отправляет на склад, и попутно рассылает уведомления пользователям и вебхуки другим информационными системам. Ваши клиенты покупают подписку и пользуются ее функциями.
В системах подобного класса клиенты (тенанты) могут быть представлены как плоским списком, так и отношениями организация - клиенты. Примером последнего может являться 3PL оператор в логистике. Этот участник предоставляет склады как услугу. И у него есть собственные клиенты-юридические-лица, которые покупают складские мощности и продают какие-либо товары через интернет-магазины конечным покупателям (нам с вами).
Следовательно, такой 3PL оператор покупает доступ к нашему SaaS продукту и создает внутри своих клиентов. Получается некая иерархия. И фоновую обработку можно организовать как на верхнем уровне, то есть различные 3PL операторы не будут мешать друг другу, но в рамках одного оператора его клиенты-юр-лица будут становиться в одну очередь. Так и на нижем уровне, когда каждый клиент-юр-лицо каждого 3PL оператора будет работать независимо от других.
Технологии и место, куда надо ударить
Для реализации можно использовать брокеры сообщений совместно с библиотеками типа MassTransit, NServiceBus, Rebus (мои личные симпатии).
Но в данном конкретном случае будем использовать давний и широко известный (?) Hangfire, который, по сути, объединяет в себе планировщик и очередь с приоритетами.
Очереди и параметры
Ключевым моментом для реализации является возможность назначить имя очереди на вызываемый метод-обработчик, в рамках которой hangfire будет обрабатывать задачу. И это необходимо соединить с параметрами метода, которые могут быть автоматически переданы в имя очереди. Атрибут Queue корректно подставит значения из параметров метода, согласно его исходному коду, строка 71
[Queue("queue-portal-{0}")] // для разделения по верхнему уровню
// или [Queue("queue-portal-{0}-tenant-{1}")] для разделения по нижнему уровню
public void DoSomeWork(int portalId, int tenantId) { ... }
Где в portal-{0} - подставляется идентификатор организации / 3PL оператора, а в tenant-{1} идентификатор тенанта.
Очереди и приоритеты
Далее у нас идут приоритеты. Hangfire обрабатывает очереди в алфавитном порядке. Поэтому необходимо определить что-то вроде этого:
public const string Priority0100 = "priority-0100_portal-{0}";
public const string Priority0200 = "priority-0200_portal-{0}";
public const string Priority0300 = "priority-0300_portal-{0}";
public const string Priority0400 = "priority-0400_portal-{0}";
И распределить бизнес-процессы по ним. Например, создание отгрузки на основе заказа - это приоритет 01, скачивание новых заказов из интернет-магазина 02, а отправка уведомлений и веб-хуков 03 и 04.
Сервера
Это у нас третий компонент нашего технического решения. В Hangfire определена такая сущность как виртуальный сервер, для которой можно указать уникальное название, список очередей, которые он обрабатывает, и максимальное количество одновременных потоков, которые разгребают очереди в порядке приоритета.
В конфигурации по умолчанию на один физический сервер создается один сервер Hangfire. Но нам же нужно отделить наших клиентов друг от друга. Поэтому мы создадим несколько виртуальных серверов в рамках одного физического. Каждый виртуальный сервер будет обрабатывать конкретную организацию или тенанта с использованием своих выделенных потоков.
Тут встает вопрос горизонтальной масштабируемости. Ведь у вас может быть несколько физических серверов, и надо как-то распределить всех клиентов по ним с учетом нагрузки. Возможно даже придется заложить опцию выделенного (dedicated) физического сервера для одного крупного клиента с миллионом заказов в день. Этот емкий вопрос выходит за рамки данной статьи.
Покажи код
Ссылка на тг канал github

Краткая пояснительная записка
Core
HangfireConstants - здесь определен список очередей с приоритетами
DataBase - рандомный генератор организаций и их тенантов по заданному количеству
Work - эмулятор рабочей нагрузки для наших условных бизнес-процессов (скачивание заказов, создание отгрузки, рассылка уведомлений) с рандомными задержками. Там внутри построено таким образом, что корневая задача (скачивание заказа) каскадно порождает задачу на обработку заказа и затем на рассылку уведомлений.
Controllers
ApplicationController - тут находится конечная точка для запуска эмуляции
Startup - собственно конфигурация виртуальных серверов. Для начала создается рандомный список организаций и тенантов, затем отдельные виртуальные сервера для каждой организации, по 10 потоков в каждом. И один отдельный системный сервер для условных recurring задач. Там вы, например, можете запускать какую-либо общую операцию для всех организаций или тенантов.
Демонстрация
Скачав и запустив код, вы можете перейти по ссылке https://localhost:44326/Application/Run и запустить эмуляцию.
По ссылке https://localhost:44326/hangfire/jobs/enqueued будет доступен hangfire. Там будут появляться фоновые задачи. Если регулярно обновлять страницу, то можно увидеть как задачи будут постепенно обрабатываться в приоритетном порядке параллельно для portal-1 и portal-2.

Опционально, если у вас есть доступ к платным библиотекам Hangfire.Pro.dll и Hangfire.Pro.Redis.dll - вы можете локально поднять redis и провести свои нагрузочные тесты (билдить и запускать надо в release). Для примера мой i7-8700k / 32 Гб ОЗУ / SSD / Windows / WSL (там под линкусом поднят redis) справляется с порядка 800 отдельными клиентам и по 10 потоков в каждом.
На этом все.