Aplikasi backend berbasis Rust umumnya andal dalam menangani memori dan runtime safety. Namun, crash yang terjadi sebelum fungsi main() dieksekusi memunculkan tantangan diagnostik tersendiri. Layanan langsung berhenti tanpa meninggalkan log kesalahan atau jejak stack di terminal, menyebabkan kontainer berada dalam siklus CrashLoopBackOff.

Gejala: CrashLoopBackOff Tanpa Log Aplikasi

Pada lingkungan Kubernetes atau Docker, service mati mendadak dengan exit code 134 (SIGABRT) atau 139 (SIGSEGV). Perintah kubectl logs atau docker logs tidak menampilkan keluaran apapun. Kondisi ini terjadi karena subsistem logging seperti tracing-subscriber atau env_logger umumnya diinisialisasi pada baris pertama fungsi main(). Jika proses terhenti di tahap C Runtime (CRT) bootstrap atau inisialisasi static linker, runtime tracing belum aktif sehingga tidak ada buffer log yang terkirim ke stdout maupun stderr.

Root Cause: Eksekusi Kode pada Tahap CRT Initialization

Sebelum sistem operasi mengeksekusi entry point Rust (main), dynamic linker (ld.so di Linux) memuat dependensi pustaka bersama dan menjalankan fungsi-fungsi yang terdaftar pada seksi .init_array pada binary ELF. Di ekosistem Rust, kode dapat dijalankan pada tahap ini melalui:

  • Penggunaan crate pihak ketiga seperti ctor yang menandai fungsi dengan atribut konstruktor global.
  • Pustaka FFI C/C++ dependen yang mengeksekusi __attribute__((constructor)).
  • Inisialisasi static global yang melakukan operasi rentan kegagalan (misalnya alokasi dinamis kompleks, pembacaan environment variable, atau I/O jaringan).

Jika terjadi panic di dalam blok konstruktor global, mekanisme unwinding standar Rust mungkin belum sepenuhnya siap, atau panic hook kustom (std::panic::set_hook) belum terpasang. Akibatnya, runtime Rust langsung memanggil instruksi abort (SIGABRT) tanpa format error dump standar.

Langkah Diagnosis: Menangkap Crash Menggunakan strace dan LLDB

Karena log aplikasi tidak tersedia, pelacakan harus dilakukan pada level system call dan dynamic loader.

1. Deteksi System Call dengan strace

Jalankan binary secara lokal atau di dalam container debug menggunakan strace untuk memantau system call sebelum terminasi:

strace -f -e trace=process,write,rt_sigaction,rt_sigprocmask,tgkill,abort ./target/release/my-service

Output tipikal akan menunjukkan proses memuat file ELF, mendaftarkan sinyal, lalu langsung mengeksekusi rt_sigprocmask dan tgkill(..., SIGABRT) tanpa pernah ada system call write ke file descriptor 1 atau 2:

...
rt_sigaction(SIGABRT, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=SA_RESTORER, sa_restorer=0x...}, NULL, 8) = 0
tgkill(12345, 12345, SIGABRT) = 0
--- SIGABRT {si_signo=SIGABRT, si_code=SI_TKILL, si_pid=12345, si_uid=1000} ---
+++ killed by SIGABRT +++

2. Pelacakan Stack Trace dengan LLDB atau rust-gdb

Gunakan debugger untuk menangkap lokasi pemanggilan abort() secara presisi. Buat build debug terlebih dahulu agar simbol tidak di-strip:

cargo build
rust-lldb ./target/debug/my-service

Di dalam shell debugger, jalankan aplikasi:

(lldb) run
Process 12345 stopped
* thread #1, name = 'my-service', stop reason = signal SIGABRT
    frame #0: 0x00007ffff7dc9884 libc.so.6`__pthread_kill_implementation + 260
(lldb) bt
* frame #0: 0x00007ffff7dc9884 libc.so.6`__pthread_kill_implementation + 260
  frame #1: 0x00007ffff7d7a8ee libc.so.6`raise + 26
  frame #2: 0x00007ffff7d648ff libc.so.6`abort + 211
  frame #3: 0x000055555557a123 my-service`rust_panic + 147
  frame #4: 0x0000555555562d5a my-service`my_service::init_config::{{closure}} + 218
  frame #5: 0x0000555555562b0e my-service`ctor::ctor_function + 14
  frame #6: 0x00007ffff7fe1632 ld-linux-x86-64.so.2`call_init + 146
  frame #7: 0x00007ffff7fe1724 ld-linux-x86-64.so.2`_dl_init + 132
  frame #8: 0x00007ffff7fcd0aa ld-linux-x86-64.so.2`_dl_start_user + 50

Backtrace di atas membuktikan pemanggilan panic berasal dari call_init milik dynamic linker yang mengeksekusi ctor::ctor_function, jauh sebelum main sempat dipanggil.

Solusi: Refaktor ke Explicit Lazy Initialization dengan OnceLock

Pendekatan yang benar adalah menghapus seluruh side-effect (pembacaan konfigurasi, parsing file, atau inisialisasi pool koneksi) dari fase static allocation/konstruktor global, dan memindahkannya ke dalam main().

Contoh Masalah (Anti-pattern dengan ctor):

use ctor::ctor;
use std::env;

static mut DB_URL: Option<String> = None;

#[ctor]
fn init_config() {
    // Panic jika ENV tidak ditemukan sebelum main() berjalan
    let url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
    unsafe {
        DB_URL = Some(url);
    }
}

fn main() {
    println!("App started");
}

Jika variabel DATABASE_URL tidak ada pada environment, program mengalami panic di level CRT, tidak mengeluarkan teks log, dan mati seketika via SIGABRT.

Solusi: Safe Startup Sequence Menggunakan std::sync::OnceLock

Gunakan std::sync::OnceLock (tersedia di Rust standar sejak 1.70) untuk state global yang thread-safe, dan kendalikan inisialisasinya secara eksplisit setelah logger aktif.

use std::env;
use std::sync::OnceLock;

static DB_URL: OnceLock<String> = OnceLock::new();

fn get_db_url() -> &'static str {
    DB_URL.get().expect("DB_URL must be initialized")
}

fn init_runtime() -> Result<(), Box<dyn std::error::Error>> {
    // 1. Baca konfigurasi tanpa panic implisit
    let url = env::var("DATABASE_URL")
        .map_err(|_| "DATABASE_URL environment variable is missing")?;

    // 2. Set static state secara aman
    DB_URL.set(url).map_err(|_| "Failed to set DB_URL: already initialized")?;
    Ok(())
}

fn main() {
    // 1. Daftarkan panic hook eksplisit agar selalu tercetak ke stderr
    std::panic::set_hook(Box::new(|info| {
        eprintln!("FATAL UNCAUGHT PANIC: {}", info);
    }));

    // 2. Inisialisasi logging/tracing runtime
    eprintln!("Bootstrapping application runtime...");

    // 3. Eksekusi startup sequence berurut dengan penanganan error terstruktur
    if let Err(err) = init_runtime() {
        eprintln!("Application startup failed: {err}");
        std::process::exit(1);
    }

    println!("Service ready. DB URL configured: {}", get_db_url());
}

Prinsip Mencegah Crash Pre-main

  • Hindari crate konstruktor global: Jangan gunakan #[ctor] atau atribut .init_array kecuali saat membangun dynamic shared library (C ABI) yang benar-benar mewajibkannya.
  • Tolak I/O di static lifetime: Konstruksi static global hanya boleh berupa operasi konstanta murni (const fn). Operasi file, network, atau pemanggilan std::env::var harus terjadi di dalam aliran eksekusi fungsi main().
  • Pasang panic hook di baris pertama: Selalu posisikan std::panic::set_hook dan logging framework di urutan teratas main() agar setiap kegagalan berikutnya dapat tercatat pada monitoring pod.