Panic muncul ketika upload streaming file diterima Axum; log menunjukkan panic karena borrow overlap pada state async yang dibagikan. Artikel ini menjelaskan gejala, root cause, serta langkah debugging dan perbaikan agar upload tetap aman dan stabil.

1. Gejala Panic Dalam Streaming Upload

Ketika client mengirim file besar lewat multipart atau chunked upload, server Axum menerima data dengan Body streaming. Panic tidak terjadi saat upload awal, tetapi muncul setelah beberapa chunk masuk, disertai stack trace seperti berikut:

thread 'tokio-runtime-worker' panicked at 'already borrowed: BorrowError', /workspace/.cargo/registry/src/.../parking_lot/src/raw.rs:123:21
stack backtrace:
  0: std::panicking::begin_panic
  1: parking_lot_core::mutex::Mutex::lock
  2: my_service::handlers::upload::handle_stream
  3: axum::handler::Handler::call
  ...

Log juga menunjukkan request hampir selesai tapi handler tiba-tiba drop, lalu future yang memegang Arc> ditimpa borrow kedua saat chunk terakhir ditangani.

2. Kenali Root Cause: Borrow Overlap pada Async State

Axum bekerja di atas Tokio, sehingga setiap request berjalan di future yang dapat ditunggangi (polled) beberapa kali. Panic terjadi karena handler memegang state.lock().await saat membaca stream, lalu memanggil fungsi async yang kembali mencoba meminjam state secara mutable lagi.

Contoh ringkas yang mencerminkan pola bermasalah:

async fn handle_stream(state: Arc>, mut body: Body) {
    let mut guard = state.lock().await;
    while let Some(chunk) = body.data().await {
        process_payload(&mut guard, chunk?).await; // meminjam lagi ketika guard masih aktif
    }
}

Masalahnya bukan hanya borrow token, tapi fakta bahwa guard tidak dilepas sebelum masuk ke await lain. Ketika process_payload membutuhkan state kembali, Mutex masih terkunci, memicu panic borrow error.

3. Strategi Perbaikan dan Penguatan Handler

Solusi utama adalah menghindari lock jangka panjang selama operasi async yang memerlukan borrow ulang. Pendekatan yang efektif:

  • Rancang ulang state agar data yang diperlukan dipindahkan ke buffer sementara atau disalin sebelum await panjang.
  • Gunakan channel atau aggregator untuk menyalurkan chunk, sehingga mutex hanya dibuka sebentar untuk menyimpan metadata.
  • Tambahkan context cancellation dengan timeout/token agar future abort jika client drop.

Contoh perbaikan minimal:

async fn handle_stream(state: Arc>, mut body: Body) {
    let mut buffer = Vec::new();
    while let Some(chunk) = body.data().await {
        buffer.extend_from_slice(&chunk?);
        flush_buffer_if_needed(&mut buffer, state.clone()).await;
    }
}

async fn flush_buffer_if_needed(buffer: &mut Vec, state: Arc>) {
    if buffer.len() >= CHUNK_LIMIT {
        let data = buffer.split_off(0);
        drop(buffer); // pastikan borrow selesai sebelum lock ulang
        let mut guard = state.lock().await;
        guard.append_data(data);
    }
}

Perhatikan bahwa split_off memindahkan data dan drop(buffer) memastikan mutex tidak dikelola dua kali bersamaan.

4. Observability dan Verifikasi

Setelah mengganti implementasi, penting mengecek beberapa aspek:

  • Observabilitas: Tambahkan tracing span atau log level debug untuk setiap chunk dan saat state lock diambil. Gunakan tracing untuk melihat durasi lock().await.
  • Unit test: Isolasi flush_buffer_if_needed dengan buffer besar, menguji bahwa mutex tidak dipegang lebih lama dari yang diperlukan.
  • Integration test: Gunakan tokio-test::block_on atau reqwest untuk mensimulasikan upload multipart dengan chunk kecil dan observasi log panic.

Pemeriksaan manual dapat dilakukan dengan RUST_LOG=debug dan menambah assert! pada state untuk memastikan tidak ada borrow terduplikasi.

5. Catatan Tambahan

Trade-off: Buffer tambahan menambah memori sementara, namun menghindari panic dan mempermudah backpressure. Jaga agar state hanya berisi metadata kritikal, sementara payload besar disimpan di disk atau storage eksternal.

Kesalahan umum adalah tetap memegang MutexGuard di antara await tanpa menyadari polling ulang future. Di lingkungan async Rust, panggilannya bisa di-pause, lalu polled ulang di thread lain; karena itu release guard sebelum .await panjang sangat krusial.

Dengan pendekatan ini, upload streaming file di Axum berjalan tanpa panic, sekaligus menjaga observabilitas dan kemudahan debugging.