Penanganan Pembatalan di Rust Asinkron
(sunshowers.io)- Penanganan pembatalan di lingkungan Rust asinkron memang praktis, tetapi jika ditangani dengan keliru dapat menimbulkan bug tak terduga dan kesulitan yang serius
- Di Rust sinkron, biasanya diperlukan pemeriksaan flag secara eksplisit atau penghentian proses, tetapi di Rust asinkron, pembatalan bisa dilakukan dengan sangat mudah hanya dengan me-drop future
- Keamanan pembatalan (cancel safety) dan kebenaran pembatalan (cancel correctness) adalah konsep yang berbeda; pembatalan satu future dapat menimbulkan masalah pada keseluruhan sistem
- Pola masalah utama terkait pembatalan mencakup Tokio mutex, makro
select,try_join, serta kesalahan dalam penggunaan future - Belum ada solusi sempurna, tetapi penggunaan API yang aman terhadap pembatalan, pinning future, dan pemisahan task dapat mengurangi masalah yang disebabkan oleh pembatalan
Pendahuluan
- Tulisan ini didasarkan pada materi presentasi RustConf 2025 tentang pembatalan (cancellation) di Rust asinkron
- Dalam contoh kode async Rust pada umumnya, ketika timeout ditambahkan ke loop penerimaan atau pengiriman pesan, sering kali ditemukan masalah hilangnya pesan
- Artikel ini membahas masalah pembatalan serta contoh bug nyata yang dialami saat memanfaatkan async Rust di sistem berskala besar yang sesungguhnya, termasuk di Oxide Computer Company
- Tulisan ini terdiri dari tiga bagian: 1) konsep pembatalan, 2) analisis pembatalan, 3) solusi praktis
- Penulis telah mengalami langsung kelebihan dan kesulitan Rust asinkron melalui pengembangan Rust signal handling, cargo-nextest, dan lainnya
1. Apa itu pembatalan?
Makna pembatalan
- Pembatalan (cancellation) adalah situasi ketika suatu pekerjaan asinkron telah dimulai lalu dihentikan di tengah jalan
- Contoh: unduhan besar/permintaan jaringan, pembacaan file parsial, dan kasus lain yang bisa dibatalkan di tengah proses
Cara pembatalan di Rust sinkron
- Secara umum, ada pendekatan seperti memeriksa flag atomik secara berkala untuk melihat apakah perlu dibatalkan, memakai pengecualian khusus (
panic), atau memaksa penghentian seluruh proses - Beberapa framework (seperti Salsa) menggunakan payload panic, tetapi ini tidak berjalan di semua platform Rust, terutama di lingkungan Wasm
- Menghentikan hanya thread tertentu secara paksa tidak diizinkan karena sifat keamanan Rust dan struktur mutex-nya
- Ringkasnya, di Rust sinkron tidak ada protokol pembatalan yang umum dan aman
Rust asinkron: apa itu Future?
- Future adalah state machine yang dihasilkan oleh compiler Rust, dan pada dasarnya hanyalah data sederhana di memori
- Future tidak berjalan hanya karena dibuat; ia hanya maju ketika await atau poll dipanggil
- Future di Rust bersifat pasif (inert); tanpa poll/await yang eksplisit, ia tidak akan memproses pekerjaan apa pun
- Ini berbeda dengan Go/JavaScript/C# yang umumnya mulai mengeksekusi future segera setelah dibuat
Protokol pembatalan di Rust asinkron
- Membatalkan Future berarti cukup dengan me-drop-nya atau berhenti memanggil poll/await padanya
- Karena berupa state machine, Future bisa dibuang kapan saja
- Di Rust asinkron, pembatalan sangat kuat sekaligus sangat mudah diterapkan
- Namun, justru karena terlalu mudah, future bisa diam-diam di-drop, dan future anak pun ikut dibatalkan secara berantai sesuai model ownership
- Sifat ini membuat pembatalan menjadi fenomena non-lokal (non-local) yang dapat memengaruhi seluruh rantai pemanggilan
2. Analisis pembatalan
Keamanan pembatalan dan kebenaran pembatalan
- Keamanan pembatalan (cancel safety): sifat suatu future individual yang dapat dibatalkan dengan aman tanpa efek samping
- Contoh: future
sleepmilik Tokio aman terhadap pembatalan - Sebaliknya,
sendMPSC Tokio berisiko menyebabkan pesan hilang saat di-drop (tidak aman terhadap pembatalan)
- Contoh: future
- Kebenaran pembatalan (cancel correctness): sifat global di mana keseluruhan sistem tetap mempertahankan properti esensialnya saat terjadi pembatalan
- Jika future yang tidak aman terhadap pembatalan tidak ada dalam sistem, maka tidak ada masalah kebenaran pembatalan
- Masalah hanya muncul jika future yang tidak aman terhadap pembatalan benar-benar sampai dibatalkan
- Jika pembatalan menyebabkan kehilangan data, pelanggaran invarians, atau cleanup yang tidak dilakukan, maka terjadi pelanggaran kebenaran pembatalan
Sulitnya Tokio mutex
- Tokio mutex bekerja dengan mengambil lock, menyesuaikan data, lalu melepaskannya
- Masalahnya: jika di dalam lock keadaan sementara dibuat melanggar invarians (misalnya mengubah
Option<T>menjadiNone) lalu melewatiawait, ketika future dibatalkan, data bisa membeku dalam keadaan yang salah - Dalam praktik nyata (misalnya manajemen state sled di Oxide), titik
awaitpernah menyebabkan keadaan tidak stabil akibat pembatalan - Dengan demikian, pembatalan dalam pengelolaan state kode asinkron dapat menjadi sumber cacat yang sangat berbahaya
Pola terjadinya pembatalan dan contohnya
- Pemanggilan future tanpa
.await: Rust memang memberi peringatan untuk future yang tidak digunakan, tetapi jika nilaiResultditerima sebagai_, peringatan bisa tidak muncul (perlu lint Clippy terbaru) - Operasi try seperti
try_join: ketika satu future gagal, future lain ikut dibatalkan (dalam layanan nyata dapat berujung pada bug dalam logika penghentian) - Makro
select: setelah memproses beberapa future secara paralel, semua future selain yang selesai akan dibatalkan (di dalam loopselect, risiko hilangnya data bisa sangat besar) - Pola-pola ini memang disebutkan dalam dokumentasi, tetapi dalam praktiknya pembatalan asinkron bisa terjadi secara implisit di banyak tempat
3. Apa yang bisa dilakukan?
- Sampai saat ini belum ada solusi mendasar dan sepenuhnya lengkap untuk masalah kebenaran pembatalan
- Namun secara praktis, kemungkinan cacat akibat pembatalan dapat dikurangi dengan cara-cara berikut
Menyusun ulang dengan future yang aman terhadap pembatalan
- Contoh MPSC
send: pisahkan antara reservasi (reserve) dan pengiriman aktual (send) untuk memperoleh keamanan pembatalan parsial- Tindakan reservasi bisa dibatalkan tanpa membuat pesan terkait hilang
- Setelah permit diperoleh, pengiriman dapat dilakukan tanpa khawatir akan pembatalan
write_allmilik AsyncWrite:write_alluntuk seluruh buffer tidak stabil terhadap pembatalan, sedangkanwrite_all_bufmenggunakan cursor buffer sehingga status kemajuan saat dibatalkan dapat dilacak- Di dalam loop, kemajuan parsial dapat dilanjutkan kembali dengan aman menggunakan
write_all_buf
- Di dalam loop, kemajuan parsial dapat dilanjutkan kembali dengan aman menggunakan
Mengoperasikan future dengan menghindari pembatalan
- Future pinning: di loop
selectdan situasi serupa, future dapat dipasang tetap dengan pin agar tidak dibatalkan dan menunggu dengan mem-poll lewat referensi- Contoh: jika future
reservedigunakan kembali, urutan antrean menunggu reservasi dapat dipertahankan
- Contoh: jika future
- Memanfaatkan task: jika future dijalankan sebagai task melalui
tokio::spawndan sejenisnya, me-drop handle tidak otomatis membatalkan task itu sendiri karena task tetap dikelola terpisah oleh runtime- Di Oxide Dropshot HTTP server dan sejenisnya, setiap request dijalankan sebagai task terpisah sehingga meskipun koneksi klien terputus, penyelesaian pemrosesan request tetap terjamin
Solusi yang sistematis?
- Saat ini pendekatannya masih terbatas di level safe Rust, tetapi ada beberapa arah yang sedang dibahas
- Async drop: memungkinkan menjalankan kode cleanup asinkron ketika future dibatalkan
- Linear types: memaksa eksekusi kode tertentu saat drop, atau menandai future tertentu sebagai tidak dapat dibatalkan
- Semua pendekatan ini tetap memiliki kesulitan implementasi
Kesimpulan dan rekomendasi
- Kita perlu memahami secara mendasar bahwa Future bersifat pasif (passive)
- Penting untuk memahami konsep keamanan pembatalan (cancel safety) dan kebenaran pembatalan (cancel correctness)
- Kasus bug utama dan pola kode terkait pembatalan perlu dipahami lebih awal agar strategi penanganannya dapat dipersiapkan sebelumnya
- Beberapa rekomendasi praktis
- Hindari penggunaan Tokio mutex dan pertimbangkan alternatif lain
- Rancang atau gunakan API yang mendukung penyelesaian parsial atau aman terhadap pembatalan
- Untuk future yang tidak aman terhadap pembatalan, pastikan struktur kode menjamin penyelesaiannya
- Selain itu, disarankan juga meninjau topik lanjutan seperti cooperative cancellation, actor model, structured concurrency, panic safety, dan mutex poisoning
- Materi terkait dapat dilihat di sunshowers/cancelling-async-rust
Terima kasih telah membaca. Penulis menyampaikan terima kasih kepada rekan-rekan di Oxide yang telah meninjau presentasi serta materi referensi dan memberikan masukan
1 komentar
Komentar Hacker News
send/recvsangat menarik; saya jadi sadar bahwa pada bahasa yang mengeksekusi future tanpa polling terlebih dahulu saat belum dijalankan, situasinya justru bisa terbalik. Jikasenddiberi timeout, pesan masih bisa terkirim setelah timeout, tetapi pesannya tidak hilang sehingga aman; namun jikarecvdiberi timeout, ada situasi di mana pesan sudah dibaca dari channel lalu timeout yang terpilih, sehingga pesannya malah dibuang begitu saja dan bisa jadi tidak aman. Solusinya adalah memilih timeout atau "ada sesuatu yang tersedia" dari channel, dan pada kasus kedua melihat data dengan aman lewat peektry_joindibatalkan karena errorSemua kasus ini tampak seperti perilaku yang wajar ketika context dibatalkan sehingga pekerjaan tidak selesai. Kalau pekerjaannya memang harus selesai, bukankah tinggal dipisahkan ke task independen? Saya merasa mungkin ada nuance penting yang saya lewatkan. Pemahaman saya, work yang hilang karena cancellation memang bagian dari desain futures; jadi saya ingin dijelaskan lagi apa sebenarnya masalahnya
await, sisanya tinggal teknik-teknik rincispawndisebut "cancellation safe", tetapi jika saatdroptask itu terus berjalan, pekerjaan yang tidak diperlukan bisa menumpuk dan menguasai lock atau port sehingga menimbulkan masalah. Sebaliknya,spawnhandle yang menghentikan task saatdropmemang akan disebut "cancellation unsafe", tetapi pola itu justru sangat penting untuk cleanup task yang dependenselect, dan primitive konkurensi lainnyaawaitselalu merupakan titik pengembalian potensial. Sebaiknya hindari menaruhawaitdi antara dua aksi yang harus dieksekusi secara atomikdbisa tidak terpanggil? Apakah cancellation terjadi dic? Atau ada sesuatu di level atas daria?awaitdi sebuah "critical section", di antaranya bisa terjadi jeda, sementara mungkin ada situasi di mana keduanya tetap harus lanjut dieksekusi. Misalnya setelah perubahan DB lalu menulis audit log, kalau keduanya wajib dieksekusi, apakah satu-satunya jawaban hanya menambahkan komentar do not cancel?pollsecara langsung, state harus dikelola sendiri di dalamstruct. Semua ini adalah jebakan yang cukup umum. Dan belakangan ini cancellation di async Rust menjadi variabel baru dalam state management. Saat saya mengembangkan library mea (Make Easy Async), jika cancel safety tidak trivial saya selalu mendokumentasikannya, dan saya ingat pernah ada kasus async cancellation yang sembrono menyebabkan masalah di stack IO mea kasus reddit.awaitmemiliki future itu sehinggadrop()tidak bisa dilakukan, dan karena future itu lazy, tidak jelas bagaimana cancellation bekerja setelah.await. Setelah itu saya menelusuriselect!danAbortable()lalu memahaminya, tetapi kalau presentasi berikutnya bagian ini dipanggil secara eksplisit sejak awal, menurut saya akan jadi sempurnaasync dropsegera jadi mungkin