3 puan yazan GN⁺ 2025-10-05 | 1 yorum | WhatsApp'ta paylaş
  • 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, select makrosu, try_join ve 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-nextest geliş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 await veya poll çağrıldığında ilerler
  • Rust'taki future'lar edilgindir (inert); açık bir poll/await yoksa 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 drop etmek ya da artık poll/await etmemek 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 drop edilebilir 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 sleep future'ı iptal güvenlidir
    • Buna karşılık Tokio'nun MPSC send işlemi, drop edildiğinde mesaj kaybı riski taşır (iptal güvenli değildir)
  • İ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ğerini None yapmak) ve sonra bir await noktasında future iptal edilirse, veri yanlış durumda kalıcı hale gelebilir
  • Gerçek üretim ortamında da (ör. Oxide'da sled durum yönetimi) await noktaları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

  • .await eksik future çağrısı: Rust kullanılmayan future için uyarı verir, ancak Result dönüş değeri _ ile alınırsa uyarı vermeyebilir (Clippy'nin yeni lint'leri gerekir)
  • try_join gibi try işlemleri: bir future başarısız olursa geri kalanlar iptal edilir (gerçek servis durdurma mantığında hatalara yol açabilir)
  • select makrosu: birden çok future paralel çalıştırılır ve tamamlanan dışındakilerin tümü iptal edilir (select dö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
    • permit alındıktan sonra, iptal kaygısı olmadan gönderim yapılabilir
  • AsyncWrite içindeki write_all: tüm tamponu yazan write_all iptal açısından kararsızdır; write_all_buf ise tampon imlecini kullanarak iptal anındaki ilerlemeyi izlemeyi mümkün kılar
    • Döngü içinde write_all_buf ile kısmi ilerleme güvenle sürdürülebilir

İptalden kaçınan future kullanım biçimleri

  • future pinning: select döngüsü gibi yerlerde future'ı pin'leyip referans üzerinden poll ederek iptal olmadan bekletmek mümkündür
    • Örnek: reserve future'ını yeniden kullanmak, rezervasyon bekleme sırasını korur
  • task kullanımı: tokio::spawn ile future bir task olarak çalıştırılırsa, handle drop edilse 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): drop sı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

 
GN⁺ 2025-10-05
Hacker News yorumu
  • send/recv için timeout konulan örneğin çok ilginç olduğunu düşünüyorum; future'ların çalıştırılmadan önce doğrudan polling olmadan yürütüldüğü dillerde bunun tam tersi bir durumun ortaya çıkabileceğini fark ettim. send için timeout koyarsanız timeout sonrasında da mesaj gönderilebilir, ancak mesaj kaybolmadığı için bu güvenlidir; recv iç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 veriyi peek ile güvenli biçimde görmektir.
    • Sanırım cancellation-safety'nin özü tam olarak bu.
    • Bunun iyi bir nokta olduğunu düşünüyorum.
  • Bu konu hakkında yazdığım birkaç kaynağı paylaşmak istiyorum
    • 2020'de async fonksiyonların mutlaka sonuna kadar çalışması gerektiğine dair bir öneri yazmıştım; graceful cancellation özelliğini de içeriyor ve hâlâ daha iyi bir fikir çıkmadığını düşünüyorum öneri bağlantısı
    • sync ve async Rust genelinde unified cancellation için de bir öneri var ("A case for CancellationTokens") gist bağlantısı
    • Yukarıdakilerin gerçek bir implementasyonu da mevcut min_cancel_token
  • Future'ların iptal edilmesinin neden bir sorun olduğunu pek anlayamıyorum; future'lar task değildir ve ilgili yazı da bunu içeride kabul ediyor. Öyleyse bir future sonuna kadar çalışmazsa bu zaten beklenen şey değil mi, ayrıca bunun neden sorun olduğunu da anlamıyorum. Örnekte "cancel unsafe" bir future olduğu söyleniyor ama bence asıl mesele beklentiyle gerçeklik arasındaki yanlış anlama.
    • örnek 1'de try_join içindekilerden biri hata verince cancel oluyor
    • örnek 2'de iptal edildiğinde veri yazılmıyor
      Bu ö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.
    • Doğru! Gerçekte Oxide'da bu yüzden çok sayıda bug yaşandı. Future'ların pasif biçimde her await noktasında her an iptal edilebileceğini yeterince kavradıktan sonra geriye ayrıntılı teknikler kalıyor.
  • RustConf'ta bu sunumu dinlemek gerçekten çok keyifliydi; cancel safety ile cancel correctness kavramları arasındaki ayrım gerçekten çok faydalı. Sunumun blog yazısı olarak da yayımlanmasına çok sevindim; sunum güzel ama blog olarak derlenmiş hali paylaşım ve referans açısından daha kullanışlı.
    • "cancel correctness" ifadesinin cancellation bağlamını çok iyi yakaladığını düşünüyorum. Buna karşılık "cancel safety" terimini pek sevmiyorum; Rust'taki safety kavramıyla da tam örtüşmüyor ve gereksiz yere yargılayıcı bir his veriyor. safe/unsafe sanki 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.
    • Blog yazısının okunmasının daha kolay ve daha iyi olduğunu düşünüyorum, katılıyorum.
  • https://sunshowers.io/posts/cancelling-async-rust/#the-pain-of-tokio-mutexes bölümündeki içerik özellikle ilginçti; ben de kolayca böyle bir hata yapabilirdim.
    • Go geliştiricisi olmama rağmen bu kısımlar bana da yardımcı oluyor. Rust'ta araçlar daha sıkı şekilde destek veriyor ama goroutine'ler, kanallar, select ve diğer eşzamanlılık primitive'lerinde Go'da da aynı tuzaklara düşmek kolay.
  • İlk örnekte istenen davranışın ne olduğu belirsiz. Kuyruk dolduğunda drop, bekleme ya da panic arasında seçim yapmak gerekir. Blocking'e timeout koymak çoğunlukla deadlock tespiti içindir. Kod "tüm mesajlar kanala gitmiyor" diyor ama kaynaklar yetersizse bunun olması zaten doğal. Amaç ne? Programı temiz biçimde kapatmak mı? Bu thread ortamında oldukça zor, async'te de kolay değil. Gerçek kullanım senaryosu, uzak uçla mesaj alışverişi sırasında karşı taraf koptuğunda kendi tarafımdaki durumu temizlemek.
    • İdeal olarak, kanalda yer açılana kadar mesajları bir buffer'da tutmak istersiniz. Sunumun sonlarındaki "What can be done" kısmı bunu ele alıyor.
    • Yanıt örneğin içinde var; 5 saniye boyunca yer açılmazsa log atan kod tanılama amaçlı ama fark edilmeden veri kaybına yol açma riski taşıyor. Biraz yapay bir örnek olsa da pratikte "neden çalışmıyor?" gibi sorunlara müdahale etmek için bu tür kodları sistemin her yerine eklemek çok kolay.
    • Bu arada bu yazının yazarı they/she zamirlerini kullanıyor about
  • await ifadesinin her zaman potansiyel bir dönüş noktası olduğunu akılda tutmak gerekir; mutlaka birlikte atomik şekilde çalışması gereken iki eylemin arasına await koymaktan kaçınmak iyi olur.
    • Bunun pratikte nasıl sorun çıkardığını merak ediyorum, örneğin
      async fn a() {
        b().await
      }
      async fn b() {
        c().await
        d().await
      }
      async fn c() {}
      async fn d() {}
      
      bu kodda d'nin çağrılmaması nasıl bir durumda ortaya çıkıyor? İptal c sırasında mı oluyor? Yoksa üst seviyede a tarafında bir şey olduğu için mi?
    • O zaman bu biraz tehlikeli değil mi? Elbette kaçınılmaz bir yönü var ama bir "critical section" içinde iki kez await varsa 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.
  • Rust'taki 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ı
  • Gerçekten çok iyi bir sunumdu! Tam bir acemi olarak, SOP'de Future'ın cancel edilemeyeceği noktasının önceden vurgulanmasını isterdim. .await future'ın sahipliğini aldığı için drop() yapılamıyor; future lazy olduğu için de .await sonrasında cancellation'ın nasıl işlediği pek net gelmemişti. Sonrasında select! ve Abortable()'ı araştırıp anladım ama ileride tekrar sunulursa bu noktanın da en başta özellikle belirtilmesi harika olur.
    • Soru: Burada SOP ne anlama geliyor, merak ettim.
  • Zamanlaması gerçekten çok iyiydi; bugün tam da yeni bir fonksiyonun doc comment'ine "bu fonksiyon cancel safe'dir" diye yazıyordum ve böyle konuları düşünüyordum. Keşke async drop bir an önce mümkün olsa.
    • Hangi fonksiyon olduğunu merak ettim, biraz daha anlatabilir misin?