.NET 8 का TimeProvider और SynchronizationContext में एक छोटा सा बदलाव retry-logic यूनिट टेस्ट से कई सेकंड बचा सकता है, जो अन्यथा वास्तविक 'sleeps' के कारण खाली बैठे रहते हैं। एक टेस्ट जिसे पहले सात सेकंड लगते थे, अब कुछ ही मिलीसेकंड में पूरा हो जाता है, और इस बदलाव का प्रोडक्शन में चलाने पर कोई अतिरिक्त खर्च नहीं आता है।

देरी क्यों मायने रखती है

कई retry helpers exponential backoff का उपयोग करते हैं: पहले 1 सेकंड रुकें, फिर 2 सेकंड, फिर 4 सेकंड, इत्यादि। प्रोडक्शन में, यह सेवाओं को किसी विफल एंडपॉइंट (failing endpoint) पर बार-बार हमला करने से बचाता है। लेकिन एक टेस्ट सूट में, वही कोड वास्तविक घड़ी पर Task.Delay को कॉल करता है, जिससे टेस्ट थ्रेड वास्तव में सो जाता है। यदि इसे दर्जनों टेस्ट्स के साथ गुणा किया जाए, तो बिना किसी कार्यात्मक लाभ (functional gain) के continuous-integration (CI) पाइपलाइन मिनटों तक बढ़ जाती है। यह “sleep tax” पूरी तरह से बर्बाद किया गया समय है।

TimeProvider कैसे काम करता है

.NET 8 ने TimeProvider पेश किया है, जो सिस्टम क्लॉक (system clock) पर एक एब्स्ट्रैक्शन (abstraction) है। एक क्लास जिसे वर्तमान समय की आवश्यकता है या जिसे देरी (delay) करने की आवश्यकता है, वह DateTime.UtcNow या Task.Delay को सीधे कॉल करने के बजाय TimeProvider इंस्टेंस स्वीकार कर सकती है। प्रोडक्शन में आप TimeProvider.System पास करते हैं, जो वास्तविक घड़ी को फॉरवर्ड करता है। एक टेस्ट में आप FakeTimeProvider प्रदान करते हैं। यह 'fake' आपको Advance(TimeSpan) के साथ समय को मैन्युअल रूप से आगे बढ़ाने की अनुमति देता है। आंतरिक रूप से Task.Delay प्रोवाइडर को पढ़ता है, इसलिए फेक क्लॉक को आगे बढ़ाने से किसी भी लंबित (pending) देरी की आवश्यकता तुरंत पूरी हो जाती है।

विचार सरल है: वास्तविक घड़ी को एक नियंत्रणीय (controllable) घड़ी से बदलें, फिर उस बिंदु तक कूदें जहाँ टेस्ट किया जा रहा कोड फिर से शुरू (resume) होता।

xUnit SynchronizationContext का जाल

यह अवधारणा एक कंसोल ऐप में काम करती है, लेकिन xUnit में इसने डेडलॉक (deadlock) की स्थिति पैदा कर दी। जब retry helper किसी देरी का इंतज़ार (awaiting a delay) कर रहा था, तब टेस्ट ने Advance() को कॉल किया। xUnit अपना स्वयं का SynchronizationContext इंस्टॉल करता है जो continuations को कैप्चर करता है और उन्हें टेस्ट थ्रेड पर चलाता है। जब Advance() ने घड़ी को आगे बढ़ाया, तो continuation टेस्ट थ्रेड के बजाय थ्रेड पूल (thread pool) में कतारबद्ध (queued) हो गया। टेस्ट थ्रेड समय को आगे बढ़ाता रहा, जिससे सिम्युलेटेड क्लॉक उस क्षण से आगे निकल गई जब अगला retry चलना चाहिए था। नया टाइमर भविष्य के एक ऐसे बिंदु के लिए सेट किया गया था जहाँ कभी पहुँचा ही नहीं जा सकता था क्योंकि घड़ी को फिर से आगे बढ़ाने के लिए कोई थ्रेड नहीं बचा था। टेस्ट अनिश्चित काल के लिए अटक गया।

एक लाइन वाला समाधान

टेस्ट की शुरुआत में एक सिंगल लाइन जोड़ने से अपेक्षित व्यवहार (expected behavior) बहाल हो जाता है:

SynchronizationContext.SetSynchronizationContext(null);

कस्टम कॉन्टेक्स्ट को क्लियर करने से await continuations को थ्रेड पूल पर चलने के लिए मजबूर किया जाता है, जहाँ फेक टाइमर के कॉल-बैक (callbacks) निष्पादित हो सकते हैं। इस बदलाव के साथ, वही टेस्ट सात सेकंड से घटकर लगभग 36 मिलीसेकंड रह जाता है।

व्यापक लाभ

खाली 'sleeps' को खत्म करने के अलावा, एक फेक क्लॉक टाइम-ज़ोन-संवेदनशील (time-zone-sensitive) लॉजिक का परीक्षण करना आसान बनाती है। आप वास्तविक घड़ी के बारह बजने का इंतज़ार किए बिना यह सत्यापित कर सकते हैं कि दैनिक कोटा सही स्थानीय आधी रात (local midnight) पर रीसेट होता है। यही दृष्टिकोण किसी भी ऐसे कोड के लिए काम करता है जो वर्तमान समय के आधार पर ब्रांच (branch) करता है: प्रोवाइडर को इंजेक्ट करें, Thread.Sleep या Task.Delay के छिपे हुए कॉल से बचें, और आपको deterministic, तेज़ टेस्ट मिलेंगे।

किन बातों का ध्यान रखें

  • प्रत्येक देरी (delay) को इंजेक्ट किए गए TimeProvider के माध्यम से रूट किया जाना चाहिए। एक भी भटकता हुआ Thread.Sleep या सीधा Task.Delay अभी भी वास्तविक घड़ी को कॉल करेगा और फिर से लेटेंसी (latency) पैदा कर देगा।
  • यदि आप जिस लाइब्रेरी पर निर्भर हैं, वह प्रोवाइडर को उजागर किए बिना आंतरिक रूप से Task.Delay को कॉल करती है, तो आप अधिक इनवेसिव शिम (invasive shim) के बिना इसके समय को नियंत्रित नहीं कर सकते। ऐसे मामलों में लाभ सीमित हो सकता है।
  • छिपे हुए इंतज़ार (waits) का पता लगाने के लिए टेस्ट फ़ोल्डर में Task.Delay और Thread.Sleep को grep करें। फेक क्लॉक को प्रभावी बनाए रखने का एकमात्र तरीका उन कॉल्स को प्रोवाइडर का उपयोग करने के लिए रिफैक्टर (refactor) करना है।

निष्कर्ष

सिस्टम क्लॉक को TimeProvider से बदलना और xUnit के SynchronizationContext को अक्षम (disable) करना सुस्त retry टेस्ट को लगभग तत्काल जांच में बदल देता है, जिससे CI संसाधन मुक्त होते हैं और समय-निर्भर (time-dependent) कोड को सत्यापित करना आसान हो जाता है। प्रयास केवल कुछ लाइनों का कोड है; इसका प्रतिफल (payoff) प्रति टेस्ट रन बचाए गए सेकंडों में मापा जाता है।