---
title: BullMQ Worker لا يعالج المهام؟ إليك الحل (2026)
description: عامل BullMQ لا يقوم بمعالجة المهام؟ ابحث في اسم الطابور والبادئة (prefix)، وmaxRetriesPerRequest، وأقفال المهام المتوقفة (stalled-job locks)، وتغيير التبعية المتقاطعة (peer dependency) لـ ioredis في الإصدار v6.
url: https://nerdleveltech.com/ar/bullmq-worker-not-processing-jobs
language: ar
date: '2026-08-03'
category: backend
tags:
  - bullmq
  - redis
  - job queue
  - nodejs
  - backend
  - stalled jobs
  - ioredis
markdown_url: https://nerdleveltech.com/ar/bullmq-worker-not-processing-jobs.md
attribution: 'Source: NerdLevelTech — https://nerdleveltech.com/ar/bullmq-worker-not-processing-jobs'
terms: https://nerdleveltech.com/llms.txt
---

# BullMQ Worker لا يعالج المهام؟ إليك الحل (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 على الإطلاق.[^2][^3]

## ما ستتعلمه

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

إرجاع `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) بدون تعيين، فإنهما يعملان على مساحتي مفاتيح منفصلتين تماماً، ولن يبلغ أي منهما عن خطأ.

```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`.[^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` يتجاوز كليهما أيضاً. ومن الجدير بالذكر أيضاً أن دليل الإنتاج يصف كلتا الحالتين بأنهما تنتجان تحذيراً؛[^10] لكن إطلاق الخطأ (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` على قيمته الافتراضية (أو تعيينها إلى 1) للمنتجين (producers) بحيث يفشل مستدعي HTTP بسرعة،[^8] وفي دليل الإنتاج — يتم تعطيل `enableOfflineQueue` الخاص بـ ioredis في الـ `Queue` مع تركه مفعلاً في الـ `Worker`.[^10]

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

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

```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\_\_ بنفسك.[^9] القيمة الافتراضية هي `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_modules` — `bullmq` واعتمادياته المباشرة، بالإضافة إلى حزم `luxon` و msgpackr-extract التي تسحبها تلك الحزم. لا يوجد أي منها كعميل Redis. وبالتالي يفشل استدعاء الحزمة فوراً:

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

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

```bash
npm install bullmq ioredis
```

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

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

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

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

```text
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`.[^11][^1]

## إيه هي الـ 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]

| الخيار                               | الافتراضي          | ما الذي يتحكم فيه                                            |
| ------------------------------------ | ------------------ | ------------------------------------------------------------ |
| `lockDuration`                       | 30000 مللي ثانية   | المدة التي يظل فيها مطالبة العامل بمهمة ما سارية دون تجديد   |
| `lockRenewTime`                      | نصف `lockDuration` | وتيرة التجديد؛ تنصح الوثائق بعدم تغييرها                     |
| `stalledInterval`                    | 30000 مللي ثانية   | عدد مرات تشغيل فحص التوقف (stalled check)                    |
| `maxStalledCount`                    | 1                  | عدد مرات الاستعادة من التوقف قبل اعتبار المهمة فاشلة         |
| `concurrency`\_\_PRESER preserve\_\_ | 1                  | عدد المهام التي يشغلها مثيل عامل واحد بالتوازي               |
| `drainDelay`                         | 5 (ثوانٍ)          | نافذة الاستطلاع الطويل (long-poll) عندما يكون الطابور فارغًا |
| `maximumRateLimitDelay`              | 30000 مللي ثانية   | الحد الأقصى للخمول أثناء تقييد المعدل (rate limited)         |

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

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

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

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

تبدو الحالات الثلاث متطابقة من الخارج — حيث تتراكم المهام في `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` مطلوباً".[^6][^14][^5]

هذا الأمر مهم للتشخيص لأن الإرشادات المكتوبة لـ 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]

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

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

**ليه المهام بتاعتي واقفة في حالة الانتظار (waiting state)؟** حالة `waiting` هي منطقة الانتظار للمهام اللي مفيش حد استلمها، وللمهام اللي عليها rate-limited أو اللي علقت (stalled). المهام اللي عليها rate-limited بتفضل صراحة في حالة `waiting`،[^5] والمهمة اللي بتعلق بترجع هناك تاني بدل ما تروح لحالة خاصة بيها.[^6]

**هل الـ queue والـ worker محتاجين نفس الـ prefix؟** أيوه. الـ `prefix` الافتراضي هو `bull` وهو اللي بيحدد الـ namespace لكل مفتاح Redis في الـ queue دي.[^1] الـ producer والـ consumer اللي عندهم prefixes مختلفة بيشتغلوا على مساحات مفاتيح منفصلة وعمرهم ما بيطلعوا خطأ.

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

**إزاي أعمل debug لـ worker مش بيسحب المهام؟** اسأل الـ queue بدل ما تقرأ الـ processor بتاعك: `isPaused()`، و `getJobCounts()`، و `getWorkersCount()` و `getRateLimitTtl()` بيغطوا الاتصال، والهوية، والحالة في خطوة واحدة. تعامل مع `getWorkersCount()` لو رجعت `1` بالضبط بشك لو كنت بتستخدم Redis مدارة (managed) بتعمل block لأمر `CLIENT`.[^1]

**ليه الـ worker بتاعي وقف معالجة المهام بعد ما حصل خطأ (error)؟** على الأغلب مفيش `error` listener متصل. توثيقات BullMQ بتحذر إن "لو الـ error handler مش موجود، الـ worker بتاعك ممكن يوقف معالجة المهام لما يحصل error".[^9] اربط listener بالـ worker والـ queue.

**إيه هي المهام المعلقة (stalled jobs)؟** هي المهام اللي وصلت لحالة `active` وبعدين فشلت في تجديد القفل (lock) بتاعها، فـ BullMQ بتفترض إن الـ worker مات وبترجعهم لحالة `waiting`. مفيش حالة اسمها stalled، فيه بس حدث (event) اسمه stalled.[^6] بعد عدد `maxStalledCount` من عمليات الاستعادة (الافتراضي 1)، المهمة بتعتبر فاشلة نهائياً.[^1][^6]

**هل الـ queue بتاعتي معمول لها pause، أو rate limited، أو الـ concurrency بتاعها محدود؟** التلات حالات شكلهم زي بعض من بره. جرب `await queue.isPaused()`،[^4] و `await queue.getRateLimitTtl(maxJobs)`،[^5] و `await queue.getGlobalConcurrency()`[^13] — نداء واحد لكل واحدة، والأولى بس هي اللي بتعتبر حالة خطأ حقيقية.

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

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

**هل يجب أن أرفع قيمة lockDuration لإيقاف الوظائف المتوقفة (stalled jobs)؟** فقط إذا كانت وظائفك تستغرق فعلياً وقتاً أطول من مدة القفل (lock). إذا كان المعالج (processor) يحجب حلقة أحداث Node.js (event loop)، فإن القفل الأطول يؤخر الفشل بدلاً من منعه — قم بإعادة التحكم إلى حلقة الأحداث أو استخدم معالجاً معزولاً (sandboxed processor) بدلاً من ذلك.[^12][^6]

**هل لا زلت بحاجة إلى QueueScheduler؟** لا — لم يعد ضرورياً منذ BullMQ 2.0.[^6] أي دليل لا يزال يوصي به للوظائف المؤجلة هو دليل قديم يسبق ذلك الإصدار.

**هل يمكن أن يتسبب إخلاء (eviction) Redis في ذلك؟** نعم. قم بتعيين `maxmemory-policy` إلى `noeviction`؛ حيث يذكر دليل الإنتاج أن هذا هو الإعداد الوحيد الذي يضمن سلوكاً صحيحاً للطابور، ويتم إدراج السياسة الخاطئة كسبب لفقدان أقفال الوظائف.[^10][^7]

## الخلاصة

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

إذا كنت تعيد تقييم الطابور نفسه بدلاً من تصحيح أخطائه، فمن المفيد معرفة تكلفة البدائل. طابور وظائف مدعوم بـ [Postgres باستخدام pg-boss](https://nerdleveltech.com/pg-boss-postgres-job-queue-node-TypeScript-production-tutorial) يزيل Redis من الصورة تماماً، و [NATS JetStream مع عمال دائمين و DLQ](https://nerdleveltech.com/nats-jetstream-TypeScript-durable-worker-retries-dlq-tutorial) يستبدله بنموذج تشغيلي مختلف، وتغطي [مقارنة Kafka مقابل RabbitMQ مقابل SQS مقابل NATS](https://nerdleveltech.com/message-queue-showdown-kafka-vs-rabbitmq-vs-sqs-vs-nats) الأوسع نطاقاً أين يناسب كل منها. إذا كنت ستستمر في استخدام Redis، فإن نفس المخاوف بشأن `noeviction` وإعادة استخدام الاتصال تظهر أيضاً في [أنماط التخزين المؤقت لـ Redis](https://nerdleveltech.com/Redis-caching-patterns-the-complete-guide-for-scalable-systems).

---

[^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]: بيانات 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>

[^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>

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

[^5]: توثيق BullMQ — تحديد معدل الطلبات (Rate limiting). <https://docs.bullmq.io/guide/rate-limiting>

[^6]: توثيقات BullMQ — المهام المتوقفة (stalled jobs). <https://docs.bullmq.io/guide/jobs/stalled>

[^7]: توثيقات BullMQ — استكشاف الأخطاء وإصلاحها. <https://docs.bullmq.io/guide/troubleshooting>

[^8]: توثيقات BullMQ — الاتصالات. <https://docs.bullmq.io/guide/connections>

[^10]: توثيقات BullMQ — الانتقال إلى بيئة الإنتاج. <https://docs.bullmq.io/guide/going-to-production>

[^9]: توثيق BullMQ — Workers. <https://docs.bullmq.io/guide/workers>

[^11]: توثيق BullMQ — الانتقال من v5 إلى v6. <https://docs.bullmq.io/guide/migrations/migrate-from-v5-to-v6>

[^12]: توثيق BullMQ — المهام المتوقفة (workers). <https://docs.bullmq.io/guide/workers/stalled-jobs>

[^13]: توثيق BullMQ — التزامن العالمي (Global Concurrency). <https://docs.bullmq.io/guide/queues/global-concurrency>

[^14]: توثيق BullMQ — Queues. <https://docs.bullmq.io/guide/queues>
