Обработать фоновую обработку один раз

У меня есть приложение Blazor Interactive Server, работающее в службах приложений Azure. Он масштабируется/вводится как минимум с двумя экземплярами, а один раз - до 5.

В приложении у меня есть значок BackgroundService, который я использую для отправки электронных писем, синхронизации с внешней службой, пометки событий как закрытых, когда они достигли даты окончания и т. д. Он запускается раз в 5 минут, и когда я сигнализирую об этом (меняю что-то, что хочу синхронизируйтесь как можно скорее).

Если был один случай, то это раз плюнуть. Я читаю все ожидающие элементы из БД и действую на них и/или обновляю запись базы данных.

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

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

Итак, есть ли способ создать семафор или что-то в базе данных. Где тогда единственный фоновый поток, которому разрешено работать в течение 2 минут (ограничение по времени на случай, если Azure затем отключит этот сервер)? Или какой-то другой подход?

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

Обновление: Основываясь на комментариях ниже, позвольте мне немного добавить.

  1. Выборы руководства - да, я к этому склоняюсь. Для этого используйте таблицу БД. Но это не может быть окончательным решением, поскольку экземпляры серверов закрываются. Кроме того, мне нужен любой экземпляр, способный запустить процесс. Так что я думаю, что это больше похоже на то, что происходит с сетевым взаимодействием, когда каждый через БД пытается захватить семафор, когда он хочет запуститься. А если победит, убежит. Но тогда большой вопрос в том, как конкретно это сделать.
  2. Создание БД очереди сообщений для некоторых из этих задач работает. Однако это добавляет сложности, когда необходимо попытаться назначить элемент. А если предмет был захвачен 5 минут назад, но все еще удерживается, то предположим, что этот экземпляр умер. Однако большая проблема заключается в том, что самый важный набор элементов необходимо обрабатывать по порядку. Пункт 1 вызывает добавление объекта в базу данных, который необходим элементу 2. Таким образом, более сложная, хитрая логика повторной попытки и возможная остановка очереди. Так что не дикость в таком подходе.
  3. Я подумываю о том, чтобы функция сделала это. Но это добавляет проблему с сигнализацией об этом (я предполагаю, что у Azure есть способ сделать это). Более серьезная проблема: теперь у меня есть одна точка отказа. Или у меня есть 2 функции в разных дата-центрах, а потом я возвращаюсь к исходной проблеме.

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

Хорошая новость: если один экземпляр получает сигнал, а другой работает, это работает. Второй экземпляр ничего не может сделать.

Пусть ваши инстансы проведут выборы руководства, и победителем станет тот, кто отвечает за управление BackgroundService. (Хотя экземпляры Служб приложений Azure не могут взаимодействовать друг с другом напрямую (т. е. через HTTP), они все равно могут использовать другие общие заявленные/общие постоянные носители, такие как ваша СУБД или экземпляр Redis (или даже общая файловая система), для обмена сообщениями и быть в курсе друг друга и таким образом координировать свои действия.

Dai 28.08.2024 01:46

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

Dale K 28.08.2024 02:53

Другой подход — просто не использовать какие-либо автоматические фоновые задания/планировщики, а вместо этого использовать внешнее cron-задание (например, Azure Logic Apps) для выполнения запросов планировщика и позволить балансировщикам нагрузки Службы приложений Azure решать, какой экземпляр получает возможность запускать задание каждый раз.

Dai 28.08.2024 03:23

Рабочим, обрабатывающим очередь, не нужно знать друг о друге или общаться между собой. Используют ли ваши сотрудники транзакции? uplock подсказки? readpast подсказки?

AlwaysLearning 28.08.2024 03:54

К сожалению, @AlwaysLearning Azure SQL не поддерживает распределенные транзакции. Я оплакиваю медленную смерть X/Open XA.

Dai 28.08.2024 04:39

На самом деле самый простой способ сделать это — использовать существующие технологии. Например, NServiceBus... Он обрабатывает все, что вам нужно, «под капотом», без необходимости развертывания собственного. В противном случае, как вы говорите, любое из решений сложное с проблемами ремонтопригодности.

Dale K 28.08.2024 05:11

Возможно, вы можете использовать sp_getapplock, чтобы заблокировать функцию от других потоков.

siggemannen 28.08.2024 08:19
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
7
65
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Здесь у вас есть объекты базы данных SQL Server реализации распределенного мьютекса с ключом:

CREATE TABLE dbo.resource_locks
(
    resource_id varchar(256) CONSTRAINT pk_resource_locks PRIMARY KEY,
    expires_at datetime NOT NULL,
    session_id uniqueidentifier NOT NULL
)

GO

CREATE PROCEDURE dbo.try_acquire_lock
    @resource_id varchar(256),
    @expires_at datetime,
    @session_id uniqueidentifier OUT
AS
SET NOCOUNT ON;
SET @session_id = NEWID()
BEGIN TRY
    INSERT INTO dbo.resource_locks(resource_id, expires_at, session_id)
    VALUES (@resource_id, @expires_at, @session_id);
    -- lock acquired
    RETURN 0;
END TRY
BEGIN CATCH
    /* 2627 is primary key violation error */
    IF ERROR_NUMBER() != 2627 THROW

    UPDATE dbo.resource_locks
    SET
        expires_at = @expires_at,
        session_id = @session_id
    WHERE 
        resource_id = @resource_id
        /* It acquires expired locks only */
        AND  expires_at < GETUTCDATE()
    IF @@ROWCOUNT = 0 
    BEGIN
        -- failed to acquire lock. the lock is still active or the lock was just released (deleted)
        SET @session_id = NULL
        RETURN 1
    END
    
    -- expired lock acquired
    RETURN 0;
END CATCH

GO

CREATE PROCEDURE dbo.try_renew_lock
    @resource_id varchar(256),
    @expires_at datetime,
    @session_id uniqueidentifier
AS
SET NOCOUNT ON;
UPDATE dbo.resource_locks
SET expires_at = @expires_at
WHERE
    resource_id = @resource_id
    AND session_id = @session_id
IF @@ROWCOUNT = 0
BEGIN
    -- failed to renew lock. 
    RETURN 1
END
-- lock renewed
RETURN 0

GO

CREATE PROCEDURE dbo.try_release_lock
    @resource_id nvarchar(384),
    @session_id uniqueidentifier
AS
SET NOCOUNT ON
DELETE FROM dbo.resource_locks
WHERE
    resource_id = @resource_id
    AND session_id = @session_id;
IF @@ROWCOUNT = 0
BEGIN
    -- failed to release lock. 
    RETURN 1
END
-- lock released
RETURN 0

GO

Блокировки хранятся в таблице dbo.resource_locks. Срок действия блокировок истекает в определенную дату и время. По истечении срока блокировки другие сеансы могут получить блокировку ресурса resource_id. Таким образом, если процесс завершается, не сняв блокировку, другие процессы могут получить блокировку после истечения ее срока действия.

Когда нужно получить распределенный мьютекс для данного ключа, вызывается dbo.try_acquire_lock, передавая ключ как resource_id и соответствующее expires_at datetime. Если хранимая процедура может получить блокировку, она вернет 0 и установит выходной параметр. Если ему не удастся получить блокировку, он вернет 1, а @session_id будет @session_id.

Если операция, которую необходимо выполнить под мьютексом, может завершиться после истечения срока действия мьютекса, вам необходимо продлить блокировку до истечения срока ее действия, передав новую дату истечения срока действия и NULL, который вы получили при получении блокировки.

В конце операции вам следует снять блокировку, вызвав хранимую процедуру.

Другие вопросы по теме