Планирование архитектуры с помощью Timed Webhook - PullRequest
0 голосов
/ 17 апреля 2019

Фон

У меня есть веб-сайт, написанный на ядре C # + ASP.NET (v2.2), и я предоставляю этот API:

POST /api/account/signup
POST /api/account/send-greeting

Моя бизнес-стратегия заключается в том, чтобы отправить пользователю приветствие (POST /api/account/send-greeting) ровно через 15 минут после регистрации.

Проблема

Итак, мне нужно как-то зарегистрироваться на этом новом мероприятии. Я хоть про 2 варианта:

  1. Запускать фоновую задачу каждую минуту, которая запрашивает базу данных для новых пользователей. кто может получить это письмо.
  2. Использовать распределенные очереди. Как и Azure Storage Queues. С помощью этой очереди вы можете поставить сообщение в очередь с тайм-аутом видимости. Таким образом, вы можете определить, что отправляете сейчас сообщение в очередь, но оно появится там только через 15 минут. Затем вам нужно будет развернуть фоновую службу, которая будет ждать новых активных сообщений в очереди и выполнит их.

У этих двух решений есть значение минусов:

  1. Решение № 1 - наивное решение. Он потребляет много ресурсов базы данных, так как он должен работать каждую минуту и ​​запрашивать всех зарегистрированных пользователей в таблице. Это неэффективно, так как большую часть дня у меня нет новых зарегистрированных пользователей.
  2. Решение № 2 слишком громоздко. Вам нужно использовать очереди и развернуть фоновый сервис, чтобы сделать это. Звук для меня, как слишком много работы.

Эта задача звучит для меня как нечто очевидное. Лучшее решение, которое я не уверен, что существует, может быть: Внешний сервис, которому вы отправляете ему сообщение типа

POST /api/register-to-timed-callback?when=15m&target-url=http://example.com/api/account/send-greeting

Вопрос

Я что-то пропустил? Как вы можете решить эту проблему самым простым и эффективным способом?

1 Ответ

1 голос
/ 17 апреля 2019

Вы можете создать фоновую службу в очереди на основе IHostedService. Затем вы добавляете элемент в очередь, когда пользователь регистрируется и обрабатывает эту очередь через фоновую службу. Когда вы вытаскиваете элементы из очереди, вы проверяете, готовы ли они к отправке, в зависимости от времени. Если это так, вы попали в конечную точку send-greeting, в противном случае вы потребуете предмет. документы предоставляют образец такой услуги.

Очередь:

public interface IBackgroundTaskQueue
{
    void QueueBackgroundWorkItem(Func<CancellationToken, Task> workItem);

    Task<Func<CancellationToken, Task>> DequeueAsync(
        CancellationToken cancellationToken);
}

public class BackgroundTaskQueue : IBackgroundTaskQueue
{
    private ConcurrentQueue<Func<CancellationToken, Task>> _workItems = 
        new ConcurrentQueue<Func<CancellationToken, Task>>();
    private SemaphoreSlim _signal = new SemaphoreSlim(0);

    public void QueueBackgroundWorkItem(
        Func<CancellationToken, Task> workItem)
    {
        if (workItem == null)
        {
            throw new ArgumentNullException(nameof(workItem));
        }

        _workItems.Enqueue(workItem);
        _signal.Release();
    }

    public async Task<Func<CancellationToken, Task>> DequeueAsync(
        CancellationToken cancellationToken)
    {
        await _signal.WaitAsync(cancellationToken);
        _workItems.TryDequeue(out var workItem);

        return workItem;
    }
}

И размещенный сервис:

public class QueuedHostedService : BackgroundService
{
    private readonly ILogger _logger;

    public QueuedHostedService(IBackgroundTaskQueue taskQueue, 
        ILoggerFactory loggerFactory)
    {
        TaskQueue = taskQueue;
        _logger = loggerFactory.CreateLogger<QueuedHostedService>();
    }

    public IBackgroundTaskQueue TaskQueue { get; }

    protected async override Task ExecuteAsync(
        CancellationToken cancellationToken)
    {
        _logger.LogInformation("Queued Hosted Service is starting.");

        while (!cancellationToken.IsCancellationRequested)
        {
            var workItem = await TaskQueue.DequeueAsync(cancellationToken);

            try
            {
                await workItem(cancellationToken);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, 
                   $"Error occurred executing {nameof(workItem)}.");
            }
        }

        _logger.LogInformation("Queued Hosted Service is stopping.");
    }
}

Этот код прямо из документации. Он в основном поддерживает ваш сценарий использования, но требует нескольких настроек, чтобы вы могли пройти весь путь туда. Во-первых, поскольку есть компонент времени (т. Е. Вы хотите обрабатывать элемент в очереди только, если ему 15 минут), вам нужно сделать параметр типа ConcurrentQueue<T> тем, что вы можете кодировать как в datetime, так и в func , Это может быть ValueTuple или реальный объект, созданный специально для этой цели: решать только вам. Например:

ConcurrentQueue<(DateTimeOffset added, Func<CancellationToken, Task> task)>

Затем вам нужно изменить логику удаления, чтобы переместить ее в очередь, если не прошло достаточно времени:

public async Task<Func<CancellationToken, Task>> DequeueAsync(
    CancellationToken cancellationToken)
{
    await _signal.WaitAsync(cancellationToken);
    _workItems.TryDequeue(out var workItem);
    if (DateTimeOffset.UtcNow.AddMinutes(-15) < workItem.added)
    {
        _workItems.Enqueue(workItem);
        return ct => ct.IsCancellationRequested ? Task.FromCanceled(ct) : Task.CompletedTask;;
    }
    return workItem;
}

Возвращение туда, когда еще не время, это просто фиктивная лямбда для удовлетворения ограничения. Вы можете вместо этого вернуть что-то вроде null, но тогда вам также потребуется изменить метод ExecuteAsync фоновой службы, чтобы добавить проверку на ноль перед обработкой функции.

Стоит также отметить, что пример кода разработан как универсальный и позволяет ставить в очередь что угодно для обработки. В результате, из-за требуемых изменений времени, вы должны использовать более конкретные имена: ITimedBackgroundTaskQueue, TimedBackgroundTaskQueue и TimedQueuedHostedService, например. Это особенно верно в свете того факта, что примеры интерфейсов / классов из документов будут фактически интегрированы в ASP.NET Core 3.0.

...