.NET 8-এর TimeProvider এবং একটি মাত্র SynchronizationContext পরিবর্তন রিট্রাই-লজিক (retry-logic) ইউনিট টেস্টের সময় কয়েক সেকেন্ড কমিয়ে দিতে পারে, যা অন্যথায় রিয়েল স্লিপের (real sleeps) কারণে অলসভাবে বসে থাকে। একটি টেস্ট যা আগে সাত সেকেন্ড সময় নিত, তা এখন মাত্র কয়েক ডজন মিলিসেকেন্ডে শেষ হয়, এবং এই পরিবর্তনের জন্য প্রোডাকশনে বাড়তি কোনো খরচ নেই।
কেন এই বিলম্ব গুরুত্বপূর্ণ
অনেক রিট্রাই হেল্পার (retry helpers) এক্সপোনেনশিয়াল ব্যাকঅফ (exponential backoff) ব্যবহার করে: প্রথমে ১ সেকেন্ড অপেক্ষা, তারপর ২ সেকেন্ড, তারপর ৪ সেকেন্ড, এবং এভাবেই চলতে থাকে। প্রোডাকশনে এটি সার্ভিসগুলোকে একটি ফেইলিং এন্ডপয়েন্টে (failing endpoint) ক্রমাগত আঘাত করা থেকে রক্ষা করে। কিন্তু একটি টেস্ট সুইটে একই কোড রিয়েল ক্লকের ওপর Task.Delay কল করে, ফলে টেস্ট থ্রেডটি প্রকৃতপক্ষে ঘুমিয়ে থাকে। ডজন ডজন টেস্টের ক্ষেত্রে এটি গুণ করলে কোনো কার্যকরী লাভ ছাড়াই কন্টিনিউয়াস-ইন্টিগ্রেশন (CI) পাইপলাইন কয়েক মিনিট বেড়ে যায়। এই “sleep tax” হলো পুরোপুরি সময়ের অপচয়।
TimeProvider কীভাবে কাজ করে
.NET 8-এ TimeProvider প্রবর্তন করা হয়েছে, যা সিস্টেম ক্লকের একটি অ্যাবস্ট্রাকশন (abstraction)। যে ক্লাসের বর্তমান সময়ের প্রয়োজন বা যেটির বিলম্ব (delay) প্রয়োজন, সেটি সরাসরি DateTime.UtcNow বা Task.Delay কল করার পরিবর্তে একটি TimeProvider ইনস্ট্যান্স গ্রহণ করতে পারে। প্রোডাকশনে আপনি TimeProvider.System পাস করবেন, যা রিয়েল ক্লকে নির্দেশ পাঠায়। একটি টেস্টে আপনি একটি FakeTimeProvider সরবরাহ করবেন। এই ফেক (fake) প্রোভাইডারটি আপনাকে Advance(TimeSpan) ব্যবহার করে ম্যানুয়ালি সময় এগিয়ে নিতে সাহায্য করে। ইন্টারনালি Task.Delay এই প্রোভাইডারটি পড়ে, তাই ফেক ঘড়িটি এগিয়ে দিলে তাৎক্ষণিকভাবে যেকোনো পেন্ডিং ডিলে (pending delay) সম্পন্ন হয়ে যায়।
ধারণাটি সহজ: রিয়েল ঘড়িটিকে একটি নিয়ন্ত্রণযোগ্য ঘড়ি দিয়ে প্রতিস্থাপন করুন, তারপর সরাসরি সেই সময়ে চলে যান যেখানে টেস্ট করা কোডটি পুনরায় চালু হওয়ার কথা ছিল।
xUnit SynchronizationContext-এর ফাঁদ
এই ধারণাটি একটি কনসোল অ্যাপে কাজ করলেও xUnit-এ এটি ডেডলক (deadlock) তৈরি করে। রিট্রাই হেল্পারটি যখন একটি ডিলে-র জন্য অপেক্ষা (awaiting) করছিল, তখন টেস্টটি Advance() কল করেছিল। xUnit তার নিজস্ব SynchronizationContext ইনস্টল করে যা কন্টিনিউয়েশনগুলোকে (continuations) ক্যাপচার করে এবং টেস্ট থ্রেডে রান করে। যখন Advance() ঘড়িটি এগিয়ে নিয়ে গেল, তখন কন্টিনিউয়েশনটি টেস্ট থ্রেডের পরিবর্তে থ্রেড পুলে (thread pool) কিউ (queue) হয়ে গেল। টেস্ট থ্রেডটি সময় এগিয়ে নিতে থাকল, যা সিমুলেটেড ঘড়িটিকে সেই মুহূর্তের ওপারে নিয়ে গেল যখন পরবর্তী রিট্রাইটি হওয়ার কথা ছিল। নতুন টাইমারটি এমন একটি ভবিষ্যতের সময়ের জন্য সেট করা হয়েছিল যা কখনোই পৌঁছানো সম্ভব ছিল না, কারণ ঘড়িটি পুনরায় এগিয়ে নেওয়ার জন্য আর কোনো থ্রেড অবশিষ্ট ছিল না। ফলে টেস্টটি অনির্দিষ্টকালের জন্য আটকে (hung) গেল।
এক লাইনের সমাধান
টেস্টের শুরুতে মাত্র একটি লাইন যোগ করলে প্রত্যাশিত আচরণ ফিরে আসে:
SynchronizationContext.SetSynchronizationContext(null);
কাস্টম কনটেক্সটটি ক্লিয়ার করলে await কন্টিনিউয়েশনগুলো থ্রেড পুলে রান করতে বাধ্য হয়, যেখানে ফেক টাইমারের কলব্যাকগুলো (callbacks) এক্সিকিউট হতে পারে। এই পরিবর্তনের ফলে একই টেস্ট সাত সেকেন্ড থেকে কমে মাত্র ৩৬ মিলিসেকেন্ডে নেমে আসে।
ব্যাপক সুবিধা
অলস স্লিপ (idle sleeps) দূর করার পাশাপাশি, একটি ফেক ঘড়ি টাইম-জোন-সেনসিটিভ (time-zone-sensitive) লজিক টেস্ট করা সহজ করে তোলে। রিয়েল ঘড়িতে বারোটা বাজার জন্য অপেক্ষা না করেই আপনি যাচাই করতে পারেন যে একটি দৈনিক কোটা সঠিক স্থানীয় মধ্যরাতে রিসেট হচ্ছে কি না। বর্তমান সময়ের ওপর ভিত্তি করে কাজ করে এমন যেকোনো কোডের জন্য একই পদ্ধতি কাজ করে: প্রোভাইডারটি ইনজেক্ট করুন, Thread.Sleep বা Task.Delay-এর লুকানো কলগুলো এড়িয়ে চলুন, এবং আপনি পাবেন ডিটারমিনিস্টিক (deterministic) ও দ্রুতগতির টেস্ট।
যা খেয়াল রাখতে হবে
- প্রতিটি ডিলে অবশ্যই ইনজেক্ট করা
TimeProvider-এর মাধ্যমে রুট করতে হবে। কোনো বিচ্ছিন্নThread.Sleepবা সরাসরিTask.Delayতখনও রিয়েল ঘড়িকে কল করবে এবং পুনরায় ল্যাটেন্সি (latency) তৈরি করবে। - আপনি যদি এমন কোনো লাইব্রেরির ওপর নির্ভর করেন যা প্রোভাইডার প্রকাশ না করেই ইন্টারনালি
Task.Delayকল করে, তবে আরও ইনভেসিভ শিম (invasive shim) ছাড়া আপনি এর টাইমিং নিয়ন্ত্রণ করতে পারবেন না। এমন ক্ষেত্রে সুবিধা সীমিত হতে পারে। - লুকানো ওয়েট (waits) শনাক্ত করতে টেস্ট ফোল্ডারে
Task.DelayএবংThread.Sleepলিখে গ্রিপ (grep) করুন। ফেক ঘড়িটিকে কার্যকর রাখতে এই কলগুলোকে প্রোভাইডার ব্যবহার করার জন্য রিফ্যাক্টর (refactoring) করাই একমাত্র উপায়।
সারসংক্ষেপ
সিস্টেম ঘড়িকে TimeProvider দিয়ে প্রতিস্থাপন করা এবং xUnit-এর SynchronizationContext ডিজেবল করা ধীরগতির রিট্রাই টেস্টগুলোকে প্রায় তাৎক্ষণিক চেকের রূপ দেয়, যা CI রিসোর্স সাশ্রয় করে এবং সময়-নির্ভর কোড যাচাই করা সহজ করে তোলে। এর জন্য প্রয়োজন মাত্র কয়েক লাইন কোড; আর এর প্রতিদান পাওয়া যায় প্রতিটি টেস্ট রানের ক্ষেত্রে সাশ্রয় হওয়া সেকেন্ডের মাধ্যমে।
