تخطَّ إلى المحتوى
NerdLevelTech
تطوير الواجهة الخلفية

BullMQ Worker لا يعالج المهام؟ إليك الحل (٢٠٢٦)

عامل BullMQ لا يقوم بمعالجة المهام؟ ابحث في اسم الطابور والبادئة (prefix)، وmaxRetriesPerRequest، وأقفال المهام المتوقفة (stalled-job locks)، وتغيير التبعية المتقاطعة (peer dependency) لـ ioredis في الإصدار v6.

Nerd Level Techنُشر في ٢١ دقيقة للقراءةEnglish
صورة غلاف: BullMQ Worker لا يعالج المهام؟ إليك الحل (2026)

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

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

ما ستتعلمه

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

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

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

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

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

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

typescript
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) متوقف عالمياً — لن يتم سحب أي مهام٤
waiting في تزايد، بينما active تظل 0عدم تطابق الهوية، توقف، تحديد معدل (rate limit)، أو حد التزامن العالمي (global concurrency)
active ليست صفراً ولكن المهام لا تنتهي أبداًمسار متوقف: أقفال (locks)، حظر حلقة الأحداث (event-loop blocking)، أو معالج ينهار (crashing processor)
rate-limit ttl أكبر من 0المحدد (limiter) يحتجز المهام في حالة waiting٥

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

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

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

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

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

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

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

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

typescript
// 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.٧ قم بالتحقق من هذه المتغيرات عند بدء التشغيل بدلاً من تركها تصل إلى Redis.

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

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

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

ما ليس واضحاً — وهو ما يغير طريقة تصحيحك لهذا الخطأ — هو أن 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 بتعيين هذا الإعداد افتراضياً إلى ٢٠ عندما تقوم بإنشاء عميل بنفسك،٨ لذا فإن أي عميل مشترك تم إنشاؤه يدوياً سيمر عبر أحد هذين المسارين. هناك حدان يستحقان المعرفة: الفحص يتم فقط للاتصالات الحاجبة (blocking)، لذا فإن نفس العميل الممرر إلى Queue لا يفعل أي من المسارين، وهو اختبار للقيمة الحقيقية (truthiness test)، لذا فإن maxRetriesPerRequest: 0 يتجاوز كليهما أيضاً. ومن الجدير بالذكر أيضاً أن دليل الإنتاج يصف كلتا الحالتين بأنهما تنتجان تحذيراً؛٩ لكن إطلاق الخطأ (throw) المذكور أعلاه هو ما يفعله bullmq@6.0.5 فعلياً عند تزويده بمثيل عميل.

typescript
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 على قيمته الافتراضية (أو تعيينها إلى ١) للمنتجين (producers) بحيث يفشل مستدعي HTTP بسرعة،٨ وفي دليل الإنتاج — يتم تعطيل enableOfflineQueue الخاص بـ ioredis في الـ Queue مع تركه مفعلاً في الـ Worker.٩

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

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

typescript
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__ بنفسك.١٠ القيمة الافتراضية هي true في bullmq@6.0.5،١ لذا فإن هذا الأمر يؤثر فقط على الأشخاص الذين قاموا بتعيين هذا الخيار عمداً في الاختبارات أو في تسلسل بدء التشغيل المرحلي ثم نسوا استدعاء 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) معلمة كاختيارية.٢ الاعتماديات النظيرة الاختيارية لا يتم تثبيتها تلقائياً، لذا فإن التثبيت النظيف ينتج شجرة حزم لا تحتوي على أي عميل Redis.

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

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

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

bash
npm install bullmq ioredis

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

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

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

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

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>.١ التفصيلة دي مهمة هنا — لو تجاهلت مستمع الـ error من القسم السابق، الرسالة دي هتوصل في صمت، وكل اللي هتلاحظه هو إن الوظيفة المتكررة وقفت فجأة عن التكرار.

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

المفروض عملية الهجرة دي تحصل وأنت لسه على v5: احصر التعريفات القديمة باستخدام getRepeatableJobs(), وأعد إنشاء كل واحدة كـ Job Scheduler، وتأكد باستخدام getJobSchedulers(), وبعدين امسح المدخلات القديمة — وما تعملش deploy لـ v6 غير لما كل producer و worker يكونوا شغالين بالـ schedulers.١١ وفي حاجتين صغيرين يستاهلوا تدور عليهم (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.١١١

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

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

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

الخيارالافتراضيما الذي يتحكم فيه
lockDuration٣٠٠٠٠ مللي ثانيةالمدة التي يظل فيها مطالبة العامل بمهمة ما سارية دون تجديد
lockRenewTimeنصف lockDurationوتيرة التجديد؛ تنصح الوثائق بعدم تغييرها
stalledInterval٣٠٠٠٠ مللي ثانيةعدد مرات تشغيل فحص التوقف (stalled check)
maxStalledCount١عدد مرات الاستعادة من التوقف قبل اعتبار المهمة فاشلة
concurrency__PRESER preserve__١عدد المهام التي يشغلها مثيل عامل واحد بالتوازي
drainDelay٥ (ثوانٍ)نافذة الاستطلاع الطويل (long-poll) عندما يكون الطابور فارغًا
maximumRateLimitDelay٣٠٠٠٠ مللي ثانيةالحد الأقصى للخمول أثناء تقييد المعدل (rate limited)

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

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

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

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

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

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

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

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

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

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

هذا الأمر مهم للتشخيص لأن الإرشادات المكتوبة لـ 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.١ كما أن بعض تحسينات استدعاءات الحظر (blocking-call) مقيدة بـ Redis 6.0.0 و 7.0.8.١

الخلاصة

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

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


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

ليه الـ BullMQ worker بتاعي مش بيعالج المهام (jobs)؟

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

ليه المهام بتاعتي واقفة في حالة الانتظار (waiting state)؟

حالة waiting هي منطقة الانتظار للمهام اللي مفيش حد استلمها، وللمهام اللي عليها rate-limited أو اللي علقت (stalled). المهام اللي عليها rate-limited بتفضل صراحة في حالة waiting، والمهمة اللي بتعلق بترجع هناك تاني بدل ما تروح لحالة خاصة بيها.

هل الـ queue والـ worker محتاجين نفس الـ prefix؟

أيوه. الـ prefix الافتراضي هو bull وهو اللي بيحدد الـ namespace لكل مفتاح Redis في الـ queue دي. الـ producer والـ consumer اللي عندهم prefixes مختلفة بيشتغلوا على مساحات مفاتيح منفصلة وعمرهم ما بيطلعوا خطأ.

يعني إيه "maxRetriesPerRequest must be null"؟

ده معناه إن اتصال الـ blocking Redis الخاص بالـ worker عنده حد أقصى لإعادة المحاولة (retry ceiling) والمفروض ميكونش عنده. لو بعت ioredis instance بقيمة غير null هيطلع error؛ أما لو بعت options object هيطلع تحذير بس و BullMQ هتغير القيمة لـ null.

إزاي أعمل debug لـ worker مش بيسحب المهام؟

اسأل الـ queue بدل ما تقرأ الـ processor بتاعك: isPaused()، و getJobCounts()، و getWorkersCount() و getRateLimitTtl() بيغطوا الاتصال، والهوية، والحالة في خطوة واحدة. تعامل مع getWorkersCount() لو رجعت ١ بالضبط بشك لو كنت بتستخدم Redis مدارة (managed) بتعمل block لأمر CLIENT.

ليه الـ worker بتاعي وقف معالجة المهام بعد ما حصل خطأ (error)؟

على الأغلب مفيش error listener متصل. توثيقات BullMQ بتحذر إن "لو الـ error handler مش موجود، الـ worker بتاعك ممكن يوقف معالجة المهام لما يحصل error". اربط listener بالـ worker والـ queue.

إيه هي المهام المعلقة (stalled jobs)؟

هي المهام اللي وصلت لحالة active وبعدين فشلت في تجديد القفل (lock) بتاعها، فـ BullMQ بتفترض إن الـ worker مات وبترجعهم لحالة waiting. مفيش حالة اسمها stalled، فيه بس حدث (event) اسمه stalled. بعد عدد maxStalledCount من عمليات الاستعادة (الافتراضي ١)، المهمة بتعتبر فاشلة نهائياً.

هل الـ queue بتاعتي معمول لها pause، أو rate limited، أو الـ concurrency بتاعها محدود؟

التلات حالات شكلهم زي بعض من بره. جرب await queue.isPaused()، و await queue.getRateLimitTtl(maxJobs)، و await queue.getGlobalConcurrency() — نداء واحد لكل واحدة، والأولى بس هي اللي بتعتبر حالة خطأ حقيقية.

هل يقوم BullMQ v6 بتثبيت ioredis نيابة عني؟

لا. في الإصدار v6، يعتبر ioredis تبعية نظيرة (peer dependency) اختيارية بدلاً من أن تكون تبعية مباشرة، لذا فإن تنفيذ npm install bullmq@6 بمفرده سيتركك بدون عميل Redis، وسيفشل require('bullmq') مع ظهور الخطأ Cannot find module 'ioredis/built/utils'. قم بتثبيت bullmq و ioredis معاً.

لماذا تعطل الـ worker الخاص بي بعد الترقية إلى v6؟

على الأرجح بسبب فقدان عميل Redis، أو بسبب بقايا بيانات وصفية (metadata) للوظائف المتكررة (repeatable-job). فقد قام الإصدار v6 بإزالة واجهات برمجة التطبيقات (APIs) القديمة للوظائف المتكررة ويقوم بإصدار خطأ عند مواجهة بيانات وصفية متكررة من الإصدار v5 بدلاً من التراجع في الأداء.

الحواشي

  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 ↩ ↩٢ ↩٣ ↩٤ ↩٥ ↩٦ ↩٧ ↩٨ ↩٩ ↩١٠ ↩١١ ↩١٢ ↩١٣ ↩١٤ ↩١٥

  • بيانات 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 ↩ ↩٢ ↩٣

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

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

  • توثيق BullMQ — تحديد معدل الطلبات (Rate limiting). https://docs.bullmq.io/guide/rate-limiting ↩ ↩٢ ↩٣ ↩٤ ↩٥ ↩٦ ↩٧ ↩٨

  • توثيقات BullMQ — المهام المتوقفة (stalled jobs). https://docs.bullmq.io/guide/jobs/stalled ↩ ↩٢ ↩٣ ↩٤ ↩٥ ↩٦ ↩٧ ↩٨ ↩٩ ↩١٠

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

  • توثيقات BullMQ — الاتصالات. https://docs.bullmq.io/guide/connections ↩ ↩٢ ↩٣ ↩٤

  • توثيقات BullMQ — الانتقال إلى بيئة الإنتاج. https://docs.bullmq.io/guide/going-to-production ↩ ↩٢ ↩٣ ↩٤ ↩٥ ↩٦

  • توثيق BullMQ — Workers. https://docs.bullmq.io/guide/workers ↩ ↩٢ ↩٣

  • توثيق BullMQ — الانتقال من v5 إلى v6. https://docs.bullmq.io/guide/migrations/migrate-from-v5-to-v6 ↩ ↩٢ ↩٣ ↩٤ ↩٥

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

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

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

  • ابقَ على مسار النيرد

    رسالتان كلّ أسبوع: شرح معمّق لموضوع واحد يوم الاثنين، وملخّص يوم الخميس للدورات والأدلّة والمقالات والبودكاست والوظائف.

    بدون إزعاج. إلغاء الاشتراك في أي وقت.