- टूटे हुए Rust concurrent counter के उदाहरण से दिखाया गया है कि सामान्य thread load test जिन समस्याओं को छोड़ देते हैं, उन्हें reproducible और minimize की जा सकने वाली execution-order control से कैसे सामने लाया जा सकता है
- टेस्ट के लिए बना
AtomicU32wrapperpause()डालता है, और managed thread atomic operations से पहले/बाद रुकता है, फिर टेस्ट द्वारा चुने गए order में आगे बढ़ता है - साधारण test में
100threads, हर एक100बार increment करके expected value10000के बजाय9598जैसी failure ला सकते हैं, लेकिन यह timing-dependent है, इसलिए reproduce, debug और shrink करना मुश्किल है arbtest-based property test उसी seed से वही interleaving reproduce करता है, और failure case को0: increment,1: increment,0: unpause,1: unpauseतक minimize करता है- इसी structure को
exhaustigenसे बढ़ाने पर अधिकतम5increments तक सभी interleavings enumerate की जा सकती हैं, औरfetch_addfix के बाद81133interleavings pass होती हैं
Atomic न होने वाला concurrent counter
- उदाहरण Rust के
AtomicU32का उपयोग करता है, लेकिनincrement()loadके बादstore(value + 1)करता है, इसलिए increment operation खुद atomic नहीं है Counterstructure सरल हैvalue: AtomicU32increment()SeqCstसे value पढ़ता है, पढ़ी गई value में1जोड़कर फिर store करता हैget()current value कोSeqCstसे पढ़ता है
- दो threads एक ही value पढ़कर वही incremented result store कर सकते हैं, जिससे update गायब हो जाता है
सामान्य thread test क्यों पर्याप्त नहीं हैं
- सबसे सरल verification तरीका है कि कई threads एक ही counter को बार-बार increment करें और अंत में value check करें
thread_count = 100increment_count = 100- expected value
10000है
- उदाहरण run
left: 9598,right: 10000के साथ fail होता है - यह तरीका scheduling timing पर बहुत निर्भर करता है
- वही failure deterministically reproduce करना मुश्किल है
- debugging मुश्किल है
- thread count या increment count कम करने पर किस्मत से pass हो सकता है, इसलिए failure case को minimize करना मुश्किल है
Property-based testing से interleavings संभालना
- Property-based testing (PBT) state machine testing के साथ अच्छा match करती है
- arbitrary inputs generate करना आसान है
- यह property रखी जा सकती है कि concurrent execution का result sequential execution model जैसा होना चाहिए
- failure input को minimize करने की जरूरत से भी मेल खाती है
- मुश्किल यह है कि वास्तविक OS threads को इच्छित समय पर एक-एक step आगे बढ़ाना कठिन है
- समाधान ऐसा structure है जिसमें हर iteration में किसी random thread को चुनकर एक step आगे बढ़ाया जाता है
- एक thread के
loadऔरstoreके बीच दूसरे thread को insert कर सकना चाहिए - इसके लिए threads को सीधे control करने वाला managed thread API बनाया गया है
- एक thread के
Test के लिए AtomicU32 और pause insertion
- test build में
std::sync::atomic::AtomicU32के बजाय अपनाmanaged_thread::AtomicU32उपयोग किया जाता है#[cfg(test)] use managed_thread::AtomicU32#[cfg(not(test))] use std::sync::atomic::AtomicU32
- wrapper
AtomicU32,load()औरstore()से पहले/बादpause()call करता हैload:pause()→ actualload→pause()store:pause()→ actualstore→pause()
- इन insertion points की वजह से test atomic operations के आसपास threads को रोक और फिर आगे बढ़ाकर execution order control कर सकता है
managed thread API का रूप
- test
std::thread::scopeके अंदर दो managed threads बनाता है- scoped thread का उपयोग होने से stack-local data borrow किया जा सकता है
spawn(scope, &counter)की तरह counter reference को state के रूप में pass किया जाता है
- managed thread शुरू से कोई खास
mainfunction नहीं चलाता, बल्कि control thread द्वाराsubmit()से भेजे गए closure को execute करता हैt.submit(|c| c.increment())- thread अपनी state
Tपर closure execute करता है
- test loop entropy बची रहने तक हर thread के लिए random action करता है
- अगर thread रुका हुआ है तो
unpause() - अगर रुका हुआ नहीं है तो
submit()सेincrement()execute करता है - sequential model
counter_modelको भी उतनी ही बार increment करता है
- अगर thread रुका हुआ है तो
- अंत में सभी threads को
join()किया जाता है औरcounter_modelकी तुलना actualcounter.get()से की जाती है
pause और unpause implementation
pause()test targetCounterAPI को बदले बिना current managed thread का context खोजने के लिएthread_local!का उपयोग करता है- context
Arc<SharedContext>के रूप में shared है SharedContextमेंMutex<State>औरCondvarहोते हैं
- context
- state को
Ready,Running,Pausedमें बांटा गया हैReady: अगले closure का इंतजार करने वाली stateRunning: managed thread के running होने की statePaused:pause()point पर रुकी हुई state
- जब managed thread
pause()पर पहुंचता है, तो state कोRunningसेPausedमें बदलता है और condition variable के जरिए control thread को notify करता है unpause()state कोPausedसेRunningमें बदलता है और managed thread को जगाने के बाद फिर तब तक wait करता है जब तक stateRunningन रहे- control thread और managed thread के एक साथ चलते रहने की स्थिति को रोकता है
- किसी भी समय दोनों में से केवल एक ही चले, ऐसा बनाकर non-determinism घटाता है
Failure reproduction और minimization
arbtestrun टूटे हुए counter में failure ढूंढता है- उदाहरण failure में model value
4, actual value3है - failure seed
0x4fd7ddff00000020है
- उदाहरण failure में model value
- वही seed देने पर वही interleaving फिर मिलती है, इसलिए failure reproduce करना आसान हो जाता है
.minimize()का उपयोग करने पर failure case छोटी execution में घट जाता है- अंतिम minimal case seed
0x9c2a13a600000001है - minimal trace चार steps का है
0: increment1: increment0: unpause1: unpause
- अंतिम minimal case seed
- इस minimal case में expected value
2है, लेकिन actual value1होती है, जिससेload/store-based increment की खामी सामने आती है
सभी interleavings enumerate करने तक विस्तार
- इसी structure को random interleaving के बजाय enumeration-based बनाया जा सकता है
exhaustigenका उपयोग करके अधिकतम5increments तक सभी interleavings explore करने वाला test लिखा गया है- test dummy loops से बचता है और हमेशा thread को
unpauseकरता है याincrementsubmit करता है
- test dummy loops से बचता है और हमेशा thread को
- टूटा हुआ implementation वही bug ढूंढता है
- example failure
left: 2,right: 1है
- example failure
Counter::increment()कोfetch_add(1, SeqCst)से fix करने पर test pass होता हैAtomicU32wrapper में भीfetch_add()से पहले/बादpause()जोड़ा जाता है- execution result
all 81133 interleavings are fine!है - run time
real 8.65s, CPU8.16s, RSS63.91mbहै
Weak memory model और model checking तक विस्तार
- मौजूदा toy implementation का
AtomicU32actual atomic को delegate करता है - extension idea यह है कि हर atomic के लिए लिखी गई values का set रखा जाए, और read के समय weak memory model से consistent कोई arbitrary value return की जाए
- interleaving exploration को random से ज्यादा smart भी बनाया जा सकता है
- model checking approach से verify किया जा सकता है कि meaningfully different सभी interleavings consider हुई हैं या नहीं
- Generate All The Things वाले तरीके की तरह छोटे scope की सभी interleavings enumerate की जा सकती हैं
shrinking के बिना minimization क्यों संभव है
- इस्तेमाल किया गया
arbtestपरिचित PRNG interface जैसा दिखता है, लेकिन finite PRNG का उपयोग करता है- random values मांगते रहने पर किसी point पर
Err(OutOfEntropy)return करता है - इसलिए test code में
?औरwhile !rng.is_empty()दिखाई देते हैं
- random values मांगते रहने पर किसी point पर
- जब test entropy खत्म कर देता है, तो वह जल्दी समाप्त हो जाता है; इसलिए उपलब्ध entropy घटाने पर test execution भी छोटा हो जाता है
- internal implementation conceptually
&mut &[u8]के करीब है- हर बार random number मांगने पर byte slice घटता है
- initial slice जितना छोटा होगा, test उतना ही सरल होगा
- इस तरीके की वजह से अलग से shrinking logic खुद implement किए बिना भी failure case छोटा हो सकता है
- example source code properly-concurrent में है
1 टिप्पणियां
Hacker News की राय
Rust में मिलते-जुलते approach से Temper नाम की library बना रहा हूँ: https://github.com/reitzensteinm/temper/tree/main
हालांकि Rust के पूरे memory model से निकलने वाले अजीब implications को model करने के लिए काफी आगे जाना पड़ता है, इसलिए एक ledger चाहिए जो track करे कि हर thread ने कौन-सी writes देखी हैं। atomic memory ordering, read/write fences वगैरह के हिसाब से ऐसी guarantees बन सकती हैं कि अगर write X दिखाई देती है, तो write Y भी जरूर दिखाई देनी चाहिए
मुझे लगता है कि यह C++/Rust memory model test cases का सबसे बड़ा collection है; किताबों, C++ standard, Stack Overflow, blogs आदि में जो भी मिल सकता था, लगभग सब इकट्ठा किया है। उदाहरण के लिए Mara Bos की Rust Atomics and Locks के लिए file यहां है: https://github.com/reitzensteinm/temper/blob/main/memlog/tes...
लेख में बताई गई Loom मिलती-जुलती, लेकिन कहीं ज्यादा polished library है, जो mutex या queue जैसे higher-level components को thorough तरीके से test करने देती है: https://github.com/tokio-rs/loom हालांकि memory model को खुद Temper जितनी बारीकी से model नहीं करती, और मैं test cases को Loom में port करने के बारे में सोच रहा था
Will Wilson की FoundationDB testing presentation से inspiration मिली, और वे अब Antithesis में arbitrary Docker containers पर इसी तरह की testing करने वाला hypervisor-based solution बना रहे हैं: https://www.youtube.com/watch?v=4fFDFbi3toc, https://antithesis.com/
मेरा मजबूत विश्वास है कि अगले 10 साल में यह क्षेत्र बहुत बड़ा होगा। WebAssembly एक ऐसा sweet spot है: arbitrary software compile करने के लिए पर्याप्त complete, फिर भी इतना simple कि Antithesis जैसी चीज बनाना किसी ऐसी elite team का 5 साल का project न बन जाए जिसने पहले ही database ship किया हो
Rust में shared-memory atomic snapshot implement किया था, और automated testing को भी जितना हो सके serious लिया: https://github.com/kaymanb/todc/tree/main/todc-mem
शुरुआत में लेख में बताई गई Loom इस्तेमाल की, लेकिन बाद में shuttle पर switch किया: https://github.com/tokio-rs/loom, https://github.com/awslabs/shuttle
shuttle, Loom की तरह exhaustive exploration करने के बजाय randomized approach अपनाता है, लेकिन scheduler फिर भी bug detection पर probabilistic guarantees देता है। इस्तेमाल करने पर shuttle ज्यादा तेज निकला और ज्यादा complex test scenarios तक scale हुआ
लेख के तरीके की तरह ही, अगर कोई खास schedule test failure कराता है तो random seed save किया जा सकता है। failing test को जल्दी reproduce कर पाने की क्षमता बहुत अहम है, और इससे पहले पकड़े व fix किए गए bugs के लिए explicit test cases लिखना संभव होता है: https://github.com/kaymanb/todc/blob/0e2874a70ec8beed8fae773...
Kotlin/Java में JetBrains की Lincheck इस तरह के काम के लिए अच्छी library है: https://github.com/JetBrains/lincheck
खास तौर पर इसका declarative होना और linearizability results को output करने का तरीका पसंद है
जानना चाहता हूँ कि C++ में भी Loom जैसी कोई library है या नहीं। कुछ lock-free data structures test करना चाहता हूँ
यह काफी पुराना tool है और संभालना आसान है। इसे concurrency expert Dmitry Vyukov ने बनाया है
https://github.com/facebook/folly/blob/main/folly/test/Deter...
अगर मैंने ठीक से समझा है, तो इस approach की कमज़ोर forward progress guarantee के संदर्भ में सीमाएँ हैं
लेख में calculation बहुत मामूली नहीं है, लेकिन असली hardware और असली scheduler पर ऐसे
cmpxchgloop के बारे में सोच सकते हैं जिसके किसी खास CPU पर रुक जाने की संभावना बेहद कम होती है। अगर CPU की संख्याnहै, तो worst case में progress करने की probability1/nहै, लेकिन इस testing तरीके में यह1/t^pहो जाती है। यहाँtकामों की संख्या है, जो CPU की संख्या से काफी ज्यादा हो सकती है, औरpउस loop body के अंदर pause की संख्या है, जो आसानी से 3 या उससे ज्यादा हो जाती है। इतना किसी ऐसे algorithm को, जो असल में काम करता है, टूटा हुआ दिखाने के लिए काफी हैउल्टा, अगर आप कमजोर forward progress को bug की तरह पकड़ना चाहते हैं और इसलिए strong forward progress मांगते हैं, तब भी यह तरीका कोई उपयोगी tool देता हुआ नहीं लगता
फिर भी, कई concurrency समस्याओं के लिए यह निश्चित रूप से उपयोगी है
1/t^pसही नहीं लगता, मैं इसे बस1/tमानता हूँ। आखिरकारtसमय बीतने पर कोई न कोई task जरूर आगे बढ़ा होगा, और अगर taskstहैं, तो जिस task ने progress किया वह मेरा task होने की probability1/tहैमुख्य भ्रम शायद यह है कि रुक जाने का मतलब जरूरी नहीं कि CAS में हारना ही हो
“ईमानदारी से कहूँ तो यहाँ थोड़ी background knowledge है। inline assembly में बेहद शापित काम किए बिना असली thread creation से बच पाना संभव नहीं लगता। अगर कोई चीज़
pause()function को call करती है, और हम चाहते हैं कि वह अगला instruction मिलने तक रुकी रहे, तो वह काम ऐसे thread में होना चाहिए जो test के stack से अलग stack रखता हो” — इस हिस्से के बारे में सोच रहा हूँ कि क्या किसी तरह का async runtime इस्तेमाल नहीं किया जा सकतायह atomic operations को instrument करके cooperative multitasking हासिल करने जैसा दिखता है। शायद मुझे और coffee पीनी पड़े, लेकिन threads के बिना करना ज्यादा सरल लगता है
इस approach की एक कमी यह है कि test किए जा रहे code को ही test code के हिसाब से modify करना पड़ता है
लगता है दो threads शुरू करके
ptraceसे single-step execution करते हुए instruction execution को “randomly” interleave करके भी यही काम किया जा सकता है। rr के chaos mode जैसा तरीकालेकिन कुछ instructions atomic नहीं हो सकते, इसलिए बिना emulation के अगर यह संभव भी हो, तो “atomic microcode” units पर single-step execution करने का तरीका चाहिए होगा
Loom इस्तेमाल करने के लिए conditional compilation की जरूरत लगती है, और एक library test करते समय यह ठीक होगा, लेकिन काफी invasive है
#[cfg(loom)]pub(crate) use loom::sync::atomic::AtomicUsize;#[cfg(not(loom))]pub(crate) use std::sync::atomic::AtomicUsize;सोच रहा हूँ कि क्या ऐसी कोई language है जो अपने scheduler को बेहतर तरीके से इस्तेमाल करने देती हो
अगर सचमुच बहुत thorough होना हो, तो
ptraceके साथ test चलाकर threads को single-step में आगे बढ़ाते हुए instruction level पर अलग-अलग interleavings बनाए जा सकते हैं। जानना चाहूँगा कि क्या असल में ऐसा तरीका कभी देखा हैजहाँ, यहाँ की तरह, code को instrument नहीं किया जा सकता, वहाँ black-box testing के लिए कोई alternative है?
ninstructions चलाता है, तो signal को बीच में डालने से पहले 0 सेninstructions तक चलाने वाले सिर्फnruns चाहिए होते हैं, और उसके बाद signal handler अंत तक चलता है, फिर main thread भी अंत तक चलता है। कुल समयO(n^2)हैलेकिन अगर
tthreads हैं और हर एकninstructions चलाता है, और हर boundary पर वे एक-दूसरे को रोक सकते हैं, तो realisticnvalues पर यह approach कठिन हो जाता है। लगता है कि interesting behavior वाले operations को चुनकर simulate करने की तरह इसे घटाना पड़ेगाकाफी शानदार लग रहा है, इसे एक बार आज़माना चाहिए। हालांकि यह हर तरह की error नहीं पकड़ेगा।
pause()की हर call पर threads के बीच synchronization बन जाता है, जिससे कुछ data race issues छिप नहीं जाएँगे? Rust में शायद यह समस्या न हो