3 poin oleh GN⁺ 2025-10-05 | 1 komentar | Bagikan ke WhatsApp
  • 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 sleep milik Tokio aman terhadap pembatalan
    • Sebaliknya, send MPSC Tokio berisiko menyebabkan pesan hilang saat di-drop (tidak aman terhadap pembatalan)
  • 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> menjadi None) lalu melewati await, ketika future dibatalkan, data bisa membeku dalam keadaan yang salah
  • Dalam praktik nyata (misalnya manajemen state sled di Oxide), titik await pernah 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 nilai Result diterima 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 loop select, 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_all milik AsyncWrite: write_all untuk seluruh buffer tidak stabil terhadap pembatalan, sedangkan write_all_buf menggunakan cursor buffer sehingga status kemajuan saat dibatalkan dapat dilacak
    • Di dalam loop, kemajuan parsial dapat dilanjutkan kembali dengan aman menggunakan write_all_buf

Mengoperasikan future dengan menghindari pembatalan

  • Future pinning: di loop select dan situasi serupa, future dapat dipasang tetap dengan pin agar tidak dibatalkan dan menunggu dengan mem-poll lewat referensi
    • Contoh: jika future reserve digunakan kembali, urutan antrean menunggu reservasi dapat dipertahankan
  • Memanfaatkan task: jika future dijalankan sebagai task melalui tokio::spawn dan 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

 
GN⁺ 2025-10-05
Komentar Hacker News
  • Menurut saya contoh memberi timeout pada send/recv sangat menarik; saya jadi sadar bahwa pada bahasa yang mengeksekusi future tanpa polling terlebih dahulu saat belum dijalankan, situasinya justru bisa terbalik. Jika send diberi timeout, pesan masih bisa terkirim setelah timeout, tetapi pesannya tidak hilang sehingga aman; namun jika recv diberi 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 peek
    • Saya sedang berpikir, bukankah ini inti dari cancellation-safety?
    • Menurut saya ini poin yang bagus
  • Saya ingin memperkenalkan beberapa materi yang saya tulis tentang topik ini
    • Pada 2020 saya pernah menulis proposal bahwa fungsi async harus selalu dijalankan sampai selesai; proposal itu mencakup graceful cancellation, dan menurut saya sampai sekarang belum ada ide yang lebih baik tautan proposal
    • Ada juga proposal untuk unified cancellation di seluruh sync dan async Rust ("A case for CancellationTokens") tautan gist
    • Ada juga contoh implementasi nyata dari hal di atas min_cancel_token
  • Saya kurang paham apa masalahnya jika futures dibatalkan. Futures bukan task, dan tulisan itu sendiri mengakui poin ini secara internal. Kalau begitu, bukankah memang wajar kalau future tidak berjalan sampai selesai? Dan saya juga tidak mengerti kenapa itu jadi masalah. Dalam contohnya disebut future yang "cancel unsafe", tetapi menurut saya intinya adalah salah paham antara ekspektasi dan kenyataan
    • Contoh 1: salah satu try_join dibatalkan karena error
    • Contoh 2: data tidak tertulis saat dibatalkan
      Semua 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
    • Benar sekali! Di Oxide kami benar-benar pernah mendapat banyak bug karena ini. Kalau sudah benar-benar paham bahwa futures itu pasif dan bisa dibatalkan kapan saja di setiap titik await, sisanya tinggal teknik-teknik rinci
  • Saya sangat menikmati presentasi ini di RustConf. Pembedaan konsep cancel safety dan cancel correctness benar-benar berguna. Senang sekali presentasinya juga terbit sebagai posting blog; presentasi memang bagus, tetapi versi blog lebih mudah dibagikan dan dijadikan referensi
    • Saya suka istilah "cancel correctness" karena lebih pas menangkap konteks cancellation. Sebaliknya saya kurang suka istilah "cancel safety". Istilah itu tidak terlalu cocok dengan konsep safety di Rust, dan terasa terlalu menghakimi. Safe/unsafe memberi kesan lebih baik atau lebih buruk, padahal apakah perilaku cancel itu diinginkan atau tidak bergantung pada situasi. Misalnya, future yang menunggu task hasil spawn disebut "cancellation safe", tetapi jika saat drop task itu terus berjalan, pekerjaan yang tidak diperlukan bisa menumpuk dan menguasai lock atau port sehingga menimbulkan masalah. Sebaliknya, spawn handle yang menghentikan task saat drop memang akan disebut "cancellation unsafe", tetapi pola itu justru sangat penting untuk cleanup task yang dependen
    • Saya setuju, tulisan blog memang lebih mudah dibaca dan lebih enak
  • Isi https://sunshowers.io/posts/cancelling-async-rust/#the-pain-of-tokio-mutexes terasa sangat menarik. Saya juga merasa sangat mungkin melakukan kesalahan seperti itu
    • Walaupun saya pengembang Go, bagian seperti ini tetap membantu. Rust memang dibantu lebih ketat oleh tooling, tetapi di Go pun sangat mudah jatuh ke jebakan yang sama dengan goroutines, channel, select, dan primitive konkurensi lainnya
  • Pada contoh pertama, perilaku yang diinginkan terasa kurang jelas. Jika queue penuh, kita perlu memilih antara drop, menunggu, atau panic. Memberi timeout pada blocking biasanya untuk mendeteksi deadlock. Kodenya bilang "tidak semua pesan masuk ke channel", tetapi itu kan memang pasti terjadi kalau resource kurang. Tujuannya apa? Terminasi program yang rapi? Itu cukup sulit di lingkungan thread, dan juga tidak mudah di async. Use case nyata biasanya adalah saat bertukar pesan dengan pihak jarak jauh dan kita perlu membereskan state di sisi kita ketika lawan bicara terputus
    • Idealnya saya ingin menyimpan pesan di buffer sampai ada ruang lagi di channel. Hal ini dibahas di bagian akhir presentasi, "What can be done"
    • Jawabannya ada di contohnya: kode yang mencatat log saat tidak ada ruang selama 5 detik itu untuk diagnosis, tetapi diam-diam berisiko menyebabkan kehilangan data. Memang agak dibuat-buat, tetapi dalam praktik sangat mudah menempelkan kode seperti ini ke berbagai sudut sistem sebagai penanganan masalah seperti "kenapa ini tidak jalan?"
    • Sebagai catatan, penulis tulisan ini menggunakan pronomina they/she about
  • Kita harus selalu ingat bahwa await selalu merupakan titik pengembalian potensial. Sebaiknya hindari menaruh await di antara dua aksi yang harus dieksekusi secara atomik
    • Saya penasaran seperti apa ini benar-benar bisa menimbulkan masalah, misalnya,
      async fn a() {
        b().await
      }
      async fn b() {
        c().await
        d().await
      }
      async fn c() {}
      async fn d() {}
      
      Pada kode ini, dalam kondisi seperti apa d bisa tidak terpanggil? Apakah cancellation terjadi di c? Atau ada sesuatu di level atas dari a?
    • Jadi ini agak berbahaya juga ya? Memang mungkin tak terhindarkan, tetapi kalau ada dua await di 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?
  • Future di Rust mirip dengan move semantics di C++: setelah Future selesai, state-nya bisa menjadi invalid. Karena Rust didesain sebagai stackless coroutine, saat mengimplementasikan struktur async berbasis poll secara langsung, state harus dikelola sendiri di dalam struct. 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
  • Presentasinya benar-benar bagus! Sebagai pemula total, saya berharap di SOP ditekankan lebih awal bahwa Future tidak bisa di-cancel. .await memiliki future itu sehingga drop() tidak bisa dilakukan, dan karena future itu lazy, tidak jelas bagaimana cancellation bekerja setelah .await. Setelah itu saya menelusuri select! dan Abortable() lalu memahaminya, tetapi kalau presentasi berikutnya bagian ini dipanggil secara eksplisit sejak awal, menurut saya akan jadi sempurna
    • Pertanyaan. Saya penasaran, SOP di sini maksudnya apa?
  • Timing-nya benar-benar pas. Hari ini saya baru saja menambahkan "fungsi ini cancel safe" di doc comment untuk sebuah fungsi, dan jadi memikirkan hal-hal seperti ini. Saya harap async drop segera jadi mungkin
    • Saya penasaran fungsi seperti apa itu; kalau berkenan, bisa dijelaskan sedikit