Meneruskan CancellationToken sampai ke lapisan terbawah di ASP.NET Core
Kenapa request HTTP yang dibatalkan seharusnya juga menghentikan query database-nya, dan kebiasaan kecil yang membuatnya terjadi.
3 menit baca
Di halaman ini
Pekerjaan yang tidak lagi ditunggu siapa pun
Seorang pengguna membuka halaman laporan, mulai tidak sabar, lalu pindah halaman. Browser menutup koneksi, tetapi server terus bekerja: controller masih menunggu query SQL yang berat, memetakan ribuan baris, dan menyerialisasi respons yang tidak akan dibaca siapa pun.
Saat sepi, hal ini tidak berbahaya. Saat beban tinggi, request yang ditinggalkan ikut berebut koneksi
database dan waktu thread pool dengan request yang sungguhan. ASP.NET Core sebenarnya sudah tahu
kapan client pergi, dan memberitahukannya lewat CancellationToken. Tugas kita adalah meneruskan
token itu ke mana-mana.
Dari mana token berasal
Tambahkan parameter CancellationToken pada action, dan ASP.NET Core otomatis mengikatnya ke
HttpContext.RequestAborted:
[HttpGet("reports/monthly")]
public async Task<IActionResult> Monthly(int year, int month, CancellationToken ct)
{
var report = await reports.BuildMonthlyAsync(year, month, ct);
return Ok(report);
}Minimal API bekerja dengan cara yang sama:
app.MapGet("/reports/monthly", (int year, int month, IReportService reports, CancellationToken ct) =>
reports.BuildMonthlyAsync(year, month, ct));Teruskan ke semua yang di-await
Token hanya berguna kalau sampai ke kode yang benar-benar menunggu. Biasanya itu berarti EF Core,
HttpClient, dan I/O lainnya:
public sealed class ReportService(AppDbContext db, HttpClient rates) : IReportService
{
public async Task<MonthlyReport> BuildMonthlyAsync(int year, int month, CancellationToken ct)
{
var from = new DateTime(year, month, 1);
var to = from.AddMonths(1);
var premiums = await db.Payments
.Where(p => p.PaidAt >= from && p.PaidAt < to)
.GroupBy(p => p.ProductCode)
.Select(g => new { Product = g.Key, Total = g.Sum(p => p.Amount) })
.ToListAsync(ct);
var fx = await rates.GetFromJsonAsync<FxRates>("rates/latest", ct);
return MonthlyReport.Create(premiums, fx);
}
}Ketika token dibatalkan, EF Core meminta SQL Server membatalkan command yang sedang berjalan, dan
HttpClient menghentikan panggilan keluarnya. Keduanya melempar OperationCanceledException.
Aturan sederhana supaya konsisten
- Setiap method async yang melakukan I/O menerima
CancellationToken, biasanya sebagai parameter terakhir. NamaictataucancellationToken, dan konsisten di seluruh codebase. - Jangan beri nilai default
= defaultdi kode aplikasi. Parameter default membuatnya mudah terlupa, dan compiler tidak akan mengingatkan lagi. Pengecualiannya adalah library. - Jangan pernah mengoper
CancellationToken.Nonehanya supaya bisa di-compile. Kalau sebuah panggilan memang tidak boleh dibatalkan, tulis komentar yang menjelaskan alasannya. - Loop memeriksa token. Untuk loop yang berat di CPU, panggil
ct.ThrowIfCancellationRequested()sekali per iterasi atau per batch.
Pekerjaan yang tidak boleh dibatalkan
Ada pekerjaan yang harus selesai walaupun pengguna sudah pergi, misalnya menulis catatan audit atau mencatat pembayaran yang sudah diterima provider. Jangan teruskan token request ke sana:
// The provider has already charged the customer; recording it must not depend on the browser.
await payments.MarkAsPaidAsync(paymentId, CancellationToken.None);Untuk pekerjaan yang lebih panjang, serahkan ke background queue atau hosted service yang punya siklus hidup sendiri, bukan dijalankan di dalam request.
Jangan catat pembatalan sebagai error
Request yang dibatalkan itu normal, bukan bug. Kalau penanganan error global mencatat semua exception sebagai error, request yang ditinggalkan akan membanjiri log. Saring ketika client memang sudah pergi:
catch (OperationCanceledException) when (httpContext.RequestAborted.IsCancellationRequested)
{
// Client disconnected; nothing to report.
}Menambahkan timeout sendiri
Kadang Anda juga butuh batas waktu yang pasti, terlepas dari client. Gabungkan keduanya:
using var timeout = CancellationTokenSource.CreateLinkedTokenSource(ct);
timeout.CancelAfter(TimeSpan.FromSeconds(10));
var result = await slowService.RunAsync(timeout.Token);Sekarang operasinya berhenti ketika client pergi atau setelah sepuluh detik, mana yang lebih dulu.
Ringkasan
- ASP.NET Core memberikan
RequestAbortedsecara gratis; terima sebagai parameter. - Teruskan ke EF Core,
HttpClient, dan setiap panggilan I/O yang di-await. - Lewati token dengan sengaja hanya untuk pekerjaan yang wajib selesai, dan tulis alasannya.
- Perlakukan pembatalan sebagai hal normal, bukan error di log.