Express রাউটের ভেতরে ইমেজ রিসাইজিং চালানো বিপর্যয়ের একটি বড় কারণ হতে পারে। একজন ব্যবহারকারী দশ মেগাবাইটের একটি ছবি আপলোড করলে আপনার সার্ভার পিক্সেল প্রসেস করতে শুরু করে এবং ত্রিশ সেকেন্ড পরে রিকোয়েস্টটি টাইম-আউট হয়ে যায়। ঠিক এই ধরনের সমস্যা এড়াতেই ব্যাকগ্রাউন্ড জব কিউ (Background job queues) ব্যবহার করা হয়। Node.js ইকোসিস্টেমে, Redis-এর মাধ্যমে অ্যাসিনক্রোনাস কাজ পরিচালনার জন্য Bull এবং BullMQ হলো দুটি প্রধান নাম। এদের মূল ভিত্তি একই হলেও দর্শন এবং প্রতিদিনের ব্যবহারের ক্ষেত্রে এদের মধ্যে বড় পার্থক্য রয়েছে। সঠিকটি বেছে নেওয়া অত্যন্ত গুরুত্বপূর্ণ, কারণ পরবর্তীতে একটি থেকে অন্যটিতে পরিবর্তন করা কেবল একটি প্যাকেজ আপডেট করার মতো সহজ কাজ নয়।

The Shared Foundation

উভয় লাইব্রেরিই তাদের মূল ভিত্তি হিসেবে Redis ব্যবহার করে। Redis অ্যাটমিক অপারেশন (atomic operations), বিলম্বিত জবের জন্য সর্টেড সেট (sorted sets) এবং ইভেন্টের জন্য পাব/সাব (pub/sub) পরিচালনা করে। আপনি যদি ক্যাশিং বা সেশনের জন্য ইতিমধ্যে Redis ব্যবহার করে থাকেন, তবে একটি জব কিউ যোগ করার জন্য নতুন কোনো অবকাঠামোর প্রয়োজন নেই। Bull এবং BullMQ উভয়ই প্রায়োরিটি (priorities), ব্যাকঅফসহ রিট্রাই (retries with backoff), কনকারেন্সি কন্ট্রোল (concurrency controls) এবং রিপিটেবল জব (repeatable jobs) সমর্থন করে। এই মিলগুলো পছন্দ করাকে সহজ করার বদলে আরও কঠিন করে তোলে। আপনি কেবল ফিচারের তালিকা দেখে সিদ্ধান্ত নিতে পারবেন না। পরিবর্তে, আপনাকে দেখতে হবে প্রতিটি লাইব্রেরি আপনার কোড কীভাবে সাজাতে চায়।

Bull: The Battle-Tested Veteran

Bull বহু বছর ধরে বাজারে আছে এবং হাজার হাজার প্রোডাকশন অ্যাপ্লিকেশনে ব্যবহৃত হচ্ছে। এটি কার্যকর। এর API সবকিছুকে একটি একক Queue ইনস্ট্যান্সের মধ্যে নিয়ে আসে। আপনি এটি ইনস্ট্যান্সিয়েট করেন, একটি প্রসেসিং ফাংশন সংজ্ঞায়িত করেন এবং একই অবজেক্টের মাধ্যমে ইভেন্টগুলো পর্যবেক্ষণ করেন। আপনি যদি পুরনো Node.js প্যাটার্ন থেকে আসেন, তবে এই মনোলিথিক ডিজাইনটি আপনার কাছে পরিচিত মনে হবে। যেসব কোডবেস ব্যাপক async/await আসার আগের সময়ের, তাদের জন্য Bull ব্যবহার করা স্বাভাবিক কারণ এটি কলব্যাক এবং পুরনো Redis ক্লায়েন্টের সাথেই বিকশিত হয়েছে।

এর অসুবিধা হলো টাইট কাপলিং (tight coupling)। যখন আপনার API সার্ভার একটি জব তৈরি করে, তখন এটি সেই একই Queue অবজেক্ট ইম্পোর্ট করে যাতে ওয়ার্কার লজিকও থাকে। বাস্তবে এর মানে হলো আপনার ওয়েব প্রসেস এমন সব ডিপেন্ডেন্সি টেনে আনে যা আসলে তার চালানোর প্রয়োজন নেই। এটি কোনো মারাত্মক ত্রুটি নয়, তবে এটি ক্লিন আর্কিটেকচারের ক্ষেত্রে বাধা সৃষ্টি করে। সাধারণ কাজের জন্য আপনি হয়তো এটি খেয়ালও করবেন না। কিন্তু ডজন ডজন মডিউল বিশিষ্ট বড় টিমের ক্ষেত্রে এই সমস্যাটি বাড়তে থাকে।

BullMQ: A Ground-Up Rebuild

BullMQ হলো এর অফিসিয়াল উত্তরসূরি। এটি প্রথম দিন থেকেই TypeScript-এ পুনরায় লেখা হয়েছে, তাই টাইপগুলো (types) জাভাস্ক্রিপ্ট সোর্সের ওপর পরে জুড়ে দেওয়া কোনো বিষয় নয়। এর API দায়িত্বগুলোকে আলাদা আলাদা ক্লাসে ভাগ করে দেয়। Queue জব যোগ করার কাজ সামলায়। Worker সেগুলো প্রসেস করে। QueueEvents অবজারভেবিলিটি (observability) বা পর্যবেক্ষণ সামলায়। এই বিভাজন আধুনিক ডিস্ট্রিবিউটেড সিস্টেমগুলো যেভাবে কাজ করে তার প্রতিফলন। আপনার API পডগুলোর (pods) শুধুমাত্র Queue ক্লাস এবং একটি Redis কানেকশন প্রয়োজন। আপনার ওয়ার্কার পডগুলো Worker ক্লাস ইম্পোর্ট করে। এই সীমানাটি কেবল ধারণাগত নয়, বরং বাস্তব বা ফিজিক্যাল।

বড় টিমের ক্ষেত্রে এই পরিবর্তনটি দারুণ ফল দেয়। একজন ডেভেলপার নতুন ফিচার রিলিজ করার সময় প্রসেসরটি কোন ফাইলে আছে তা না জেনেই একটি জব এনকিউ (enqueue) করতে পারেন। কম্পাইলার রানটাইমের পরিবর্তে জব ডেটা এবং হ্যান্ডলারের মধ্যে টাইপ মিসম্যাচগুলো আগেই শনাক্ত করে ফেলে। আধুনিক Node.js-এ এর async/await API ব্যবহার করাও খুব স্বাভাবিক মনে হয়। আপনাকে পুরনো বা লেগাসি কনভেনশনের সাথে লড়াই করতে হবে না।

Job Flows: From Hacks to First-Class Citizens

মাল্টি-স্টেপ ওয়ার্কফ্লোর ক্ষেত্রে এই দুটি লাইব্রেরির মধ্যে সবচেয়ে বড় পার্থক্য দেখা যায়।

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

BullMQ এতে FlowProducer প্রবর্তন করেছে। আপনি জবের একটি ট্রি (tree) বা কাঠামো তৈরি করতে পারেন যেখানে প্যারেন্ট জবগুলো স্বয়ংক্রিয়ভাবে তাদের চাইল্ড জবের জন্য অপেক্ষা করে। ইনভয়েসিংয়ের উদাহরণে, আপনি finalize-order নামে একটি রুট জব তৈরি করবেন যার তিনটি চাইল্ড থাকবে: reserve-inventory, charge-payment, এবং generate-pdf। আপনি ইমেল নোটিফিকেশনকে PDF জবের একটি চাইল্ড হিসেবে রাখতে পারেন। Redis এই গ্রাফ স্ট্রাকচারটি সংরক্ষণ করে। প্রতিটি ডিপেন্ডেন্সি সফল হওয়ার পরেই প্যারেন্ট জবটি সক্রিয় হয়। যদি একটি চাইল্ড ব্যর্থ হয়, তবে পুরো শাখাটি থেমে যায়। আপনাকে কোনো পোলিং লুপ বা রিকার্সিভ জব স্পনার লিখতে হবে না। এটি কেবল সিনট্যাকটিক সুগার (syntactic sugar) নয়; এটি আপনার বিজনেস লজিক মডেল করার পদ্ধতিই বদলে দেয়।

Rate Limiting: Blunt Instrument vs. Scalpel

উভয় লাইব্রেরিই থ্রুপুট (throughput) নিয়ন্ত্রণ করতে পারে, তবে এর সূক্ষ্মতা বা গ্র্যানুলারিটি (granularity) বহুগুণ আলাদা।

Bull প্রতিটি কিউ-এর (queue) জন্য রেট লিমিট প্রয়োগ করে। আপনি যদি একটি কিউ-তে প্রতি সেকেন্ডে একশটি জব প্রসেস করার জন্য সেট করেন, তবে সেই সীমা কিউ-এর প্রতিটি জবের জন্য সমানভাবে প্রযোজ্য হবে। এটি সমজাতীয় (homogeneous) কাজের চাপের জন্য ঠিক আছে। কিন্তু মাল্টি-টেন্যান্ট (multitenant) SaaS প্ল্যাটফর্মের ক্ষেত্রে এটি অকার্যকর হয়ে পড়ে। কল্পনা করুন একজন 'নয়েজি' (noisy) কাস্টমার একটি শেয়ার্ড কিউ-তে দশ লক্ষ ওয়েবহুক ডেলিভারি ঢেলে দিচ্ছে। Bull-এর কিউ-লেভেল লিমিটের মানে হলো, আপনি অন্য সবাইকে ধীর না করে শুধুমাত্র সেই নির্দিষ্ট টেন্যান্টের গতি কমাতে পারবেন না। আপনার কাছে বিকল্পগুলো খুব একটা সুখকর নয়। হয় প্রতিটি কাস্টমারের জন্য আলাদা আলাদা Redis কিউ তৈরি করে সেগুলোকে ডায়নামিকভাবে ম্যানেজ করতে হবে, অথবা এই অন্যায্য পরিস্থিতি মেনে নিতে হবে।

BullMQ গ্রুপ-ভিত্তিক রেট লিমিটিং সুবিধা যোগ করে। আপনি প্রতিটি জবকে একটি গ্রুপ কী (সাধারণত একটি টেন্যান্ট বা ইউজার আইডি) দিয়ে ট্যাগ করতে পারেন এবং প্রতিটি গ্রুপের জন্য আলাদা লিমিট নির্ধারণ করতে পারেন। একই কিউ সব টেন্যান্টের জব প্রসেস করে, কিন্তু শিডিউলার প্রতিটি গ্রুপকে স্বাধীনভাবে নিয়ন্ত্রণ (throttle) করে। কাস্টমার A-এর কাজের হঠাৎ চাপ (burst) কাস্টমার B-এর কাজের সুযোগ কেড়ে নেবে না। আপনি কিউ-এর সংখ্যাবৃদ্ধি (queue sprawl) এড়াতে পারেন এবং আপনার Redis keyspace পরিচ্ছন্ন রাখতে পারেন। যেসব প্ল্যাটফর্মে 'নয়েজি-নেইবার' (noisy-neighbor) সমস্যা রয়েছে, তাদের জন্য শুধুমাত্র এই ফিচারটিই মাইগ্রেশনের যৌক্তিকতা প্রমাণ করার জন্য যথেষ্ট।

বাস্তবে আরও পরিচ্ছন্ন আর্কিটেকচার

কিউ (Queue) এবং ওয়ার্কারের (Worker) মধ্যকার পার্থক্যটি খুব সূক্ষ্ম, যতক্ষণ না আপনি প্রোডাকশন ইনসিডেন্ট ডিবাগ করছেন। Bull-এর ক্ষেত্রে, রুট হ্যান্ডলারের গভীরে জব ক্রিয়েশন কোড দেখা খুব সাধারণ বিষয়, যা অনেক সময় ভারী প্রসেসিং ডিপেন্ডেন্সিগুলোকেও ইমপোর্ট করে ফেলে। BullMQ আপনাকে সিদ্ধান্ত নিতে বাধ্য করে যে কাজ কোথায় সম্পন্ন হবে। আপনার ওয়েব সার্ভারগুলো হালকা (lean) থাকে। আপনার ওয়ার্কার কন্টেইনারগুলো ভারী লাইব্রেরি, ইমেজ প্রসেসর বা হেডলেস ব্রাউজার ধারণ করে। যদি কোনো মেমরি লিক দেখা দেয়, আপনি ঠিকভাবে জানেন কোন প্রসেস টাইপটি প্রোফাইল করতে হবে। এর মেন্টাল মডেলটি Celery বা Sidekiq-এর মতো সিস্টেমের কাছাকাছি।

সিদ্ধান্ত গ্রহণ

আপনি যদি নতুন কোনো প্রজেক্ট শুরু করেন, তবে BullMQ দিয়ে শুরু করুন। এর TypeScript ডেফিনিশনগুলো নির্ভুল এবং সম্পূর্ণ। জব ফ্লো-এর কারণে প্রচুর অর্কেস্ট্রেশন কোড লেখার প্রয়োজন হয় না। গ্রুপ রেট লিমিটিং সমস্যা শুরু হওয়ার আগেই তা সমাধান করে দেয়। এর async/await API ব্যবহার করা খুবই সহজ ও স্বাভাবিক মনে হয়। একটি গ্রিনফিল্ড (greenfield) প্রজেক্টের জন্য পুরনো লাইব্রেরিটি বেছে নেওয়ার তেমন কোনো কারণ নেই।

যদি Bull ইতিমধ্যে ঠিকঠাক কাজ করে, তবে সেটিতেই থাকুন। মাইগ্রেশন করতে সময় লাগে এবং এটি স্থিতিশীলতার ঝুঁকি তৈরি করতে পারে। আপনার জবগুলো যদি সরল এবং স্বাধীন হয়, তবে আপনার আসলে প্রয়োজনীয় ফিচারগুলোর অভাব নেই। যে কিউ পাসওয়ার্ড রিসেট ইমেল পাঠায় এবং অবতার রিসাইজ করে, তার জন্য ফ্লো গ্রাফের প্রয়োজন নেই। তাত্ত্বিক বিশুদ্ধতার জন্য কাজ করা কোড পুনরায় লেখা ইঞ্জিনিয়ারিং নয়; এটি কেবল শখ।

মাইগ্রেশনের বাস্তব চিত্র

আপনি যদি পরিবর্তন করেন, তবে এটিকে কোড রিফ্যাক্টর হিসেবে নয়, বরং একটি ইনফ্রাস্ট্রাকচার পরিবর্তন হিসেবে বিবেচনা করুন। Bull এবং BullMQ ভিন্ন ভিন্ন Redis key স্কিমা ব্যবহার করে। তারা একে অপরের জব ডেটা বা স্টেট পড়তে পারে না। আপনি কেবল একটি ফিচার ফ্ল্যাগ অন করে আশা করতে পারেন না যে পুরনো জবগুলো শেষ হয়ে যাবে। আপনাকে প্রতিটি বিদ্যমান কিউ খালি করতে হবে, নতুন ওয়ার্কারগুলো ডেপ্লয় করতে হবে এবং BullMQ দিয়ে নতুন করে এনকিউ (enqueue) করা শুরু করতে হবে। একটি মেইনটেন্যান্স উইন্ডো অথবা ব্লু-গ্রিন ডেপ্লয়মেন্টের পরিকল্পনা করুন, যেখানে পুরনো ওয়ার্কারগুলো লেগাসি কিউ প্রসেস করবে এবং নতুন ওয়ার্কারগুলো নতুন কিউ হ্যান্ডেল করবে।

আসল কথা