Menjalankan kode synchronous yang memakan waktu di dalam async handler Actix Web menyebabkan worker thread starvation. Permasalahan ini sering tidak terdeteksi saat pengujian beban rendah, tetapi memicu cascading failure pada trafik produksi tinggi.

Anatomi Masalah: Arsitektur Worker Actix Web

Secara default, Actix Web mengalokasikan satu worker thread per core CPU logis menggunakan tokio::task::LocalSet. Setiap worker menjalankan single-threaded event loop yang menangani koneksi secara kooperatif. Desain ini mengharapkan setiap future menyerahkan kendali kembali ke runtime (melalui .await) dalam durasi mikrodetik.

Ketika handler mengeksekusi operasi synchronous CPU-intensive (seperti kalkulasi hash Argon2/Bcrypt) atau synchronous file/network I/O langsung di dalam async handler, worker thread tersebut ditahan secara eksklusif. Worker tidak dapat memproses .poll() untuk request lain yang sudah antre di thread yang sama.

Gejala Insiden di Lingkungan Produksi

Insiden worker starvation memiliki karakteristik metrik yang khas:

  • Latensi p99 dan p999 Melonjak: Endpoint ringan seperti /healthz membutuhkan detik hingga puluhan detik untuk merespons jika dialokasikan ke worker yang terblokir.
  • HTTP 504 Gateway Timeout: Reverse proxy (Nginx, Envoy, atau AWS ALB) memutus koneksi klien karena Actix Web tidak menyelesaikan handshake HTTP dalam batas timeout.
  • Penggunaan CPU Tidak Merata: Sistem dengan 16 core bisa saja hanya menunjukkan pemakaian CPU 25-50%, tetapi server menolak atau menunda request konkuren baru. Worker yang terblokir sibuk pada loop komputasi, sementara request menumpuk di antrean TCP backlog.

Metode Investigasi dan Profiling Runtime

Identifikasi worker starvation membutuhkan metrik latensi task scheduler, bukan sekadar durasi total HTTP request.

1. Deteksi Stall dengan Tracing Tokio Console

Integrasikan console-subscriber pada inisialisasi aplikasi untuk memantau waktu polling future:

// Inisialisasi pada main.rs untuk profiling lokal
console_subscriber::init();

Metrik busy_duration yang mendekati total duration tanpa jeda idle mengindikasikan task synchronous menahan runtime tanpa yield.

2. Profiling CPU Flamegraph

Gunakan cargo flamegraph atau perf pada Linux untuk melihat call stack thread worker:

cargo flamegraph --bin my-actix-service -- --bench

Jika visualisasi didominasi oleh fungsi kriptografi (seperti argon2::hash atau bcrypt::hash) langsung di dalam frame pemanggilan actix_net::service, thread pool worker Actix terkonfirmasi mengalami blocking langsung.

Solusi: Isolasi Task Menggunakan actix_web::web::block

Operasi synchronous harus dipindahkan keluar dari worker event loop ke thread pool blocking khusus yang disediakan Tokio runtime menggunakan abstraksi actix_web::web::block.

Implementasi Bermasalah (Anti-Pattern)

use actix_web::{post, web, HttpResponse, Responder};

#[post("/auth/hash")]
async fn hash_password(payload: web::Json<String>) -> impl Responder {
    // BUG: Memblokir worker thread selama ~300ms
    let hash = bcrypt::hash(payload.as_str(), 12).unwrap();
    HttpResponse::Ok().body(hash)
}

Implementasi Perbaikan dengan Error Handling

Fungsi web::block mengeksekusi closure di worker pool blocking terpisah dan mengembalikan future yang menghasilkan Result<T, BlockingError>:

use actix_web::{error, post, web, Error, HttpResponse};

#[post("/auth/hash")]
async fn hash_password(payload: web::Json<String>) -> Result<HttpResponse, Error> {
    let raw_password = payload.into_inner();

    // Offload komputasi berat ke dedicated blocking thread pool
    let hash = web::block(move || {
        bcrypt::hash(&raw_password, 12)
    })
    .await
    .map_err(|blocking_err| {
        // BlockingError terjadi jika runtime pool dibatalkan atau thread panik
        log::error!("Blocking thread pool error: {blocking_err}");
        error::ErrorInternalServerError("Task execution failed")
    })?
    .map_err(|hash_err| {
        // Error operasional spesifik dari library bcrypt
        log::error!("Bcrypt calculation error: {hash_err}");
        error::ErrorBadRequest("Invalid hashing payload")
    })?;

    Ok(HttpResponse::Ok().body(hash))
}

Trade-off dan Limitasi web::block

Meskipun web::block menyelesaikan starvation pada HTTP worker thread, ada batas kapasitas yang perlu diperhatikan:

  • Kapasitas Thread Pool Blocking: Tokio blocking pool memiliki batas maksimum default (512 thread). Memindahkan ribuan kalkulasi berat ke web::block secara bersamaan tetap memicu antrean pada thread pool blocking, menyebabkan lonjakan penggunaan memori (RAM) akibat stack thread OS.
  • CPU Saturation Tetap Terjadi: Jika beban komputasi murni melampaui jumlah core fisik, OS context switching antar-thread OS tetap akan meningkatkan waktu eksekusi total.
  • Kapan Harus Menggunakan Task Queue: Gunakan web::block untuk kalkulasi CPU jangka pendek (di bawah 500ms) seperti hashing kredensial atau kompresi payload kecil. Untuk komputasi di atas 1 detik (rendering file, machine learning inference, pengolahan video), delegasikan beban ke antrean terpisah via broker pesan (Redis Streams, RabbitMQ, atau Kafka).