আপনি যখন আজারবাইজানে একটি কার ব্রোকারেজ চালান এবং মার্কিন যুক্তরাষ্ট্র থেকে স্যালেজ ভেহিকেল (salvage vehicles) আমদানি করেন, তখন আপনার সফটওয়্যার সমস্যাগুলো সিলিকন ভ্যালির কোনো স্টার্টআপের থেকে সম্পূর্ণ আলাদা হয়। আপনি লক্ষ লক্ষ সমসাময়িক ব্যবহারকারীর (concurrent users) জন্য অপ্টিমাইজ করছেন না। আপনি অপ্টিমাইজ করছেন স্বচ্ছতা, আপটাইম এবং মাঝরাতে বারোটি টাইম জোন দূরে থাকা একটি অকশন হাউসের সাথে সমন্বয় করার সময় নিজে থেকে সবকিছু ঠিক করে ফেলার ক্ষমতার জন্য। আমি যখন AutoMakler তৈরি করছিলাম, তখন ঠিক এই পরিস্থিতির মধ্যেই ছিলাম। এই প্ল্যাটফর্মটি লাইভ অকশন স্ক্র্যাপিং এবং Carfax লুকআপ থেকে শুরু করে ডেলিভারি এস্টিমেট এবং পেমেন্ট প্রসেসিং পর্যন্ত সবকিছু সামলায়। এটি একটি প্রকৃত প্রোডাকশন সিস্টেম যা প্রকৃত গ্রাহকদের সেবা দেয়, এবং এটি এমন একটি স্ট্যাকের ওপর চলে যাকে বেশিরভাগ ডেভেলপার অত্যন্ত সাদামাটা বা 'বোরিং স্ট্যাক' বলবেন।

এমন একটি স্ট্যাক যা কেউ প্রচার করতে চায় না

এখানে কোনো React নেই। কোনো Vue নেই। কোনো Redis নেই, কোনো Celery নেই এবং কোনো WebSocket সার্ভারও নেই। ব্যাকএন্ড হলো সাধারণ Python সহ FastAPI। ডাটাবেস হলো PostgreSQL। ফ্রন্টএন্ড হলো Jinja2 টেমপ্লেট, Bootstrap এবং সামান্য ভ্যানিলা JavaScript ব্যবহার করে সার্ভার-রেন্ডারড HTML। স্ক্র্যাপিংয়ের জন্য আমি Playwright ব্যবহার করি। সবকিছু একটি সিঙ্গেল Python প্রসেস হিসেবে চলে যা সরাসরি HTML সার্ভ করে।

এখানে কোনো বিল্ড স্টেপ নেই। অডিট করার জন্য কোনো node_modules ফোল্ডার নেই, কনফিগার করার জন্য কোনো ট্রান্সপিলার নেই এবং তাল মিলিয়ে চলার জন্য কোনো ফ্রন্টএন্ড ফ্রেমওয়ার্কের পরিবর্তনশীলতা নেই। আমি যখন ডেপ্লয় করি, তখন আমি পাইথন ফাইল এবং টেমপ্লেট মুভ করি, কোনো বান্ডলারের পাইপলাইন অর্কেস্ট্রেট করি না। এই সরলতা কোনো আপস নয়। এটিই মূল উদ্দেশ্য।

মেসেজ ব্রোকার ছাড়াই কীভাবে জব কিউ (Queue) করবেন

একটি লাইভ কার অকশন স্ক্র্যাপিং সিনক্রোনাসলি (synchronously) করা সম্ভব নয়। Playwright যখন পেজ লোড করে, JavaScript এক্সিকিউট করে এবং ডেটা এক্সট্র্যাক্ট করে, তখন একটি সিঙ্গেল স্ক্র্যাপ করতে কয়েক সেকেন্ড সময় নিতে পারে। এই সময়ে ব্যবহারকারীকে আটকে রাখা কোনো সমাধান নয়। স্ট্যান্ডার্ড নিয়ম অনুযায়ী Redis ইনস্টল করা, Celery কনফিগার করা এবং একটি ওয়ার্কার পুল তৈরি করার কথা বলা হয়। আমি এই সবকিছুই বাদ দিয়েছি।

এর পরিবর্তে, AutoMakler তার নিজস্ব জব কিউ হিসেবে Postgres ব্যবহার করে। যখন একজন ব্যবহারকারী একটি স্ক্র্যাপ ট্রিগার করেন, অ্যাপ্লিকেশনটি একটি 'pending' স্ট্যাটাসসহ 'tasks' টেবিলে একটি নতুন রো (row) লিখে রাখে। একটি asyncio ব্যাকগ্রাউন্ড টাস্ক সেই রোটি সংগ্রহ করে এবং ব্রাউজার স্ক্র্যাপিং শুরু করে। ইতিমধ্যে, ব্রাউজার স্ট্যাটাস চেক করার জন্য প্রতি তিন সেকেন্ড অন্তর একটি লাইটওয়েট এন্ডপয়েন্ট (endpoint) পোল (poll) করে। যখন রোটি 'completed' হিসেবে আপডেট হয়, তখন পেজটি রিফ্রেশ হয় এবং ফলাফল প্রদর্শন করে।

এই প্যাটার্নটি কাজ করে কারণ পোলিং ইন্টারভালটি রেসপন্সিভ মনে হওয়ার জন্য যথেষ্ট ছোট, আবার সার্ভারের ওপর চাপ এড়ানোর জন্য যথেষ্ট বড়। একটি কম্পিউটারের জন্য তিন সেকেন্ড অনেক দীর্ঘ সময়, কিন্তু একটি এক্সটার্নাল অকশন সাইটের জন্য অপেক্ষা করা মানুষের কাছে এটি খুব একটা বোঝা যায় না। ডাটাবেসটি নেটিভলি কনকারেন্সি হ্যান্ডেল করে, এবং যেহেতু জবগুলো কেবল Postgres-এর রো, তাই আমি Celery লগ বা Redis কী খোঁজার পরিবর্তে একটি সাধারণ SQL কুয়েরি দিয়ে কিউটি পরিদর্শন করতে পারি।

ওয়ার্কার পুল ছাড়াই সার্ভার সচল রাখা

ব্রাউজার অটোমেশন প্রচুর মেমরি ব্যবহার করে। একসাথে অনেক বেশি Playwright ইনস্ট্যান্স চালু করলে আপনার সার্ভার ভেঙে পড়বে। প্রচলিত সমাধান হলো কনকারেন্সি লিমিটসহ একটি ম্যানেজড ওয়ার্কার পুল, যা প্রায়শই সেই একই Redis এবং Celery কম্বিনেশন দ্বারা পরিচালিত হয়। আমি পাইথনের মাত্র একটি লাইন ব্যবহার করি: একটি asyncio.Semaphore

সেমাফোর (Semaphore) নির্ধারণ করে দেয় যে একসাথে কতগুলো ব্রাউজার ইনস্ট্যান্স চলতে পারে। যখন একটি নতুন স্ক্র্যাপ রিকোয়েস্ট আসে, এটি হয় সাথে সাথে একটি স্লট দখল করে অথবা একটি স্লট খালি না হওয়া পর্যন্ত অপেক্ষা করে। এই সবকিছুই একই প্রসেসের ভেতরে ঘটে। এখানে ব্যর্থ হওয়ার মতো কোনো এক্সটার্নাল অর্কেস্ট্রেটর নেই, নিঃশব্দে মারা যাওয়ার মতো কোনো ওয়ার্কার প্রসেস নেই এবং মনিটর করার জন্য কোনো অতিরিক্ত ইনফ্রাস্ট্রাকচার নেই। আমার মেমরি ব্যবহার অনুমানযোগ্য থাকে এবং সার্ভারকে রক্ষা করার কোডটি ঠিক সেই কোডের পাশেই থাকে যা এটি ব্যবহার করে, কোনো ডেপ্লয়মেন্ট ম্যানিফেস্টে লুকিয়ে থাকে না।

একটি কলব্যাক URL দিয়ে টাকা লেনদেন পরিচালনা করা

পেমেন্ট প্রসেসিং এমন একটি সীমাবদ্ধতা তৈরি করেছিল যা আমি পরিবর্তন করতে পারতাম না। আমার পেমেন্ট গেটওয়ে প্রতিটি মার্চেন্ট অ্যাকাউন্টের জন্য ঠিক একটি কলব্যাক URL অনুমতি দেয়, কিন্তু আমার প্রয়োজন ছিল সেই একটি অ্যাকাউন্ট থেকেই দুটি আলাদা প্রজেক্টের লেনদেন প্রসেস করা। দ্বিতীয় একটি মার্চেন্ট প্রোফাইল তৈরি করার অর্থ হতো অতিরিক্ত ফি, অতিরিক্ত কমপ্লায়েন্স এবং অতিরিক্ত কাগজপত্র—যার জন্য একটি ছোট ব্রোকারেজের হাতে সময় নেই।

এর সমাধান ছিল গ্রাহককে গেটওয়েতে পাঠানোর আগে অর্ডার আইডি স্ট্রিংয়ের মধ্যেই সরাসরি প্রজেক্টের নাম এনকোড করে দেওয়া। যখন কলব্যাকটি আমার সার্ভারে আসে, AutoMakler সেই আইডিটি ডিকোড করে, পেমেন্টটি কোন প্রজেক্টের তা শনাক্ত করে এবং সঠিক ইন্টারনাল হ্যান্ডলারের কাছে নোটিফিকেশন পাঠিয়ে দেয়। বিদ্যমান লজিক অপরিবর্তিত ছিল। এটি হলো অ্যাডিটিভ ডিজাইন (additive design): আমি পেমেন্ট ফ্লো নতুন করে লিখিনি, আমি শুধু আইডেন্টিফায়ারটিকে কিছুটা অতিরিক্ত কনটেক্সট প্রদান করেছি। এটি এমন এক ধরণের হ্যাক যা পরে দেখলে খুব সহজ মনে হয়, কিন্তু এটি ঘণ্টার পর ঘণ্টা জটিল আর্কিটেকচারাল কাজ বাঁচিয়ে দেয়।

WebSockets ছাড়াই কার্যকর চ্যাট

কাস্টমার সাপোর্ট চ্যাট সাধারণত এমন একটি জায়গা যেখানে ইঞ্জিনিয়াররা হার মেনে WebSockets যোগ করে দেন। আমার ইন-অ্যাপ মেসেজিং প্রয়োজন ছিল, কিন্তু একই সাথে ইনফ্রাস্ট্রাকচার ফুটপ্রিন্টও খুব ছোট রাখতে চেয়েছিলাম। তাই আমি সেই একই পোলিং স্ট্র্যাটেজি ব্যবহার করেছি যা অকশন স্ক্র্যাপস (auction scrapes) পরিচালনা করে।

মেসেজগুলো Postgres-এ সংরক্ষিত থাকে। যখন কোনো ইউজার মেসেজ পাঠান, সেটি টেবিলে লেখা হয়। ক্লায়েন্ট আপডেটের জন্য পোল (poll) করে এবং UI প্রায় রিয়েল-টাইমে নতুন মেসেজ এবং রিড রিসিট (read receipts) প্রদর্শন করে। কনভারসেশন টেবিল বড় হওয়ার সাথে সাথে এটি দ্রুত রাখতে আমি একটি Postgres partial index যোগ করেছি যা শুধুমাত্র সক্রিয় কনভারসেশনের আনরিড (unread) মেসেজগুলোকে কভার করে। ডাটাবেস পুরনো হিস্ট্রি স্ক্যান করে সময় নষ্ট করে না এবং কুয়েরি প্ল্যানার একটি টাইট ইনডেক্স রেঞ্জ স্ক্যানের মাধ্যমে বেশিরভাগ চ্যাট লুকআপ সম্পন্ন করতে পারে।

একটি সাপোর্ট চ্যাটের জন্য যেখানে কয়েক সেকেন্ডের ল্যাটেন্সি (latency) গ্রহণযোগ্য, সেখানে এটি সম্পূর্ণ পর্যাপ্ত। ইউজাররা তাদের প্রয়োজনীয় ফিডব্যাক পান এবং আমাকে কখনোই কোনো stale WebSocket কানেকশন ডিবাগ করতে বা আলাদা কোনো সকেট সার্ভার ম্যানেজ করতে হয়নি।

প্রকৃত অসুবিধাগুলো

এই আর্কিটেকচারে কিছু বাস্তবসম্মত ট্রেড-অফ (trade-offs) করতে হয়, আর অন্য কিছু দাবি করা হবে অসততা। পোলিং পদ্ধতিটি বেশ চ্যাটি (chatty)। প্রতি তিন সেকেন্ড অন্তর প্রতিটি সক্রিয় ক্লায়েন্ট সার্ভারে রিকোয়েস্ট পাঠায়। ব্যান্ডউইথ এবং কুয়েরি লোড একটি পারসিস্টেন্ট সকেট কানেকশনের তুলনায় বেশি হয়। যদি Python প্রসেসটি রিস্টার্ট হয়, তবে চলমান (in-flight) যেকোনো ব্যাকগ্রাউন্ড টাস্ক সাথে সাথে বন্ধ হয়ে যায় কারণ এটিকে পুনরায় শুরু করার জন্য কোনো এক্সটার্নাল ওয়ার্কার নেই। আমি এটি মেনে নিয়েছি কারণ টাস্কগুলো ছোট এবং পুনরায় চেষ্টা করার (retry) খরচও কম। একটি ব্যর্থ ব্রাউজার স্ক্র্যাপ ইউজার নিজেই পুনরায় ট্রিগার করতে পারেন।

এই পদ্ধতিরও একটি সীমাবদ্ধতা আছে। যদি AutoMakler-এর কখনো হাজার হাজার সমসাময়িক (simultaneous) স্ক্র্যাপ সার্ভিস দেওয়ার প্রয়োজন হয়, তবে পোলিং-ভিত্তিক এই সিঙ্গেল-প্রসেস মডেলটি চাপের মুখে পড়বে। কিন্তু আমার ব্যবসার ধরন তেমন নয়। আমার প্রয়োজন ডজন ডজন কনকারেন্ট ইউজারের জন্য নির্ভরযোগ্যতা, নয়তো