Memproses payload berukuran besar pada batch API atau webhook di Rust sering kali melibatkan transformasi data menggunakan iterator adaptor seperti .map(). Masalah muncul ketika fungsi transformasi menghasilkan Result<T, E>. Menangani tipe kembalian ini secara naif sering berujung pada silent failure akibat unwrap_or_default(), atau memicu panic mendadak di lingkungan produksi akibat anti-pattern unwrap().
Artikel ini membahas strategi idiomatik untuk mengelola error handling batch API di Rust menggunakan fitur bawaan trait Iterator, membandingkan pendekatan fail-fast, partial success, hingga evaluasi efisiensi alokasi memori.
Masalah: Result di Dalam Iterator Closure
Saat memetakan elemen koleksi menggunakan .map() dengan closure yang mengembalikan Result, tipe kembalian iterator bertransformasi menjadi Iterator<Item = Result<T, E>>. Dua kesalahan umum yang sering ditemukan:
- Anti-pattern
.unwrap()atau.expect(): Memaksa ekstraksi nilai langsung di dalam closure. Jika ada satu item input yang tidak valid, seluruh worker thread API panik dan permintaan HTTP terputus. - Silent failure via
.filter_map(Result::ok): Mengabaikan error secara diam-diam. Payload yang korup dibuang tanpa logging atau audit, menghasilkan data yang hilang dan menyulitkan proses debugging.
// Anti-pattern: Satu record gagal langsung menyebabkan panic seluruh proses batch
let items: Vec<ValidatedItem> = raw_items
.into_iter()
.map(|raw| validate(raw).unwrap())
.collect();Pola 1: Fail-Fast dengan collect::<Result<Vec<T>, E>>()
Jika satu validasi gagal dan seluruh transaksi batch harus dibatalkan (skenario atomik), implementasi standar FromIterator di Rust memungkinkan collect membalikkan struktur dari Iterator<Item = Result<T, E>> menjadi Result<Vec<T>, E>.
Metode ini langsung menghentikan iterasi (short-circuit) begitu menemukan kegagalan pertama. Item berikutnya tidak akan diproses, menghemat alokasi memori dan siklus CPU.
#[derive(Debug, PartialEq)]
pub struct WebhookEvent {
pub id: u64,
pub event_type: String,
}
#[derive(Debug, PartialEq)]
pub enum ValidationError {
InvalidId,
MissingType,
}
pub fn validate_batch_fail_fast(
raw_events: Vec<(u64, String)>,
) -> Result<Vec<WebhookEvent>, ValidationError> {
raw_events
.into_iter()
.map(|(id, event_type)| {
if id == 0 {
return Err(ValidationError::InvalidId);
}
if event_type.is_empty() {
return Err(ValidationError::MissingType);
}
Ok(WebhookEvent { id, event_type })
})
.collect() // Mengubah Iterator<Item = Result<T, E>> -> Result<Vec<T>, E>
}
#[test]
fn test_fail_fast() {
let input = vec![(1, "charge.created".into()), (0, "user.deleted".into())];
let result = validate_batch_fail_fast(input);
assert_eq!(result, Err(ValidationError::InvalidId));
}Pola 2: Partial Success dengan partition
Dalam pipeline ingestion webhook atau batch API massal, strategi fail-fast kerap tidak diinginkan. Sistem biasanya diwajibkan memproses data yang valid dan mengembalikan daftar kegagalan ke client.
Gunakan partition untuk memisahkan hasil valid dan error dalam satu kali pass traversal.
pub fn validate_batch_partial(
raw_events: Vec<(u64, String)>,
) -> (Vec<WebhookEvent>, Vec<ValidationError>) {
let (successes, failures): (Vec<_>, Vec<_>) = raw_events
.into_iter()
.map(|(id, event_type)| {
if id == 0 {
return Err(ValidationError::InvalidId);
}
if event_type.is_empty() {
return Err(ValidationError::MissingType);
}
Ok(WebhookEvent { id, event_type })
})
.partition(Result::is_ok);
(
successes.into_iter().map(Result::unwrap).collect(),
failures.into_iter().map(Result::unwrap_err).collect(),
)
}Catatan: PemanggilanResult::unwrapdanResult::unwrap_errdi atas dijamin aman (panic-free) karena elemen sudah dipisahkan menggunakan predikatResult::is_okpada tahap sebelumnya.
Pola 3: Validasi Berantai dengan try_fold
Ketika transformasi elemen batch bergantung pada state atau membutuhkan akumulasi bersyarat dengan batas toleransi error (misalnya membatalkan proses hanya jika error melebihi threshold tertentu), Iterator::try_fold menyediakan kontrol imperatif secara fungsional.
pub fn validate_with_budget(
raw_events: Vec<(u64, String)>,
max_errors: usize,
) -> Result<Vec<WebhookEvent>, String> {
let initial = (Vec::with_capacity(raw_events.len()), 0usize);
let (valid_items, _) = raw_events.into_iter().try_fold(
initial,
|(mut acc, mut err_count), (id, event_type)| {
if id == 0 || event_type.is_empty() {
err_count += 1;
if err_count > max_errors {
return Err("Error threshold exceeded".to_string());
}
Ok((acc, err_count))
} else {
acc.push(WebhookEvent { id, event_type });
Ok((acc, err_count))
}
},
)?;
Ok(valid_items)
}Analisis Performa: Iterator vs Manual For-Loop
Rust mengompilasi iterator chains menjadi zero-cost abstractions. Terdapat perbedaan profil memori yang signifikan antara pola iterasi dan for-loop manual:
- For-Loop Konvensional: Kerap menyebabkan multiple re-allocation jika vektor penampung diinisialisasi menggunakan
Vec::new()tanpa ukuran awal yang pasti (reserve). collect::<Result<Vec<T>, E>>(): Menggunakansize_hint()dari iterator sumber untuk mengalokasikan kapasitas bufferVecsekaligus di awal. Jika kegagalan terjadi di tengah stream, alokasi memori segera diputus dan sisa siklus pemrosesan dihentikan seketika.- Overhead
partition(): Pola ini melakukan alokasi ke duaVecyang terpisah. Jika jumlah elemen masukan besar, disarankan menggunakan loop manual denganwith_capacityuntuk menghindari re-allokasi berulang pada vektor error.
Pemetaan Status Response HTTP
Gunakan status code HTTP yang akurat berdasarkan strategi penanganan yang dipilih:
- Fail-Fast (Semua atau Tidak Sama Sekali): Kembalikan
400 Bad Requestatau422 Unprocessable Entitydisertai detail satu item spesifik yang memicu error. - Partial Success: Kembalikan
207 Multi-Status(RFC 4918) dengan body JSON yang memuat array data yang berhasil diproses beserta array rincian error per record index. - Budget Exceeded: Kembalikan
422 Unprocessable Entitydisertai keterangan bahwa batas kegagalan validasi payload terlampaui.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!