هر توسعه‌دهنده Node.js دیر یا زود با یک مانع مشابه روبرو می‌شود. کاربر روی یک دکمه کلیک می‌کند، هندلر مسیر (route handler) شما شروع به انجام یک کار سنگین می‌کند و درخواست HTTP همان‌جا معطل می‌ماند. شاید در حال ارسال ایمیل‌های دسته‌ای، همگام‌سازی رکوردها با یک CRM شخص ثالث، یا تولید یک گزارش PDF هستید. مرورگر در حال چرخش است. اپلیکیشن موبایل با خطا (timeout) مواجه می‌شود. کاربران شما ناراضی می‌شوند و سرور شما اسلات‌های اتصالی را مصرف می‌کند که نمی‌تواند از دست بدهد. راه حل این است که آن کار را از مسیر درخواست خارج کرده و به یک صف کار پس‌زمینه (background job queue) که توسط Redis پشتیبانی می‌شود، منتقل کنید. در اکوسیستم Node.js، دو کتابخانه این فضا را در اختیار دارند: Bull و BullMQ. انتخاب بین آن‌ها کمتر به معنای انتخاب یک برنده است و بیشتر به درک این موضوع مربوط می‌شود که پروژه شما در چه مرحله‌ای قرار دارد و به کدام سمت می‌رود.

The Original Workhorse

Bull سال‌هاست که استاندارد پردازش پس‌زمینه در Node.js بوده است. این کتابخانه پایدار، امتحان‌پس‌داده و در بی‌شمار اپلیکیشن عملیاتی (production) در حال اجراست. اگر نیاز دارید کاری را برای بعد زمان‌بندی کنید، یک وارد کردن (import) ناموفق را به‌طور خودکار دوباره تلاش (retry) کنید، یا اولویت‌های سخت‌گیرانه‌ای تعیین کنید تا وب‌هوک‌های پرداخت قبل از ارسال خبرنامه‌ها اجرا شوند، Bull بدون دردسر این کار را انجام می‌دهد. API آن مبتنی بر callback است، به این معنی که به‌خوبی با کدهای قدیمی که در آن‌ها promiseها هنوز یک نوآوری بودند، سازگار است. تیم‌هایی که مدت‌ها به Bull تکیه کرده‌اند، دقیقاً می‌دانند چه انتظاری داشته باشند. این کتابخانه وضعیت (state) را در Redis نگه می‌دارد، بنابراین اگر فرآیند Node شما دوباره راه‌اندازی شود، کارها باقی می‌مانند. همین قابلیت اطمینان باعث شده است که بسیاری از کسب‌وکارها هرگز فشاری برای تغییر سیستمی که از قبل کار می‌کرد، احساس نکنند.

What BullMQ Changes

BullMQ جانشین آن است. این کتابخانه از پایه با TypeScript بازنویسی شده و تمام سطح کاربری آن حول محور async/await ساخته شده است. اگر چند سال اخیر را صرف نوشتن کدهای مدرن Node.js کرده باشید، نحو (syntax) آن بلافاصله برایتان آشنا خواهد بود. اما تفاوت عمیق‌تر از تعاریف تایپ و زنجیره‌های promise است. BullMQ جداسازی تمیزی بین صف‌ها (queues) و کارگران (workers) ایجاد می‌کند. در Bull، صف اغلب نقش اجرای کارگر را نیز ایفا می‌کند. در BullMQ، شما یک صف را در یک فایل و یک کارگر را در فایلی دیگر تعریف می‌کنید. این جداسازی بازتاب‌دهنده نحوه مقیاس‌پذیری واقعی سیستم‌های عملیاتی است. شما می‌توانید مجموعه‌ای از کانتینرهای کارگر را مستقر کنید که فقط کارها را پردازش می‌کنند، در حالی که سرورهای API شما فقط کارها را به صف اضافه می‌کنند. با رشد سیستم، معماری خوانا باقی می‌ماند.

Features That Tilt the Scale

جایی که BullMQ واقعاً پیشتاز می‌شود، در قابلیت‌هایی است که Bull به سادگی ارائه نمی‌دهد. سه ویژگی در اپلیکیشن‌های واقعی بیشترین اهمیت را دارند.

Job Flows

جریان‌های کاری پیچیده به‌ندرت در یک تابع پس‌زمینه واحد جای می‌گیرند. تصور کنید در حال ساخت یک خط لوله (pipeline) پردازش تصویر هستید. کاربر یک عکس خام آپلود می‌کند و بک‌اند شما باید یک تصویر بندانگشتی (thumbnail) بسازد، یک پیش‌نمایش فشرده تولید کند، یک اسکن OCR انجام دهد و سپس به فرانت‌اند اطلاع دهد که همه چیز آماده است. با Bull، احتمالاً تمام این مراحل را در یک هندلر بزرگ و شکننده می‌گنجانید. BullMQ جریان‌های کاری (job flows) را معرفی می‌کند که به شما اجازه می‌دهد کار‌های والد (parent) و فرزند (child) را به‌طور صریح به هم زنجیر کنید. شما می‌توانید وابستگی‌ها را طوری تعریف کنید که مرحله اطلاع‌رسانی تنها پس از موفقیت هر دو کار thumbnail و OCR اجرا شود. اگر OCR شکست خورد، می‌توانید فقط همان بخش را دوباره تلاش کنید بدون اینکه نیاز باشد thumbnail را دوباره پردازش کنید. منطق کار ماژولار، قابل مشاهده و هنگام بروز خطا در ساعت سه صبح، بسیار راحت‌تر برای عیب‌یابی (debug) می‌شود.

Group Rate Limiting

اگر یک اپلیکیشن SaaS چندمستاجری (multi-tenant) را اجرا می‌کنید، احتمالاً نگران این بوده‌اید که یک مشتری، کارگران شما را با درخواست‌های زیاد اشباع کند. یک مستاجر می‌تواند ده هزار کار خروجی (export) را در صف قرار دهد و بقیه را غرق کند. BullMQ قابلیت محدودسازی نرخ گروهی (group rate limiting) را اضافه می‌کند که به شما اجازه می‌دهد پردازش را به ازای هر مستاجر یا هر کلید API محدود کنید. به عنوان مثال، ممکن است به مستاجر A اجازه دهید هر دقیقه پنجاه فراخوانی API خارجی انجام دهد، در حالی که مستاجر B ظرفیت مشابهی را به‌طور مستقل دریافت می‌کند. صف این محدودیت‌ها را به‌صورت سراسری در تمام نمونه‌های کارگر رعایت می‌کند، نه فقط به‌صورت محلی روی یک ماشین. این همان نوع شیر اطمینانی است که تا زمانی که ناگهان به آن نیاز پیدا نکنید، قدرش را نمی‌دانید.

A Modern Surface

BullMQ امضاهای قدیمی callback را کنار گذاشته و از یک API مدرن استقبال می‌کند. مدیریت خطا از الگوهای استاندارد promise پیروی می‌کند. تعاریف TypeScript در درجه اول در نظر گرفته شده‌اند، نه اینکه صرفاً یک پکیج جداگانه از جامعه کاربری باشند. اگر در حال شروع یک پروژه جدید (greenfield) هستید، تجربه توسعه‌دهنده (developer experience) به‌طور محسوسی روان‌تر است. ویرایشگر شما گزینه‌های صف را به‌صورت خودکار تکمیل می‌کند. لینتر شما نام‌های مفقود شده‌ی کارها را شناسایی می‌کند. بار ذهنی کاهش می‌یابد.

The Redis Constant

یک تسکین عملی در این تصمیم، زیرساخت است. هر دو کتابخانه Bull و BullMQ وضعیت کار (job state)، متادیتا و زمان‌بندی‌ها را در Redis ذخیره می‌کنند. آن‌ها از ساختارهای کلید داخلی متفاوتی استفاده می‌کنند، اما فناوری زیربنایی یکسان است. اگر در حال حاضر از Redis برای Bull استفاده می‌کنید، نیازی نیست برای استفاده از BullMQ پایگاه داده جدیدی جایگزین کنید یا در توپولوژی استقرار (deployment topology) خود بازنگری کنید. چالش مهاجرت در کد اپلیکیشن شماست، نه در هزینه‌های سرور.

واقعیت مهاجرت

با این حال، انتقال از Bull به BullMQ یک جایگزینی مستقیم و بدون دردسر (drop-in replacement) نیست. فراخوانی‌های API تغییر می‌کنند. نام رویدادها متفاوت است. روش تعریف پردازنده‌ها (processors) و مدیریت همزمانی (concurrency) به قدری بازنویسی شده است که باید تمام فایل‌هایی را که با صف در ارتباط هستند، تغییر دهید. مهم‌تر از آن، نمی‌توانید صرفاً با فشردن یک کلید، امیدوار باشید که کارهای قدیمی در سیستم جدید تمام شوند. باید پیش از راه‌اندازی ورکر‌های (workers) BullMQ روی همان نمونه (instance) Redis، صف‌های فعلی Bull را کاملاً تخلیه کنید. در غیر این صورت، خطر برخورد دو فرمت متفاوت در یک فضای کلید (keyspace) واحد وجود دارد. برای یک بازه زمانی تعمیر و نگهداری یا یک انتقال blue-green برنامه‌ریزی کنید. این کار مستلزم تلاش واقعی است و این تلاش باید ارزش خود را اثبات کند.

مسیر انتخاب

اگر تنظیمات فعلی Bull شما بدون مشکل به خوبی کار می‌کند، دست به آن نزنید. پایداری ارزشمند است. یک صف پس‌زمینه، زیرساخت است، نه یک موضوع برای دنبال کردن مد روز. اگر تیم شما با معماری فعلی درگیر است چون به شدت به جریان‌های کاری والد-فرزندی (parent-child workflows) یا محدودیت نرخ (rate limits) به ازای هر مستاجر (per-tenant) نیاز دارید، آنگاه مهاجرت منطقی است. جداسازی بهتر وظایف (separation of concerns) و API مدرن، در طول زمان تلاش انجام شده را جبران خواهد کرد.

برای هر پروژه جدید، انتخاب ساده‌تر است. با BullMQ شروع کنید. این کتابخانه به‌روزرسانی‌های منظمی دریافت می‌کند، از استانداردهای فعلی JavaScript به‌صورت پیش‌فرض پشتیبانی می‌کند و به شما فضای کافی برای ساخت جریان‌های کاری پیچیده می‌دهد، بدون اینکه ظرف شش ماه نیاز به جایگزینی کتابخانه داشته باشید. با این کار از ایجاد بدهی فنی (technical debt) روی API ای که نگهدارندگان آن از آن عبور کرده‌اند، جلوگیری می‌کنید.

نتیجه‌گیری اصلی

هدف از وجود یک صف کار، سریع نگه داشتن پاسخ‌های HTTP و صبور نگه داشتن کاربران شماست. Bull هنوز این کار را به شکلی تحسین‌برانگیز انجام می‌دهد. BullMQ این کار را با ساختاری انجام می‌دهد که با نحوه ساخت و مقیاس‌پذیری اپلیکیشن‌های مدرن Node.js مطابقت دارد. سوال این نیست که کدام کتابخانه در حالت مطلق بهتر است. سوال این است که آیا درد فعلی شما ارزش مهاجرت را دارد، و آیا پروژه بعدی شما شایسته زیربنایی است که قبل از دور بعدی جذب سرمایه یا عرضه محصول، نیاز به جایگزینی نداشته باشد.