Токен Отмены: Ошибки, Которые Незаметно Приводят к Проблемам. Продолжение
Начало
Ошибка №3: Игнорирование OperationCanceledException
Отмена в .NET работает путём генерации исключений. Когда срабатывает токен, следующая ожидаемая операция генерирует исключение OperationCanceledException (или его подкласс TaskCanceledException). Блок catch, перехватывающий все исключения, перехватит и это — и теперь ваши логи полны «сбоев», которые на самом деле являются просто закрытием вкладок пользователями:
try
{
await service.BuildReportAsync(id, ct);
}
catch (Exception ex)
{
_logger.LogError(ex, "Генерация отчёта не удалась");
throw;
}
Решение – ожидать отмену:
try
{
await service.BuildReportAsync(id, ct);
}
catch (OperationCanceledException)
when (ct.IsCancellationRequested)
{
_logger.LogDebug("Генерация отчёта №{Id} отменена клиентом", id);
throw; // позволяем ASP.NET Core перехватить это и вернуть правильный ответ
}
catch (Exception ex)
{
_logger.LogError(ex, "Генерация отчёта не удалась");
throw;
}
ASP.NET Core умеет обрабатывать необработанное OperationCanceledException, связанное с RequestAborted — оно завершает запрос без записи кода 500.
Ошибка №4: Предположение, что фоновый сервис получает токен запроса
Эта ошибка сбивает с толку, потому что выглядит как противоположная проблема. У фонового сервиса есть собственный токен отмены, и он не имеет отношения к HTTP-запросу. Он срабатывает только при завершении работы хоста:
public class OutboxProcessor : BackgroundService
{
private readonly IServiceProvider _services;
public OutboxProcessor(IServiceProvider services)
=> _services = services;
protected override async Task ExecuteAsync(
CancellationToken stopToken)
{
while (!stopToken.IsCancellationRequested)
{
using var scope = _services.CreateScope();
var db = scope.ServiceProvider
.GetRequiredService<AppDbContext>();
// stopToken здесь означает «приложение закрывается»,
// а не «эта часть работы отменена кем-то»
await ProcessAsync(db, stopToken);
await Task.Delay(TimeSpan.FromSeconds(5), stopToken);
}
}
}
Если вы запускаете фоновые процессы внутри обработчика запросов и передаёте в него RequestAborted, вы создаёте ошибку: фоновые процессы прекращаются в тот же момент, когда отправляется HTTP-ответ (потому что RequestAborted также срабатывает в этих случаях при некоторых конфигурациях хоста) или когда клиент отключается. Фоновым процессам, которые должны продолжаться после завершения запроса, нужен собственный токен — обычно IHostApplicationLifetime.ApplicationStopping, а не токен запроса:
app.MapPost("/reports/{id}/export", async (
int id,
ReportService svc,
IHostApplicationLifetime lt) =>
{
// НЕ RequestAborted – сервис должен продолжить
// работу, даже когда ответ клиенту отправлен
_ = svc.GenerateAsync(id, lt.ApplicationStopping);
return Results.Accepted();
});Важное различие: RequestAborted означает «тот, кто вызывал этот запрос, отключился». ApplicationStopping означает «процесс завершается». Использование одного значения вместо другого либо приводит к преждевременной отмене работы, либо к лишней работе, которая должна была быть отменена вместе с запросом.
Окончание следует…
Источник: https://thecodeman.net/posts/cancellation-tokens-in-aspnet-core-mistakes