একদম শুরু থেকে একটি অপারেটিং সিস্টেম তৈরি করা শুনতে অনেকটা C ভাষায় কোড লেখা কার্নেল হ্যাকারদের কাজের মতো মনে হয়। কিন্তু আপনি একটি বিকেলেই পাইথনে একটি সহজ সিমুলেশন তৈরি করতে পারেন, এবং আপনি দ্রুত আবিষ্কার করবেন যে প্রসেস ম্যানেজমেন্টের লজিক একটি হাই-লেভেল ল্যাঙ্গুয়েজেও ঠিক ততটাই কঠিন। আমি এটি কঠিনভাবে শিখেছি। আমি একটি ছোট ওএস সিমুলেটর লেখার জন্য বসেছিলাম। লক্ষ্য ছিল সামান্য: কয়েকটি প্রসেস তৈরি করা, সেগুলোকে শিডিউল করা এবং কাজ শেষ হয়ে গেলে সেগুলোকে 'finished' হিসেবে চিহ্নিত করা। কোডটি ছিল ছোট। লজিকটি ছিল নিখুঁত বলে মনে হচ্ছিল। তারপর আমি এটি রান করলাম, এবং কোনো কিছুই শেষ হচ্ছিল না।

কেন পাইথনে একটি মিনি ওএস তৈরি করবেন?

একটি আসল অপারেটিং সিস্টেম মেমরি পেজিং, ফাইল সিস্টেম, হার্ডওয়্যার ইন্টারাপ্ট এবং ডিভাইস ড্রাইভার সামলায়। একটি সিমুলেশন এই সবকিছু সরিয়ে ফেলে আপনাকে মূল ধারণায় মনোনিবেশ করতে দেয়: স্টেট (state)। আপনি একটি প্রসেস সংজ্ঞায়িত করেন। এর একটি PID, একটি বার্স্ট টাইম (burst time) এবং একটি লাইফসাইকেল স্ট্যাটাস থাকে। Ready. Running. Finished. একটি শিডিউলার লুপ পরবর্তী প্রার্থীকে বেছে নেয়, তার স্টেট পরিবর্তন করে, একটি টাইম স্লাইস সিমুলেট করে এবং তাকে 'done' অবস্থায় নিয়ে যায়।

পাইথন এই ধরণের পরীক্ষার জন্য একটি চমৎকার মাধ্যম কারণ এটি আপনাকে পয়েন্টার অ্যারিথমেটিক (pointer arithmetic) এবং মেমরি অ্যালাইনমেন্ট (memory alignment) উপেক্ষা করতে দেয়। ডিকশনারির একটি লিস্ট আপনার প্রসেস টেবিল হয়ে ওঠে। একটি while লুপ আপনার কার্নেল শিডিউলার হয়ে ওঠে। আপনি স্ট্যান্ডার্ড লাইব্রেরি টুল দিয়ে রাউন্ড-রবিন শিডিউলিং বা প্রায়োরিটি কিউ (priority queues) ইমপ্লিমেন্ট করতে পারেন। এটি বেশ সহজবোধ্য মনে হয়, আর ঠিক এই কারণেই পরবর্তী বাগটি এত বিরক্তিকর ছিল।

সেটআপ

আমার সিমুলেশনে process_table নামে একটি লিস্ট ব্যবহার করা হয়েছিল। প্রতিটি এন্ট্রি ছিল এইরকম একটি ডিকশনারি:

{
    "pid": 1,
    "burst_time": 3,
    "status": "ready"
}

শিডিউলার একটি সাধারণ while লুপ চালাত। এটি টেবিলটি স্ক্যান করে এমন প্রথম প্রসেসটি খুঁজত যার স্ট্যাটাস "finished" ছিল না। যখন এটি একটি প্রসেস খুঁজে পেত, তখন এটি একটি হেল্পার ফাংশন, execute_tick(p) কল করত যাতে সেই প্রসেসটিকে একটি সিমুলেটেড সাইকেলের জন্য চালানো যায়। execute_tick-এর ভেতরে, আমি প্রসেসের স্ট্যাটাস "running" হিসেবে সেট করতাম, বার্স্ট টাইম কমিয়ে দিতাম এবং চেক করতাম অবশিষ্ট কাজ শূন্য হয়েছে কি না। যদি হতো, আমি স্ট্যাটাস আপডেট করে "finished" করে দিতাম। বাইরের লুপটি তখনই শেষ হওয়ার কথা ছিল যখন প্রতিটি প্রসেস 'finished' অবস্থায় পৌঁছাবে।

কাগজে-কলমে ফ্লো বা প্রবাহটি ছিল পরিষ্কার। একটি রেডি প্রসেস খুঁজে বের করো। সেটি রান করো। কাজ শেষ হয়েছে কি না চেক করো। শেষ না হওয়া পর্যন্ত পুনরাবৃত্তি করো। এমনকি শিডিউলার কীভাবে কাজ করছে তা দেখার জন্য আমি প্রিন্ট স্টেটমেন্টও যোগ করেছিলাম। আমি দেখতে পাচ্ছিলাম যে প্রসেসগুলো নির্বাচন করা হচ্ছে। লুপটি চলতেই ছিল। তবুও প্রসেসগুলো যেন এক অনন্ত বর্তমান সময়ে আটকে গিয়েছিল, চিরকাল চলছে কিন্তু কখনোই শেষ হচ্ছে না।

লক্ষণ

এটি হলো সবথেকে খারাপ ধরণের ব্যর্থতা: নীরব ব্যর্থতা। টার্মিনালে কোনো স্ট্যাক ট্রেস (stack trace) দেখা দেয়নি। কোনো IndexError বা KeyError আমাকে কোনো সূত্র দেয়নি। ইন্টারপ্রেটার একদম ঠিকঠাক ছিল। প্রোগ্রামটি কেবল প্রত্যাশিত আচরণ করছিল না। প্রসেসগুলো শুরু হচ্ছিল, কিন্তু সেগুলো কখনোই শেষ হচ্ছিল না। আমি ঘণ্টার পর ঘণ্টা ফ্লোটি পুনরায় পরীক্ষা করলাম।

লুপ কন্ডিশন কি ভুল ছিল? হয়তো বার্স্ট টাইম গণনায় আমার কোনো off-by-one এরর ছিল। প্রসেস টেবিলটি কি ইন-প্লেস আপডেট হওয়ার পরিবর্তে শ্যাডো বা কপি হচ্ছিল? আমার টার্মিনেশন কন্ডিশন কি ভুল কি (key) চেক করছিল? আমি আরও প্রিন্ট স্টেটমেন্ট যোগ করলাম। আমি প্রতিটি বুলিয়ান এক্সপ্রেশন পরীক্ষা করলাম। আমি এমন একটি লাইন ছাড়া সবকিছু নিয়েই প্রশ্ন তুললাম যা আসলে গুরুত্বপূর্ণ ছিল।

অপরাধী

তারপর আমি সেটি দেখতে পেলাম। execute_tick-এর ভেতরে আমি লিখেছিলাম:

p["status"] == "running"

দুটি সমান চিহ্ন। এটি একটি তুলনা (comparison), অ্যাসাইনমেন্ট (assignment) নয়। সমাধানটি ছিল মাত্র একটি ক্যারেক্টারের ব্যবধানে:

p["status"] = "running"

পাইথনে, p["status"] == "running" একটি সম্পূর্ণ বৈধ এক্সপ্রেশন। এটি True বা False রিটার্ন করে, এবং তারপর ইন্টারপ্রেটার ফলাফলটি ফেলে দেয় কারণ আমি এটি কোথাও অ্যাসাইন করিনি। এই লাইনটি আসলে কিছুই করছে না। ডিকশনারি এন্ট্রিটি অপরিবর্তিত থেকে গেল, এর আগের স্ট্যাটাসটি বজায় রাখল, এবং প্রসেসটি তার লাইফসাইকেলের পরবর্তী ধাপে এগোতে পারল না।

আমি এটি পরিবর্তন করে একটি মাত্র সমান চিহ্ন দিয়ে দিলাম। আমি স্ক্রিপ্টটি আবার রান করলাম। সিমুলেশনটি প্রাণ ফিরে পেল। প্রসেসগুলো ঠিক পরিকল্পনা অনুযায়ী ready, running এবং finished অবস্থার মধ্য দিয়ে চক্রাকারে চলল। একটি অতিরিক্ত কি-স্ট্রোকের জন্য আমার ঘণ্টার পর ঘণ্টা সময় নষ্ট হলো।

কেন এই বাগগুলো লুকিয়ে থাকে

এটি এত বেশি যন্ত্রণাদায়ক হওয়ার কারণ হলো, পাইথন কোনো এক্সপ্রেশন স্টেটমেন্টকে এরর হিসেবে চিহ্নিত করে না যদি না সেটি সরাসরি ইনভ্যালিড সিনট্যাক্স হয়। বাগটি ছিল একটি সিম্যান্টিক টাইপো (semantic typo)। প্রোগ্রামটি স্ট্যাটাস তুলনা করেছিল, একটি বুলিয়ান তৈরি করেছিল এবং সেটি ফেলে দিয়েছিল। যেহেতু তুলনাটি নিজেই False রিটার্ন করতে পারত, তাই প্রসেসটি তার আগের স্টেটেই আটকে ছিল এবং বাইরের লুপটির ব্রেক করার কোনো কারণ ছিল না।

আপনি একে কনফার্মেশন বায়াস (confirmation bias) দ্বারা আরও জটিল করে তোলেন। আপনি জানেন যে আপনি একটি অ্যাসাইনমেন্ট টাইপ করেছেন কারণ আপনি একটি অ্যাসাইনমেন্ট করার উদ্দেশ্যেই তা করেছিলেন। যখন আপনি পঞ্চমবার কোডটি পড়েন, আপনার মস্তিষ্ক প্রতীকটিকে স্বয়ংক্রিয়ভাবে সংশোধন করে নেয়। এই কারণেই রাবার ডাকিং (rubber ducking) কাজ করে। এটি আপনাকে প্রতিটি লাইন এত ধীরে স্পষ্টভাবে বলতে বাধ্য করে যে, যা লেখা হয়েছে এবং আপনি যা বোঝাতে চেয়েছিলেন—এই দুইয়ের মধ্যকার ব্যবধানটি দৃশ্যমান হয়ে ওঠে।

এই ধরণের ছোট বাগগুলো বড় ধরণের ক্র্যাশ বা বিপর্যয়ের চেয়ে খুঁজে পাওয়া অনেক বেশি কঠিন। একটি segfault বা syntax error তাৎক্ষণিকভাবে নিজেকে প্রকাশ করে দেয়। একটি নীরব no-op কেবল স্টেট (state) নষ্ট করে দেয় এবং প্রোগ্রামটিকে কোনোমতে চলতে সাহায্য করে। ব্যর্থতাটি ঘটে অনেক পরে (downstream), এবং আপনার সহজাত প্রবৃত্তি হলো কারণের পরিবর্তে উপসর্গটি (symptom) ডিবাগ করা।

একটি উন্নত প্রতিরক্ষা

আপনি কেবল নিজের চোখের ওপর ভরসা করতে পারেন না। এই ঘটনার পর, আমি কিছু অভ্যাস পরিবর্তন করেছি যা ভুলটি আরও আগেই ধরতে পারত।

প্রথমত, আপনি যদি একটি ডিকশনারিতে স্টেট (state) বজায় রাখেন, তবে প্রসেস স্টেটের জন্য dataclass বা enum.Enum ব্যবহার করার কথা বিবেচনা করুন। আপনার স্ট্যাটাসগুলোকে কনস্ট্যান্ট (constant) বা enum মেম্বার হিসেবে সংজ্ঞায়িত করুন:

from enum import Enum

class ProcessState(Enum):
    READY = "ready"
    RUNNING = "running"
    FINISHED = "finished"

এক্সপ্লিসিট টাইপ (explicit types) ব্যবহারের ফলে, mypy-এর মতো টুলগুলো স্ট্যাটিক অ্যানালাইসিসের (static analysis) সময় সন্দেহজনক তুলনাগুলোকে চিহ্নিত করতে পারে। যেখানে একটি অ্যাসাইনমেন্ট থাকা উচিত ছিল সেখানে ভুলবশত একটি তুলনা হয়ে গেলে, টাইপগুলো প্রত্যাশা অনুযায়ী না মিললে তা ধরা অনেক সহজ হয়ে যায়।

দ্বিতীয়ত, শিডিউলার লজিক (scheduler logic) লেখার আগেই স্টেট ট্রানজিশনগুলোর (state transitions) জন্য ইউনিট টেস্ট (unit tests) লিখুন। একটি সাধারণ টেস্ট যা একটি প্রসেস তৈরি করে যার মাত্র এক টিক (tick) কাজ আছে, শিডিউলার চালায় এবং নিশ্চিত করে যে চূড়ান্ত স্টেটটি FINISHED, তা তাৎক্ষণিকভাবে ব্যর্থ হতো। সেই ব্যর্থতাটি আমাকে পুরো লুপ জুড়ে ঘুরে বেড়াতে না দিয়ে বরং স্টেট আপডেট লজিকের মধ্যেই অনুসন্ধানকে সীমাবদ্ধ রাখত।