Токен Отмены: Ошибки, Которые Незаметно Приводят к Проблемам. Начало
Пользователь закрывает вкладку во время выполнения запроса. Клиент отключается. На панели управления этот запрос должен отображаться как отменённый в течение миллисекунд. Вместо этого он продолжает выполняться ещё секунды — обращается к БД, API, записывает результат, который никто никогда не прочтёт. При тысячах прерванных запросов в день вы платите за вычислительные ресурсы, которые никому не служат. Это чаще всего токен отмены, который корректно создаётся в начале конвейера обработки запросов, а затем незаметно перестаёт передаваться дальше. ASP.NET Core предоставляет возможность отмены практически бесплатно. Токен интегрирован в конвейер, и фреймворк отменяет его в момент отключения клиента. Все ошибки происходят в коде, который забывает о существовании токена.
Каждый HTTP-запрос в ASP.NET Core содержит токен, который вы можете запросить напрямую:
app.MapGet("/reports/{id}", async (int id, HttpContext http, ReportService service) =>
{
// Этот токен отменяется при отключении клиента
// или на старте процедуры завершения работы сервера
CancellationToken ct = http.RequestAborted;
var report = await service.BuildReportAsync(id, ct);
return Results.Ok(report);
});Редко требуется явно использовать HttpContext.RequestAborted — обработчики в минимальных API и методы-действия в MVC могут просто принимать параметр CancellationToken, и фреймворк автоматически привязывает RequestAborted к нему:
app.MapGet("/reports/{id}", async (int id, ReportService service, CancellationToken ct) =>
{
var report = await service.BuildReportAsync(id, ct);
return Results.Ok(report);
});Вот и весь контракт: токен передаётся вам, а ваша задача — передать его каждому ожидаемому вызову ниже. Описанные ниже ошибки — это варианты нарушения этой цепочки.
Ошибка 1: «Проглатывание» токена на уровне сервиса
Наиболее распространённое нарушение - сигнатура метода сервиса игнорирует параметр:
public class ReportService
{
// …
public async Task<Report> BuildReportAsync(int id)
{
var order = await _db.Orders.FindAsync(id);
var pricing = await _prcClient.GetAsync($"/price/{id}");
return Map(order, pricing);
}
}
Здесь ничего не вызывает ошибок. На код-ревью всё выглядит нормально, если только вы специально это не проверяете. Но с этого момента запрос становится неотменяемым — клиент может исчезнуть, a запрос к БД и HTTP-вызов всё равно завершатся. Верный вариант:
public async Task<Report> BuildReportAsync(
int id, CancellationToken ct)
{
var order = await _db.Orders.FindAsync(new object[] { id }, ct);
var pricing = await _prcClient.GetAsync($"/price/{id}", ct);
return Map(order, pricing);
}
Правило: если метод ожидает чего-либо, он обязан принимать CancellationToken. Анализатор, вроде CA2016 (часть встроенных анализаторов .NET), отметит большинство таких случаев — включите его как предупреждение, а не просто как рекомендацию.
Ошибка №2: Task.Run без токена или с неправильным токеном
Task.Run имеет две перегрузки, и легко вызвать ту, которая делает вашу работу неотменяемой:
public async Task<byte[]> GenerateExportAsync(
int reportId, CancellationToken ct)
{
return await Task.Run(() => BuildReport(reportId), ct);
}
Передача
ct в Task.Run останавливает запуск задачи только в том случае, если она уже отменена, но ничего не делает, если делегат уже запущен, т.к. BuildReport не может отслеживать отмену. Решение – передавать токен в исполняемый метод, а не только в Task.Run:public async Task<byte[]> GenerateExportAsync(
int reportId, CancellationToken ct)
{
return await Task.Run(() => BuildReport(reportId, ct), ct);
}
private byte[] BuildReport(
int id, CancellationToken ct)
{
for (int page = 0; page < TotalPages(id); page++)
{
// периодически проверяем отмену
ct.ThrowIfCancellationRequested();
RenderPage(id, page);
}
…
}
Для циклов, интенсивно использующих процессор, вызывайте
ct.ThrowIfCancellationRequested() между итерациями, а не только один раз в начале. Цикл, который проверяет токен один раз, а затем работает десять секунд, не может быть корректно отменён.Продолжение следует…
Источник: https://thecodeman.net/posts/cancellation-tokens-in-aspnet-core-mistakes