3 पॉइंट द्वारा GN⁺ 2025-10-05 | 1 टिप्पणियां | WhatsApp पर शेयर करें
  • असिंक्रोनस Rust वातावरण में cancellation हैंडलिंग सुविधाजनक है, लेकिन इसे गलत तरीके से संभालने पर अप्रत्याशित बग और कठिनाइयाँ पैदा हो सकती हैं
  • सिंक्रोनस Rust में स्पष्ट flag checks या process termination जैसी चीज़ों की ज़रूरत होती है, लेकिन असिंक्रोनस Rust में सिर्फ future को drop करने से cancellation बहुत आसानी से हो सकता है
  • cancellation safety और cancellation correctness अलग-अलग अवधारणाएँ हैं, और किसी एक future का cancellation पूरे सिस्टम में समस्या पैदा कर सकता है
  • cancellation से जुड़े प्रमुख समस्या पैटर्न में Tokio mutex, select! macro, try_join, और future के उपयोग में होने वाली गलतियाँ शामिल हैं
  • कोई पूर्ण समाधान नहीं है, लेकिन cancellation-safe API का उपयोग, future pinning, task separation आदि से cancellation से होने वाली समस्याएँ कम की जा सकती हैं

परिचय

  • यह पोस्ट असिंक्रोनस Rust में cancellation पर RustConf 2025 प्रस्तुति की सामग्री पर आधारित है
  • सामान्य Rust async code के उदाहरणों में, message receive या send loop में timeout जोड़ने पर अक्सर messages के खो जाने की समस्या सामने आती है
  • Oxide Computer Company जैसे वास्तविक बड़े सिस्टमों में async Rust का उपयोग करते समय आए cancellation issues और वास्तविक bug cases पर चर्चा की गई है
  • लेख तीन भागों में विभाजित है: 1) cancellation की अवधारणा, 2) cancellation analysis, 3) व्यावहारिक समाधान
  • लेखक ने Rust signal handling, cargo-nextest development आदि के माध्यम से असिंक्रोनस Rust के लाभ और कठिनाइयों का अनुभव किया है

1. cancellation क्या है?

cancellation का अर्थ

  • cancellation वह स्थिति है जिसमें कोई asynchronous task शुरू की जाती है, लेकिन बीच में उसे रोक दिया जाता है
  • उदाहरण: बड़े download/network request, partial file read आदि को बीच में cancel किया जा सकता है

सिंक्रोनस Rust में cancellation के तरीके

  • आम तौर पर atomic flag के माध्यम से समय-समय पर cancellation की जाँच, विशेष exceptions (panic) का उपयोग, या पूरे process को forcefully terminate करने जैसी विधियाँ मौजूद हैं
  • कुछ frameworks (जैसे Salsa) panic payload का उपयोग करते हैं, लेकिन यह Rust के सभी platforms पर काम नहीं करता, खासकर Wasm environment में
  • सिर्फ thread को forcefully terminate करना Rust safety और mutex संरचना के कारण अनुमति नहीं है
  • संक्षेप में, सिंक्रोनस Rust में कोई सार्वभौमिक और सुरक्षित cancellation protocol मौजूद नहीं है

असिंक्रोनस Rust: Future क्या है?

  • Future Rust compiler द्वारा बनाई गई एक state machine है, जो memory में साधारण data के रूप में मौजूद रहती है
  • इसे सिर्फ बनाने से यह execute नहीं होती; यह केवल await या poll call होने पर आगे बढ़ती है
  • Rust की Future passive होती है; स्पष्ट poll/await के बिना यह कोई काम नहीं करती
  • यह Go/JavaScript/C# जैसी भाषाओं से अलग है, जहाँ future बनते ही execution शुरू हो जाता है

असिंक्रोनस Rust का cancellation protocol

  • Future cancellation का मतलब बस उसे drop कर देना, या poll/await को आगे न चलाना है
  • state machine होने के कारण Future को किसी भी समय छोड़ दिया जा सकता है
  • असिंक्रोनस Rust में cancellation बहुत शक्तिशाली है और बहुत आसानी से लागू हो जाता है
  • लेकिन यह बहुत ज़्यादा आसान भी है, जिससे future चुपचाप drop हो सकता है, और ownership model के अनुसार child futures भी क्रमशः cancel हो सकते हैं
  • इस वजह से cancellation एक non-local घटना बन जाती है, जो पूरे call chain को प्रभावित कर सकती है

2. cancellation analysis

cancellation safety और cancellation correctness

  • cancellation safety: किसी individual future का वह गुण कि उसे बिना side effects के सुरक्षित रूप से cancel किया जा सके
    • उदाहरण: Tokio का sleep future cancellation-safe है
    • जबकि Tokio का MPSC send, drop होने पर message loss का जोखिम रखता है, इसलिए यह cancellation-safe नहीं है
  • cancellation correctness: यह पूरे सिस्टम का global property है कि cancellation की स्थिति में उसकी मूल विशेषताएँ बनी रहें
    • यदि cancellation-safe न होने वाला future सिस्टम में मौजूद ही न हो, तो correctness problem नहीं होती
    • समस्या तभी उत्पन्न होती है जब cancellation-safe न होने वाला future वास्तव में cancel हो जाए
    • cancellation के कारण data loss, invariant violation, या cleanup छूट जाना cancellation correctness violation माना जाता है

Tokio mutex की कठिनाई

  • Tokio mutex lock लेकर data को adjust करने और फिर release करने के तरीके से काम करता है
  • समस्या: lock के भीतर state को अस्थायी रूप से invalid बनाना, जैसे Option<T> को None करना, और फिर await के दौरान future cancel हो जाए तो data गलत state में अटक सकता है
  • वास्तविक production work, जैसे Oxide में sled state management, में await point पर cancellation के कारण unstable state देखी गई है
  • इस तरह असिंक्रोनस code में state management के दौरान cancellation बहुत खतरनाक defect का कारण बन सकता है

cancellation होने के पैटर्न और उदाहरण

  • .await के बिना future call: Rust unused future पर warning देता है, लेकिन यदि return value के Result को _ में लिया जाए तो warning नहीं मिलती; इसके लिए नए Clippy lint की ज़रूरत होती है
  • try_join जैसे try operations: एक future fail होते ही बाकी cancel हो जाते हैं, जिससे वास्तविक service shutdown logic में bugs हो सकते हैं
  • select! macro: कई futures को parallel चलाकर, जो complete नहीं हुए उन्हें cancel कर देता है; select loop में data loss का जोखिम बड़ा हो सकता है
  • इन patterns का documentation में उल्लेख है, लेकिन व्यवहार में async cancellation कई जगहों पर implicitly हो सकता है

3. क्या किया जा सकता है?

  • cancellation correctness से जुड़ी समस्याओं का कोई मौलिक और पूर्ण समाधान अभी उपलब्ध नहीं है
  • फिर भी व्यवहारिक रूप से, नीचे दिए गए तरीकों से cancellation defects की संभावना कम की जा सकती है

cancellation-safe futures में पुनर्रचना

  • MPSC send उदाहरण: reserve और actual send को अलग करके partial cancellation-safety हासिल की जा सकती है
    • reserve operation cancel होने पर संबंधित message खोता नहीं है
    • permit मिलने के बाद बिना cancellation की चिंता के send किया जा सकता है
  • AsyncWrite का write_all: पूरे buffer पर write_all cancellation के प्रति अस्थिर है, जबकि write_all_buf buffer cursor का उपयोग करके cancellation होने पर progress track कर सकता है
    • loop के भीतर write_all_buf से partial progress को सुरक्षित रूप से resume किया जा सकता है

cancellation से बचने वाला future संचालन

  • future pinning: select loop आदि में future को pin करके cancel होने से बचाया जा सकता है, और reference के रूप में poll करके इंतज़ार किया जा सकता है
    • उदाहरण: reserve future को reuse करने पर reservation wait order बना रहता है
  • task का उपयोग: tokio::spawn आदि से future को task के रूप में चलाने पर, handle को drop कर देने के बाद भी task स्वयं runtime द्वारा अलग से manage होती है और forcefully cancel नहीं होती
    • Oxide Dropshot HTTP server आदि में प्रत्येक request को अलग task में चलाया जाता है, ताकि client connection टूटने पर भी request processing पूरी हो सके

कोई व्यवस्थित समाधान?

  • फिलहाल safe Rust स्तर पर सीमाएँ हैं, लेकिन कुछ approaches पर चर्चा चल रही है
    • Async drop: future cancel होने पर asynchronous cleanup code चलाने की अनुमति
    • linear types: drop के समय किसी विशेष code को अनिवार्य रूप से चलाना, या कुछ futures को non-cancellable के रूप में चिह्नित करना
  • इन सभी approaches के implementation में कठिनाइयाँ मौजूद हैं

निष्कर्ष और सिफारिशें

  • Future passive होती है — इस मूल गुण को स्पष्ट रूप से समझना आवश्यक है
  • cancellation safety और cancellation correctness की अवधारणाओं को अच्छी तरह समझना चाहिए
  • cancellation bugs के प्रमुख उदाहरणों और code patterns को पहचानकर, पहले से mitigation strategy तैयार रखनी चाहिए
  • कुछ व्यावहारिक सिफारिशें:
    • जहाँ संभव हो, Tokio mutex से बचें और alternatives पर विचार करें
    • partial-completion API या cancellation-safe API को design/उपयोग करें
    • cancellation-safe न होने वाले futures के लिए ऐसा code structure अपनाएँ जो completion को सुनिश्चित करे
  • अतिरिक्त रूप से, cooperative cancellation, actor model, structured concurrency, panic safety, mutex poisoning जैसे advanced topics की भी समीक्षा करने की सिफारिश की जाती है
  • संबंधित सामग्री sunshowers/cancelling-async-rust पर उपलब्ध है

पढ़ने के लिए धन्यवाद। प्रस्तुति और संदर्भ सामग्री की समीक्षा तथा feedback देने वाले Oxide सहयोगियों के प्रति आभार।

1 टिप्पणियां

 
GN⁺ 2025-10-05
Hacker News टिप्पणियाँ
  • send/recv पर timeout लगाने वाला उदाहरण मुझे बहुत दिलचस्प लगा। इससे समझ में आता है कि उन भाषाओं में, जहाँ future बिना poll हुए भी तुरंत चल सकता है, उलटी स्थिति भी आ सकती है। send पर timeout लगाने से timeout के बाद भी message भेजा जा सकता है, लेकिन message खोता नहीं है इसलिए यह सुरक्षित है। लेकिन recv पर timeout लगाने से ऐसी स्थिति आ सकती है जहाँ channel से message पढ़ लेने के बाद timeout चुना जाए, और तब message बस फेंक दिया जाए, इसलिए यह सुरक्षित नहीं हो सकता। इसका समाधान यह है कि timeout या channel में से "कुछ उपलब्ध है" को select किया जाए, और दूसरे मामले में peek के ज़रिए data को सुरक्षित रूप से देखा जाए
    • मुझे लगता है यही cancellation-safety का मूल बिंदु है
    • मुझे यह अच्छा point लगा
  • मैं इस विषय पर अपने लिखे कुछ संसाधन साझा करना चाहता हूँ
    • मैंने 2020 में एक proposal लिखा था कि async function को ज़रूर अंत तक चलना चाहिए। इसमें graceful cancellation शामिल था, और मुझे अब तक इससे बेहतर idea नहीं दिखा है proposal link
    • sync और async Rust दोनों में unified cancellation के लिए एक proposal भी है ("A case for CancellationTokens") gist link
    • ऊपर की बात का एक वास्तविक implementation भी है min_cancel_token
  • मुझे ठीक से समझ नहीं आता कि futures के cancel होने में समस्या क्या है। futures task नहीं हैं, और वह लेख भी अंदर ही अंदर इस बात को मानता है। तो फिर अगर कोई future अंत तक execute न हो, तो क्या यह मूल रूप से वैसा ही नहीं है? और यह समस्या क्यों है, मैं समझ नहीं पा रहा हूँ। उदाहरण में इसे "cancel unsafe" future कहा गया है, लेकिन मुझे लगता है कि असल मुद्दा expectation और reality के बीच की गलतफहमी है।
    • उदाहरण 1 में try_join का एक हिस्सा error के कारण cancel होता है
    • उदाहरण 2 में cancel होने पर data लिखा नहीं जाता
      इन दोनों ही मामलों में context cancel होने की वजह से काम पूरा न होना स्वाभाविक व्यवहार है। अगर काम का पूरा होना ज़रूरी है, तो उसे अलग independent task में बाँट देना चाहिए। मुझे लग रहा है शायद मैं कोई महत्वपूर्ण nuance मिस कर रहा हूँ। मेरी समझ में futures का design intent यही है कि work cancellation की वजह से गायब हो सकता है। तो फिर समस्या क्या है, यह कोई फिर से समझाए तो अच्छा होगा
    • सही बात है! Oxide में इसी वजह से वास्तव में बहुत bugs हुए थे। जब आप यह पूरी तरह समझ लेते हैं कि futures passive होते हैं और हर await point पर कभी भी cancel हो सकते हैं, तो फिर बाकी चीज़ें बारीक technical techniques भर रह जाती हैं
  • मैंने RustConf में यह talk सच में बहुत मज़े से सुनी थी। cancel safety और cancel correctness के बीच का वैचारिक भेद बहुत उपयोगी है। अच्छा लगा कि talk एक blog post के रूप में भी आ गई, क्योंकि talk अच्छी होती है लेकिन blog के रूप में संकलित चीज़ share और reference करना कहीं आसान होता है
    • मुझे "cancel correctness" शब्द पसंद आया क्योंकि यह cancellation के संदर्भ को अच्छी तरह पकड़ता है। दूसरी तरफ, मुझे "cancel safety" शब्द खास पसंद नहीं है। यह Rust के safety concept से भी पूरी तरह मेल नहीं खाता, और बेवजह कुछ judgmental-सा लगता है। safe/unsafe यह संकेत देता है कि एक दूसरी से बेहतर या बदतर है, जबकि cancellation behavior की वांछनीयता स्थिति पर निर्भर करती है। उदाहरण के लिए, किसी spawned task का इंतज़ार करने वाला future "cancellation safe" कहलाता है, लेकिन अगर drop होने पर task चलता रहे तो बेकार का काम जमा हो सकता है, और lock या port भी घिरे रह सकते हैं, जो समस्या है। उलटे, drop होने पर task को रोक देने वाला spawn handle "cancellation unsafe" कहा जा सकता है, लेकिन dependent task की cleanup के लिए यह बहुत महत्वपूर्ण pattern है
    • मुझे भी लगता है कि blog post पढ़ने में ज़्यादा आसान और बेहतर है
  • https://sunshowers.io/posts/cancelling-async-rust/#the-pain-of-tokio-mutexes का हिस्सा खास तौर पर दिलचस्प लगा। मुझे लगता है मैं भी आसानी से ऐसी गलती कर सकता हूँ
    • मैं Go developer हूँ, फिर भी यह हिस्सा मददगार लगा। Rust में tools ज़्यादा सख्ती से मदद करते हैं, लेकिन goroutines, channels, select, और दूसरी concurrency primitives के साथ Go में भी बिल्कुल ऐसे ही traps में फँसना आसान है
  • पहले उदाहरण में चाहा गया behavior क्या है, यह स्पष्ट नहीं है। अगर queue भर जाए तो drop, wait, या panic में से कुछ चुनना होगा। blocking पर timeout लगाना आम तौर पर deadlock detection के लिए होता है। code कहता है कि "सभी messages channel तक नहीं पहुँचते", लेकिन resources कम हों तो यह तो स्वाभाविक है। उद्देश्य क्या है? साफ-सुथरा program shutdown? thread environment में यह काफ़ी कठिन है, और async में भी आसान नहीं। वास्तविक use case तो remote side के साथ message exchange में यह है कि जब दूसरा पक्ष disconnect हो जाए तो अपनी तरफ का state साफ किया जाए
    • आदर्श रूप से, मैं चाहता हूँ कि channel में जगह खाली होने तक messages को buffer में रखा जाए। यह बात talk के बाद वाले हिस्से "What can be done" में आती है
    • उदाहरण में इसका जवाब है। 5 सेकंड तक जगह न होने पर logging करने वाला code diagnostic purpose के लिए है, लेकिन इसमें चुपचाप data loss का जोखिम है। थोड़ा बनावटी ज़रूर है, लेकिन असल में "यह काम क्यों नहीं कर रहा?" जैसी troubleshooting के लिए इस तरह का code सिस्टम में जगह-जगह जोड़ देना आसान है
    • वैसे, इस लेख की लेखिका they/she pronouns इस्तेमाल करती हैं about
  • हमेशा यह ध्यान रखना चाहिए कि await एक संभावित return point है। जिन दो actions को साथ में atomic तरीके से execute होना ही चाहिए, उनके बीच await रखने से बचना चाहिए
    • मुझे जिज्ञासा है कि व्यवहार में यह समस्या कैसे पैदा करती है, जैसे,
      async fn a() {
        b().await
      }
      async fn b() {
        c().await
        d().await
      }
      async fn c() {}
      async fn d() {}
      
      इस code में किस तरह d कॉल न होने की समस्या पैदा हो सकती है? c में cancellation होने से? या a में ऊपर की तरफ कुछ होने से?
    • तो क्या यह थोड़ा खतरनाक नहीं है? मानता हूँ कि कुछ हद तक यह अपरिहार्य है, लेकिन अगर किसी "critical section" में दो await हों, तो उनके बीच pause तो हो सकता है, पर अंततः आगे execute होना ज़रूरी हो सकता है। उदाहरण के लिए, अगर DB change के बाद audit log भी ज़रूर लिखना हो, तो क्या इसका एकमात्र जवाब बस do not cancel जैसी comment लिखना है?
  • Rust का Future कुछ हद तक C++ की move semantics जैसा है; Future के समाप्त होने के बाद वह invalid state में हो सकता है। Rust की stackless coroutine design की वजह से, poll-based async structure को सीधे implement करते समय state को struct में खुद manage करना पड़ता है। यह सब अपने-आप में आम pitfalls हैं। और हाल के async Rust में cancellation state management का एक नया variable है। जब मैं mea (Make Easy Async) library विकसित कर रहा था, तब भी अगर cancel safety trivial न हो तो मैं उसे ज़रूर document करता था। और मुझे याद है कि लापरवाह async cancellation की वजह से IO stack में समस्याएँ आई थीं mea reddit case
  • यह सच में बहुत अच्छा talk था! मैं बिलकुल शुरुआती हूँ, और चाहता था कि SOP में पहले ही यह ज़ोर देकर कहा जाता कि Future को cancel नहीं किया जा सकता। .await future का ownership ले लेता है, इसलिए drop() नहीं कर सकते, और future lazy होने की वजह से .await के बाद cancellation कैसे काम करती है, यह साफ़ नहीं था। बाद में मैंने select! और Abortable() देखकर समझा, लेकिन अगर आगे कभी यह talk फिर दी जाए तो शुरुआत में इस हिस्से को भी highlight किया जाए तो यह बिल्कुल perfect होगा
    • सवाल: यहाँ SOP का मतलब क्या है?
  • timing सच में कमाल की थी। आज ही मैं एक नई function के doc comment में "यह function cancel safe है" लिख रहा था, और इसी तरह की बात सोच रहा था। काश async drop जल्दी संभव हो जाए
    • वह function कौन-सी है, यह जानने की जिज्ञासा है। क्या आप थोड़ा और समझा सकते हैं?