Asenkron Rust'ta iptal işleme
(sunshowers.io)- Asenkron Rust ortamında iptal işleme kullanışlıdır, ancak yanlış ele alındığında beklenmedik hatalara ve zorluklara yol açabilir
- Senkron Rust'ta açık bayrak kontrolleri veya süreç sonlandırma gerekirken, asenkron Rust'ta bir future'ı drop etmek tek başına iptali çok kolay hale getirir
- İptal güvenliği (cancel safety) ile iptal doğruluğu (cancel correctness) farklı kavramlardır; tek bir future'ın iptali sistem genelinde sorunlara yol açabilir
- İptalle ilgili başlıca sorun örüntüleri arasında Tokio mutex,
selectmakrosu,try_joinve future kullanım hataları yer alır - Kusursuz bir çözüm yoktur, ancak iptal-güvenli API'ler kullanmak, future'ları pin'lemek ve task'ları ayırmak gibi yöntemlerle iptalin yol açtığı sorunlar azaltılabilir
Giriş
- Bu yazı, asenkron Rust'ta iptal işleme (cancellation) konulu RustConf 2025 sunumuna dayanmaktadır
- Genel Rust asenkron kod örneklerinde mesaj alma veya gönderme döngüsüne timeout eklendiğinde, çoğu zaman mesaj kaybı yaşandığı görülebilir
- Oxide Computer Company gibi gerçek büyük ölçekli sistemlerde async Rust kullanılırken karşılaşılan iptal sorunları ve gerçek hata vakaları ele alınır
- Yazı üç bölümden oluşur: 1) iptal kavramı, 2) iptalin analizi, 3) pratik çözüm yolları
- Yazar, Rust signal handling,
cargo-nextestgeliştirme vb. deneyimleriyle asenkron Rust'ın avantajlarını ve zorluklarını bizzat yaşamıştır
1. İptal nedir?
İptalin anlamı
- İptal (cancellation), bir asenkron işi başlattıktan sonra onu tamamlanmadan durdurma durumudur
- Örnek: büyük dosya indirme / ağ isteği, dosyanın bir kısmını okuma gibi işlemler ortada iptal edilebilir
Senkron Rust'ta iptal yöntemleri
- Genellikle atomik bayraklar ile periyodik olarak iptal durumu kontrol edilir ya da özel istisnalar (
panic), tüm süreci zorla sonlandırma gibi yöntemler kullanılır - Bazı framework'ler (Salsa vb.) panic payload kullanır, ancak bu Rust'ın tüm platformlarında çalışmaz (özellikle Wasm ortamında)
- Yalnızca thread'i zorla sonlandırmak, Rust güvenliği ve mutex yapısı nedeniyle izin verilen bir şey değildir
- Özetle, senkron Rust'ta genel amaçlı ve güvenli bir iptal protokolü yoktur
Asenkron Rust: Future nedir?
- Future, Rust derleyicisinin ürettiği bir durum makinesidir (state machine) ve bellekteki basit veriden ibarettir
- Yalnızca oluşturmakla çalışmaz; ancak
awaitveyapollçağrıldığında ilerler - Rust'taki future'lar edilgindir (inert); açık bir
poll/awaityoksa hiçbir iş yapmazlar - Bu yönüyle, future oluşturulduğu anda çalışmaya başlayan Go/JavaScript/C# gibi dillerden ayrılır
Asenkron Rust'ın iptal protokolü
- Bir future'ı iptal etmek, onu basitçe
dropetmek ya da artıkpoll/awaitetmemek anlamına gelir - Bu bir durum makinesi olduğu için Future her an terk edilebilir
- Asenkron Rust'ta iptal hem çok güçlüdür hem de çok kolay uygulanır
- Ancak fazla kolay olması nedeniyle future'lar sessizce
dropedilebilir ve sahiplik modeli gereği alt future'lar da zincirleme iptal olur - Bu özellik yüzünden iptal yerel olmayan (non-local) bir olgu haline gelir ve tüm çağrı zincirini etkiler
2. İptalin analizi
İptal güvenliği ve iptal doğruluğu
- İptal güvenliği (cancel safety): tek tek future'ların yan etki olmadan güvenle iptal edilebilme özelliğidir
- Örnek: Tokio'nun
sleepfuture'ı iptal güvenlidir - Buna karşılık Tokio'nun MPSC
sendişlemi,dropedildiğinde mesaj kaybı riski taşır (iptal güvenli değildir)
- Örnek: Tokio'nun
- İptal doğruluğu (cancel correctness): sistemin bütünüyle iptal durumunda temel özelliklerini korumasını ifade eden küresel bir özelliktir
- Sistemde iptal-güvenli olmayan bir future yoksa doğruluk sorunu oluşmaz
- Sorun, iptal-güvenli olmayan future gerçekten iptal edildiğinde ortaya çıkar
- İptal nedeniyle veri kaybı, değişmezlerin ihlali veya eksik cleanup oluşursa iptal doğruluğu bozulmuş olur
Tokio mutex'in zorlukları
- Tokio mutex, kilidi alıp veriyi düzeltip ardından bırakma mantığıyla çalışır
- Sorun: kilit içindeyken durum geçici olarak bozulursa (ör.
Option<T>değeriniNoneyapmak) ve sonra birawaitnoktasında future iptal edilirse, veri yanlış durumda kalıcı hale gelebilir - Gerçek üretim ortamında da (ör. Oxide'da sled durum yönetimi)
awaitnoktalarında iptal nedeniyle kararsız durumlar oluşmuştur - Bu nedenle asenkron kodda durum yönetiminde iptal, son derece tehlikeli kusurların kaynağı olabilir
İptalin ortaya çıkış örüntüleri ve örnekler
.awaiteksik future çağrısı: Rust kullanılmayan future için uyarı verir, ancakResultdönüş değeri_ile alınırsa uyarı vermeyebilir (Clippy'nin yeni lint'leri gerekir)try_joingibi try işlemleri: bir future başarısız olursa geri kalanlar iptal edilir (gerçek servis durdurma mantığında hatalara yol açabilir)selectmakrosu: birden çok future paralel çalıştırılır ve tamamlanan dışındakilerin tümü iptal edilir (selectdöngülerinde veri kaybı riski büyüktür)- Bu örüntüler belgelerde geçse de, pratikte asenkron iptal çok farklı yerlerde örtük biçimde ortaya çıkabilir
3. Ne yapılabilir?
- İptal doğruluğu sorunlarına yönelik köklü ve tam bir çözüm henüz yoktur
- Yine de pratikte aşağıdaki yöntemlerle iptal kaynaklı hata olasılığı azaltılabilir
İptal-güvenli future'lara göre yeniden yapılandırma
- MPSC
sendörneği: rezervasyon (reserve) ile gerçek gönderimi (send) ayırarak kısmi iptal güvenliği elde edilebilir- Rezervasyon işlemi iptal edilse bile ilgili mesaj kaybolmaz
permitalındıktan sonra, iptal kaygısı olmadan gönderim yapılabilir
AsyncWriteiçindekiwrite_all: tüm tamponu yazanwrite_alliptal açısından kararsızdır;write_all_bufise tampon imlecini kullanarak iptal anındaki ilerlemeyi izlemeyi mümkün kılar- Döngü içinde
write_all_bufile kısmi ilerleme güvenle sürdürülebilir
- Döngü içinde
İptalden kaçınan future kullanım biçimleri
- future pinning:
selectdöngüsü gibi yerlerde future'ı pin'leyip referans üzerindenpollederek iptal olmadan bekletmek mümkündür- Örnek:
reservefuture'ını yeniden kullanmak, rezervasyon bekleme sırasını korur
- Örnek:
- task kullanımı:
tokio::spawnile future bir task olarak çalıştırılırsa, handledropedilse bile task'in kendisi runtime tarafından ayrı yönetildiği için zorla iptal edilmez- Oxide'ın Dropshot HTTP sunucusunda olduğu gibi, her isteği ayrı bir task'ta çalıştırmak istemci bağlantısı koptuğunda bile isteğin tamamlanmasını garanti edebilir
Sistematik bir çözüm?
- Şu anda safe Rust düzeyinde bu imkanlar sınırlıdır, ancak tartışılan bazı yaklaşımlar vardır
- Async drop: future iptal edildiğinde asenkron cleanup kodunun çalışmasına izin vermek
- Doğrusal tipler (linear types):
dropsırasında belirli kodların zorunlu çalıştırılması ya da bazı future'ların iptal edilemez olarak işaretlenmesi
- Ancak bu yaklaşımların hepsi uygulama açısından zorluklar içerir
Sonuç ve öneriler
- Future'ların edilgin (passive) olduğu gerçeği temel olarak kavranmalıdır
- İptal güvenliği (cancel safety) ve iptal doğruluğu (cancel correctness) kavramları iyi bilinmelidir
- Başlıca iptal hata örnekleri ve kod örüntüleri anlaşılmalı, bunlara karşı stratejiler önceden hazırlanmalıdır
- Bazı pratik öneriler:
- Mümkünse Tokio mutex kullanımından kaçının ve alternatifleri değerlendirin
- Kısmi-tamamlanma API'leri veya iptal-güvenli API'ler tasarlayın / kullanın
- İptal-güvenli olmayan future'lar için tamamlanmayı mutlaka garanti eden kod yapıları kurun
- Ayrıca cooperative cancellation, actor modeli, structured concurrency, panic safety, mutex poisoning gibi ileri konuların da incelenmesi önerilir
- İlgili materyaller için sunshowers/cancelling-async-rust kaynağına bakılabilir
Okuduğunuz için teşekkürler. Sunum ve ilgili materyalleri inceleyip geri bildirim veren Oxide'daki çalışma arkadaşlarına teşekkür ederim.
1 yorum
Hacker News yorumu
sendiçin timeout koyarsanız timeout sonrasında da mesaj gönderilebilir, ancak mesaj kaybolmadığı için bu güvenlidir;recviçin timeout koyarsanız ise kanaldan mesajı okuduktan sonra timeout'un seçildiği bir durumda mesajı doğrudan atmış olursunuz ve bu güvenli olmayabilir. Çözüm, timeout ile kanal üzerinde "bir şeyin kullanılabilir olması" arasında seçim yapmak ve ikinci durumda veriyipeekile güvenli biçimde görmektir.try_joiniçindekilerden biri hata verince cancel oluyorBu örneklerin hepsinde context cancel edildiği için işin tamamlanmaması gayet doğal davranış. Eğer işin mutlaka bitmesi gerekiyorsa bunu bağımsız bir task'a ayırmak yeterli olur. Önemli bir nüansı mı kaçırıyorum diye merak ediyorum; benim anladığım kadarıyla işin cancellation nedeniyle ortadan kalkması future'ların tasarım niyeti zaten. Sorunun ne olduğunu yeniden açıklarsanız sevinirim.
awaitnoktasında her an iptal edilebileceğini yeterince kavradıktan sonra geriye ayrıntılı teknikler kalıyor.safe/unsafesanki daha iyi ya da daha kötü bir şeyi ima ediyor ama cancellation davranışının istenip istenmediği duruma göre değişir. Örneğin spawn edilmiş bir task'ı bekleyen future "cancellation safe" diye adlandırılabilir, ama drop edildiğinde task çalışmaya devam ederse gereksiz işler birikir, lock veya port da tutulmaya devam eder ve bu sorun yaratabilir. Tersine, drop edildiğinde task'ı durduran bir spawn handle "cancellation unsafe" denebilir ama bağımlı task'ların cleanup'ı için çok önemli bir pattern'dir.selectve diğer eşzamanlılık primitive'lerinde Go'da da aynı tuzaklara düşmek kolay.they/shezamirlerini kullanıyor aboutawaitifadesinin her zaman potansiyel bir dönüş noktası olduğunu akılda tutmak gerekir; mutlaka birlikte atomik şekilde çalışması gereken iki eylemin arasınaawaitkoymaktan kaçınmak iyi olur.d'nin çağrılmaması nasıl bir durumda ortaya çıkıyor? İptalcsırasında mı oluyor? Yoksa üst seviyedeatarafında bir şey olduğu için mi?awaitvarsa arada duraklatılabilir, fakat yine de sonrasında devam etmesi gereken durumlar olabilir. Örneğin DB değişikliğinden sonra audit log yazmanız gerekiyorsa ve ikisinin de mutlaka yürütülmesi gerekiyorsa, çözüm sadece "do not cancel" yorumu eklemek mi diye merak ediyorum.Future, C++'taki move semantics gibi, tamamlandıktan sonra geçersiz bir duruma girebilir. Rust stackless coroutine tasarımını kullandığı için poll tabanlı async yapıyı doğrudan implemente ederken durumu struct içinde elle yönetmeniz gerekir. Bunların hepsi yaygın tuzaklar. Ayrıca son dönemde async Rust'ta cancellation, state management için yeni bir değişken hâline geldi. Ben de mea (Make Easy Async) kütüphanesini geliştirirken cancel safety trivial değilse bunu mutlaka belgeliyorum ve düşüncesiz async cancellation nedeniyle IO stack'te sorun çıktığı bir vakayı hatırlıyorum mea reddit vakası.awaitfuture'ın sahipliğini aldığı içindrop()yapılamıyor; future lazy olduğu için de.awaitsonrasında cancellation'ın nasıl işlediği pek net gelmemişti. Sonrasındaselect!veAbortable()'ı araştırıp anladım ama ileride tekrar sunulursa bu noktanın da en başta özellikle belirtilmesi harika olur.