আপনার ইমেল টেস্টগুলো যদি ল্যাপটপে নিখুঁতভাবে চলে কিন্তু CI-তে পৌঁছানোর সাথে সাথেই ভেঙে পড়ে, তবে আপনি একা নন। সাধারণত এর সমাধান হিসেবে টেস্ট কোডে sleep কল ব্যবহার করা হয় অথবা বিল্ড পাস না হওয়া পর্যন্ত রিট্রাই (retry) কাউন্ট বাড়িয়ে দেওয়া হয়। এটি সাময়িকভাবে সমস্যা কমিয়ে দিতে পারে, কিন্তু এটি বাগ (bug) ঠিক করে না। এটি কেবল সমস্যাটিকে লুকিয়ে রাখে।
আসল সমস্যা হলো আপনার টেস্ট কীভাবে শনাক্ত করে যে কোন ইমেলটি ওপেন করতে হবে।
শেয়ারড ইনবক্সের সমস্যা (The Shared Inbox Problem)
আপনার লোকাল মেশিনে আপনি একবারে একটি টেস্ট চালান। একটি ইমেল আসে। আপনি সেটি নিয়ে নেন। সহজ ব্যাপার।
CI সম্পূর্ণ ভিন্ন একটি পরিবেশ। একটি মাত্র পুল রিকোয়েস্ট (pull request) চারটি, আটটি বা ষোলোটি প্যারালাল জব (parallel jobs) ট্রিগার করতে পারে। যদি তারা সবাই একটি টেস্ট ইনবক্স শেয়ার করে—তা Mailosaur সার্ভার হোক, Mailtrap ইনবক্স হোক বা কোনো স্টেজিং ডোমেইনের রিয়েল অ্যাকাউন্ট হোক—তবে তারা সবাই একই সময়ে একই বাকেটে (bucket) ডেটা লিখছে। জব A একটি পাসওয়ার্ড রিসেট পাঠায়। জব B একটি ইনভাইট পাঠায়। জব C একটি ফেইল হওয়া ওয়েলকাম ফ্লো (welcome flow) রিট্রাই করে। এরই মধ্যে, ব্যাকগ্রাউন্ড ওয়ার্কার এবং ডেলিভারি কিউ (delivery queues) এমন কিছু অনিশ্চয়তা (jitter) তৈরি করে যা আপনি নিয়ন্ত্রণ করতে পারেন না।
যখন প্রতিটি জব সেই শেয়ারড ইনবক্সে গিয়ে "Reset your password" সাবজেক্টের সবচেয়ে নতুন মেসেজটি চায়, তখন এটি একটি রেসে (race) পরিণত হয়। যে টেস্টটি জিতবে সেটি সঠিক ইমেলটি পাবে। আর যে টেস্টটি হেরে যাবে, সেটি অন্য কোনো জবের জন্য নির্ধারিত লিঙ্কে ক্লিক করে, ভুল কন্টেন্টের বিপরীতে অ্যাসার্ট (assert) করে এবং এমন একটি এরর (error) দিয়ে ফেইল করে যা দেখতে টাইমিং সমস্যার মতো মনে হয়। এটি টাইমিং সমস্যা নয়; এটি একটি আইডেন্টিটি (identity) বা শনাক্তকরণ সমস্যা।
কেন "Newest Message" পদ্ধতিটি ব্যর্থ হয়
এই ভঙ্গুর প্যাটার্নটি অনুসরণ করা সহজ কারণ এটি বেশ স্বাভাবিক মনে হয়:
- ইউজার ফ্লো ট্রিগার করুন।
- প্রতি কয়েক সেকেন্ড অন্তর ইনবক্স পোল (poll) করুন।
- সাবজেক্ট লাইনের সাথে মিলে যায় এমন সবচেয়ে সাম্প্রতিক মেসেজটি ওপেন করুন।
- প্রথম লিঙ্কটিতে ক্লিক করুন এবং অ্যাসারশন (assertions) চালান।
সাধারণ প্যারালেলিজম ছাড়াও আরও বেশ কিছু কারণে এটি ব্যর্থ হয়। আগের কোনো ফেইল হওয়া রানের রিট্রাই দেরিতে এসে পৌঁছাতে পারে, যা আপনার বর্তমান টেস্ট পোল করার ঠিক মুহূর্তেই হঠাৎ করে সবচেয়ে নতুন মেসেজ হয়ে যেতে পারে। আপনার অ্যাপ্লিকেশনের ভেতরের ব্যাকগ্রাউন্ড ওয়ার্কার দুটি ইমেল কিউ (queue) করতে পারে এবং প্রথমটির আগে দ্বিতীয়টি ডেলিভারি করতে পারে। শুধুমাত্র সাবজেক্ট লাইন একটি দুর্বল আইডেন্টিফায়ার; আপনার স্টেজিং অ্যাপ্লিকেশন বিভিন্ন পথ থেকে একই ধরনের ইমেল পাঠাতে পারে। টাইমস্ট্যাম্প (timestamp) অনুযায়ী সর্ট করা দেখতে যতটা সহজ মনে হয় তার চেয়েও বেশি জটিল, কারণ CI রানার এবং মেইল প্রোভাইডারের মধ্যে ক্লক স্কিউ (clock skew) বা সময়ের পার্থক্য থাকা খুবই স্বাভাবিক, এবং মেইল API-গুলো প্রায়ই তাদের ইনডেক্স ক্যাশ (cache) বা ব্যাচ (batch) করে রাখে।
ব্যস্ত পরিবেশে টাইমস্ট্যাম্প অস্পষ্ট হয়ে যায়। আপনার সরাসরি কিছু প্রয়োজন।
রান টোকেন (Run Token) আসলে কী
রান টোকেন হলো আপনার টেস্টের শুরুতে তৈরি করা একটি ইউনিক স্ট্রিং যা আপনার অ্যাপ্লিকেশন পাঠানো ইমেলে যুক্ত করা হয়। এটি ব্যবহারকারীর চোখে পড়ার মতো বা দেখতে সুন্দর হওয়ার প্রয়োজন নেই। এর একমাত্র কাজ হলো এটি নিশ্চিত করা যে, আপনি প্রমাণ করতে পারছেন এই নির্দিষ্ট মেসেজটি এই নির্দিষ্ট টেস্ট এক্সিকিউশনের অংশ।
বাস্তব উদাহরণগুলো সবচেয়ে ভালো কাজ করে। টেস্ট শুরু করার আগে, নিচের মতো একটি টোকেন তৈরি করুন:
- একটি UUID:
550e8400-e29b-41d4-a716-446655440001 - একটি build-scoped request ID:
req_ci_build_4821_a7f3 - একটি ইনভাইট স্ল্যাগ (slug) বা মেটাডেটা সাফিক্স:
signup-token-8k2m9n - টেস্ট রানার দ্বারা তৈরি একটি র্যান্ডম হেক্স স্ট্রিং:
test-run-a4f9c2d1
আপনি যদি ব্যাকএন্ড কোড নিয়ন্ত্রণ করতে পারেন, তবে ইমেল কনটেক্সটে টোকেনটি পাস করুন এবং বডির কোথাও এটি রেন্ডার করুন। আপনি যদি একটি ব্ল্যাক-বক্স (black-box) অ্যাপ্লিকেশনের বিরুদ্ধে টেস্ট করেন, তবে দেখুন অ্যাপটি ইতিমধ্যে এমন কোনো রেফারেন্স ফিল্ড গ্রহণ করে কি না যা আপনি ব্যবহার করতে পারেন। যদি না থাকে, তবে আপনি প্লাস অ্যাড্রেসিং (plus addressing) ব্যবহার করে প্রাপকের লোকাল-পার্টে টোকেনটি এমবেড করতে পারেন—যেমন testuser+a4f9c2d1@example.com—যদিও এটি কেবল তখনই কাজ করবে যদি আপনার অ্যাপ্লিকেশনটি এটিকে সংরক্ষণ করে এবং ইমেলে ফেরত পাঠায়।
মূল বিষয়টি হলো মেইল সিস্টেমের মালিকানাধীন মেটাডেটার ওপর ভিত্তি করে ম্যাচ করা বন্ধ করা। বরং আপনার টেস্টের মালিকানাধীন ডেটার ওপর ভিত্তি করে ম্যাচ করুন।
নির্ভরযোগ্য প্যাটার্ন (The Reliable Pattern)
"newest message" অ্যালগরিদমটিকে একটি সুনির্দিষ্ট, টোকেন-চালিত সার্চ দিয়ে প্রতিস্থাপন করুন:
- কোনো ফ্লো ট্রিগার করার আগেই রান টোকেনটি তৈরি করুন।
- ইউজার অ্যাকশন শুরু করুন এবং নিশ্চিত করুন যে অ্যাপ্লিকেশনটি আউটবাউন্ড ইমেলে টোকেনটি অন্তর্ভুক্ত করবে।
- সেই টোকেন দ্বারা সীমাবদ্ধ ফিল্টার ব্যবহার করে মেইল প্রোভাইডারকে পোল করুন। যদি API বডি সার্চ সাপোর্ট করে, তবে সেটি ব্যবহার করুন। যদি না করে, তবে সম্ভাব্য মেসেজগুলো নিয়ে ক্লায়েন্ট-সাইডে তাদের বডি
grepকরুন। - কোনো লিঙ্ক, বাটন বা ভেরিফিকেশন কোড স্পর্শ করার আগে মেসেজ বডিতে টোকেনটি আছে কি না তা অ্যাসার্ট করুন।
- কেবল তখনই কনফার্মেশন URL বা কোডটি এক্সট্র্যাক্ট করুন এবং পরবর্তী কাজ চালিয়ে যান।
এই ক্রমটি অত্যন্ত গুরুত্বপূর্ণ। আপনি যদি প্রথমে লিঙ্ক এক্সট্র্যাক্ট করেন এবং পরে টোকেন চেক করেন, তবে আপনি ইতিমধ্যে ভুল ইমেলে ক্লিক করে ফেলেছেন। অ্যাসারশন হলো আপনার গেটকিপার (gatekeeper)।
ব্যবহারিক ক্ষেত্রে, আপনার হেল্পারটির Subject:"Welcome to AppName" sort:-received-এর পরিবর্তে Subject:"Welcome to AppName" AND Body:"a4f9c2d1" খোঁজা উচিত। অনেক মেইল টেস্টিং সার্ভিস এমন সার্চ API প্রদান করে যা বডি কন্টেন্ট ফিল্টার গ্রহণ করতে পারে। সেগুলো ব্যবহার করুন। আপনি যদি কোনো সাধারণ প্রোভাইডারের সাথে কাজ করেন, তবে আপনার পোলিং লজিক (polling logic) এক জায়গায় রাখুন যাতে আপনি প্রতিটি টেস্টে একইভাবে ক্লায়েন্ট-সাইড ফিল্টারিং যোগ করতে পারেন।
সিস্টেমকে নির্ভুল রাখতে তিনটি নিয়ম
একটি রান টোকেন (run token) সিলেকশনকে নির্দিষ্ট করে দেয়, কিন্তু আপনি কীভাবে পোল করছেন এবং কোনো সমস্যা হলে কী করছেন, সে বিষয়ে আপনার শৃঙ্খলা বজায় রাখা প্রয়োজন।
ব্যর্থতার সময় ইনবক্সের অবস্থা লগ (log) করুন। যখন একটি টেস্ট ব্যর্থ হয়, তখন ইনবক্স আইডেন্টিফায়ার, আপনি যে সাবজেক্ট লাইনটি কুয়েরি করেছেন তা, সঠিক টাইমস্ট্যাম্প উইন্ডো এবং আপনার ক্রাইটেরিয়া অনুযায়ী কতগুলো মেসেজ মিলেছে তা আউটপুট হিসেবে দেখান। এটি একটি অস্পষ্ট "email not found" এররকে একটি সুনির্দিষ্ট তথ্যে রূপান্তরিত করে। যদি জব 7823, জব 7821-এর একটি রিট্রাই মেসেজ গ্রহণ করে কারণ সেটি তিন সেকেন্ড পরে এসেছে, তবে আপনার লগ সেটি স্পষ্টভাবে প্রকাশ করা উচিত। এই প্রেক্ষাপট ছাড়া, আপনি সময়ের দোষ দেবেন এবং আরও একটি sleep যোগ করবেন।
সব ইমেল পোলিং একটি হেল্পার ফাইলে রাখুন। setTimeout এবং cy.task কলগুলোকে বিশটি টেস্ট ফাইলের মধ্যে ছড়িয়ে দেবেন না। মেসেজের জন্য অপেক্ষা করা, API কল রিট্রাই করা এবং ব্যাকঅফ (backoff) প্রয়োগ করার লজিকটিকে কেন্দ্রীভূত করুন। যদি প্রতিটি টেস্ট একই হেল্পার ব্যবহার করে, তবে আপনার ফিল্টারিং নিয়মগুলো সামঞ্জস্যপূর্ণ থাকবে এবং আপনি যখন সার্চ লজিক উন্নত করবেন, তখন প্রতিটি টেস্টের সুবিধা হবে। এটি টোকেন চেক প্রয়োগ করাকেও সহজ করে তোলে; যদি হেল্পারটির জন্য একটি টোকেন আর্গুমেন্ট প্রয়োজন হয়, তবে কেউ ভুলবশত "latest message"-এর ওপর নির্ভর করতে পারবে না।
আপনার রিট্রাইগুলোর (retries) দিকে নজর দিন। CI-তে টেস্ট রিট্রাই হওয়া সাধারণ বিষয়, কিন্তু প্রতিটি রিট্রাই ইনবক্সে আরেকটি ইমেল তৈরি করে। যদি আপনার টেস্টটি তৃতীয় প্রচেষ্টায় সফল হয়, তবে আপনি হয়তো উদযাপন করে এগিয়ে যাবেন। আপনি যা মিস করছেন তা হলো, প্রথম এবং দ্বিতীয় প্রচেষ্টাগুলো একটি আসল বাগ প্রকাশ করেছিল—যেমন একটি race condition, ডুপ্লিকেট সেন্ড বা একটি missing index—যা অতিরিক্ত মেসেজগুলো ঢেকে দিয়েছিল। যদি আপনাকে রিট্রাই ব্যবহার করতেই হয়, তবে ব্যর্থতার পর ইনবক্সে অনাকাঙ্ক্ষিত ডুপ্লিকেট মেসেজ আছে কিনা তা পরীক্ষা করুন। আরও ভালো হয় যদি আপনি ইনবক্স পরিষ্কার করার কথা ভাবেন অথবা আপনার প্রোভাইডার ডাইনামিক ইনবক্স সাপোর্ট করলে প্রতিটি জবের জন্য একটি ইউনিক অ্যাড্রেস ব্যবহার করেন। রিট্রাই যেন অনির্ভরযোগ্য সিলেকশন লজিক ঢেকে রাখার কৌশল না হয়ে দাঁড়ায়।
মূল শিক্ষা
তারিখ অনুযায়ী ইনবক্স সর্ট করা এবং প্রথম ফলাফলটি নেওয়া কোনো টেস্টিং নয়। এটি কোডের ছদ্মবেশে কেবল একটি অনুমান মাত্র। একটি রান টোকেন ব্যবহার করতে প্রায় কিছুই খরচ হয় না—একটি স্ট্রিং ভেরিয়েবল, একটি অতিরিক্ত ফিল্টার প্যারামিটার, হয়তো একটি ছোট টেমপ্লেট পরিবর্তন—এবং এটি আপনার টেস্টকে একটি deterministic পরিচয় দেয়। এটি প্রমাণ করে যে আপনার সামনে থাকা মেসেজটি আপনি বর্তমানে যে রানটি চালাচ্ছেন তারই অংশ।
sleep যোগ করা এবং নেটওয়ার্ক ঠিকঠাক কাজ করবে এই আশায় বসে থাকা বন্ধ করুন। একটি টোকেন তৈরি করুন, সেটি ইমেলে রাখুন এবং সরাসরি সেটি দিয়ে সার্চ করুন। আপনার CI রানগুলো দ্রুততর হবে, আপনার লগগুলো পাঠযোগ্য হবে এবং আপনি অবশেষে আপনার ইমেল সুইট যা বলছে তা বিশ্বাস করতে পারবেন।
