.NET 8 चा TimeProvider आणि SynchronizationContext मधील एक छोटा बदल, retry-logic च्या युनिट टेस्ट्सचा वेळ सेकंदांमधून कमी करू शकतो, ज्या अन्यथा रिअल स्लीप्समुळे (real sleeps) रिकाम्या बसून राहतात. एकेकाळी सात सेकंद घेणारी टेस्ट आता काही मिलीसेकंदात पूर्ण होते आणि हा बदल प्रोडक्शनमध्ये वापरण्यासाठी कोणताही अतिरिक्त खर्च येत नाही.

विलंब का महत्त्वाचा आहे

अनेक retry helpers 'exponential backoff' वापरतात: १ सेकंद थांबा, मग २ सेकंद, मग ४ सेकंद, आणि असेच पुढे. प्रोडक्शनमध्ये, हे फेल होत असलेल्या एंडपॉइंटवर (endpoint) सतत ताण पडण्यापासून सर्व्हिसेसचे संरक्षण करते. टेस्ट सुईटमध्ये (test suite) तोच कोड रिअल क्लॉकवर Task.Delay कॉल करतो, ज्यामुळे टेस्ट थ्रेड प्रत्यक्षात झोपतो (sleeps). जेव्हा अशा डझन्स टेस्ट्स असतात, तेव्हा कोणत्याही कार्यात्मक फायद्याशिवाय continuous-integration (CI) पाइपलाइन मिनिटांनुसार वाढते. हा “sleep tax” म्हणजे निव्वळ वाया गेलेला वेळ आहे.

TimeProvider कसे काम करते

.NET 8 ने TimeProvider सादर केले, जे सिस्टम क्लॉकवरील एक ॲबस्ट्रॅक्शन (abstraction) आहे. ज्या क्लासला सध्याची वेळ हवी आहे किंवा ज्याला विलंब (delay) हवा आहे, तो क्लास DateTime.UtcNow किंवा Task.Delay थेट कॉल करण्याऐवजी TimeProvider चा इन्स्टन्स स्वीकारू शकतो. प्रोडक्शनमध्ये तुम्ही TimeProvider.System पास करता, जे रिअल क्लॉकला फॉरवर्ड करते. टेस्टमध्ये तुम्ही FakeTimeProvider पुरवता. 'Fake' मुळे तुम्ही Advance(TimeSpan) वापरून वेळ मॅन्युअली पुढे नेऊ शकता. अंतर्गतरीत्या Task.Delay या प्रोव्हायडरला वाचते, त्यामुळे फेक क्लॉक पुढे सरकवल्यामुळे प्रलंबित (pending) असलेला कोणताही विलंब त्वरित पूर्ण होतो.

ही कल्पना साधी आहे: रिअल क्लॉकच्या जागी एक नियंत्रित (controllable) क्लॉक वापरा आणि त्यानंतर टेस्ट केले जाणारे कोड ज्या ठिकाणी पुन्हा सुरू व्हायला हवे होते, तिथे थेट उडी मारा.

xUnit SynchronizationContext चा सापळा

ही संकल्पना कन्सोले ॲपमध्ये (console app) काम करते, परंतु xUnit मध्ये यामुळे डेडलॉक (deadlock) निर्माण झाला. जेव्हा retry helper एका डिलेची वाट पाहत (awaiting) होता, तेव्हा टेस्टने Advance() कॉल केला. xUnit स्वतःचा SynchronizationContext इन्स्टॉल करते जो continuations कॅप्चर करतो आणि ते टेस्ट थ्रेडवर चालवतो. जेव्हा Advance() ने क्लॉक पुढे नेली, तेव्हा continuation टेस्ट थ्रेडऐवजी थ्रेड पूलमध्ये (thread pool) रांगेत (queue) लागली. टेस्ट थ्रेड वेळ पुढे नेत राहिला, ज्यामुळे सिम्युलेटेड क्लॉक पुढच्या रिट्राईच्या वेळेच्याही पुढे गेली. नवीन टाइमर अशा भविष्यातील वेळेसाठी सेट केला गेला जो कधीच येणार नव्हता, कारण क्लॉक पुन्हा पुढे नेण्यासाठी कोणताही थ्रेड शिल्लक नव्हता. टेस्ट अनिश्चित काळासाठी अडकून पडली (hung indefinitely).

एक ओळीतील उपाय

टेस्टच्या सुरुवातीला एक ओळ जोडल्याने अपेक्षित वर्तन पुन्हा प्राप्त होते:

SynchronizationContext.SetSynchronizationContext(null);

कस्टम कॉन्टेक्स्ट क्लिअर केल्यामुळे await continuations थ्रेड पूलवर चालण्यास भाग पाडले जातात, जिथे फेक टाइमरचे कॉल बॅक्स (callbacks) कार्यान्वित होऊ शकतात. या बदलामुळे तीच टेस्ट सात सेकंदांवरून साधारण ३६ मिलीसेकंदात पूर्ण होते.

व्यापक फायदे

रिकाम्या स्लीप्स (idle sleeps) काढून टाकण्याव्यतिरिक्त, फेक क्लॉकमुळे टाइम-झोन-संवेदनशील (time-zone-sensitive) लॉजिक टेस्ट करणे सोपे होते. रिअल क्लॉक बारा वाजण्याची वाट न पाहता, दैनंदिन कोटा योग्य स्थानिक मध्यरात्रीला रिसेट होतो की नाही, हे तुम्ही तपासू शकता. सध्याच्या वेळेवर आधारित असलेल्या कोणत्याही कोडसाठी हीच पद्धत लागू होते: प्रोव्हायडर इंजेक्ट करा, Thread.Sleep किंवा Task.Delay चे छुपे कॉल्स टाळा आणि तुम्हाला डिटरमिनिस्टिक (deterministic) आणि वेगवान टेस्ट्स मिळतील.

काय लक्षात ठेवावे

  • प्रत्येक विलंब (delay) इंजेक्ट केलेल्या TimeProvider द्वारेच वळवला (route) गेला पाहिजे. एखादा विसरलेला Thread.Sleep किंवा थेट Task.Delay अजूनही रिअल क्लॉकला कॉल करेल आणि पुन्हा लॅटन्सी (latency) निर्माण करेल.
  • जर तुम्ही वापरत असलेल्या लायब्ररीमध्ये अंतर्गतरीत्या प्रोव्हायडर न दाखवता Task.Delay कॉल केला जात असेल, तर अधिक इनव्हेसिव्ह शिम (invasive shim) शिवाय तुम्ही त्याचे टायमिंग नियंत्रित करू शकत नाही. अशा परिस्थितीत फायदा मर्यादित असू शकतो.
  • छुपे वेट्स (hidden waits) शोधण्यासाठी टेस्ट फोल्डरमध्ये Task.Delay आणि Thread.Sleep साठी 'grep' करा. फेक क्लॉक प्रभावी ठेवण्यासाठी त्या कॉल्सना प्रोव्हायडर वापरण्यासाठी रिफॅक्टर करणे हाच एकमेव मार्ग आहे.

निष्कर्ष

सिस्टम क्लॉकच्या जागी TimeProvider वापरणे आणि xUnit चा SynchronizationContext अक्षम (disable) करणे, यामुळे संथ (sluggish) रिट्राई टेस्ट्स जवळजवळ त्वरित तपासणीत रूपांतरित होतात, ज्यामुळे CI रिसोर्सेस मोकळे होतात आणि वेळेवर अवलंबून असलेले कोड सत्यापित करणे सोपे होते. यासाठी फक्त काही ओळींचा कोड लागतो; पण त्याचा फायदा प्रत्येक टेस्ट रनमध्ये वाचलेल्या सेकंदांमध्ये मोजता येतो.