شرح pg-boss: طابور مهام Postgres في Node 24 (2026)

١٥ مايو ٢٠٢٦

pg-boss Tutorial: Postgres Job Queue in Node 24 (2026)

لبناء طابور مهام (job queue) بمستوى الإنتاج في Postgres باستخدام Node.js، قم بتثبيت pg-boss@12.18.2 مقابل قاعدة بيانات Postgres 18، واستدعِ boss.createQueue() لكل طابور تحتاجه مع ضبط retryLimit و retryBackoff و deadLetter، ثم سجل معالجات مكتوبة (typed handlers) باستخدام boss.work(queue, ([job]) => ...). يستخدم pg-boss خاصية Postgres المسماة SELECT ... FOR UPDATE SKIP LOCKED لعملية سحب المهام (dequeue) الذرية، بحيث تلتقط العمال (workers) عبر عمليات Node المتعددة المهام دون تعارض — لا حاجة لـ Redis.12

سؤال "هل يجب أن أضيف Redis فقط من أجل المهام؟" يظهر في كل خلفية برمجية (backend) لـ Node.js تتجاوز بضعة آلاف طلب في الثانية (RPS). يجيب pg-boss على ذلك بالاعتماد على Postgres الذي تشغله بالفعل: نفس النسخ الاحتياطية، نفس المراقبة، ونفس مديري قواعد البيانات.1 إنه ليس بديلاً مباشراً لـ BullMQ — فسقف أداء SKIP LOCKED يقع بوضوح تحت ما يمكن أن تتحمله الطوابير المدعومة بـ Redis — ولكن بالنسبة لـ 90% من الفرق التي تشغل عشرات أو مئات المهام في الثانية مع دلالات سير عمل غنية (إعادة المحاولة مع التذبذب "jitter"، الجدولة الزمنية "cron"، طوابير الرسائل المهملة "dead-letter queues"، النشر/الاشتراك "pub/sub")، فإنه يحذف جزءاً متحركاً كاملاً من بيئة الإنتاج.

يستعرض هذا البرنامج التعليمي إعداداً كاملاً لـ TypeScript مقابل الإصدار الحالي pg-boss@12.18.2 (أحدث إصدار على npm بتاريخ 11-05-2026)3 و Node 24 LTS. تم تثبيت إصدار كل تبعية (dependency). كل كتلة برمجية قابلة للتشغيل كما هي مكتوبة.

ملخص

ستقوم بإنشاء مشروع TypeScript مقابل Node 24 LTS، وتوجيهه إلى حاوية Postgres 18، وإنشاء طابورين مكتوبين (send-email و send-email-dlq) مع إعادات محاولة بتراجع أسي (exponential-backoff)، ونقل المهام الفاشلة إلى طابور الرسائل المهملة، وجدولة مهمة cron يومية بتوقيت Europe/Dublin، وتثبيت لوحة التحكم الرسمية @pg-boss/dashboard@1.1.3 للمراقبة، والإغلاق النظيف عند إشارة SIGTERM. وقت التشغيل من البداية للنهاية: حوالي 25 دقيقة. تم التحقق في 15 مايو 2026 مقابل pg-boss 12.18.2، و dashboard 1.1.3، و Postgres 18.3.345

ما ستتعلمه

  • كيف يستخدم pg-boss خاصية SKIP LOCKED لتقديم المهام مرة واحدة بالضبط بدون Redis
  • كيفية إعداد pg-boss 12 مقابل Postgres 18 في Docker مع TypeScript
  • كيفية تعريف حمولات الطابور الآمنة من حيث النوع (type-safe) باستخدام generics في send() و work()
  • كيفية تكوين إعادات المحاولة مع تراجع أسي متذبذب وطابور رسائل مهملة
  • كيفية جدولة مهام cron بمنطقة زمنية محددة من IANA
  • كيفية تثبيت حزمة @pg-boss/dashboard للمراقبة
  • كيفية التعامل مع الإغلاق السلس عند إشارة SIGTERM حتى لا تضيع المهام قيد التنفيذ
  • متى تختار pg-boss مقابل البدائل المدعومة بـ Redis مثل BullMQ

المتطلبات الأساسية

  • Node.js 24.15.0 أو أحدث (يتطلب pg-boss 12 إصدار node >=22.12.0)3
  • محرك Docker 20.10+ مع Compose v2 (يأتي Docker Desktop 3.4+ به تلقائياً)
  • معرفة بـ PostgreSQL على مستوى psql وسلاسل الاتصال
  • منفذ TCP شاغر على localhost:5432 لحاوية Postgres

ثبّت إصدار Node 24 تحديداً — فهو خط الدعم طويل الأمد (LTS) النشط حتى أكتوبر 2026، مع دعم أمني حتى أبريل 2028.6 لا يزال Node 22 يعمل (يحتاج pg-boss فقط إلى >=22.12.0) ولكنه الآن في مرحلة الصيانة (Maintenance LTS).6

الخطوة 1: إنشاء هيكل المشروع

أنشئ المجلد وثبّت كل تبعية. لا توجد أي من الإصدارات أدناه هي "الأحدث" (latest) — بل هي العلامات الدقيقة المنشورة على npm اعتباراً من يوم كتابة هذا البرنامج التعليمي (تم التحقق باستخدام npm view).

mkdir pg-boss-tutorial && cd pg-boss-tutorial
npm init -y
npm pkg set type=module
npm install pg-boss@12.18.2 pg@8.20.0 dotenv@17.4.2
npm install -D typescript@6.0.3 tsx@4.22.0 \
  @types/node@22.19.17 @types/pg@8.20.0

سطر type=module غير قابل للتفاوض: يتم شحن pg-boss 12 كحزمة ESM نقية ("type": "module" في ملف package.json الخاص بها).3 تحميلها من مشروع CommonJS يتطلب آلية require(esm) في Node، ولهذا السبب رفع pg-boss متطلبات المحركات إلى >=22.12.0 (عندما أصبح require(esm) متاحاً بدون علامات تجريبية).3

أنشئ ملف tsconfig.json:

{
  "compilerOptions": {
    "target": "ES2024",
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "resolveJsonModule": true,
    "outDir": "dist",
    "rootDir": "src"
  },
  "include": ["src/**/*.ts"]
}

أنشئ ملف docker-compose.yaml لـ Postgres 18:

services:
  postgres:
    image: postgres:18-alpine
    container_name: pgboss-db
    environment:
      POSTGRES_USER: pgboss
      POSTGRES_PASSWORD: pgboss
      POSTGRES_DB: pgboss
    ports:
      - "5432:5432"
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U pgboss -d pgboss"]
      interval: 2s
      timeout: 3s
      retries: 10

علامة postgres:18-alpine تشير حالياً إلى Postgres 18.3 على Alpine 3 (تم التحقق في Docker Hub في 15 مايو 2026).5 يدعم pg-boss رسمياً أي إصدار Postgres 13 أو أحدث، لذا فإن 18 يقع ضمن النطاق المطلوب تماماً.1

قم بتشغيل الحاوية وتأكد من أن قاعدة البيانات تعمل بشكل جيد:

docker compose up -d
docker compose ps

أخيراً، أنشئ ملف .env:

DATABASE_URL=postgresql://pgboss:pgboss@localhost:5432/pgboss

الخطوة 2: تهيئة pg-boss مع حمولات مكتوبة

أنشئ ملف src/boss.ts. هذا هو المكان الوحيد الذي ينشئ نسخة PgBoss للتطبيق بأكمله:

import 'dotenv/config';
import PgBoss from 'pg-boss';

const connectionString = process.env.DATABASE_URL;
if (!connectionString) throw new Error('DATABASE_URL is required');

export const boss = new PgBoss({
  connectionString,
  schema: 'pgboss',
  application_name: 'pg-boss-tutorial',
  // إعدادات افتراضية حكيمة للإنتاج — راجع مرجع تكوين pg-boss
  monitorIntervalSeconds: 60,
  maintenanceIntervalSeconds: 86_400,
  superviseIntervalSeconds: 60,
  warningQueueSize: 1_000,
  warningSlowQuerySeconds: 5,
});

// إلزامي في الإنتاج: pg-boss هو EventEmitter وسيقوم بإيقاف عملية
// Node بحدث 'error' غير معالج إذا نسيت هذا.
boss.on('error', (err) => {
  console.error('[pg-boss] internal error', err);
});

الإعدادات الافتراضية هنا مقصودة. قيم monitorIntervalSeconds=60 و maintenanceIntervalSeconds=86400 تطابق الإعدادات الافتراضية الموثقة، و warningQueueSize=1000 تجعل pg-boss يظهر تحذيراً عند كبر حجم الطابور عندما يتجاوز تراكم أي طابور 1000 مهمة — وهي إشارة أبكر بكثير لتعطل أحد العمال من انتظار تحول لوحة التحكم إلى اللون الأحمر.7

استدعاء boss.start() سيقوم، في أول تشغيل، بإنشاء مخطط pgboss، وجدول pgboss.job المقسم، وكل الجداول الداعمة.1 افتراضياً migrate: true، لذا فإن الترقيات اللاحقة من إصدار فرعي لـ pg-boss إلى الإصدار التالي ستشغل عمليات الهجرة الخاصة بها بشفافية. في بيئة الإنتاج، قد ترغب في ضبط migrate: false وتطبيق عمليات الهجرة كخطوة نشر متعمدة؛ في هذا البرنامج التعليمي سنترك الخيار الافتراضي مفعلاً.

الخطوة 3: تعريف الطوابير مع إعادات المحاولة وطابور الرسائل المهملة

منذ الإصدار 11.0 من pg-boss، يجب أن تكون الطوابير موجودة قبل استدعاء send() أو insert() عليها — حيث تمت إزالة سلوك الإنشاء التلقائي عند تقديم نموذج التقسيم (partitioning model).8 عرّف طوابيرك مركزياً بحيث يعيش العقد في ملف واحد.

أنشئ ملف src/queues.ts:

import { boss } from './boss.js';

// عقود الحمولة المكتوبة — يتم تصديرها بحيث يتشاركها المنتجون والعمال
export type SendEmailJob = {
  to: string;
  subject: string;
  bodyHtml: string;
};

export type DailyDigestJob = {
  date: string; // YYYY-MM-DD
};

export const QUEUES = {
  sendEmail: 'send-email',
  sendEmailDlq: 'send-email-dlq',
  dailyDigest: 'daily-digest',
} as const;

export async function ensureQueues(): Promise<void> {
  // يجب إنشاء طابور الرسائل المهملة قبل الطابور الذي يشير إليه
  await boss.createQueue(QUEUES.sendEmailDlq, { policy: 'standard' });

  await boss.createQueue(QUEUES.sendEmail, {
    policy: 'standard',
    retryLimit: 5,            // المحاولة حتى 5 مرات قبل النقل لـ DLQ
    retryDelay: 30,           // تأخير أساسي 30 ثانية
    retryBackoff: true,       // تراجع أسي متذبذب
    expireInSeconds: 600,     // اعتبار المهام العالقة فاشلة بعد 10 دقائق
    retentionDays: 14,        // الاحتفاظ بالمهام المكتملة/الفاشلة لمدة 14 يوماً
    deadLetter: QUEUES.sendEmailDlq,
  });

  await boss.createQueue(QUEUES.dailyDigest, {
    policy: 'standard',
    retryLimit: 3,
    retryDelay: 60,
    retryBackoff: true,
  });
}

هناك ثلاث تفاصيل تستحق التوقف عندها:

  • طابور الرسائل المهملة (Dead-letter queue) هو مجرد طابور آخر. لا يقوم pg-boss بإنشاء جدول DLQ مخفي؛ بل تقوم أنت بإنشاء طابور عادي وتوجيه الطابور الأساسي إليه بالاسم باستخدام خيار deadLetter.9 عندما يتم استنفاد retryLimit، يتم نقل الوظيفة الفاشلة إلى DLQ كوظيفة جديدة بياناتها هي الحمولة الأصلية. هذا يعني أنك تكتب معالج DLQ تماماً مثل أي معالج آخر.
  • retryBackoff: true يفعل التراجع الأسي مع التذبذب (jittered exponential backoff). إذا قمت بضبط retryBackoff دون ضبط retryDelay، فإن pg-boss يجعل التأخير الأساسي الافتراضي ثانية واحدة.10 منحنى التراجع الدقيق هو "أسي مع تذبذب"، لذا فإن الوظيفة التي تفشل بشكل دائم تستقر في DLQ على مقياس من دقيقة إلى ساعة تقريباً بدلاً من ثوانٍ - وهي فترة طويلة بما يكفي لتنجلي الانقطاعات المؤقتة في الخدمات التابعة، وقصيرة بما يكفي لترى إدخال DLQ خلال وردية عمل.
  • expireInSeconds هو شبكة الأمان الخاصة بك للعمال الخارجين عن السيطرة. إذا تم جلب وظيفة ولكن عملية العامل توقفت قبل أن ينتهي المعالج، فستقوم حلقة الإشراف بتمييز الوظيفة كفاشلة بعد انتهاء هذه المهلة و(إذا بقيت محاولات إعادة) ستعيد وضعها في الطابور.7

الخطوة 4: إرسال ومعالجة الوظائف بمعالجات آمنة النوع

أنشئ src/workers.ts لجانب المستهلك:

import type { Job } from 'pg-boss';
import { boss } from './boss.js';
{
  QUEUES,
  type SendEmailJob,
  type DailyDigestJob,
} from './queues.js';

// Stand-in for your real email provider
async function deliverEmail(payload: SendEmailJob): Promise<void> {
  console.log(`[email] -> ${payload.to}: ${payload.subject}`);
  if (payload.to.endsWith('@bounce.example')) {
    throw new Error('mailbox does not exist');
  }
}

export async function registerWorkers(): Promise<void> {
  // Single-job handler (batchSize defaults to 1). The handler ALWAYS
  // receives an array since pg-boss v10, so destructure it.
  await boss.work<SendEmailJob>(
    QUEUES.sendEmail,
    { batchSize: 1, pollingIntervalSeconds: 2 },
    async ([job]: Job<SendEmailJob>[]) => {
      await deliverEmail(job.data);
    },
  );

  // Dead-letter handler: log, page on-call, or move to a Slack channel
  await boss.work<SendEmailJob>(
    QUEUES.sendEmailDlq,
    async ([job]: Job<SendEmailJob>[]) => {
      console.error(
        `[dlq] giving up on email to ${job.data.to} (job ${job.id})`,
      );
      // In production: emit metric, page on-call, or stash in a "needs human"
      // table. The job stays here until you explicitly resolve it.
    },
  );

  // Batched handler — process 50 digest jobs per fetch
  await boss.work<DailyDigestJob>(
    QUEUES.dailyDigest,
    { batchSize: 50, pollingIntervalSeconds: 5 },
    async (jobs: Job<DailyDigestJob>[]) => {
      console.log(`[digest] processing batch of ${jobs.length}`);
      for (const job of jobs) {
        // Real work goes here
        await new Promise((r) => setTimeout(r, 10));
      }
    },
  );
}

يقوم النوع العام Job<T> بتمرير نوع الحمولة إلى المعالج الخاص بك، لذا فإن job.data.to يتم تعريفه كـ string. إذا حاول منتج ما استخدام send() مع كائن غير صحيح، فسوف يرفضه TypeScript في وقت الترجمة. يعمل نفس النوع العام أيضاً على boss.send<SendEmailJob>(...).

نقطتان تقنيتان غالباً ما يتم إغفالهما:

  • تمت إزالة teamSize و teamConcurrency و teamRefill في الإصدار 10. الدروس التعليمية الأقدم من 2024 لا تزال تذكرهم؛ لكنهم لم يعودوا موجودين.11 يتم التحكم في الضغط العكسي (Backpressure) من خلال الجمع بين batchSize وعدد استدعاءات boss.work() التي تسجلها وعدد العمليات التي تقوم بتشغيلها.
  • المعالج يحصل دائماً على مصفوفة. تفكيك الوظيفة الواحدة (([job]) => ...) هو نمط الهجرة للإصدار 10+ من توقيع (job) => ... القديم.11 إذا نسيت التفكيك مع batchSize: 1، فستعمل بصمت على المصفوفة ككل.

الآن قم بربط كل شيء معاً في src/index.ts:

import { boss } from './boss.js';
import { ensureQueues, QUEUES } from './queues.js';
import { registerWorkers } from './workers.js';

async function main(): Promise<void> {
  await boss.start();
  console.log('[pg-boss] started');

  await ensureQueues();
  await registerWorkers();

  // Producer: queue a few jobs
  const id1 = await boss.send(QUEUES.sendEmail, {
    to: 'alice@example.com',
    subject: 'Hello from pg-boss',
    bodyHtml: '<p>It works.</p>',
  });
  console.log(`[send] queued job ${id1}`);

  const id2 = await boss.send(QUEUES.sendEmail, {
    to: 'broken@bounce.example', // will fail and eventually hit DLQ
    subject: 'This one is going to fail',
    bodyHtml: '<p>Bounce me.</p>',
  });
  console.log(`[send] queued job ${id2}`);
}

main().catch((err) => {
  console.error(err);
  process.exit(1);
});

قم بتشغيله:

npx tsx src/index.ts

يجب أن ترى وظيفة واحدة تنجح على الفور، والوظيفة المرتدة تعيد المحاولة بتأخيرات متزايدة، وفي النهاية يظهر سطر [dlq] بعد استنفاد المحاولات. اترك العملية تعمل للخطوات التالية.

الخطوة 5: جدولة وظيفة cron مع منطقة زمنية

استخدم boss.schedule(name, cron, data, options) لتسجيل وظيفة متكررة. معلمة name هي اسم الطابور الذي تستقر فيه الوظيفة - وليس "اسم جدول" منفصل. يقوم pg-boss بتشغيل Timekeeper الذي يقيم تعبيرات cron كل 30 ثانية وينشئ وظائف في الطابور المطابق.12

أضف هذا داخل main()، بعد registerWorkers():

// 09:00 Europe/Dublin every day — uses IANA timezone via the tz option
await boss.schedule(
  QUEUES.dailyDigest,
  '0 9 * * *',
  { date: new Date().toISOString().slice(0, 10) },
  { tz: 'Europe/Dublin' },
);
console.log('[schedule] daily-digest registered for 09:00 Europe/Dublin');

بعض الحقائق المتعلقة بـ cron:

  • يستخدم pg-boss نظام cron المكون من 5 حقول (minute hour day month dayOfWeek).12 نظام cron المكون من ستة حقول (مع الثواني) غير مدعوم.
  • عبر عمليات Node المتعددة، تكتسب عملية واحدة فقط قفل cron وتطلق الجدول الزمني في أي لحظة معينة - يستخدم pg-boss كائناً منفرداً (singleton) لمدة 60 ثانية بالإضافة إلى قفل موزع لضمان ذلك.12
  • يعيش إدخال الجدول الزمني في جدول pgboss.schedule، وليس في الكود الخاص بك، لذا فإن إعادة تشغيل العملية لا تؤدي إلى إطلاق cron مرتين. إذا قمت بتغيير اسم طابور، فاحذف الصف القديم من pgboss.schedule صراحةً - وإلا سيستمر pg-boss في إرسال الوظائف إلى اسم الطابور المهجور دون وجود معالج.

الخطوة 6: تثبيت لوحة التحكم

تأتي واجهة مراقبة المراقبة الرسمية كحزمة منفصلة، @pg-boss/dashboard@1.1.3.4 يتم توزيعها كواجهة سطر أوامر مستقلة (ملف pg-boss-dashboard الثنائي)، وليس كمكتبة - لا يوجد createServer() لاستيراده. يتم التكوين بالكامل عبر متغيرات البيئة.

npm install @pg-boss/dashboard@1.1.3

قم بتشغيلها في نافذة أوامر ثانية، مع اختيار منفذ مختلف عن تطبيقك:

DATABASE_URL=postgresql://pgboss:pgboss@localhost:5432/pgboss \
PGBOSS_SCHEMA=pgboss \
PORT=3001 \
npx pg-boss-dashboard

متغيرات البيئة المدعومة هي:

  • DATABASE_URL — سلسلة اتصال Postgres. القيمة الافتراضية هي postgres://localhost/pgboss.
  • PGBOSS_SCHEMA — المخطط (schema) الذي تم ترحيل pg-boss إليه. القيمة الافتراضية هي pgboss.
  • PORT — منفذ HTTP للاستماع عليه. القيمة الافتراضية هي 3000.
  • PGBOSS_DASHBOARD_AUTH_USERNAME / PGBOSS_DASHBOARD_AUTH_PASSWORD — تفعيل مصادقة HTTP الأساسية (يوصى بها بشدة خارج localhost).

افتح http://localhost:3001. سترى طوابيرك، وحالات الوظائف الأخيرة، وعارض حمولة كل وظيفة. تتيح لك لوحة التحكم أيضاً إلغاء الوظائف أو إعادة محاولتها أو استئنافها أو حذفها مباشرة من الواجهة - وهو أمر مفيد لفرز الإدخالات في طابور الرسائل المهملة دون كتابة سكربت لمرة واحدة.4

الخطوة 7: الإغلاق التدريجي عند استقبال SIGTERM

في بيئة الإنتاج، يرسل المنسق (Kubernetes، ECS، fly.io، systemd) إشارة SIGTERM وينتظر فترة سماح قبل فرض SIGKILL. إذا كان pg-boss في منتصف وظيفة عندما تصل SIGKILL، فستبقى تلك الوظيفة في حالة active حتى تنقضي expireInSeconds - وهذا بالضبط نوع الفشل "العالق لمدة عشر دقائق" الذي من المفترض أن يمنعه طابور الوظائف.

أضف هذا إلى نهاية src/index.ts:

async function shutdown(signal: string): Promise<void> {
  console.log(`[pg-boss] received ${signal, draining…`);
  try {
    await boss.stop({
      timeout: 25_000,  // Match this to your platform's grace period
      graceful: true,   // Wait for in-flight handlers to resolve
      destroy: false,   // Let pg release connections cleanly
    });
    console.log('[pg-boss] stopped cleanly');
    process.exit(0);
  } catch (err) {
    console.error('[pg-boss] forced shutdown', err);
    process.exit(1);
  }
}

process.on('SIGTERM', () => void shutdown('SIGTERM'));
process.on('SIGINT', () => void shutdown('SIGINT'));

يجب أن تكون قيمة timeout أقل قليلاً من terminationGracePeriodSeconds الخاصة بمنصتك (القيمة الافتراضية في Kubernetes هي 30). ضبطها بقيمة أقل من فترة السماح يضمن أن لدى pg-boss وقتاً للانتهاء قبل أن يقوم المنسق بالقتل القسري.

التحقق

افتح ثلاث نوافذ أوامر وقم بإجراء اختبار الدخان. في النافذة الأولى:

docker compose up -d
npx tsx src/index.ts

في النافذة الثانية:

DATABASE_URL=postgresql://pgboss:pgboss@localhost:5432/pgboss \
PORT=3001 \
npx pg-boss-dashboard

في النافذة الثالثة، ألقِ نظرة مباشرة على جدول الوظائف المقسم:

docker exec -it pgboss-db \
  psql -U pgboss -d pgboss -c \
  "SELECT name, state, retry_count, created_on
   FROM pgboss.job
   ORDER BY created_on DESC LIMIT 10;"

المتوقع: وظيفة send-email واحدة على الأقل في حالة completed، والوظيفة المرتدة تنتقل عبر retryfailed ← وأخيراً وظيفة send-email-dlq في حالة created أو completed. يجب أن تظهر واجهة الطوابير في لوحة التحكم نفس الشيء.

الآن اضغط على Ctrl-C في النافذة الأولى. يجب أن ترى received SIGINT, draining… متبوعة بـ stopped cleanly في غضون ثوانٍ قليلة - والوظيفة النشطة (إن وجدت) يجب أن تكون completed في قاعدة البيانات، وليست عالقة في active.

الأخطاء الشائعة وإصلاحها

  • Error: queue "send-email" does not exist — قمت باستدعاء boss.send() قبل boss.createQueue(). تمت إزالة الإنشاء التلقائي في pg-boss 11.8 احرص دائمًا على استدعاء ensureQueues() بعد boss.start().
  • المعالج يعمل مرة واحدة ولكن المتغير undefined — لقد نسيت تفكيك مصفوفة الوظائف (destructure). منذ الإصدار v10، تتلقى معالجات boss.work() دائمًا Job<T>[]، وليس Job<T>.11 استخدم async ([job]) => { ... } للمعالجات التي تعمل على وظيفة واحدة.
  • وظائف Cron لا تعمل أبدًا — تأكد من ضبط schedule: true (القيمة الافتراضية) في منشئ PgBoss وأن supervise: true (القيمة الافتراضية أيضًا) لم يتم تعطيلها. يقوم مكون Timekeeper بتشغيل وظائف داخلية على طابور __pgboss__send-it؛ يمكنك فحص هذا الطابور في لوحة التحكم للتأكد من أن الجدول الزمني يعمل.12
  • لوحة التحكم تقول "schema not found" — يجب أن تكون قاعدة البيانات في connectionString هي نفسها التي قام pg-boss بترحيلها (migrated). إذا وجهت لوحة التحكم إلى قاعدة بيانات مختلفة، فلن تجد أي جداول. تأكد باستخدام \dn في psql من وجود مخطط (schema) pgboss.
  • الوظائف عالقة في حالة active — يعني هذا دائمًا تقريبًا أن العامل (worker) تعطل في منتصف المعالجة. يخبر expireInSeconds (المحدد في الطابور) pg-boss بتمييز هذه الوظائف كفاشلة بعد انتهاء المهلة؛ ثم يبدأ منطق إعادة المحاولة. إذا رأيت هذا كثيرًا، فقم بتقليل expireInSeconds وراجع كود المعالج بحثًا عن وعود (promises) مرفوضة لم يتم التعامل معها.

متى تختار شيئًا آخر

يعتمد pg-boss على Postgres SELECT ... FOR UPDATE SKIP LOCKED لعملية سحب المهام الذرية (atomic dequeue).2 هذه الآلية تتوسع أفقيًا حتى النقطة التي يهيمن فيها التنافس وقت الإرسال (commit-time contention) على تقسيمات الوظائف — وهي نقطة عالية بما يكفي لمعظم التطبيقات، ولكنها أقل من طابور مدعوم بـ Redis. إذا كنت تحتاج حقًا إلى عشرات الآلاف من الوظائف في الثانية، مع تأخيرات بدقة الملي ثانية وتبعيات معقدة بين الأب والابن، فإن BullMQ على Redis يظل أداة أكثر حدة — تظهر اتجاهات npm أنه يحقق تقريبًا عشرة أضعاف التحميلات الأسبوعية مقارنة بـ pg-boss أو Graphile Worker لهذا السبب.13

بالنسبة للآخرين — الفريق الذي يدير Postgres لكل شيء آخر، والشركات الناشئة الصغيرة (SaaS) التي لا تريد إضافة Redis كنقطة فشل واحدة (SPOF)، والمشاريع الجانبية التي تريد جدولة cron موثوقة ودلالات DLQ جاهزة للاستخدام — فإن pg-boss هو خيار افتراضي ممتاز.

الخطوات التالية ومزيد من القراءة

Footnotes

  1. timgit/pg-boss README — "يعتمد pg-boss على SKIP LOCKED في Postgres، وهي ميزة تم بناؤها خصيصًا لطوابير الرسائل... تسليم لمرة واحدة بالضبط وأمان الالتزامات الذرية المضمونة لمعالجة الوظائف غير المتزامنة." https://GitHub.com/timgit/pg-boss 2 3 4

  2. وثائق PostgreSQL — FOR UPDATE SKIP LOCKED (تم تقديمها في PostgreSQL 9.5). https://www.postgresql.org/docs/current/sql-select.html#SQL-FOR-UPDATE-SHARE 2

  3. سجل pg-boss npm — الإصدار 12.18.2، "type": "module"، "engines": {"node": ">=22.12.0"}، التبعيات pg ^8.20.0 و cron-parser ^5.5.0. https://www.npmjs.com/package/pg-boss 2 3 4 5

  4. سجل @pg-boss/dashboard npm — الإصدار 1.1.3، يتطلب Node 22.12+ و pg-boss 12.11+. https://www.npmjs.com/package/@pg-boss/dashboard 2 3

  5. إعلان إصدار PostgreSQL 18 (25 سبتمبر 2025) بالإضافة إلى وسم postgres:18-alpine الحالي على Docker Hub (Postgres 18.3). https://www.postgresql.org/about/news/postgresql-18-released-3142/ و https://hub.Docker.com/_/postgres 2

  6. جدول إصدارات Node.js — دخل Node 24 مرحلة Active LTS في أكتوبر 2025؛ نهاية الدعم (EOL) في 30 أبريل 2028. https://endoflife.date/nodejs 2

  7. مرجع تكوين pg-boss — الافتراضي monitorIntervalSeconds=60، maintenanceIntervalSeconds=86400، superviseIntervalSeconds=60، warningQueueSize، warningSlowQuerySeconds. https://deepwiki.com/timgit/pg-boss/15.5-configuration-reference 2

  8. ملاحظات إصدار pg-boss v11 — يجب إنشاء الطوابير (queues) قبل الإرسال/الإدراج؛ تم تقديم تقسيم جداول المهام (job table partitioning). https://GitHub.com/timgit/pg-boss/releases/tag/11.0.0 2

  9. ملاحظات إصدار pg-boss v10 — تمت إضافة خيار deadLetter إلى send() و insert() و createQueue(). https://GitHub.com/timgit/pg-boss/releases/tag/10.0.0

  10. تكوين إعادة المحاولة في pg-boss — خيار retryBackoff: true يفعل التراجع الأسي مع التذبذب (jittered exponential backoff) ويجعل retryDelay افتراضياً 1 إذا لم يتم تحديده. https://deepwiki.com/timgit/pg-boss/11.1-retry-configuration

  11. ملاحظات إصدار pg-boss v10 — تم توحيد معالج boss.work() ليتلقى دائماً Job[]؛ وتمت إزالة teamSize و teamConcurrency و teamRefill. https://GitHub.com/timgit/pg-boss/releases/tag/10.0.0 2 3

  12. وثائق الجدولة المعتمدة على Cron في pg-boss — يقوم Timekeeper بتقييم تعبيرات cron المكونة من 5 حقول؛ خيار tz يقبل المناطق الزمنية IANA؛ نظام الـ singleton لمدة 60 ثانية بالإضافة إلى القفل الموزع (distributed lock) يمنع تكرار تشغيل cron. https://deepwiki.com/timgit/pg-boss/10.1-cron-based-scheduling 2 3 4

  13. مقارنة npm-trends بين BullMQ و pg-boss و graphile-worker (يتصدر BullMQ التحميلات الأسبوعية بنحو عشرة أضعاف تقريباً). https://npmtrends.com/bullmq-vs-graphile-worker-vs-pg-boss

  14. بوابة وثائق pg-boss API. https://timgit.GitHub.io/pg-boss/


نشرة أسبوعية مجانية

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

بريد واحد أسبوعياً — دورات، مقالات معمّقة، أدوات، وتجارب ذكاء اصطناعي.

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