backend

BullMQ Worker لا يعالج المهام؟ إليك الحل (2026)

٣ أغسطس ٢٠٢٦

BullMQ Worker Not Processing Jobs? Fix It (2026)

عادةً ما يفشل عامل BullMQ الذي لا يلتقط المهام في واحد من أربعة فحوصات: إما أنه غير متصل بـ Redis، أو أنه يشير إلى اسم قائمة أو بادئة (prefix) مختلفة عن المنتج، أو أن القائمة متوقفة مؤقتاً أو مقيدة بمعدل محدد، أو أن المهام تتعثر (stalling). تحقق منها بهذا الترتيب.

ملخص: ابدأ بإثبات أن العامل يعمل ومتصل، ثم أثبت أنه يتشارك مع المنتج في نفس اسم القائمة والبادئة بالضبط، ثم تحقق من isPaused() و getJobCounts() قبل أن تلمس أي خيارات توقيت. فقط بعد ذلك تأتي إعدادات المهام المتعثرة — حيث تكون كل من lockDuration و stalledInterval افتراضياً 30,000 مللي ثانية في bullmq@6.0.5.1 إذا بدأت الأعراض مباشرة بعد التحديث، فإن BullMQ v6 صدر في 30 يوليو 2026 ونقل ioredis إلى تبعية نظيرة (peer dependency) اختيارية، لذا فإن تنفيذ npm install bullmq@6 بمفرده لا يثبت أي عميل Redis على الإطلاق.23

ما ستتعلمه

  • قائمة التحقق المرتبة التي تعزل السبب في أربع خطوات بدلاً من التخمين
  • لماذا تظل المهام في حالة waiting بينما يبدو العمال في حالة صحية ممتازة
  • كيف يؤدي عدم تطابق اسم القائمة أو الـ prefix إلى فصل المنتج عن المستهلك بصمت
  • ماذا يعني فعلياً أن maxRetriesPerRequest must be null، ولماذا يتسبب في خطأ في حالة وتحذير فقط في حالة أخرى
  • لماذا يمكن أن يؤدي فقدان مستمع error إلى إيقاف العامل تماماً
  • وضعي الفشل اللذان قدمهما BullMQ v6: التبعية النظيرة لـ ioredis والبيانات الوصفية القديمة للمهام المتكررة
  • ما هي المهام المتعثرة (stalled jobs) حقاً، وأي القيم الافتراضية يجب تغييرها (وأيها يجب تركها كما هي)
  • ما إذا كانت القائمة المتوقفة مؤقتاً، أو محدد معدل النقل، أو التزامن العالمي هو ما يقيد عملك
  • ما إذا كنت لا تزال بحاجة إلى QueueScheduler للمهام المؤجلة

لماذا لا يقوم عامل BullMQ بمعالجة المهام؟

لأن العامل والمهام لا يلتقيان. يحدث هذا في واحد من أربعة أماكن، والتحقق منها بالترتيب أسرع من قراءة كود المعالج الخاص بك: الاتصال (العامل لم يصل أبداً إلى Redis)، الهوية (اسم القائمة أو الـ prefix يختلف عن المنتج)، الحالة (القائمة متوقفة مؤقتاً، أو مقيدة بمعدل، أو محدودة التزامن)، و دورة الحياة (المهام تدخل حالة active، وتفقد القفل الخاص بها، ثم تعود إلى حالة waiting).

السبب في أن الترتيب مهم هو أن مسار المهام المتعثرة — وهو التصنيف الذي تلجأ إليه معظم نصائح استكشاف الأخطاء وإصلاحها أولاً — هو الوحيد من بين الأربعة الذي يتطلب أن تكون المهمة قد وصلت إلى حالة active أصلاً. إذا كان عدد المهام النشطة active ثابتاً عند الصفر، فلن يساعدك أي قدر من ضبط lockDuration، لأنه لا يوجد شيء يحصل على قفل في المقام الأول.

كيف أقوم بتصحيح أخطاء عامل BullMQ لا يلتقط المهام؟

اسأل القائمة عما تعتقد أنه يحدث قبل أن تغير أي كود. هذه الاستدعاءات الأربعة تجيب على أسئلة "الاتصال، الهوية، الحالة" في تمريرة واحدة:

import { Queue } from 'bullmq';

const queue = new Queue('emails', { connection, prefix: 'bull' });

console.log('paused:', await queue.isPaused());
console.log('counts:', await queue.getJobCounts());
console.log('workers:', await queue.getWorkersCount());
console.log('rate-limit ttl:', await queue.getRateLimitTtl(100));

اقرأ المخرجات بهذا الشكل:

الملاحظةإلى ماذا تشير
getWorkersCount() تساوي 0العامل (worker) غير متصل، أو مسجل باسم/بادئة مختلفة
paused: trueالطابور (queue) متوقف عالمياً — لن يتم سحب أي مهام4
waiting في تزايد، بينما active تظل 0عدم تطابق الهوية، توقف، تحديد معدل (rate limit)، أو حد التزامن العالمي (global concurrency)
active ليست صفراً ولكن المهام لا تنتهي أبداًمسار متوقف: أقفال (locks)، حظر حلقة الأحداث (event-loop blocking)، أو معالج ينهار (crashing processor)
rate-limit ttl أكبر من 0المحدد (limiter) يحتجز المهام في حالة waiting5

إرجاع getWorkersCount() للقيمة صفر بينما عمليتك تعمل بوضوح هو السطر الأكثر فائدة هنا، لأنه يدمج سؤالي "هل هو متصل؟" و"هل هو نفس الطابور؟" في إجابة واحدة. عند استدعائها بدون وسائط، تقوم getJobCounts() بتقديم تقارير عن active، و completed، و delayed، و failed، و prioritized، و waiting و waiting-children — وهي المجموعة الافتراضية في bullmq@6.0.5.1

هناك ملاحظة واحدة بشأن السطر الأول، لأنها تغير مدى ثقتك به. تعتمد getWorkersCount() على أمر CLIENT LIST الخاص بـ Redis، وتحذر تعريفات الأنواع الخاصة بـ BullMQ من أن "GCP لا يدعم SETNAME، لذا فإن هذا الاستدعاء لن يعمل". في حالة Redis المدار الذي يرفض أمر CLIENT، يقوم BullMQ بالتقاط الخطأ واستبداله بإدخال نائب واحد، لذا يظهر العدد كـ 1 سواء كان هناك عامل متصل فعلياً أم لا.1 إذا كنت تستخدم Memorystore أو أي مزود مقيد بشكل مشابه، تعامل مع العدد 1 بالضبط على أنه "غير معروف" بدلاً من كونه دليلاً، وارجع للتحقق مما إذا كانت قيمة active تتغير على الإطلاق.

لماذا تظل مهام BullMQ عالقة في حالة الانتظار (waiting)؟

حالة waiting هي المكان الذي تتراكم فيه المهام لعدة أسباب مختلفة، وهذا هو السبب في أنها عرض مربك للغاية. المهمة المحددة بمعدل (rate-limited) ليست مهمة معطلة — توضح وثائق BullMQ صراحة أن "المهام التي يتم تحديد معدلها ستظل في الواقع في حالة الانتظار".5 كما أن المهمة المتوقفة (stalled) تعود أيضاً إلى حالة waiting، وليس إلى حالة خاصة بها.6

لذا فإن زيادة waiting تخبرك بأن المهمة لم يتم المطالبة بها بعد، لكنها لا تخبرك بالسبب. التفسيرات الحميدة الثلاثة هي: طابور متوقف، نافذة تحديد معدل نشطة، وحد تزامن عالمي مشبع بالفعل. أما التفسيران المعطلان فهما: عدم وجود عامل متصل، وعدم تطابق هوية الطابور. ميز بينهما باستخدام getWorkersCount() و isPaused() قبل افتراض وجود خطأ برمجياً.

شيء آخر يستحق المعرفة عن المحدد (limiter): هو عالمي عبر جميع الأجهزة لديك، وليس لكل عملية على حدة. توضح الوثائق ذلك ببساطة — مع max: 10, duration: 1000، "إذا كان لديك على سبيل المثال 10 عمال لطابور واحد بهذه الإعدادات، فسيظل يتم معالجة 10 مهام فقط في الثانية".5 زيادة عدد نسخ العمال (worker replicas) لن تجعل الطابور المحدد بمعدل يتحرك بشكل أسرع.

هل يحتاج الطابور والعامل إلى نفس الاسم والبادئة؟

نعم — بالضبط نفس الشيء، في كليهما. لا يجد الـ Queue والـ Worker بعضهما البعض إلا من خلال مفاتيح Redis التي يحسبونها، وهذه المفاتيح مشتقة من اسم الـ queue بالإضافة إلى خيار الـ prefix، والذي "يكون افتراضياً bull" في bullmq@6.0.5.1 إذا قام المنتج بتعيين prefix: 'myapp' وترك العامل (worker) بدون تعيين، فإنهما يعملان على مساحتي مفاتيح منفصلتين تماماً، ولن يبلغ أي منهما عن خطأ.

// Producer and consumer must agree on BOTH arguments.
const queue = new Queue('emails', { connection, prefix: 'myapp' });
const worker = new Worker('emails', handler, { connection, prefix: 'myapp' });

هذه حالة شائعة ناتجة عن تباين متغيرات البيئة (environment-variable drift): خدمة واحدة تقرأ QUEUE_PREFIX وأخرى تم نشرها قبل وجود هذا المتغير. تشير صفحة استكشاف الأخطاء وإصلاحها في BullMQ إلى الخطر المرتبط بتمرير قيم غير معرفة أو سلاسل نصية فارغة في أسماء الـ queue، والتي تظهر كخطأ Lua غامض بدلاً من رسالة واضحة: ERR Error running script ... Lua Redis() command arguments must be strings or integers.7 قم بالتحقق من هذه المتغيرات عند بدء التشغيل بدلاً من تركها تصل إلى Redis.

لاحظ أن الـ prefix هو مفهوم خاص بـ Redis في الإصدار v6. توضح تعريفات الأنواع المرفقة أنه تعمد عدم جعله جزءاً من خيارات الـ queue المشتركة "لأنه مفهوم خاص بـ Redis: الخلفيات الأخرى تستخدم مساحات أسماء (namespace) بشكل مختلف (على سبيل المثال، خلفية PostgreSQL تستخدم schema) وتتجاهله."1

لماذا يطلق BullMQ خطأ "maxRetriesPerRequest must be null"؟

لأن العامل (worker) يحتفظ باتصال حاجب (blocking connection) بـ Redis، وإذا سُمح لـ ioredis بالتخلي عن أمر ما بعد عدد محدد من محاولات إعادة المحاولة، فقد يفشل هذا الاستدعاء الحاجب ويؤدي إلى توقف حلقة جلب البيانات (fetch loop) الخاصة بالعامل. توجيهات BullMQ هي أنه بالنسبة للعمال، يجب أن يكون هذا الخيار null بحيث "يستمر العمال في المعالجة إلى الأبد طالما كان هناك اتصال يعمل."8

ما ليس واضحاً — وهو ما يغير طريقة تصحيحك لهذا الخطأ — هو أن BullMQ يتفاعل بطريقتين مختلفتين اعتماداً على كيفية تمرير الاتصال. بتشغيل كلتا الحالتين على bullmq@6.0.5 مع ioredis@5.11.1 تظهر النتائج التالية:

كيفية تمرير الاتصالالنتيجة
مثيل (instance) ioredis موجود مع قيمة maxRetriesPerRequest ليست nullالمُنشئ (Constructor) يطلق خطأ: BullMQ: Your Redis options maxRetriesPerRequest must be null.
كائن (object) خيارات بسيط مع maxRetriesPerRequest: 20يسجل تحذيراً BullMQ: WARNING! Your Redis options maxRetriesPerRequest must be null and will be overridden by BullMQ. ويستمر، حيث يقوم BullMQ بتعيينه إلى null

الصف الثاني هو الذي يجب الحذر منه. لأنه يكتفي بالتحذير، فقد تضيع الرسالة في سجلات الحاوية (container log) بينما يتم استبدال القيمة التي وضعتها بصمت — لذا إذا قمت بتكوين سقف لإعادة المحاولة للعامل عمداً، فقد قام BullMQ بالفعل بتجاهله. يقوم ioredis بتعيين هذا الإعداد افتراضياً إلى 20 عندما تقوم بإنشاء عميل بنفسك،8 لذا فإن أي عميل مشترك تم إنشاؤه يدوياً سيمر عبر أحد هذين المسارين. هناك حدان يستحقان المعرفة: الفحص يتم فقط للاتصالات الحاجبة (blocking)، لذا فإن نفس العميل الممرر إلى Queue لا يفعل أي من المسارين، وهو اختبار للقيمة الحقيقية (truthiness test)، لذا فإن maxRetriesPerRequest: 0 يتجاوز كليهما أيضاً. ومن الجدير بالذكر أيضاً أن دليل الإنتاج يصف كلتا الحالتين بأنهما تنتجان تحذيراً؛9 لكن إطلاق الخطأ (throw) المذكور أعلاه هو ما يفعله bullmq@6.0.5 فعلياً عند تزويده بمثيل عميل.

import IORedis from 'ioredis';

// Correct for a Worker: no retry ceiling on the blocking connection.
const connection = new IORedis({ maxRetriesPerRequest: null });
const worker = new Worker('emails', handler, { connection });

بينما أنت في ذلك الـ constructor: لا تقم بتعيين keyPrefix الخاص بـ ioredis. تمرير نسخة تم بناؤها باستخدام keyPrefix يؤدي إلى ظهور الخطأ BullMQ: ioredis does not support ioredis prefixes, use the prefix option instead. — تم التحقق من ذلك مقابل نفس زوج الإصدارات. استخدم خيار prefix الخاص بـ BullMQ بدلاً من ذلك، كما تم توضيحه أعلاه.

الـ Queue المستخدم بواسطة معالج الطلبات (request handler) يتطلب سلوكاً معاكساً. توصي الوثائق بترك maxRetriesPerRequest على قيمته الافتراضية (أو تعيينها إلى 1) للمنتجين (producers) بحيث يفشل مستدعي HTTP بسرعة،8 وفي دليل الإنتاج — يتم تعطيل enableOfflineQueue الخاص بـ ioredis في الـ Queue مع تركه مفعلاً في الـ Worker.9

لماذا توقف عامل BullMQ عن معالجة المهام بعد حدوث خطأ؟

لأن الـ EventEmitter الذي يرسل error دون وجود مستمع (listener) مرتبط به يتسبب في توقف Node.js، وتحمل وثائق BullMQ نفسها هذا كتنبيه خطر: "إذا كان معالج الأخطاء مفقوداً، فقد يتوقف العامل الخاص بك عن معالجة المهام عند إرسال خطأ!"10 إنه إصلاح من سطرين، ومن السهل إغفاله، لأن لا شيء في المسار الناجح يتطلبه.

worker.on('error', err => {
  logger.error({ err }, 'bullmq worker error');
});

queue.on('error', err => {
  logger.error({ err }, 'bullmq queue error');
});

الخدعة المتعلقة بذلك هي autorun. يقوم العامل "بتشغيل المعالج فوراً" عند إنشائه، ما لم تمرر autorun: false — وفي هذه الحالة لا يتم معالجة أي شيء حتى تستدعي worker.run()__PRESERVER__ بنفسك.10 القيمة الافتراضية هي true في bullmq@6.0.5،1 لذا فإن هذا الأمر يؤثر فقط على الأشخاص الذين قاموا بتعيين هذا الخيار عمداً في الاختبارات أو في تسلسل بدء التشغيل المرحلي ثم نسوا استدعاء run() المقابل.

هل أحتاج إلى تثبيت ioredis بشكل منفصل لـ BullMQ v6؟

نعم، إذا كنت تريد استخدام backend الخاص بـ ioredis — وهذا تغير في الإصدار v6. كان BullMQ v5 يدرج ioredis كاعتمادية أساسية (مثبتة عند 5.11.1 في 5.81.3). أما BullMQ v6.0.5 فلا يدرجه على الإطلاق؛ حيث يتم التصريح عن ioredis و Redis و pg و bullmq-otel كـ اعتماديات نظيرة (peer dependencies) معلمة كاختيارية.2 الاعتماديات النظيرة الاختيارية لا يتم تثبيتها تلقائياً، لذا فإن التثبيت النظيف ينتج شجرة حزم لا تحتوي على أي عميل Redis.

تثبيت bullmq@6.0.5 بمفرده في مشروع فارغ (npm 10.9.8, Node 22.22.3) ينتج ثمانية إدخالات في المستوى الأعلى في node_modulesbullmq واعتمادياته المباشرة، بالإضافة إلى حزم luxon و msgpackr-extract التي تسحبها تلك الحزم. لا يوجد أي منها كعميل Redis. وبالتالي يفشل استدعاء الحزمة فوراً:

Error: Cannot find module 'ioredis/built/utils'

الإصلاح هو سطر واحد:

npm install bullmq ioredis

هذا نتيجة للتغيير الرئيسي في v6. تم وصف الإصدار بأنه "إصدار BullMQ v6 مع backends قابلة للتوصيل للـ queue"،3 وأصبحت طبقة الاتصال الآن تدعم node-Redis وعميل Bun's Redis جنباً إلى جنب مع ioredis، مع تقديم دليل الإنتاج نصائح منفصلة لإعادة الاتصال لكل منها.9 جعل كل عميل اختيارياً هو ما يتيح لك تثبيت العميل الذي تستخدمه فقط. وهذا يعني أيضاً أن الترقية التي تبدو وكأنها مجرد تحديث بسيط (patch bump) في ملف lockfile قد تترك الخدمة بدون برنامج تشغيل (driver) لـ Redis.

ليه الـ worker بتاعي وقف بعد التحديث لـ BullMQ v6؟

بعيداً عن مشكلة الـ client المفقود، الفخ التاني في التحديث هو الوظائف المتكررة (repeatable jobs). BullMQ v6 شال نظام الـ repeatable-job القديم API بالكامل، ودليل الهجرة واضح جداً بخصوص اللي هيحصل لو فضلت البيانات القديمة موجودة: "الوظائف المتكررة القديمة المخزنة بواسطة BullMQ v5 غير مدعومة في BullMQ v6. إذا واجه v6 بيانات وصفية (metadata) لوظائف متكررة قديمة، فإنه يطلق خطأً بدلاً من محاولة الاستمرار بسلوك متوافق جزئياً."11

الرسالة بتقول:

Legacy repeatable job metadata is not supported in BullMQ v6 (key: "..."). Migrate legacy repeatable jobs to Job Schedulers before upgrading. See https://docs.bullmq.io/guide/migrations/migrate-from-v5-to-v6

مكان ظهور الرسالة دي بيعتمد على إيه اللي لمس الـ metadata. عرض الـ schedulers باستخدام queue.getJobSchedulers() بيظهر الخطأ مباشرة. أما الـ worker اللي بيخلص وظيفة شايلة مفتاح تكرار قديم فبياخد مسار مختلف: مش بيقدر يجدول التكرار الجاي، فبيقوم بـ إرسال (emit) الرسالة على حدث الـ error الخاص بيه، وتكون مغلفة كـ Failed to add repeatable job for next iteration: <message>.1 التفصيلة دي مهمة هنا — لو تجاهلت مستمع الـ error من القسم السابق، الرسالة دي هتوصل في صمت، وكل اللي هتلاحظه هو إن الوظيفة المتكررة وقفت فجأة عن التكرار.

الـ APIs دي من v5 مابقتش موجودة في v6: Queue.add(..., { repeat }), Queue.addBulk(..., { repeat }), Queue.getRepeatableJobs(), Queue.removeRepeatable(), Queue.removeRepeatableByKey(), وكلاس Repeat. والبدائل هي queue.upsertJobScheduler(...), queue.getJobSchedulers() و queue.removeJobScheduler(...).11

المفروض عملية الهجرة دي تحصل وأنت لسه على v5: احصر التعريفات القديمة باستخدام getRepeatableJobs(), وأعد إنشاء كل واحدة كـ Job Scheduler، وتأكد باستخدام getJobSchedulers(), وبعدين امسح المدخلات القديمة — وما تعملش deploy لـ v6 غير لما كل producer و worker يكونوا شغالين بالـ schedulers.11 وفي حاجتين صغيرين يستاهلوا تدور عليهم (grep) في نفس الوقت: repeat.utc تم استبدالها بـ upsertJobScheduler(..., { tz: 'UTC' }), و Worker.resume() بقت دلوقتي asynchronous — التوقيع (signature) بتاعها اتغير من resume(): void في 5.81.3 لـ resume(): Promise<void> في 6.0.5, فبالتالي أي استدعاء بدون await هيسيب floating promise. لاحظ إن دليل الهجرة حط التغيير ده تحت عنوان "Queue.resume() is asynchronous," بس Queue.resume() كانت بالفعل بترجع Promise<void> في v5؛ تعريفات الأنواع (type definitions) في كلا الحزمتين بتوضح إن التغيير في Worker.111

إيه هي الـ stalled jobs في BullMQ وإزاي أوقفها؟

المهمة المتوقفة (stalled job) هي المهمة التي وصلت إلى حالة active، ثم فشلت في تجديد القفل (lock) الخاص بها في الوقت المحدد، لذا يفترض BullMQ أن العامل (worker) قد توقف عن العمل ويعيد المهمة إلى حالة waiting. لا توجد "حالة" (state) تسمى stalled — تشير الوثائق إلى أنه "يتم فقط إصدار حدث 'stalled' عندما يتم نقل مهمة تلقائيًا من حالة active إلى حالة waiting."6 إذا توقفت المهمة أكثر من maxStalledCount من المرات، يتم اعتبارها فاشلة بشكل دائم مع السبب job stalled more than allowable limit.6

الإعدادات الافتراضية، المأخوذة من تعريفات الأنواع المرفقة في bullmq@6.0.5:1

الخيارالافتراضيما الذي يتحكم فيه
lockDuration30000 مللي ثانيةالمدة التي يظل فيها مطالبة العامل بمهمة ما سارية دون تجديد
lockRenewTimeنصف lockDurationوتيرة التجديد؛ تنصح الوثائق بعدم تغييرها
stalledInterval30000 مللي ثانيةعدد مرات تشغيل فحص التوقف (stalled check)
maxStalledCount1عدد مرات الاستعادة من التوقف قبل اعتبار المهمة فاشلة
concurrency__PRESER preserve__1عدد المهام التي يشغلها مثيل عامل واحد بالتوازي
drainDelay5 (ثوانٍ)نافذة الاستطلاع الطويل (long-poll) عندما يكون الطابور فارغًا
maximumRateLimitDelay30000 مللي ثانيةالحد الأقصى للخمول أثناء تقييد المعدل (rate limited)

الإطار الهام هنا هو أن المهمة المتوقفة عادة ما تكون عرضًا لكود محجوب (blocked code)، وليست نتيجة لقفل تم ضبطه بشكل سيئ. Node.js أحادي المسار (single-threaded)، وتوجيهات BullMQ هي أنه "إذا كان المعالج (CPU) مشغولاً للغاية (بسبب أن العملية تستهلك الكثير من موارد المعالج)، فقد لا يجد العامل وقتًا لتجديد القفل."12 زيادة lockDuration في معالج يحجب حلقة الأحداث (event loop) لمدة دقيقة يمنحك فترة صمت أطول فقط، وليس حلاً. الحلان الحقيقيان هما إعادة التحكم إلى حلقة الأحداث بشكل متكرر، ونقل العمل المرتبط فعليًا بالمعالج إلى معالج معزول (sandboxed processor)، والذي يقوم افتراضيًا بتشغيل المهمة في عملية Node.js منفصلة (useWorkerThreads يحولها إلى خيط عامل (worker thread) بدلاً من ذلك).61

هناك أربعة أسباب غير بديهية لفقدان القفل تستحق الفحص قبل تغيير أي أرقام، وجميعها مدرجة في صفحة استكشاف الأخطاء وإصلاحها في BullMQ تحت عنوان "Missing Locks": استنزاف المعالج (CPU starvation)، فقدان الاتصال مع Redis، إزالة المهمة قسريًا بواسطة إحدى واجهات برمجة تطبيقات الإزالة (removal APIs)، وسياسة Redis maxmemory خاطئة.7 النقطة الأخيرة تستحق تركيزًا خاصًا — ينص دليل الإنتاج على أن تكوين maxmemory-policy على noeviction "هو الإعداد الوحيد الذي يضمن السلوك الصحيح للطوابير."9 مثيل Redis الذي يتم توفيره من قالب ذاكرة مؤقتة (cache template) سيقوم بطرد مفاتيح BullMQ بسهولة تحت ضغط الذاكرة.

عمليات النشر (Deploys) هي المصدر الروتيني الآخر للتوقفات. العامل الذي يتم إنهاؤه في منتصف المهمة يترك تلك المهمة ليتم استعادتها بواسطة فحص التوقف "بزمن انتظار يبلغ حوالي 30 ثانية افتراضيًا"، ولهذا السبب يوصي دليل الإنتاج بمعالجة SIGINT و SIGTERM وانتظار worker.close() قبل الخروج.9

هل الطابور الخاص بي متوقف مؤقتاً، أو محدود المعدل، أو مقيد بالتزامن؟

تبدو الحالات الثلاث متطابقة من الخارج — حيث تتراكم المهام في waiting ويبقى العمال (workers) خاملين — ويمكن الإجابة على الحالات الثلاث من خلال استدعاء واحد لكل منها.

التوقف المؤقت (Paused). الطابور المتوقف مؤقتاً بشكل عام يعني "لن يقوم أي عامل بسحب أي مهام من الطابور".4 العمال الذين بدأوا بالفعل في تنفيذ مهمة يكملونها ثم يصبحون خاملين. تحقق باستخدام await queue.isPaused() واستأنف العمل باستخدام await queue.resume(). أما التوقف المؤقت على مستوى العامل (worker) فهو آلية منفصلة: تصف الوثائق myWorker.pause() بأنها توقف ذلك المثيل عن أخذ مهام جديدة بينما ينهي مهامه الحالية،4 وبما أنها مرتبطة بالمثيل بدلاً من الحالة المشتركة للطابور، فلن تجعل queue.isPaused() تعيد القيمة true.

تحديد المعدل (Rate limited). تعيد await queue.getRateLimitTtl(maxJobs) قيمة أكبر من صفر بينما نافذة المحدد مفتوحة، وتقوم await queue.removeRateLimitKey() بمسحها والسماح للعمال بسحب المهام مرة أخرى.5 تذكر أن المحدد عام، وأن تحديد المعدل بناءً على مفتاح المجموعة (group-key) قد تمت إزالته في BullMQ 3.0.5

تقييد التزامن (Concurrency capped). تقوم setGlobalConcurrency(n) بتقييد المعالجة المتوازية عبر كل مثيل عامل، ولا يمكن أن يتجاوز التزامن (concurrency) الخاص بكل عامل هذا الحد — وتشير الوثائق إلى أن إعداد العامل الخاص "لن يتجاوز الإعداد العام".13 اقرأ القيمة الحالية باستخدام await queue.getGlobalConcurrency() وامسحها باستخدام await queue.removeGlobalConcurrency().13

هل لا زلت بحاجة إلى QueueScheduler للمهام المؤجلة؟

لا. كان QueueScheduler مطلوباً قبل BullMQ 2.0 لترقية المهام المؤجلة والمعاد محاولتها، وكان غيابه يوماً ما سبباً حقيقياً لعدم تشغيل المهام أبداً. تكرر الوثائق الحالية نفس الجملة في ثلاثة مواضع: "بدءاً من BullMQ 2.0 وما بعدها، لم يعد QueueScheduler مطلوباً".6145

هذا الأمر مهم للتشخيص لأن الإرشادات المكتوبة لـ BullMQ 1.x توجهك لإضافة QueueScheduler، وهذه النصيحة أصبحت قديمة في أي إصدار مدعوم حالياً. إذا كانت المهام المؤجلة لا تعمل في الإصدار v5 أو v6، فانظر في الأسباب المذكورة أعلاه بدلاً من ذلك، وخاصة فحوصات الاتصال والهوية.

ومع ذلك، هناك حد أدنى حقيقي للإصدار. يرفض bullmq@6.0.5 العمل مع Redis أقدم من 5.0.0، ويظهر خطأ Redis version needs to be greater or equal than 5.0.0، ويسجل توصية بالعمل على الأقل على الإصدار 6.2.0.1 كما أن بعض تحسينات استدعاءات الحظر (blocking-call) مقيدة بـ Redis 6.0.0 و 7.0.8.1

الخلاصة

قاوم الرغبة في البدء بـ lockDuration. أسرع طريق لحل هذه المشكلة هو إثبات الاتصال، ثم الهوية، ثم الحالة، وفقط بعد ذلك ابحث في مسار الوظائف المتوقفة — لأن ثلاثة من هذه الأربعة تنتج نفس العرض وهو "الـ worker خامل، والوظائف في الانتظار" وفقط الأخير هو مشكلة توقيت. منذ إصدار v6 في 30 يوليو 2026، هناك خطوة إضافية واحدة: إذا ظهر العرض مباشرة بعد تحديث التبعيات، تحقق مما إذا كنت قد انتقلت إلى v6 وفقدت عميل Redis خلال هذه العملية.

إذا كنت تعيد تقييم الطابور نفسه بدلاً من تصحيح أخطائه، فمن المفيد معرفة تكلفة البدائل. طابور وظائف مدعوم بـ Postgres باستخدام pg-boss يزيل Redis من الصورة تماماً، و NATS JetStream مع عمال دائمين و DLQ يستبدله بنموذج تشغيلي مختلف، وتغطي مقارنة Kafka مقابل RabbitMQ مقابل SQS مقابل NATS الأوسع نطاقاً أين يناسب كل منها. إذا كنت ستستمر في استخدام Redis، فإن نفس المخاوف بشأن noeviction وإعادة استخدام الاتصال تظهر أيضاً في أنماط التخزين المؤقت لـ Redis.


الحواشي

  1. تعريفات الأنواع والمصدر المجمّع (compiled source) الموفرة في bullmq@6.0.5، والتي تم تثبيتها وقراءتها في 2026-08-03 — interfaces/worker-options.d.ts (علامات @defaultValue لكل من autorun، وconcurrency، وlockDuration، وstalledInterval، وmaxStalledCount، وdrainDelay، وmaximumRateLimitDelay وuseWorkerThreads؛ أما القيمة الافتراضية لـ lockRenewTime وهي "نصف الـ lockDuration" فقد ذُكرت كنص بدلاً من علامة)، وinterfaces/queue-options.d.ts (KeyPrefixOptions.prefix، "القيمة الافتراضية هي bull)، وclasses/queue-getters.js و.d.ts (قائمة الأنواع الافتراضية لـ sanitizeJobTypes؛ واستبدال baseGetClients لإدخال نائب واحد عندما يرفض Redis أمر CLIENT؛ وملاحظة "GCP لا يدعم SETNAME، لذا فإن هذا الاستدعاء لن يعمل" في getWorkers)، وclasses/worker.js (غلاف Failed to add repeatable job for next iteration: الذي يتم إطلاقه عند حدث error الخاص بالـ worker) وclasses/Redis-connection.js (minimumVersion = '5.0.0'، وrecommendedMinimumVersion = '6.2.0'، وبوابات القدرات عند 6.0.0 و 7.0.8). تمت مقارنة Worker.resume() بين الإصدار المثبت bullmq@5.81.3 (resume(): void) وbullmq@6.0.5 (resume(): Promise<void>). https://www.npmjs.com/package/bullmq 2 3 4 5 6 7 8 9 10 11 12 13 14 15

  • بيانات npm registry الخاصة بـ bullmq، تمت قراءتها في 2026-08-03: الإصدار 6.0.5 يدرج pg، و Redis، و ioredis و bullmq-otel تحت peerDependencies مع تحديد الأربعة جميعاً كـ optional: true في peerDependenciesMeta، ولا يدرج ioredis تحت dependencies؛ بينما الإصدار 5.81.3 يدرج ioredis: 5.11.1 كاعتمادية مباشرة (direct dependency). https://registry.npmjs.org/bullmq 2 3

  • إصدار BullMQ v6.0.0، نُشر في 30 يوليو 2026: "feat!: release BullMQ v6 with pluggable queue backends." https://GitHub.com/taskforcesh/bullmq/releases/tag/v6.0.0 2

  • توثيق BullMQ — إيقاف الطوابير مؤقتاً (Pausing queues). https://docs.bullmq.io/guide/workers/pausing-queues 2 3 4

  • توثيق BullMQ — تحديد معدل الطلبات (Rate limiting). https://docs.bullmq.io/guide/rate-limiting 2 3 4 5 6 7 8

  • توثيقات BullMQ — المهام المتوقفة (stalled jobs). https://docs.bullmq.io/guide/jobs/stalled 2 3 4 5 6 7 8 9 10

  • توثيقات BullMQ — استكشاف الأخطاء وإصلاحها. https://docs.bullmq.io/guide/troubleshooting 2 3

  • توثيقات BullMQ — الاتصالات. https://docs.bullmq.io/guide/connections 2 3 4

  • توثيقات BullMQ — الانتقال إلى بيئة الإنتاج. https://docs.bullmq.io/guide/going-to-production 2 3 4 5 6

  • توثيق BullMQ — Workers. https://docs.bullmq.io/guide/workers 2 3

  • توثيق BullMQ — الانتقال من v5 إلى v6. https://docs.bullmq.io/guide/migrations/migrate-from-v5-to-v6 2 3 4 5

  • توثيق BullMQ — المهام المتوقفة (workers). https://docs.bullmq.io/guide/workers/stalled-jobs 2

  • توثيق BullMQ — التزامن العالمي (Global Concurrency). https://docs.bullmq.io/guide/queues/global-concurrency 2 3

  • توثيق BullMQ — Queues. https://docs.bullmq.io/guide/queues

  • الأسئلة الشائعة

    راجع أربع حاجات بالترتيب: إن الـ worker متصل بـ Redis، وإنه بيستخدم نفس اسم الـ queue والـ prefix اللي بيستخدمهم الـ producer، وإن الـ queue مش معمول لها pause أو rate limited، وبعد كده بس شوف لو المهام بتعلق (stalling). لو getWorkersCount() رجعت صفر، ده بيحل أول نقطتين مع بعض.