[Часть 12] Azure Functions: фоновые задания


Введение

Фоновые задания (AKA Background Jobs) широко используются в настоящее время благодаря простоте использования, которую обеспечивают нам новые технологии и инструменты. Как в облаке, так и во фреймворках, таких как .NET, есть все необходимое для того, чтобы задачи могли выполняться в процессах, отличных от основного приложения.

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

В этом посте мы рассмотрим, как иметь Web API, который отправляет работу другому процессу, расположенному в другом месте.

Полный код можно найти здесь, и, как всегда, вы можете следить за мной @balunatic для любых вопросов.

Необходимые инструменты

В этом посте я расскажу о необходимых нам инструментах.

  • Visual Studio и Azure Development: Если вы используете Visual Studio, у вас должны быть инструменты “Azure Development”:
    • Здесь автоматически будут установлены эмуляторы Storage Account.
  • Инструменты Azure Functions: всегда рекомендуется иметь установленный хост, чтобы при необходимости запускать его из консоли. См. подробнее.
  • Azurite: Если вы не используете visual studio и хотите использовать VS Code, вы можете сделать это, загрузив/запустив Azurite из:
    • Расширение VS Code
    • npm с помощью npm install -g azurite
    • docker с помощью docker pull mcr.microsoft.com/azure-storage/azurite.
    • Или, наконец, используя исходный код с github
    • Дополнительная информация здесь

Примечание 👀: В посте Журналы аудита мы уже использовали Azurite / Storage Emulator. Технически, он должен быть у вас с visual studio, если нет, то вы знаете, как его установить.

Подсобные работы

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

Существует несколько типов фоновых работ:

  • Задачи, требовательные к процессору, такие как математические расчеты или анализ моделей.
  • Задачи с интенсивным вводом/выводом
  • Запланированные задачи
  • Задачи с длительным сроком выполнения

Эти асинхронные задачи могут быть “запущены” различными способами, но они делятся на две категории:

  • Триггеры, управляемые событиями. Задача инициируется в ответ на событие, как правило, действие пользователя или следующий шаг в каком-либо рабочем процессе, запущенном другой системой/процессом.
  • Триггеры, управляемые по расписанию. Задание инициируется на основе временного графика. Это может быть что-то, что работает каждое утро или по выходным и т.д.

Триггеры, управляемые событиями

Существует несколько способов запуска задач в фоновом режиме:

  • Некоторая система или пользовательский интерфейс отправляет сообщение через очередь. Сообщение содержит данные о действии, которое только что произошло, например, пользователь что-то купил, и заказ должен быть обработан. Фоновое задание прослушивает очередь, чтобы быть в курсе поступления новых сообщений.
  • Некоторая система или пользовательский интерфейс хранит/обновляет информацию в некотором хранилище. Фоновое задание может следить за каким-либо хранилищем (например, базой данных), ожидая, пока что-то произойдет.
  • Через HTTP-вызовы к конечной точке, например, API. Через конечную точку он получает соответствующую информацию и начинает обработку.

Триггеры, управляемые по расписанию

Существует несколько форм запланированных по времени задач:

  • Наиболее распространенным является использование выражений cron.

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

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

Зачем использовать “Фоновые задания”?

Реализация фоновых заданий помогает, в основном, предложить лучший пользовательский опыт, иногда вам не нужно заставлять пользователя ждать завершения задачи. Задача просто начинается, потому что пользователь “запустил” ее, но продолжается асинхронно до завершения.

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

Задачи, на выполнение которых требуется много времени, обычно добавляются в “очередь”, чтобы она выполнялась по мере продвижения других очередей.

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

Уведомление о завершении задачи может быть сделано различными способами, например, SMS-сообщением, электронной почтой или push-уведомлением. Это действительно зависит от типа применения.

Что такое Azure Queue Storage?

Azure Queue Storage позволяет нам развертывать сообщения между приложениями. Это не Message Broker или Service Bus, как другие сервисы, Queue Storage – это упрощенная форма обмена сообщениями через хранилище Azure. Но он по-прежнему позволяет нам отправлять сообщения между различными приложениями и соединять их для взаимодействия друг с другом.

Простота реализации делает его привлекательным для приложений без большого количества требований, поскольку вам не всегда понадобится Message Broker или Service bus.

Я использовал Queue Storage для различных проектов, но главный, который приходит мне на ум, – это процессор платежей и подписки, о котором я уже упоминал.

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

Учетная запись хранилища Azure

Queue Storage является частью учетных записей Azure Storage. Счет хранения имеет 4 основные услуги:

  • Блобы: хранение файлов любого типа.
  • Таблицы: полуструктурированные данные, очень полезны для больших данных
  • Очереди: хранение сообщений
  • Файлы: Это похоже на монтирование сетевых дисков в облаке.

Аналогично, сейчас мы занимаемся только очередями:


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

Архитектура для реализации

Мы говорим о 12 части этой серии постов, мы продолжим работать с кодом, который мы уже сделали до сих пор. В любом случае, вот репозиторий, в котором вы можете просмотреть весь код.


Web API – это тот, который будет отвечать за прием HTTP-запросов от любого аутентифицированного клиента. Это отправит сообщения в Queue Storage, чтобы фоновое задание (AKA Azure Function) прочитало сообщения и обработало их.

В настоящее время у нас уже есть база данных, аккаунт хранения и API. Теперь мы добавим функцию Azure Function, которая будет спрашивать, есть ли новые сообщения в очереди.

Функции Azure

Azure Functions – это бессерверное решение, которое позволяет нам писать меньше кода, поддерживать меньше инфраструктуры и экономить расходы. Вместо того чтобы беспокоиться о том, как развернуть и обслуживать серверы, облачная инфраструктура предоставляет нам все необходимые ресурсы, чтобы мы всегда могли сосредоточиться на главном – функциональности нашего программного обеспечения.

Azure Functions вызываются различными триггерами (событиями или расписаниями), эти триггеры могут быть напрямую связаны с различными технологиями и инфраструктурами Azure, это очень полезно, поскольку, как уже упоминалось, экономит много кода и времени.

Как правило, Azure Functions используются для решения очень специфических задач, это не значит, что вы будете писать целый API с помощью Azure Functions (хотя вы могли бы), но вот некоторые случаи использования:

Если вы хотите сделать… Тогда…
Веб-интерфейс Вы можете реализовать конечную точку с помощью HTTP-триггера
Обработка загруженных файлов Вы можете запустить функцию, когда файлы загружаются на некоторую учетную запись хранилища
Рабочий процесс Вы можете выстраивать цепочки функций с помощью долговечных функций
Реагировать на изменения в БД Вы можете запускать пользовательскую логику, когда что-то происходит в Cosmos DB
Запуск запланированных задач Вы можете запускать код в соответствии с выражениями cron
Создание сообщений в очередях Вы можете получать сообщения с помощью хранилища очередей, сервисной шины или концентраторов событий
Потоки IoT Вы можете собирать данные с устройств IoT с помощью Azure IoT Hub
Обрабатывайте информацию в режиме реального времени Вы можете подключить SignalR к своей функции Azure Function

Без лишних слов, давайте посмотрим, что мы будем реализовывать сегодня…

Обработка заказов/покупок с помощью очередей.

Фоновое задание, которое мы сделаем в этот раз, касается процессора покупки. Мы работали над обработкой продуктов в предыдущих демонстрациях, поэтому имеет смысл иметь возможность “купить” их сейчас.

Мы будем создавать заказы под названием Checkout из Web API, создавать и “принимать” заказ. Затем Web API немедленно отправит сообщение через Queue Storage для Azure Function (которая ожидает сообщений), чтобы обработать запрос и выполнить необходимую работу (теоретически это может быть проверка запасов, выставление счетов, счетов-фактур и т.д.).

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

Итак, если говорить простым языком, то, что я добавил в этом посте, чтобы иметь возможность выполнять функции Azure Functions, это две новые сущности: Checkout и CheckoutProduct. У нас уже был класс Product, теперь мы создали отношения между ними.

IQueueService Contract

Этот контракт поможет нам абстрагироваться от деталей реализации очереди. Сделав это таким образом, мы можем даже перейти с Queue Storage на RabbitMQ без необходимости изменять ядро приложения.

Итак, следуя по пути ApplicationCore > Common > Services, мы имеем следующее:

namespace MediatrExample.ApplicationCore.Common.Services;
public interface IQueuesService
{
    Task QueueAsync<T>(string queueName, T item);
}
Войдите в полноэкранный режим Выход из полноэкранного режима

Общий тип позволит нам отправлять любые данные в виде сообщения.

Реализация AzureStorageQueueService

Для использования Queue Storage нам понадобится следующая библиотека внутри ApplicationCore:

dotnet add package Azure.Storage.Queues
Войдите в полноэкранный режим Выход из полноэкранного режима

Примечание 👀: Помните, что мы придерживаемся стиля “Vertical Slice Architecture”, поэтому почти все идет в Application Core.

Следуя по пути ApplicationCore > Infrastructure > Services > AzureQueues, мы имеем следующее:

using ...;

namespace MediatrExample.ApplicationCore.Infrastructure.Services.AzureQueues;

public class AzureStorageQueueService : IQueuesService
{
    private readonly IConfiguration _config;

    public AzureStorageQueueService(IConfiguration config)
    {
        _config = config;
    }

    public async Task QueueAsync<T>(string queueName, T item)
    {
        var queueClient = new QueueClient(_config.GetConnectionString("StorageAccount"), queueName, new QueueClientOptions
        {
            MessageEncoding = QueueMessageEncoding.Base64
        });

        queueClient.CreateIfNotExists();

        var message = JsonSerializer.Serialize(item);
        await queueClient.SendMessageAsync(message);
    }
}
Войдите в полноэкранный режим Выход из полноэкранного режима

На данный момент нам нужно только отправлять сообщения в очередь. О чтении позаботится функция Azure Function и ее триггер, который уже запрограммирован для этого (что мы увидим позже).

То, что здесь происходит, очень просто, важно лишь то, что Azure Function ожидает поступления сообщений в Base 64 (я не знаю почему 😁).

Строка подключения "StorageAccount": "UseDevelopmentStorage=true", поэтому, опять же, важно, чтобы был запущен симулятор хранилища.

Отправка сообщений в очередь

У нас действительно все “готово” для начала отправки сообщений, нам нужно только создать пару новых команд (помните, что все это следует CQRS и Vertical Slice Architecture).

Функции > Оформление заказа > Команды > NewCheckout

Чтобы “запустить” эту очередь, сначала мы должны создать команду, которая теоретически должна обрабатываться нашим фоновым заданием:

using ...;

namespace MediatrExample.ApplicationCore.Features.Checkouts.Commands;

[AuditLog] // <--- del post "Sistema Audiable"
public class NewCheckoutCommand : IRequest
{
    public List<NewCheckoutItems> Products { get; set; } = new();

    public class NewCheckoutItems
    {
        public string ProductId { get; set; } = default!;
        public int Quantity { get; set; }
    }
}

public class NewCheckoutCommandHandler : IRequestHandler<NewCheckoutCommand>
{
    private readonly MyAppDbContext _context;
    private readonly IQueuesService _queuesService;
    private readonly CurrentUser _user;

    public NewCheckoutCommandHandler(MyAppDbContext context, ICurrentUserService currentUserService, IQueuesService queuesService)
    {
        _context = context;
        _queuesService = queuesService;
        _user = currentUserService.User;
    }

    public async Task<Unit> Handle(NewCheckoutCommand request, CancellationToken cancellationToken)
    {
        Checkout newCheckout = await CreateCheckoutAsync(request, cancellationToken);
        await QueueCheckout(newCheckout);

        return Unit.Value;
    }
}
Войдите в полноэкранный режим Выход из полноэкранного режима

Наша команда получает в качестве входного параметра список товаров для покупки, обработчик обрабатывает этот запрос в двух частях: CreateCheckoutAsync и QueueCheckout.

Эта команда имеет зависимости:

private async Task<Checkout> CreateCheckoutAsync(NewCheckoutCommand request, CancellationToken cancellationToken)
{
    var newCheckout = new Checkout
    {
        CheckoutDateTime = DateTime.UtcNow,
        UserId = _user.Id,
        Total = 0
    };

    foreach (var item in request.Products)
    {
        var product = await _context.Products.FindAsync(item.ProductId.FromHashId());

        if (product is null)
        {
            throw new ValidationException(new List<ValidationFailure>
            {
                new ValidationFailure("Error", $"El producto {item.ProductId} no existe")
            });
        }

        var newProduct = new CheckoutProduct
        {
            ProductId = product.ProductId,
            Quantity = item.Quantity,
            UnitPrice = product.Price,
            Total = item.Quantity * product.Price
        };

        newCheckout.Products.Add(newProduct);
    }

    newCheckout.Total = newCheckout.Products.Sum(p => p.Total);

    _context.Checkouts.Add(newCheckout);

    await _context.SaveChangesAsync(cancellationToken);
    return newCheckout;
}
Войдите в полноэкранный режим Выход из полноэкранного режима

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

Примечание 👀: Здесь представлены вещи, которые уже встречались в других постах, если что-то не понятно, вы всегда можете спросить меня в моем твиттере или ниже в комментариях.

И, наконец, в очередь отправляется сообщение под названием new-checkouts.

  private async Task QueueCheckout(Checkout newCheckout)
  {
      await _queuesService.QueueAsync("new-checkouts", new NewCheckoutMessage
      {
          CheckoutId = newCheckout.CheckoutId
      });
  }
Войдите в полноэкранный режим Выход из полноэкранного режима

NewCheckoutMessage – это класс, который просто содержит Id кассы, которая была только что создана. Это находится в разделе Общие > Сообщения, с намерением, чтобы существовало больше сообщений.

DependencyInjection

Мы регистрируем новый “слой” зависимостей в текущем классе, который у нас уже есть:

public static IServiceCollection AddInfrastructure(this IServiceCollection services)
{
    services.AddScoped<IQueuesService, AzureStorageQueueService>();

    return services;
}
Войдите в полноэкранный режим Выход из полноэкранного режима

До сих пор мы работали с ApplicationCore, Persistence и Security. Итак, мы уже добавили Infrastructure и, конечно же, вызываем его в Program.cs.

Примечание 👀: Здесь будет полезно добавить больше зависимостей сторонних сервисов или инфраструктуры.

WebApi > CheckoutsController

И его конечная точка должна быть доступна для вызова:

using ...;

namespace MediatrExample.WebApi.Controllers;

[Authorize]
[ApiController]
[Route("api/checkouts")]
public class CheckoutsController : ControllerBase
{
    private readonly IMediator _mediator;

    public CheckoutsController(IMediator mediator)
    {
        _mediator = mediator;
    }

    /// <summary>
    /// Crea una nueva orden de productos
    /// </summary>
    /// <param name="command"></param>
    /// <returns></returns>
    [HttpPost]
    public async Task<IActionResult> NewCheckout([FromBody] NewCheckoutCommand command)
    {
        await _mediator.Send(command);

        return Accepted();
    }
}
Войдите в полноэкранный режим Выход из полноэкранного режима

Процессор кассы (функция Azure)

Теперь нам просто нужно создать фоновое задание, которое будет ждать сообщений, которые мы уже отправили. Это можно сделать разными способами, в том числе сделать их облачными агностиками и запускать в любом облаке. На самом деле, я убежден, что Azure – это лучшее облако 👀.

Самый простой способ создания Azure Function – из visual studio (вы также можете создать ее с помощью func new, используя Azure Functions CLI):

Я назвал его MediatRExample.CheckoutProcessor.


Как вы видите, у нас есть много вариантов “Триггеров”, а тот, который нас сегодня интересует, это триггер очереди.

Создав проект, проверяем, что у нас установлены следующие пакеты (Visual studio создает мне неправильный шаблон, не знаю почему 😒):

<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Storage" Version="4.0.4" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Sdk" Version="1.3.0" OutputItemType="Analyzer" />
<PackageReference Include="Microsoft.Azure.Functions.Worker" Version="1.5.2" />
Войдите в полноэкранный режим Выход из полноэкранного режима

Таким образом, вы сможете скомпилировать проект без каких-либо проблем.

Конфигурация CheckoutProcessor

Мы настраиваем зависимости проекта с целью их повторного использования, если Web API перестанет быть конечным приложением.

В рамках Azure Function Program мы настроим Persistence и Application Core, чтобы повторно использовать все, что у нас уже есть:

using ...;

namespace MediatRExample.CheckoutProcessor;
public class Program
{
    public static void Main()
    {
        var host = new HostBuilder()
            .ConfigureFunctionsWorkerDefaults()
            .ConfigureServices((hostBuilderContext, services) =>
            {
                var configuration = hostBuilderContext.Configuration;
                services.AddApplicationCore();
                services.AddPersistence(configuration);
                services.AddInfrastructure();

                services.AddTransient<ICurrentUserService, WorkerUserService>();
            })
            .Build();

        host.Run();
    }
}
Войдите в полноэкранный режим Выход из полноэкранного режима

Если вы помните, в посте об аутентификации мы добавили абстракцию под названием ICurrentUserService. Ядро использует эту абстракцию для доступа к информации аутентифицированного пользователя.

В этой функции не будет никаких аутентифицированных пользователей, но это будет доверенное фоновое задание, поэтому мы имитируем, что оно “аутентифицировано”, создав класс WorkerUserService.

using MediatrExample.ApplicationCore.Common.Interfaces;
using System;

namespace MediatRExample.CheckoutProcessor;
public class WorkerUserService : ICurrentUserService
{
    public CurrentUser User => new CurrentUser(Guid.Empty.ToString(), "CheckoutProcessor", true);

    public bool IsInRole(string roleName) => true;
}
Войдите в полноэкранный режим Выход из полноэкранного режима

Таким образом, любая ранее настроенная инфраструктура (например, журналы, проверяемые объекты и журналы аудита) будет продолжать работать без проблем.

Мы сможем наблюдать за тем, как работает наш сотрудник, как если бы это был реальный пользователь.

На данном этапе у нас все настроено, включая MediatR и Entity Framework.

ProcessCheckoutCommand

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

Это находится в разделе Features > Checkouts > Commands.

using ...;

namespace MediatrExample.ApplicationCore.Features.Checkouts.Commands;

[AuditLog]
public class ProcessCheckoutCommand : IRequest
{
    public int CheckoutId { get; set; }
}

public class ProcessCheckoutCommandHandler : IRequestHandler<ProcessCheckoutCommand>
{
    private readonly MyAppDbContext _context;
    private readonly ILogger<ProcessCheckoutCommandHandler> _logger;

    public ProcessCheckoutCommandHandler(MyAppDbContext context, ILogger<ProcessCheckoutCommandHandler> logger)
    {
        _context = context;
        _logger = logger;
    }

    public async Task<Unit> Handle(ProcessCheckoutCommand request, CancellationToken cancellationToken)
    {
        var checkout = await _context.Checkouts.FindAsync(request.CheckoutId);

        if (checkout is null)
        {
            throw new NotFoundException();
        }

        _logger.LogInformation(
          "Nueva orden recibida con Id {Id}", checkout.CheckoutId);
        _logger.LogInformation(
          "El usuario {UserId} ordenó {ProductCount} producto(s)", checkout.UserId, checkout.Products.Count());

        // Working
        _logger.LogInformation("Realizando cobro...");
        await Task.Delay(5000);
        _logger.LogInformation("Cobro realizado");

        checkout.Processed = true;
        checkout.ProcessedDateTime = DateTime.UtcNow;

        _logger.LogWarning("Se procesó una orden con costo total de {Total:C}", checkout.Total);

        await _context.SaveChangesAsync(cancellationToken);

        return Unit.Value;
    }
}
Войдите в полноэкранный режим Выход из полноэкранного режима

Здесь мы фактически имитируем работу, мы запрашиваем Checkout, который был получен параметром (который, в свою очередь, придет из сообщения из очереди), и через имитируемое время отмечаем его как выполненный.

Функция CheckoutProcessor

Теперь пришло время создать (или отредактировать созданную по умолчанию функцию Function1) функцию для получения сообщений:

using ...;

namespace MediatRExample.CheckoutProcessor.Functions
{
    public class CheckoutProcessor
    {
        private readonly ILogger _logger;
        private readonly IMediator _mediator;

        public CheckoutProcessor(ILoggerFactory loggerFactory, IMediator mediator)
        {
            _logger = loggerFactory.CreateLogger<CheckoutProcessor>();
            _mediator = mediator;
        }

        [Function("CheckoutProcessor")]
        public async Task Run([QueueTrigger("new-checkouts", Connection = "AzureWebJobsStorage")] string myQueueItem)
        {
            _logger.LogInformation("Nuevo mensaje recibido {Message}", myQueueItem);
            _logger.LogInformation("Procesando...");

            var message = JsonSerializer.Deserialize<NewCheckoutMessage>(myQueueItem);

            await _mediator.Send(new ProcessCheckoutCommand
            {
                CheckoutId = message.CheckoutId
            });
        }
    }
}
Войдите в полноэкранный режим Выход из полноэкранного режима

Эта функция ожидает сообщений, которые существуют в очереди new-checkouts хранилища AzureWebJobStorage.

Мы знаем формат сообщения, поэтому сериализуем его обратно и отправляем медиатору для выполнения обработчика.

В этот момент тот, кто “запустил” событие, больше не ждет результата, и здесь вы можете продержаться столько, сколько нужно. Фоновая обработка предназначена для этого, но у нас больше нет клиента, ожидающего в пользовательском интерфейсе с сообщением “Loading…”.

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

Важно отметить, что нам также необходимо включить конфигурацию appsettings.json в local.settings.json функции azure:

{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "UseDevelopmentStorage=true",
    "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated",
    "ConnectionStrings:Default": "Server=(localdb)\mssqllocaldb;Database=MediatRExample;Trusted_Connection=True;MultipleActiveResultSets=false",
    "UseInMemory": false,
    "AuditLogs:Enabled": true,
    "AuditLogs:ConnectionString": "UseDevelopmentStorage=true"
  }
}
Войдите в полноэкранный режим Выход из полноэкранного режима

Поскольку мы повторно используем ApplicationCore, есть вещи, которые нам необходимо включить (все это – из прошлых постов серии).
Короче говоря, эта Azure Function будет аудируемой, она будет отправлять логи через [AuditLog] и, если мы захотим, также в Application Insights.

В конечном итоге, эта функция похожа на еще один web api, мы настраиваем ее с теми же функциями, что и раньше.

Тестирование очередей

Итак, наконец, давайте протестируем.

Мы должны запустить оба проекта. Когда я не отлаживаю, я обычно запускаю все в консоли, потому что я могу визуализировать это лучше. Итак, внутри WebApi мы запускаем классический dotnet run, а в другой консоли запускаем функцию azure, но с хостом функций (тот, который делает триггеры и привязки). Мы можем запустить его с помощью func host start внутри CheckoutProcessor.

Мы видим, как справа Azure Function Host уже обнаружил созданную нами функцию: CheckoutProcessor типа queueTrigger.

Для того чтобы проверить и сформировать JSON ниже, нам необходимо:

  • Сгенерируйте JWT с конечной точкой, указанной в этом сообщении.
  • Запросите демо-продукты, потому что они имеют HashIds в качестве промарных ключей (видел это здесь).

И, наконец, тест:

При вызове API создается отложенный заказ для обработки (т.е. Processed = false) и сообщение отправляется в очередь. В ответ от API я сразу получаю HTTP 202 Accepted, но обработка происходит в Azure Function, поскольку это может занять больше времени и нет необходимости заставлять клиента ждать.

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

Сначала HTTP-запрос отправляется в API, и мы получаем ответ Accepted.

Затем очередь выполняется в фоновом задании, то есть в функции azure.

Примечание 👀: В журнале написано “Пользователь заказал 0 товаров”. Это происходит потому, что я не запрашивал продукты с помощью Include() 🤣.

Как работают конвейеры MediatR, так же они работают и у нас, включая журналы аудита:


Пример AuditLog:

{
    "Environment": {
        "UserName": "isaac",
        "MachineName": "DELL-G5",
        "DomainName": "DELL-G5",
        "CallingMethodName": "MediatrExample.ApplicationCore.Common.Behaviours.AuditLogsBehavioru00602u002Bu003CHandleu003Ed__4.MoveNext()",
        "AssemblyName": "MediatrExample.ApplicationCore, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null",
        "Culture": "es-MX"
    },
    "EventType": "ProcessCheckoutCommand",
    "StartDate": "2022-05-07T19:49:02.300967Z",
    "EndDate": "2022-05-07T19:49:08.7736892Z",
    "Duration": 6473,
    "User": {
        "Id": "00000000-0000-0000-0000-000000000000",
        "UserName": "CheckoutProcessor",
        "IsAuthenticated": true
    },
    "Request": {
        "CheckoutId": 29
    }
}
Войдите в полноэкранный режим Выход из полноэкранного режима

Если мы исследуем базу данных, мы действительно можем увидеть, как она прошла путь от создания в 19:49:00 до обработки в 19:49:08 (8 секунд 🤭, в конце – пример).

Заключение

Таким образом, у нас уже есть архитектура, основанная на фоновых заданиях, где Web API отправляет работу рабочему, который находится в другом процессе и на другом сервере, что облегчается благодаря Azure Functions и Queue Storage.

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

Также полезно сказать, что если вы много “продаете”, то и плата будет выше, поскольку при использовании бессерверного режима Azure Function плата взимается за процессорное время.

Это очень полезно для меня, у меня уже есть проекты в производстве с модальностями, полностью аналогичными этой. Вам не всегда нужен Message Broker, которым иногда бывает сложно управлять, если требования просты, то простое Queue Storage и Azure Functions являются очень хорошим решением.

Ссылки

  • https://docs.microsoft.com/en-us/azure/storage/queues/storage-queues-introduction
  • https://docs.microsoft.com/en-us/azure/storage/queues/storage-dotnet-how-to-use-queues?tabs=dotnet
  • https://docs.microsoft.com/en-us/azure/architecture/best-practices/background-jobs
  • https://docs.microsoft.com/en-us/azure/azure-functions/functions-run-local?tabs=v4%2Cwindows%2Ccsharp%2Cportal%2Cbash

Оцените статью
Procodings.ru
Добавить комментарий