- Firezone की Rust कनेक्शन लाइब्रेरी
connlibनेटवर्क कनेक्शन और WireGuard टनल को मैनेज करती है, और sans-IO डिज़ाइन के जरिए तेज़ testing और उच्च operational reliability हासिल करती है - प्रोटोकॉल socket को सीधे handle नहीं करता, बल्कि उसे pure state machine के रूप में implement किया जाता है, और event loop
handle_input,poll_transmit,handle_timeout,poll_timeoutजैसे API को call करता है - IO के चुनाव को state machine के बाहर धकेलने से Rust async की function colouring वाली जटिलता कम होती है, और blocking IO, non-blocking IO, या किसी खास async runtime का चुनाव application पर छोड़ा जा सकता है
- socket और time को abstract करने पर असली port या waiting time के बिना सिर्फ
InstantऔरTransmitसे समय बीतना, packet loss, और असामान्य response को verify किया जा सकता है - लेकिन event loop को खुद manage करना पड़ता है, इसलिए सूक्ष्म bugs आ सकते हैं; sequential workflow में state machine code बढ़ जाता है; और Rust ecosystem में sans-IO libraries अभी सीमित हैं
Firezone connlib ने चुना हुआ sans-IO आर्किटेक्चर
- Firezone Android, macOS, और Linux पर scalable secure remote access बनाने के लिए Rust का उपयोग करता है
- हर app के केंद्र में
connlibहै, और यह लाइब्रेरी नेटवर्क कनेक्शन और WireGuard tunnel को manage करके traffic को सुरक्षित रखती है - Firezone का Rust stack
tokio,tungstenite,boringtun,rustlsआदि का उपयोग करता है, लेकिन उसकी internal structure सामान्य async Rust code से अलग हैtokio::spawncall लगभग नहीं के बराबर हैं- सारा communication एक ही UDP socket पर multiplex होता है
- कई layers में
handle_timeout,poll_transmit,handle_inputजैसे API बार-बार दिखते हैं
- ये विशेषताएँ sans-IO डिज़ाइन का संकेत हैं, जहाँ protocol logic खुद IO नहीं करता बल्कि state और input/output intent को व्यक्त करता है
- Python ecosystem में sans-IO के लिए dedicated documentation site है, और Rust में ये libraries इस pattern का उपयोग करती हैं
async Rust और function colouring का बोझ
- Rust की async functions को सिर्फ दूसरी async functions के अंदर ही call किया जा सकता है, इसलिए call chain पूरी की पूरी async में बदलने की function colouring वाली पाबंदी पैदा होती है
- यह पाबंदी compile time पर enforce करती है कि execution को रोककर बाद में resume किया जा सकता है, और यह function के API contract का हिस्सा है
- call stack के गहरे हिस्से में मौजूद एक async function की वजह से call path की बाकी functions भी
.awaitके लिए async बन सकती हैं - वास्तविक async काम आम तौर पर call stack के सबसे नीचे होता है
- socket पर write करना
- file पढ़ना
- समय बीतने का इंतज़ार करना
- कई async functions खुद सीधे asynchronous काम नहीं करतीं, लेकिन दूसरी async functions पर निर्भर होने की वजह से async बनती हैं
- Firezone का
connlibNAT traversal के लिए ICE का उपयोग करता है, और STUN के जरिए server-reflexive candidate, यानी public address, ढूँढता है - STUN binding एक सरल protocol है जिसमें server को UDP packet भेजा जाता है और server द्वारा देखे गए IP और port वाला UDP response वापस मिलता है
- यही STUN example
tokioके asyncUdpSocketसे भी लगभग वैसा ही लिखा जा सकता है, और standard library के blocking IO से भी - अगर STUN functionality को library के रूप में देना हो, तो async version और blocking version में से एक चुनना पड़ता है, या दोनों शामिल करने से duplication बढ़ता है
- example code firezone/sans-io-blog-example में है
sans-IO का मूल: policy और IO implementation को अलग करना
- sans-IO का मूल object-oriented design के dependency inversion principle से मिलता-जुलता है
- “क्या करना है” तय करने वाला policy code, “कैसे करना है” वाली implementation details पर निर्भर नहीं होना चाहिए
- अगर network message भेजने का निर्णय लेने वाला code सीधे socket send code पर निर्भर हो, तो ऊपर की layers भी async या blocking IO के चुनाव से बँध जाती हैं
- STUN example में policy code वही रहता है, लेकिन
tokio::UdpSocketपर बनाने से वह async बनता है औरstd::net::UdpSocketपर बनाने से blocking IO बनता है - sans-IO,
UdpSocket::sendको सीधे call करने के बजाय, transmit intent दिखाने वाला एक abstraction बनाता है - example का
Transmitये जानकारी रखता है- destination
SocketAddr - भेजा जाने वाला
payload
- destination
- protocol code socket में सीधे write नहीं करता, बल्कि
Transmitemit करता है - असली
UdpSocket::sendयाsend_tocall की ज़िम्मेदारी event loop की होती है - sans-IO code को वैसे ही event loop द्वारा drive किया जाना चाहिए, जैसे Rust का
Futureruntime द्वारा poll किए जाने पर आगे बढ़ता है
STUN binding को state machine में बदलना
- STUN binding request को
SentऔरReceivedstate वाली state machine के रूप में model किया जा सकता है - example की state को नीचे दिए enum से व्यक्त किया जाता है
SentReceived { address: SocketAddr }
StunBindingमें current state और भेजे जाने की प्रतीक्षा कर रहीTransmitqueue होती है- मुख्य API की भूमिकाएँ स्पष्ट हैं
handle_input:UdpSocket::recvसे आए packet को state machine तक पहुँचाता हैpoll_transmit: state machine जोTransmitबाहर भेजना चाहती है, उसे event loop प्राप्त करता हैpublic_address: प्राप्त public address को query करता है
- इस structure में protocol logic, IO के बिना program के behavior को ही model करता है
- event loop,
poll_transmitसे यदि भेजने लायक packet हो तो उसे socket पर भेजता है, नहीं तो socket से पढ़ा गया datahandle_inputको देता है - event loop को STUN के request-response protocol होने जैसी internal detail जानने की ज़रूरत नहीं होती
- UDP एक unreliable protocol है, इसलिए packets खो सकते हैं, और STUN इसे कम करने के लिए retransmission timer मांगता है
समय को भी abstract करना
- network protocols में current time की ज़रूरत अधिकतर तब पड़ती है जब किसी reference point के बाद कितना समय बीत चुका है यह देखना हो
- request भेजने के बाद 5 सेकंड बीत गए या नहीं
- आख़िरी keep-alive के बाद 30 सेकंड बीत गए या नहीं
- ऐसे मामलों में वास्तविक wall clock time की ज़रूरत नहीं होती, सिर्फ पिछले समय बिंदु के साथ
Durationचाहिए होती है - Rust का
Instantवर्तमान समय को expose नहीं करता, बल्कि दोInstantके बीचDurationनापने देता है - time-based behavior के लिए sans-IO state machine में दो API हो सकते हैं
poll_timeout: event loop को बताता है कि अगला wake-up timer कब schedule करना हैhandle_timeout: state machine को बताता है कि timer expire हो गया है
- example state machine को इस तरह बढ़ाता है कि आख़िरी response मिलने के 5 सेकंड बाद नया binding request भेजा जाए
handle_inputpacket के साथ currentInstantभी लेता है और उसेState::Received { address, at }रूप में store करता है- event loop socket receive और timer expiry दोनों को साथ handle करता है, और फिर
poll_timeoutके परिणाम के आधार पर timer दोबारा set करता है
composition और API flexibility
StunBindingके मुख्य APIhandle_timeout,handle_input,poll_transmit,poll_timeoutसिर्फ STUN तक सीमित नहीं हैं- अधिकतर network protocols को इसी रूप या उसके किसी variant में implement किया जा सकता है, इसलिए state machine composition आसान हो जाती है
- public IP खोजने के लिए अगर 5 STUN servers से query करनी हो, तो 5
StunBindingबनाए जा सकते हैं और उन्हें क्रम से call किया जा सकता है- इस स्थिति में STUN message multiplexing को ठीक से implement करना होगा, और
TransactionIdया server address का उपयोग किया जा सकता है
- इस स्थिति में STUN message multiplexing को ठीक से implement करना होगा, और
- Firezone का
snownetICE और WireGuard को जोड़कर application को ऐसा IP tunnel देता है जो विभिन्न network environments में काम करता है snownet, sans-IO WebRTC librarystr0mऔर लगभग sans-IO WireGuard implementationboringtunके ऊपर बनाया गया है- Firezone को पूरा WebRTC stack नहीं, बल्कि RFC 8445 implement करने वाला
IceAgentही चाहिए - क्योंकि
str0msans-IO शैली में है, इसलिए सिर्फIceAgentलेकर उसे मौजूदा state machine में compose करना आसान है snownetका connectionIceAgentऔर WireGuard tunnel को रखता है, और incoming messages को इनमें से किसी एक तक पहुँचाता है
event loop को खुद लिखने के फायदे
- sans-IO code सिर्फ system state को represent करता है, side effects नहीं पैदा करता, इसलिए event loop को state query करनी, actions execute करने, और नए inputs देने पड़ते हैं
- यह structure boilerplate जैसा लग सकता है, लेकिन event loop को खुद लिख पाने की वजह से fine-grained control मिलता है
- application खुद ऐसे execution modes चुन सकती है
sendmmsgसे packet transmission के समय system calls की संख्या घटाना- कई protocols को एक socket पर multiplex करना
- library author async runtime की बहस या socket options API देने की जगह protocol features implement करने पर ध्यान दे सकता है
str0mnetwork interface enumeration को IO concern मानता है और उसे application पर छोड़ देता है- बदले में वह सिर्फ इतना API देता है कि वर्तमान state में socket address को ICE candidate के रूप में जोड़ा जा सके
- Firezone ने इसी structure का उपयोग करके connection बनने से पहले TURN candidate को पहले से collect करने और connection setup latency घटाने वाला optimization लागू किया
- ICE में दोनों पक्ष candidate, यानी sockets, collect करने के बाद उनके बीच connectivity test करते हैं
तेज़ testing और edge cases की जाँच
- sans-IO code स्वभाव से side-effect free होता है, इसलिए unit testing के लिए बहुत उपयुक्त है
- क्योंकि socket और time abstract हैं, testing में असली port खोलने या समय के गुजरने का इंतज़ार करने की ज़रूरत नहीं पड़ती
- 5 मिनट बाद के behavior को test करना हो तो बदला हुआ
Instantfunction में देकर state change verify किया जा सकता है - Firezone एक वास्तविक example देता है जिसमें test किया जाता है कि
snownet5 मिनट बाद idle connection बंद करता है या नहीं - data transfer भी असली socket से गुज़रे बिना किया जा सकता है; एक तरफ का
Transmitनिकालकर दूसरी तरफ की state machine केhandle_inputको दे देना काफ़ी है - Firezone ने एक reference state machine implement की है जो बताती है कि
connlibको कैसे behave करना चाहिए - यह reference state machine testing के baseline के रूप में उपयोग होती है
proptestकी state machine testing का उपयोग करके हर CI run में हज़ारों scenarios को deterministically sample और execute किया जाता है, और reference state machine की तुलनाconnlibकी वास्तविक state से की जाती है- IO न होने पर ऐसे failures और abnormal behaviors को भी आसानी से test किया जा सकता है
- packet खो जाने से response न मिलना
- invalid response मिलना
- server तक RTT बहुत लंबा होना
- काम करने वाला IPv6 interface न होना
- सिर्फ IPv6 interface होना
- protocol implementation और वास्तविक IO side effects को अलग करने पर error detection और handling, state machine के input processing का हिस्सा बन जाते हैं
Rust और sans-IO का अच्छा मेल क्यों बैठता है
- Rust यह स्पष्ट करने के लिए मजबूर करता है कि कौन-सा component या function किस value का owner है
UdpSocketसे पढ़ते समय वास्तविक bytes लेने के लिए&mut [u8]जैसा buffer देना पड़ता है- किसी value का owner ही उसे mutable बना सकता है या किसी दूसरी function को temporary mutable reference दे सकता है
- ownership और mutability का यह explicit model, borrow checker जैसी Rust सुविधाओं की नींव है
- sans-IO डिज़ाइन के state machine API सभी synchronous functions होते हैं, और IO या समय का इंतज़ार करते हुए block नहीं करते
- क्योंकि state machine सिर्फ data structure होती है,
&mut selfसे state change व्यक्त करना आसान होता है, और borrow checker की मदद से code की soundness सुनिश्चित की जा सकती है - इसके उलट async Rust में
&mutको संभालना अधिक कठिन महसूस हो सकता है - Rust की async functions,
Futureimplement करने वाली data structures में compile होती हैं tokioजैसे runtime पर किसीFutureको spawn करने के लिए वह data structure'staticहोनी चाहिए, इसलिए उसमें&mutजैसी references नहीं हो सकतींFutureके बाहर की state बदलने के लिए आम तौर पर इनमें से एक तरीका लिया जाता हैArc<Mutex<T>>जैसे reference-counted pointer और mutex- कई tasks को spawn करके channel से जोड़ने वाला actor pattern
- दोनों तरीकों में runtime overhead होता है
- lock contention पैदा हो सकती है
- channel message passing में copy की ज़रूरत पड़ती है
- जब कई tasks runtime में non-deterministic क्रम में execute होते हैं, तो race condition और deadlock हो सकते हैं
- sans-IO protocol code tasks spawn नहीं करता, इसलिए state change के लिए सिर्फ
&mut selfकाफ़ी होता है - task या thread न होने पर
Mutexजैसे synchronization primitives की ज़रूरत नहीं पड़ती, और channel न होने पर data copy की ज़रूरत भी घटती है - Firezone का मानना है कि sans-IO पर जाने के बाद channel के दूसरी तरफ क्या है, closed channel, या कौन-सा code
Mutexlock कर रहा है, जैसी चीज़ों को ट्रैक करना कम हो गया, जिससे code को समझना आसान हुआ
कमियाँ और उपयोग की सीमा
- sans-IO कोई सर्व-समाधान नहीं है
- event loop को खुद लिखने से मज़बूत control मिलता है, लेकिन शुरुआत में ढूँढने में कठिन सूक्ष्म bugs भी पैदा हो सकते हैं
- उदाहरण के लिए अगर state machine का
poll_timeoutreturn value आगे प्रगति नहीं करती, तो event loop busy loop में फँस सकता है - sequential workflow में अधिक code की ज़रूरत पड़ती है
- Rust की async functions, हर
.awaitpoint को दूसरे state में transition वाली state machine में compile करती हैं, इसलिए developer non-blocking IO और sequential code को आसानी से साथ लिख सकता है - sans-IO में इन steps को खुद state machine के रूप में model करना पड़ता है
StunBindingजैसे request-response protocol में यह बहुत कठिन नहीं है, लेकिन बड़े sequential workflow को व्यक्त करना उबाऊ हो सकता है- Rust community में sans-IO डिज़ाइन अभी व्यापक रूप से नहीं फैली है
- ज़्यादातर libraries sans-IO के बजाय blocking IO या non-blocking IO implement करती हैं
boringtunअंदर हीInstant::nowcall करता है, इसलिए उसमें कुछ हिस्से पूरी तरह pure नहीं हैं, और इससे जुड़ा issue cloudflare/boringtun#391 में है
समापन
- sans-IO code शुरुआत में अपरिचित लग सकता है, लेकिन एक बार इसकी आदत पड़ने पर यह Rust के state machine modeling tools के साथ बहुत अच्छा मेल खाता है
- इसकी संरचना errors को भी दूसरे inputs की तरह handle करने के लिए मजबूर करती है, इसलिए यह networking code लिखने के तरीके के साथ अच्छी तरह फिट बैठती है
- async Rust लिखने के दूसरे तरीके भी हैं, और structured concurrency, sans-IO और यहाँ चर्चा किए गए async Rust approach के बीच की जगह पर स्थित है
- structured concurrency के बारे में withoutboats का Let futures be futures देखा जा सकता है
1 टिप्पणियां
Hacker News की राय
इसे innovation और आगे की प्रगति की तरह पेश किया जा रहा है, लेकिन असल में async/await language support आने से पहले Rust सहित
$langमें async को इसी तरह संभाला जाता थाRust embedded firmware development में सबसे बड़ा productivity gain तब मिला जब I/O operations के बीच-बीच में state machine को हाथ से implement करना और local variables को custom state में ले जाना बंद करके, Rust से async/await syntax के जरिए यह काम करवाया जा सका
Rust में async आखिरकार एक automatic state machine में बदलता है, जो I/O (
await) points के बीच values को store करता हैलेकिन हमेशा ऐसा नहीं होता। QUIC, WebRTC, IP जैसे packet-oriented use cases में वास्तविक I/O खुद आसान होता है। बस अलग-अलग packets/datagrams भेजने और receive करने होते हैं
.awaitpoints ज्यादा नहीं होते, इसलिए compiler के generate करने के लिए भी बहुत कुछ नहीं होता। साथ ही कई पहलू एक साथ चलने होते हैं, इसलिए हर एक को अपने future/task में जाना पड़ता है, और ऐसे futures में state management आसानी से spaghetti code बन सकता हैruntime environment के बारे में assumptions कम हो जाते हैं, जिससे testing और composition आसान हो जाती है
theory में async/await जिस तरह state machine बनाता है, उससे भी वही काम किया जा सकता है, लेकिन practical तौर पर यह काफी दर्दनाक है और ज्यादातर async/await code pure नहीं होता
Eff, Koka, Frank जैसी experimental languages इस programming style के लिए बेहतरीन support देती हैं। Haskell में I/O पर चर्चा की बुनियाद में भी free monad और उसके variants जैसी techniques में गहरा investment है
हाल में Unison एक दिलचस्प language है, जो कई नए concepts explore करते हुए भी core में extensible effect system रखती है, ताकि ऐसी coding को language level पर अच्छी तरह support किया जा सके
.awaitलिखना पड़ता है। अच्छा होता अगर default रूप से.awaitexecute होता, और सिर्फ किसी खास syntax से ही ऐसा न होता—यानी इसे उल्टा किया जा सकताफिर भी असल में protocol library को सीधे I/O करते हुए बहुत आम तौर पर देखता हूं :-(
मैं इस problem area को लगातार दिमाग में घुमा रहा था, और यह approach मेरी सोची हुई दिशा से बहुत अच्छी तरह match करती है। हालांकि लेख के footnote 3 की तरह, अभी कुछ चीजें सुधारने लायक हैं
यह सोच मुझे function colors वाली चर्चा और एक संयोग से मिली insight से आई। VT100 library बनाते समय unit testing बहुत मुश्किल थी, क्योंकि असल में मैं
parser::new(stdin())कर रहा था। तीसरे या चौथे rewrite में बिना ज्यादा सोचे parser कोparser::push(data)में बदल दिया, और तब समझ आया कि Rust मुझे उस enterprise OOP-style anti-pattern के लिए सजा दे रहा था जिसे मैं “encapsulation obsession” कहने लगा हूंअब मुझे यह pattern और उसका नुकसान सिर्फ I/O में नहीं, हर जगह दिखता है
विडंबना यह है कि यह solution pre-university education और university के शुरुआती दौर में भी सिखाई जाने वाली बात है। computer की सबसे सरल व्याख्या है: input लेना, data को process/transform करना, और output देना। function colors वाली चर्चा से इसका संबंध इसलिए है कि colors की चिंता सिर्फ input और output के लिए करनी पड़ती है, जबकि core logic आमतौर पर data transformation होता है
यह बहुत obvious बात है, लेकिन function colors “debate” के पैमाने को देखकर लगता है कि बहुत से लोगों को problems पहले encapsulation से solve करने की ट्रेनिंग दी गई है, इसलिए वे इस बात को miss कर देते हैं या भूल जाते हैं। functional programming वाले लोग इस point पर शायद काफी खुश होंगे
Rust मेरे लिए सीखने की यात्रा से ज्यादा unlearn करके फिर से सीखने की यात्रा रहा है। यह अच्छा pattern है और मैं आगे इसे अपनाने का सोच रहा हूं
Edit: संबंधित code: https://codeberg.org/jcdickinson/termkit/src/branch/main/src...
parser::push(data)में बदलने वाली बात और Rust द्वारा enterprise OOP-style anti-pattern को सजा देने वाली बात को थोड़ा और समझा सकते हैं?एक बिल्कुल शुरुआती Rust learner के तौर पर मुझे यह साफ नहीं दिख रहा कि उस pattern की स्पष्ट समस्या क्या है, और Rust कैसे सजा देता है
type parameters और traits हर जगह थे, और structs को functionality देने वाली classes जैसी structures के रूप में misuse किया
Rust तब ज्यादा फिट बैठता है जब आप जहां संभव हो type parameters और अपने खुद के trait definitions से बचते हैं
invariants maintain करने के अर्थ में encapsulation अच्छी चीज है। यह लेख “parse, don’t validate” याद आता है: https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-va...
यह डिज़ाइन डेटा को किसी dedicated handler तक भेजने के लिए channel इस्तेमाल करने वाले तरीके की तुलना में कैसा है? channel इस्तेमाल करते समय कई समस्याएँ थीं
(1) अक्सर ऐसा जाले जैसा code बन जाता था जिसे follow करना मुश्किल होता था
(2) ऐसे message types खुद implement करने पड़ते थे जिन्हें network पर भेजे जा सकने वाले messages में बदला जा सके
(3) जिन entities में रुचि हो या जिन्हें अनुमति हो, उन्हें sender स्पष्ट रूप से pass करना पड़ता था
(4) channel message भेजना fail हुआ या नहीं, यह पता चल सकता है, लेकिन वह message network transmission में fail हुआ या नहीं, यह पता नहीं चल सकता
फिर भी यह काफ़ी सुविधाजनक है। उदाहरण के लिए अगर
ws_handlerchannel हो, तो बस वहाँ data भेजना होता है, और अगर कहीं कोई dedicated handler संभव हो तो वह message भी भेजा जा सकता हैsans-IO applications में भी इस्तेमाल हो सकता है, लेकिन मुझे लगता है कि यह खास तौर पर libraries के लिए उपयोगी है। libraries में यह consumer पर I/O का तरीका थोपता नहीं, इसलिए बहुत ज़्यादा उपयोगी हो जाता है
Rust में synchronous I/O और asynchronous I/O के बीच पहले से ही ecosystem split है, और ऊपर से अलग-अलग async runtimes भी हैं, इसलिए यह बात अहम है
लेकिन जैसा कहा गया, समस्याएँ आती हैं। उदाहरण के लिए actors/channels disconnect हो सकते हैं। और backpressure चाहिए तो channel का bounded होना ज़रूरी है। साथ ही copying की ज़रूरत होती है, इसलिए high throughput हासिल करना मुश्किल हो सकता है
साथ में देखने लायक: monads, खास तौर पर Free(r) monads और effect systems[0]
logic और execution को अलग करना वाला idea Haskell ecosystem में पहले से ही खूब चर्चा किया गया बड़ा विषय है
edit: time-related handling की ज़रूरत पड़ने पर आने वाले
tokio::select!calls को कैसे encapsulate किया गया, इसका ज़िक्र नहीं था। क्या loop code को async बनाने के लिए, बिना बाहरी code को async होने की ज़रूरत के,tokio::Runtimeसाथ लेकर चला जाता है?edit2: शायद मकसद यह दिखाना नहीं था कि encapsulated library ऐसा करती है, बल्कि यह दिखाना था कि बाहरी application async context में bindings इस्तेमाल कर सकती है
sans-IO style में, कोई action या timer wait करने वाली encapsulated function कैसे implement की जा सकती है, इसे लेकर मेरी ज़्यादा जिज्ञासा थी। या फिर expected answer busy waiting हो सकता है, या ऐसा अपना async runtime instance साथ लेकर चलना जो असल में
block_in_placeजैसी किसी चीज़ से busy waiting की जगह लेता हो[0]: https://okmij.org/ftp/Computation/free-monad.html
StunBindingstruct है। यह STUN binding की functionality को दर्शाता है। यह सिर्फ call की जा सकने वाली single function नहीं है, बल्कि event loop चाहिएमुख्य बात यह है कि
StunBindinglibrary के अंदर हो सकता है, और application side पर इसे program की state machine में combine करके इस्तेमाल किया जा सकता है। बेशक यह मानकर कि application भी sans-IO style में structured हैlinked
snownetlibrary ठीक यही काम करती है। domain है बिना I/O के ICE + WireGuard को जोड़ना, और इसे उसके ऊपर ACL combine करने वालीconnliblibrary में इस्तेमाल किया जाता हैedit: busy waiting नहीं है। इसके बजाय
StunBindingमेंpoll_timeoutके ज़रिए यह expose करने वाली function है कि वह किस चीज़ का इंतज़ार कर रहा है। caller, यानी event loop, इसे कैसे realize करेगा, यह caller पर निर्भर है। correspondingInstantके साथhandle_timeoutcall होने पर उचित action होता हैओह, thomaseizinger हैं!
मैंने rust-libp2p के internals देखे हैं, इसलिए article पढ़ते समय बीच में यह pattern बहुत familiar लगा, और लगता है यह संयोग नहीं था
Firezone अच्छा दिख रहा है। हर चीज़ को connect करो!
सही है, rust-libp2p से कुछ समानताएँ हैं। लेकिन वहाँ actual streams और connections अभी भी
Futureजैसी structure के अंदर होते हैं, इसलिए ज़्यादा उलझे हुए हैं, और यहाँ के sans-IO की तरह सख्ती से अलग नहीं हैंएक हिस्सा कहता है कि sequential workflows के लिए ज़्यादा code चाहिए। Rust में async functions state machines में compile होती हैं, और हर
.awaitpoint किसी दूसरे state में transition को दिखाता है। इसलिए developers के लिए sequential code और non-blocking I/O को साथ इस्तेमाल करना आसान होता है। async न हो तो कई steps को express करने के लिए state machine खुद लिखनी पड़ती हैक्या किसी ने async और sans-IO को combine करके देखा है? कम से कम conceptually, sans-IO को जानने वाले helper को await करने वाली async function लिखी जाए, तो पूरी चीज़ अच्छे sans-IO interface वाली struct के अंदर state machine में compile होनी चाहिए, जिसे non-async code से भी आसानी से call किया जा सके
खुद करके नहीं देखा है, लेकिन expected main issues शायद अच्छी usability और
Pinhandling होंगेRust में बताए गए use case को कुछ हद तक handle कर सकने वाले generators/coroutines हैं, लेकिन अभी यह बहुत unstable feature है
दुर्भाग्य से मौजूदा रूप में coroutines की एक झंझट भरी limitation है कि वे सिर्फ
std::ops::Coroutinetrait के ज़रिए expose होते हैं। इसलिए compiler द्वारा बनाई गई internal state machine को सीधे allocate नहीं किया जा सकता। state machine का size दिखने में compile-time constant होने के बावजूद ऐसा हैअगर यह सिर्फ एक single coroutine हो जो defined lifetime वाली function के अंदर ही रहे, तो समस्या नहीं है। compiler इसे समझकर state machine को stack पर allocate कर सकता है
लेकिन coroutines का सबसे उपयोगी application शायद event loop device के queue elements हैं। यह implementation coroutine को boxing किए बिना संभव नहीं है।
Vec>cache-friendly data structure नहीं है, और extremely high concurrency I/O में अगरVecमें दस लाख elements चाहिए हों तो परेशानी महसूस होगीRust में अगर कभी native generator syntax आ जाए, तो यह संभव हो सकता है। async task के context के अंदर रहते हुए data “लिखने” के लिए
yield transmitकहा जा सकेगा। यानी हरsocket.writeyield transmitमें बदल जाएगाdata पढ़ते समय generator suspend (
.await) होगा, और incoming data के साथ resume होने का इंतज़ार करेगा। nightly में ऐसी syntax है या नहीं, पता नहीं, लेकिन मोटे तौर पर यह ऐसी दिखनी चाहिए:// Made up
gensyntax: gen(yield_type, resume_type)gen(Transmit, &[u8]) fn stun_binding(server: SocketAddr) -> SocketAddr {
let req = make_stun_request();
yield Transmit {
server,
payload: req
};
stack के ऊपरी स्तर पर दोनों काफी अच्छी तरह साथ चलते हैं। non-blocking I/O, यानी async, socket I/O और समय का एक साथ इंतज़ार करना आसान बना देता है। blocking I/O में भी socket पर read timeout सेट करके यह किया जा सकता है, लेकिन async primitive tools का इस्तेमाल करना थोड़ा आसान है
मैंने भी लगातार सोचा है कि दोनों को कैसे जोड़ा जाए। जिन समस्याओं तक पहुँचा, उनमें से एक यह है कि async function opaque type में compile होता है। इसलिए compiler से state machine code-generate कराने वाली सुविधा का इस्तेमाल करना मुश्किल या असंभव हो जाता है। क्योंकि बन जाने के बाद उस state machine से interact नहीं किया जा सकता। यह एक तरह से borrow checker को भी बिगाड़ देता है
उदाहरण के लिए मान लें कि कोई async task है जिसमें कई चरण हैं, यानी कई
awaitpoints हैं, और उनमें से सिर्फ एक हिस्से को shared data structure के लिए mutable reference चाहिए। जैसे ही इसेasyncfunction के रूप में व्यक्त करते हैं, mutable reference बने हुएFuturetype में capture हो जाता है, और वह type सभी चरणों में मौजूद रहता है। नतीजतन Rust ऐसे दो या अधिक tasks को एक साथ चलाने की अनुमति नहीं देताआम तौर पर ऐसी स्थिति में सलाह होती है “mutable reference को जितना हो सके उतनी कम अवधि के लिए capture करो”, लेकिन async में ऐसा नहीं किया जा सकता। async function को कई हिस्सों में तोड़ना भी messy हो जाता है, और शुरुआत में सब कुछ एक ही function के रूप में व्यक्त करने का मकसद भी कुछ हद तक टूट जाता है
पहले मैंने HTTP/1.1 protocol को I/O के लिए
.awaitpoints वाली Sans-IO state machine के रूप में encode करने की कोशिश की थी, लेकिन ज़्यादा आगे नहीं बढ़ पाया। हालांकि वह I/O async runtime में waker register करने के बजाय, user को खुद I/O perform करने के लिए control वापस दे देता था। इसे ऐसे समझ सकते हैं कि.await“नीचे” नहीं बल्कि “ऊपर” unwind करता हैHTTP/1.1 context में async code इस बात का एक तरह का blueprint बन गया था कि user call behavior कैसा चाहता है। उस समय मैं इसे
no_stdऔर allocator-less environment में ज़रूर चलाने लायक बनाना चाहता था, औरBoxके जरिए dynamic dispatch, यानी allocator की जरूरत वाले हिस्से से बचने का तरीका नहीं मिला, इसलिए छोड़ दियाhttps://news.ycombinator.com/item?id=40879547
async fn stunके रूप में लिखा गया example है। पूरा working code यहाँ है: https://gist.github.com/joshka/af299be87dbd1f64060e47227b577...बढ़िया किया! state expose कर देने पर किसी भी async function को pure बनाया जा सकता है। user को बस state machine को अगले state में धकेलना होता है
पहले मैंने OpenSSL को async Rust से bind करने की कोशिश की थी, और उसका async API भी मिलते-जुलते design का पालन करता है
जो हिस्सा समान लगा, क्या वह यह है कि job के रूप में schedule होने वाला task खुद कैसे execute होता है, उससे independent होता है?
यह example [0] देखें तो वह async API Rust के future से कहीं ज्यादा मिलता-जुलता दिखता है
job के अंदर “wait context” तक access हो सकता है, कुछ conditions में suspend किया जा सकता है, और execution जारी रखने के लिए wake trigger किया जा सकता है
[0]: https://www.openssl.org/docs/man1.1.1/man3/ASYNC_is_capable....
यह तो बस coroutine के बजाय callback इस्तेमाल करने वाला सामान्य asynchronous I/O है
लेख और कुछ comments पढ़कर लगता है जैसे hexagonal architecture या ports/adapters architecture style को फिर से invent किया गया हो
मुझे ठीक से समझ नहीं आ रहा कि यहाँ से क्या takeaway लेना चाहिए। जिन बातों पर चर्चा हो रही है वे सब पहले से ही basic network programming हैं
ऐसा लगता है कि focus higher-level plumbing पर है और state management में जरूरत से ज्यादा डूबा हुआ है; यह सिर्फ पसंद का मामला है, networking से इसका लेना-देना नहीं है
लेख से सीखी सबसे दिलचस्प बात यह है कि Cloudflare public STUN server चलाता है। लेकिन वह भी बहुत मददगार नहीं है। STUN protocol का “अच्छा” और “useful” version पहला वाला था, जो NAT enumeration संभव बनाने वाले
change requestsfeature को support करता था। बाद के STUN versions में specification में योगदान देने वाले Cisco engineers के “helpful suggestions” की वजह से वह feature हटा दिया गयाअभी Rust में, उदाहरण के लिए अगर आप Tokio async runtime इस्तेमाल करने वाली WebRTC library implement करते हैं, तो synchronous I/O इस्तेमाल करने वालों, या दूसरा runtime (smol, async-std आदि) इस्तेमाल करने वालों, या सीधे iouring इस्तेमाल करने वालों के लिए उसे use करना बहुत झंझट भरा हो जाता है
इस approach से consumer पर I/O choice थोपी नहीं जाती, इसलिए library ज्यादा लोगों के लिए useful बन सकती है