2 पॉइंट द्वारा GN⁺ 2025-11-01 | 1 टिप्पणियां | WhatsApp पर शेयर करें
  • Futurelock वह deadlock स्थिति है जो तब होती है जब एक task कई Future को एक साथ मैनेज करता है, और उनमें से एक को दूसरे Future के resource की ज़रूरत होती है लेकिन उसे अब poll नहीं किया जाता
  • tokio::select! सिंटैक्स में referenced Future (&mut future) और await शामिल करने वाली branch साथ में इस्तेमाल होने पर यह आसानी से हो सकता है
  • यह समस्या task और Future की जिम्मेदारियों के अलगाव में विफलता से पैदा होती है, जहाँ एक ही task दोनों Future का इंतज़ार करता है लेकिन poll सिर्फ एक को करता है, जिससे सिस्टम रुकी हुई अवस्था में फँस जाता है
  • FuturesUnordered, bounded channel, Stream आदि में भी इसी तरह की स्थिति पैदा हो सकती है
  • सुरक्षित asynchronous design के लिए tokio::spawn से Future को अलग task में बाँटना या select के अंदर await के उपयोग से बचना सबसे महत्वपूर्ण है

Futurelock की अवधारणा और उदाहरण

  • Futurelock तब होता है जब Future A के पास मौजूद resource की Future B को ज़रूरत हो, लेकिन दोनों Future को संभालने वाला task, A को अब poll न कर रहा हो
  • उदाहरण कोड में tokio::select! के भीतर &mut future1 और sleep का एक साथ इंतज़ार किया जाता है; अगर sleep पहले पूरा हो जाए, तो future1 अभी भी lock का इंतज़ार करता रह जाता है
  • इसके बाद future3 उसी lock को माँगता है, लेकिन lock future1 को आवंटित हो चुका होता है और future1 poll नहीं हो रहा होता, इसलिए प्रोग्राम स्थायी रूप से रुक जाता है

tokio::select! और Mutex का परस्पर प्रभाव

  • tokio::sync::Mutex एक fair lock है, जो प्रतीक्षा क्रम के अनुसार lock देता है
  • lock future1 को दे दिया जाता है, लेकिन task अब केवल future3 को poll कर रहा होता है, इसलिए future1 चल नहीं पाता
  • Mutex का काम सिर्फ अगले प्रतीक्षारत task को जगाना है; कौन-सा Future वास्तव में poll होगा, यह उसे पता नहीं होता

Futurelock के सामान्य कारण

  • जब task T, Future F1 का इंतज़ार कर रहा हो, F1, F2 पर निर्भर हो, और F2 को फिर से T के poll की ज़रूरत हो — ऐसी circular dependency संरचना
  • यह आम तौर पर निम्न स्थितियों में होता है
    • tokio::select! में &mut future इस्तेमाल करने के बाद किसी दूसरी branch में await करना
    • FuturesOrdered या FuturesUnordered में कुछ Future पूरे होने के बाद कोई दूसरा asynchronous काम करना
    • manually implement किए गए Future में इसी तरह का व्यवहार

Streams और अन्य संरचनाओं में होने वाली घटनाएँ

  • FuturesOrdered या FuturesUnordered से Future निकालने के बाद, उससे जुड़े resource का उपयोग करने वाले किसी दूसरे Future का इंतज़ार करने पर Futurelock हो सकता है
  • join_all सभी Future को लगातार poll करता रहता है, इसलिए उसमें Futurelock नहीं होता

वास्तविक मामले और debugging

  • Omicron#9259 मामले में सभी database access Future Futurelock में फँस गए और HTTP request अनंत समय तक प्रतीक्षा करते रहे
  • mpsc channel send ब्लॉक हो रहा था, लेकिन receive side खाली दिख रही थी, जिससे कारण समझना कठिन था
  • debugging के दौरान tokio-console जैसे tools मददगार हो सकते हैं, लेकिन ज़्यादातर मामलों में मूल कारण का पता लगाना बहुत कठिन होता है

Futurelock से बचाव के दिशा-निर्देश

  • जब एक task कई Future को poll कर रहा हो, तो पहले से शुरू किए गए Future का poll बंद न हो जाए, इस बात का ध्यान रखें
  • जहाँ संभव हो, Future को नए task में spawn करके स्वतंत्र रूप से चलाएँ
    • JoinHandle को tokio::select! में देने पर Futurelock का जोखिम खत्म हो जाता है
  • tokio::select! इस्तेमाल करते समय सावधानियाँ
    • &mut future और await का साथ में उपयोग न करें
    • अगर दोनों स्थितियाँ मौजूद हों, तो Futurelock का जोखिम बहुत बढ़ जाता है
  • Stream इस्तेमाल करते समय JoinSet का उपयोग करके हर Future को अलग task में चलाएँ
  • bounded channel की capacity बढ़ाना मूल समाधान नहीं है
    • इसके बजाय try_send() का उपयोग करके blocking से बचा जा सकता है

गलत बचाव पैटर्न

  • channel capacity को अनंत तक बढ़ाना अव्यावहारिक है और इसके दुष्प्रभाव हो सकते हैं, जैसे latency और memory usage में वृद्धि
  • Future के बीच dependency हटाने की कोशिश भी नाज़ुक है, क्योंकि maintenance के दौरान नई dependency फिर बन सकती है
  • एकमात्र सुरक्षित तरीका tokio::spawn के ज़रिए task separation है

आगे के सुधार और सुरक्षा संबंधी विचार

  • Clippy lint के माध्यम से tokio::select! के भीतर &mut future के उपयोग या await शामिल होने पर warning देने की संभावना बताई गई है
  • Futurelock का दुरुपयोग DoS (Denial of Service) के रूप में किया जा सकता है, लेकिन मूल रूप से यह असामान्य व्यवहार है, इसलिए इसकी रोकथाम ज़रूरी है

1 टिप्पणियां

 
GN⁺ 2025-11-01
Hacker News राय
  • दस्तावेज़ को सरसरी नज़र से देखा तो यह काफ़ी पारदर्शी और बेहद thorough रिपोर्ट जैसी लगी
    ख़ासकर footnotes वाला हिस्सा दिलचस्प था
    यह बात प्रभावशाली लगी कि बहुत से लोग Rust की cancellation safety समस्या से अनजान थे, और ऐसी समस्या Omicron में काफ़ी व्यापक रूप से फैली हो सकती है
    Rust को चुनने की वजह C की memory safety समस्याओं से बचना थी, लेकिन इस बार runtime में पकड़ना मुश्किल cancellation bugs सामने आए — यह बात विडंबनापूर्ण लगी
    यह बात ख़ास तौर पर निराशाजनक लगी कि compiler जिन dynamic गुणों में मदद नहीं कर सकता, उन्हें programmer को ख़ुद सुनिश्चित करना पड़ता है

    • लगा कि ऐसी समस्याओं से बचने के लिए शायद ऊपरी abstraction layer की ज़रूरत है
      Rust के concurrency model में भी अब भी deadlock की संभावना मौजूद लगती है
      RAII स्टाइल resource management से लगता है कि ऐसी समस्याएँ रुक जानी चाहिए, लेकिन वास्तव में ऐसा नहीं होता — यह उलझाने वाला है
      जिज्ञासा है कि यह सिर्फ implementation की संयोगवश बनी स्थिति है, या Rust/Tokio model की संरचनात्मक सीमा
  • यह withoutboats की FuturesUnordered पोस्ट में समझाए गए deadlock का एक सूक्ष्म रूपांतर लगता है
    “intra-task” concurrency का उपयोग करते समय ध्यान रखना पड़ता है कि कोई भी future starvation में न फँसे
    मूल रूप से task को spawn करना सुरक्षित है, और timeout को tokio::select! से संभालना चाहिए, लेकिन सभी pending futures को उसी के भीतर manage करना चाहिए
    FuturesUnordered की सिफ़ारिश मैं तभी करूँगा जब आपने सचमुच उसके सभी edge cases टेस्ट किए हों

  • यह priority inversion समस्या जैसा सुनाई देता है
    OS में अगर low-priority thread ने lock पकड़ा हो और high-priority thread इंतज़ार कर रहा हो, तो low-priority वाले को priority inheritance देकर चलाया जाता है
    सोच रहा हूँ कि Tokio में भी ऐसा कोई विचार लागू हो सकता है या नहीं — जैसे अगर कोई non-runnable future Mutex पकड़े हुए हो, तो उसकी जगह उस future को poll कर दिया जाए
    हालाँकि “non-runnable” स्थिति को detect करने का overhead काफ़ी ज़्यादा हो सकता है

    • ऐसा approach Tokio के task स्तर पर संभव हो सकता है
      लेकिन task के भीतर के future पर इसे लागू नहीं किया जा सकता
      async Rust का मूल डिज़ाइन ही यह है कि “futures are inert” — future बस एक साधारण struct होता है, runtime उसके भीतर क्या है, यह नहीं जानता
      runtime केवल task स्तर को जानता है, इसलिए वह internal future की state को बिल्कुल track नहीं करता

    • Rust का async stackless coroutine मॉडल है, इसलिए पहले से चल रही async function का execution मनमाने ढंग से आगे बढ़ाना सुरक्षित नहीं है
      stackless मॉडल local state को shared stack पर नहीं बल्कि अपने ढाँचे में रखता है, और सुरक्षित execution मूलतः LIFO क्रम जैसी सीमाओं से बँधा रहता है
      इसी वजह से coloring की ज़रूरत पड़ती है, और stackful coroutine की तरह मनमाने ढंग से yield नहीं किया जा सकता

    • कोड बहुत ज़्यादा जटिल लगता है
      Erlang, Elixir, Go, यहाँ तक कि C में लिखने की तुलना में भी यह कहीं अधिक verbose दिखता है

    • मुझे यह मूल 2-lock deadlock जैसा लगता है
      Tokio की Mutex wait queue और task scheduling आपस में उलझकर deadlock बनाती हैं
      अगर यह OS Mutex होता तो दूसरे waiting thread को जगा कर मामला सुलझ सकता था, लेकिन async Rust में future state machine संरचना की वजह से यह मुश्किल लगता है
      wait queue के futures को क्रम से poll करके इसे खोला जा सकता है, लेकिन उससे भी अनपेक्षित side effects आ सकते हैं

  • async Rust ecosystem में ऐसी समस्याओं से साथ मिलकर जूझने का अनुभव रहा है
    अगर select! में references का उपयोग ही न करने दिया जाए, तो ऐसी समस्याओं से बचा जा सकता है, लेकिन फिर queue में अपनी जगह खोए बिना बार-बार select! चलाने वाला pattern असंभव हो जाएगा
    cancellation से जुड़ी समस्याओं के साथ मिलकर यह Rust experts के लिए भी अनपेक्षित जाल बन सकता है
    फिर भी callback-आधारित code की तुलना में चौंकाने वाली बातें बहुत कम हैं

    • सही है, हमारी टीम ने भी इस deadlock का विश्लेषण करने के बाद चर्चा की थी कि “इसे रोका कैसे जा सकता था?”, लेकिन आख़िर में निष्कर्ष यही निकला कि यह किसी की गलती नहीं थी
      Tokio के सभी primitives अपने इरादे के मुताबिक काम कर रहे थे, और code भी सही लिखा गया था, लेकिन उनकी पारस्परिक क्रिया ने एक अप्रत्याशित deadlock बना दिया
      &mut future को select! में प्रतिबंधित कर देने से इसे रोका जा सकता है, लेकिन तब बहुत-सा वैध code भी रुक जाएगा
      आख़िर में कड़वा निष्कर्ष यही निकला कि यह “बस सावधान रहने वाली चीज़” है
      इससे जुड़ी चर्चा इस टिप्पणी में भी जारी है

    • अगर select! चुने न गए futures को drop किए बिना वापस कर दे, तो state खोने से बचा जा सकता है
      हालाँकि यह असुविधाजनक है, और मूल समाधान भी नहीं
      असली कारण, जैसा इस थ्रेड में समझाया गया है, cancellation handling की अपूर्णता में है

  • FAQ में आया “क्या future1 cancel नहीं होता?” वाला सवाल दिलचस्प लगा
    cancellation के दो चरण होते हैं — poll रुकना और drop
    इस उदाहरण में drop देर से होता है, इसलिए guard पकड़ा रहता है और side effects पैदा होते हैं
    सोचने वाली बात है कि क्या इन दोनों क्रियाओं के हमेशा साथ होने की गारंटी दी जा सकती है

  • मैं Rust के डिज़ाइनरों से पूछना चाहता हूँ — actor model की जगह async pattern क्यों चुना गया
    Erlang इस्तेमाल करने पर actor model कहीं ज़्यादा साफ़ और सुरक्षित लगता है
    JS ने अपनी भाषा संरचना की वजह से async अपनाया, लेकिन Rust तो नई भाषा थी — फिर वही रास्ता क्यों चुना गया, यह जानने की जिज्ञासा है

    • Rust के async डिज़ाइन में embedded environments का समर्थन एक बड़ी वजह था
      malloc या threads के बिना भी चलना ज़रूरी था, इसलिए actor model संभव नहीं था
      Tokio के साथ actor-style code लिखा जा सकता है, लेकिन यह बहुत स्वाभाविक नहीं लगता

    • दूसरी बड़ी वजह performance थी
      actor model में message copy की लागत अधिक होती है, और Rust एक performance-केंद्रित systems language है, इसलिए async state machine के ज़रिए zero-cost abstraction लक्ष्य था
      Erlang और Go ने अलग trade-offs चुने हैं

    • Rust ने C FFI calls के समय अतिरिक्त overhead स्वीकार न करने का लक्ष्य रखा था, इसलिए green thread आधारित मॉडल को बाहर रखा गया
      async/await state machine में compile होता है, इसलिए overhead कम रहता है
      Go में शुरुआती दौर में preemption नहीं था, इसलिए वहाँ भी इसी तरह की starvation समस्या थी, जिसे बाद में scheduler ने सुधारा
      आख़िरकार हर भाषा के लक्ष्य और सीमाएँ अलग थीं

    • मुझे भी यह देखकर आश्चर्य हुआ कि Oxide ने async को इतनी गहराई से अपनाया
      embedded या HTTP server पक्ष में तो यह परिचित है, लेकिन Oxide जैसी systems company में भी इसका इतना गहरा उपयोग होगा, यह उम्मीद नहीं थी

  • दस्तावेज़ पढ़ते समय जो बात समझ नहीं आई, वह यह थी कि lock पकड़े हुए future की बजाय main thread क्यों जागता है
    अगर lock fair है, तो future1 का जागना सही होना चाहिए था; runtime ने किसी और thread को क्यों चुना, यह प्रश्न रहा

  • लेख सचमुच बहुत दिलचस्प था
    example code भी साफ़ था, और ऐसे bug ढूँढना भले ही डरावना सपना हो, लेकिन उन्हें ढूँढ लेने के बाद puzzle के टुकड़े जुड़ जाने जैसा संतोष मिलता है

    • हमारी कंपनी हर मीटिंग और debugging session रिकॉर्ड करती है, और वही “puzzle जुड़ने वाला पल” वीडियो में क़ैद है
      Eliza, Sean, John और Dave चारों को साथ brainstorming करते हुए कारण तक पहुँचते देखना प्रभावशाली था
      सोमवार को हम इस विषय पर एक podcast episode जारी करने वाले हैं
      संबंधित वीडियो RFD 537 और इस event link पर देखे जा सकते हैं
  • यह समझना मुश्किल है कि Rust सभी active tasks को एक साथ आगे बढ़ने नहीं देता — यह समझने में कठिन और bugs पैदा करने वाला डिज़ाइन लगता है
    Python के Trio की तरह structured concurrency लाई जाए तो शायद यह ज़्यादा सहज लगे
    जिज्ञासा है कि क्या Rust भी ऐसा मॉडल अपना सकता है

    • Rust में structured concurrency संभव है, लेकिन केवल task स्तर पर
      future तो बस ऐसी struct है जो poll होने पर ही आगे बढ़ती है, इसलिए “active future” जैसी कोई धारणा नहीं है
      सब कुछ task के रूप में spawn कर देने से समस्या हल होती दिखती है, लेकिन उससे कुछ उपयोगी patterns भी रुक जाते हैं

    • task और future का अंतर अहम है
      future अगर poll न हो तो कुछ नहीं करता
      अगर cancellation को “drop होने से पहले तक poll न होने की स्थिति” माना जाए, तो इस मामले की तरह lock पकड़े रुका हुआ future बन जाता है
      Rust की RAII philosophy में cleanup के लिए drop पर भरोसा किया जाता है, लेकिन poll रुक जाने की स्थिति में वह भी नहीं होता

  • आजकल लगता है कि Rust का async शायद बहुत जल्दबाज़ी में जारी कर दिया गया था

    • मुझे भी लगता है कि सुधार की काफ़ी गुंजाइश है, लेकिन मूल डिज़ाइन अपने आप में शानदार आधार है
      Pin या syntax के कुछ हिस्से सुधारे जा सकते हैं, लेकिन बुनियादी संरचना बदलने की ज़रूरत नहीं लगती
      अभी यह बस “पूरा घर न बन पाने वाली नींव” वाले चरण में है, जल्दबाज़ी का नतीजा नहीं
      हाँ, generalized coroutine जैसे निचले स्तर के और building blocks की ज़रूरत ज़रूर लगती है