Gejala Deadlock: Silent Freeze Tanpa Log Panic
Layanan HTTP berbasis Actix Web mendadak berhenti merespons permintaan. Nginx di layer reverse proxy mulai mengembalikan status 504 Gateway Timeout. Monitoring sistem menunjukkan anomali berikut:
- Penggunaan CPU anjlok mendekati 0%.
- Alokasi memori stagnan, tidak ada lonjakan memory leak.
- Tidak ada log panic, error trace, ataupun segment fault di konsol aplikasi.
- Worker thread mati satu per satu secara bertahap seiring masuknya traffic konkuren hingga seluruh instance hang total.
Kondisi ini menandakan terjadinya deadlock di level OS thread. Runtime Rust tidak melakukan abort atau unwind ketika deadlock terjadi, melainkan membekukan thread eksekusi secara permanen.
Arsitektur Worker Actix Web dan Penonaktifan Batasan Send
Pada multi-threaded Tokio runtime standar, future yang dieksekusi via tokio::spawn harus memenuhi trait bound Send. Tipe std::sync::MutexGuard secara eksplisit tidak mengimplementasikan Send (bersifat !Send). Menahan guard tersebut melewati titik .await pada Tokio standar akan memicu error kompilasi:
error: future cannot be sent between threads safely
= help: within `impl Future`, the trait `Send` is not implemented for `std::sync::MutexGuard<'_, ...>`Rust compiler menolak kode tersebut untuk mencegah thread starvation dan race condition lintas thread. Namun, aturan ini tidak berlaku di Actix Web.
Actix Web menggunakan model arsitektur multi-worker independen. Server mengalokasikan sejumlah OS thread (sesuai core CPU) di mana setiap worker menjalankan instance runtime single-threaded sendiri (tokio::task::LocalSet via actix_rt). Karena task dalam satu worker tidak pernah dipindahkan (work-stealing) ke worker lain, future dalam request handler Actix Web tidak diwajibkan memiliki trait bound Send.
Dampaknya, pemanggilan std::sync::MutexGuard yang menyeberang titik .await lolos dari pemeriksaan compiler Rust tanpa peringatan apa pun.
Reproduksi Kode: Deadlock di Bawah Traffic Konkuren
Contoh implementasi rentan berikut mendemonstrasikan bagaimana deadlock terjadi saat dua request diproses oleh worker thread yang sama:
use actix_web::{web, App, HttpServer, HttpResponse, Responder};
use std::sync::{Arc, Mutex};
use std::time::Duration;
struct AppState {
counter: Mutex<u64>,
}
async fn vulnerable_handler(data: web::Data<Arc<AppState>>) -> impl Responder {
// Mengambil lock synchronous
let mut lock = data.counter.lock().unwrap();
*lock += 1;
// Menahan MutexGuard menyeberangi titik suspensi .await
tokio::time::sleep(Duration::from_millis(100)).await;
HttpResponse::Ok().body(format!("Counter: {}", *lock))
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
let state = Arc::new(AppState {
counter: Mutex::new(0),
});
HttpServer::new(move || {
App::new()
.app_data(web::Data::new(state.clone()))
.route("/test", web::get().to(vulnerable_handler))
})
.workers(1) // Reproduksi deterministik menggunakan 1 worker
.bind(("127.0.0.1", 8080))?
.run()
.await
}Root Cause: Mengapa Worker Thread Terkunci Total
Mekanisme kegagalan sistematis ini terjadi melalui rangkaian sekuens berikut:
- Request A masuk ke Worker Thread 0. Handler mengeksekusi
lock()dan berhasil mendapatkanMutexGuard. Nilai counter dimodifikasi. - Handler mencapai
tokio::time::sleep().await. Request A menunda eksekusi (yielding) kembali ke reactor, tetapi guard belum di-drop sehingga kepemilikan lock tetap aktif di memori. - Worker Thread 0 kini bebas mengeksekusi task lain yang antre di
LocalSet. Request B masuk ke thread yang sama. - Request B memanggil
data.counter.lock().unwrap(). Karena tipe yang dipakai adalahstd::sync::Mutex, pemanggilan ini langsung memblokir OS thread yang sedang berjalan di level kernel hingga lock dilepaskan. - Deadlock tercipta: OS Worker Thread 0 diblokir oleh Request B. Sementara itu, Request A (pemilik lock sesungguhnya) tidak akan pernah bisa dijadwalkan ulang atau menyelesaikan operasi sleep-nya karena thread yang bertugas mem-poll future tersebut sedang macet total.
Worker thread tersebut mengalami hung permanen. Ketika seluruh worker pool Actix Web kehabisan thread aktif akibat skenario serupa, aplikasi berhenti melayani koneksi baru secara keseluruhan.
Solusi 1: Isolasi Scope MutexGuard (Rekomendasi)
Jangan pernah menahan lock melebihi waktu komputasi kritis yang dibutuhkan. Lepaskan MutexGuard sebelum mencapai titik .await menggunakan block scope eksplisit atau fungsi drop().
async fn fixed_handler_scope(data: web::Data<Arc<AppState>>) -> impl Responder {
let current_val = {
// Scope terbatas: guard langsung di-drop saat keluar dari kurung kurawal
let mut lock = data.counter.lock().unwrap();
*lock += 1;
*lock
};
// Aman: guard sudah di-drop, thread bebas menjalankan task lain selama yield
tokio::time::sleep(Duration::from_millis(100)).await;
HttpResponse::Ok().body(format!("Counter: {}", current_val))
}Pendekatan ini memiliki performa paling optimal karena std::sync::Mutex tidak memicu overhead alokasi memory future tambahan di heap.
Solusi 2: Migrasi ke tokio::sync::Mutex
Jika state shared data harus dipertahankan secara utuh melintasi operasi asynchronous (misalnya menahan lock saat membaca stream network atau transaksi I/O kompleks), gunakan tokio::sync::Mutex.
use actix_web::{web, App, HttpServer, HttpResponse, Responder};
use tokio::sync::Mutex; // Gunakan async mutex milik Tokio
use std::sync::Arc;
use std::time::Duration;
struct AppState {
counter: Mutex<u64>,
}
async fn fixed_handler_async_mutex(data: web::Data<Arc<AppState>>) -> impl Responder {
// Non-blocking lock. Jika terkunci, task akan yield alih-alih memblokir OS thread
let mut lock = data.counter.lock().await;
*lock += 1;
tokio::time::sleep(Duration::from_millis(100)).await;
HttpResponse::Ok().body(format!("Counter: {}", *lock))
}Komparasi Penggunaan Mutex
| Tipe Mutex | Karakteristik Saat Terjadi Kontensi | Dampak Terhadap Worker Loop | Rekomendasi Penggunaan |
|---|---|---|---|
std::sync::Mutex | Memblokir OS thread secara sinkron | Fatal jika ditahan lintas .await (Deadlock) | Komputasi in-memory murni, durasi singkat tanpa operasi I/O async |
tokio::sync::Mutex | Yield task ke runtime secara non-blocking | Aman melintasi .await | State harus dikunci saat menunggu proses asynchronous lain |
Aturan Desain: Gunakan
std::sync::Mutexuntuk operasi CPU-bound singkat dan isolasi scopenya sebelum titik suspensi. Beralih ketokio::sync::Mutexhanya jika state harus melintasi operasi I/O async.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!