.NET 8 کا TimeProvider اور SynchronizationContext میں ایک معمولی سی تبدیلی ری ٹرائی لاجک (retry-logic) کے یونٹ ٹیسٹ سے کئی سیکنڈز بچا سکتی ہے، جو کہ عام طور پر اصل 'sleeps' کی وجہ سے فارغ بیٹھے رہتے ہیں۔ ایک ٹیسٹ جو پہلے سات سیکنڈ لیتا تھا، اب چند ملی سیکنڈز میں مکمل ہو جاتا ہے، اور اس تبدیلی کا پروڈکشن میں چلانے پر کوئی اضافی خرچ نہیں آتا۔

تاخیر کیوں اہمیت رکھتی ہے

بہت سے ری ٹرائی ہیلپرز (retry helpers) ایکسپونینشل بیک آف (exponential backoff) کا استعمال کرتے ہیں: پہلے 1 سیکنڈ انتظار کریں، پھر 2 سیکنڈ، پھر 4 سیکنڈ، اور اسی طرح۔ پروڈکشن میں یہ طریقہ سروسز کو کسی ناکام اینڈ پوائنٹ (endpoint) پر بار بار حملے سے بچاتا ہے۔ لیکن ٹیسٹ سویٹ (test suite) میں یہی کوڈ اصل گھڑی پر Task.Delay کو کال کرتا ہے، جس کی وجہ سے ٹیسٹ تھریڈ (test thread) حقیقت میں سو جاتا ہے۔ اگر اسے درجنوں ٹیسٹوں سے ضرب دی جائے تو کنٹینیو اس ٹیسٹ (CI) پائپ لائن بلاوجہ کئی منٹ بڑھ جاتی ہے۔ یہ "sleep tax" محض ضائع شدہ وقت ہے۔

TimeProvider کیسے کام کرتا ہے

.NET 8 میں TimeProvider متعارف کرایا گیا، جو سسٹم کلاک (system clock) پر ایک ایبسٹریکشن (abstraction) ہے۔ کوئی بھی کلاس جسے موجودہ وقت کی ضرورت ہو یا جسے تاخیر (delay) کرنی ہو، وہ DateTime.UtcNow یا Task.Delay کو براہ راست کال کرنے کے بجائے TimeProvider کا ایک انسٹنس (instance) قبول کر سکتی ہے۔ پروڈکشن میں آپ TimeProvider.System پاس کرتے ہیں، جو اصل گھڑی کو آگے بھیجتا ہے۔ ٹیسٹ میں آپ FakeTimeProvider فراہم کرتے ہیں۔ یہ 'fake' آپ کو Advance(TimeSpan) کے ذریعے وقت کو دستی طور پر آگے بڑھانے کی اجازت دیتا ہے۔ اندرونی طور پر Task.Delay اس فراہم کنندہ (provider) کو پڑھتا ہے، اس لیے فیک کلاک کو آگے بڑھانے سے تمام زیر التوا تاخیر (pending delay) فوری طور پر پوری ہو جاتی ہے۔

خیال سادہ ہے: اصل گھڑی کو ایک قابلِ کنٹرول گھڑی سے بدل دیں، اور پھر اس مقام تک چھلانگ لگا دیں جہاں ٹیسٹ کیا جانے والا کوڈ دوبارہ شروع ہونا تھا۔

xUnit SynchronizationContext کا جال

یہ تصور کنسول ایپ میں تو کام کرتا ہے، لیکن xUnit میں اس کی وجہ سے ڈیڈ لاک (deadlock) پیدا ہوا۔ جب ری ٹرائی ہیلپر تاخیر کا انتظار (awaiting a delay) کر رہا تھا، تب ٹیسٹ نے Advance() کو کال کیا۔ xUnit اپنا خود کا SynchronizationContext انسٹال کرتا ہے جو کنٹینیویشنز (continuations) کو کیپچر کرتا ہے اور انہیں ٹیسٹ تھریڈ پر چلاتا ہے۔ جب Advance() نے گھڑی کو آگے بڑھایا، تو کنٹینیویشن ٹیسٹ تھریڈ کے بجائے تھریڈ پول (thread pool) میں لائن میں لگ گئی۔ ٹیسٹ تھریڈ وقت کو آگے بڑھاتا رہا، جس سے مصنوعی گھڑی اس لمحے سے بھی آگے نکل گئی جب اگلا ری ٹرائی ہونا چاہیے تھا۔ نیا ٹائمر مستقبل کے ایک ایسے مقام پر سیٹ ہو گیا جہاں کبھی پہنچا ہی نہیں جا سکتا تھا کیونکہ گھڑی کو دوبارہ آگے بڑھانے کے لیے کوئی تھریڈ باقی نہیں بچا تھا۔ ٹیسٹ غیر معینہ مدت کے لیے رک گیا۔

ایک لائن والا حل

ٹیسٹ کے آغاز میں ایک لائن شامل کرنے سے مطلوبہ رویہ بحال ہو جاتا ہے:

SynchronizationContext.SetSynchronizationContext(null);

کسٹم کانٹیکسٹ (custom context) کو صاف کرنے سے await کنٹینیویشنز کو تھریڈ پول پر چلنے پر مجبور کیا جاتا ہے، جہاں فیک ٹائمر کے کال بیکس (callbacks) چل سکتے ہیں۔ اس تبدیلی کے ساتھ، وہی ٹیسٹ سات سیکنڈ سے کم ہو کر تقریباً 36 ملی سیکنڈ رہ جاتا ہے۔

وسیع تر فوائد

فارغ 'sleeps' کو ختم کرنے کے علاوہ، ایک فیک کلاک ٹائم زون سے حساس لاجک (time-zone-sensitive logic) کو ٹیسٹ کرنا آسان بنا دیتی ہے۔ آپ اس بات کی تصدیق کر سکتے ہیں کہ روزانہ کا کوٹہ صحیح مقامی آدھی رات کو ری سیٹ ہوتا ہے، بغیر اس کے کہ اصل گھڑی کے بارہ بجنے کا انتظار کیا جائے۔ یہی طریقہ اس کوڈ کے لیے بھی کام کرتا ہے جو موجودہ وقت کی بنیاد پر کام کرتا ہے: فراہم کنندہ (provider) کو انجیکٹ کریں، Thread.Sleep یا Task.Delay کے چھپے ہوئے کالز سے بچیں، اور آپ کو یقینی (deterministic) اور تیز رفتار ٹیسٹ حاصل ہوں گے۔

کن باتوں کا خیال رکھیں

  • ہر تاخیر (delay) کو انجیکٹ کیے گئے TimeProvider کے ذریعے ہی بھیجا جانا چاہیے۔ کوئی بھی غیر ضروری Thread.Sleep یا براہ راست Task.Delay اب بھی اصل گھڑی کو استعمال کرے گا اور تاخیر پیدا کرے گا۔
  • اگر آپ جس لائبریری پر انحصار کرتے ہیں وہ اندرونی طور پر فراہم کنندہ (provider) ظاہر کیے بغیر Task.Delay کو کال کرتی ہے، تو آپ کسی زیادہ پیچیدہ 'shim' کے بغیر اس کے ٹائمنگ کو کنٹرول نہیں کر سکتے۔ ایسی صورتوں میں فائدہ محدود ہو سکتا ہے۔
  • چھپے ہوئے انتظار (waits) کو تلاش کرنے کے لیے ٹیسٹ فولڈر میں Task.Delay اور Thread.Sleep کو 'grep' کریں۔ ان کالز کو فراہم کنندہ استعمال کرنے کے لیے ری فیکٹر (refactor) کرنا ہی فیک کلاک کو مؤثر رکھنے کا واحد طریقہ ہے۔

خلاصہ

سسٹم کلاک کو TimeProvider سے بدلنا اور xUnit کے SynchronizationContext کو غیر فعال کرنا سست ری ٹرائی ٹیسٹ کو فوری چیکس میں بدل دیتا ہے، جس سے CI وسائل آزاد ہوتے ہیں اور وقت پر منحصر کوڈ کی تصدیق آسان ہو جاتی ہے۔ اس کے لیے صرف چند لائنوں کے کوڈ کی ضرورت ہے؛ اور اس کا فائدہ ہر ٹیسٹ رن میں بچنے والے سیکنڈز کی صورت میں ملتا ہے۔