أحداث Webhook

أعمل على سير عمل إنتاجي حيث تحاول واجهة برمجة تطبيقات خارجية أحيانًا إعادة محاولة webhook نفسه، لذا يمكن لـ n8n استقبال أحداث مكررة خلال بضع ثوان. يقوم سير العمل بإنشاء سجلات قاعدة البيانات وتشغيل الإجراءات النهائية، لذا فإن التحقق البسيط من سجل التنفيذ لا يبدو موثوقًا.
أفكر في استخدام PostgreSQL مع معرّف حدث فريد و INSERT … ON CONFLICT DO NOTHING قبل المتابعة مع سير العمل.
INSERT INTO webhook_events (event_id, received_at)
VALUES ($1, NOW())
ON CONFLICT (event_id) DO NOTHING
RETURNING event_id;
هل هذا نمط جيد، أم أنك ستتعامل مع إزالة التكرارات بطريقة مختلفة؟

صف المشكلة/الخطأ/السؤال

ما رسالة الخطأ (إن وجدت)؟

يرجى مشاركة سير العمل الخاص بك

(حدد العقد على لوحتك واستخدم اختصارات لوحة المفاتيح CMD+C/CTRL+C و CMD+V/CTRL+V لنسخ ولصق سير العمل.)

شارك المخرجات التي يعيدها آخر عقدة

معلومات عن إعداد n8n الخاص بك

  • إصدار n8n:
  • قاعدة البيانات (الافتراضي: SQLite):
  • إعداد n8n EXECUTIONS_PROCESS (الافتراضي: own, main):
  • تشغيل n8n عبر (Docker, npm, n8n cloud, تطبيق سطح المكتب):
  • نظام التشغيل:

مرحباً @Sophia_Bennett نعم، سأستخدم قاعدة البيانات كمصدر الحقيقة بدلاً من الاعتماد على سجل تنفيذ n8n.

معرّف حدث فريد event_id بالإضافة إلى INSERT … ON CONFLICT DO NOTHING يمنحك فحص إلغاء التكرار الذري، حتى عندما يتلقى عدة عمال نفس الحدث في نفس الوقت تقريباً.

INSERT INTO webhook_events (event_id, received_at)
VALUES ($1, NOW())
ON CONFLICT (event_id) DO NOTHING
RETURNING event_id;

إذا لم يتم إرجاع أي صف، تعامل معه باعتباره مكررة وأوقف سير العمل. سأضيف أيضاً فهرساً أو قيداً فريداً على event_id وأحتفظ بسجل إلغاء التكرار طالما يمكن للمزود إعادة محاولة الأحداث.

بالنسبة لسير العمل الأكثر حساسية، سأتابع أيضاً الحالة (قيد المعالجة، مكتمل، فشل) حتى تتمكن إعادة المحاولات من الاستئناف بأمان دون إنشاء تأثيرات جانبية مكررة.

مرحباً @Sophia_Bennett أهلاً وسهلاً بك في المجتمع!

نعم، هذا هو النمط الصحيح. ON CONFLICT DO NOTHING ذري، لذا لا يمكن لعمليتي تسليم أن تمران في نفس الوقت. لجعله آمناً للإنتاج:

  1. البوابة: أضف عقدة If بعد الإدراج: {{ $json.event_id }} ليس فارغاً. المكررات لا تُرجع أي صف وتتوقف هناك.
  2. الأخطاء: إذا فشل سير العمل بعد الإدراج، يتم حذف إعادة المحاولة كمكرر وفقدان الحدث. أضف عمود status، عيّنه على done في النهاية، وحذف أو ضع علامة على الصف failed عند حدوث خطأ حتى تتمكن إعادة المحاولة من المتابعة.
  3. استجب فوراً في عقدة Webhook. الاستجابات البطيئة هي ما يؤدي إلى معظم إعادات المحاولة.
  4. نظّف الصفوف القديمة أكثر من بضعة أيام باستخدام سير عمل Schedule.

مرحباً @Sophia_Bennett، نعم، باستخدام معرّف حدث فريد غير فارغ (non-null unique event_id) يعتبر بوابة ذرية جيدة. لكن لا تحذف صف الحدث عند كل فشل: واجهة برمجية تطبيقية في المصب يمكنها إكمال الإجراء قبل انتهاء مهلة n8n، لذا قد تؤدي إعادة المحاولة إلى تكراره.

إذا كانت سجلاتك التجارية في نفس قاعدة بيانات PostgreSQL، فالتزم معرّف الحدث والكتابات التجارية وصف علبة الصادرات (outbox row) للعمل في المصب معاً. يمكن لإجراء مخزن يُستدعى عبر Postgres Execute Query القيام بذلك، حيث يتم تطبيق الكتابات فقط عند نجاح إدراج الحدث. أرجع 200 عبر Respond to Webhook بعد هذا الالتزام.

معالجة علبة الصادرات بشكل منفصل، مع إعادة استخدام مفتاح معرّف ثابت (idempotency key) لكل حدث/إجراء عندما تدعمه واجهة برمجية التطبيق المستقبلة. وإلا، تحقق من النتائج غير المؤكدة قبل إعادة المحاولة. احتفظ بمعرّفات الأحداث لنافذة إعادة الكمبيوتر/التشغيل من جديد (retry/replay window) للموفر، وليس لعدد أيام عشوائي.

تفصيل واحد خاص بـ n8n على مسار النسخ المكررة، نظراً لأن كلاً من بوابة If و"Respond to Webhook after commit" تعتمد عليه:

عندما يصطدم ON CONFLICT DO NOTHING … RETURNING بنسخة مكررة، تُخرج عقدة Postgres صفر عناصر، وبشكل افتراضي لا يقوم n8n ببساطة بتشغيل أي شيء بعد عقدة بدون مخرجات. لذلك لا ترى عقدة If أبداً النسخة المكررة، وإذا كانت عقدة Respond to Webhook موجودة على تلك الفرع، فقد لا تحصل على استجابة 200 نظيفة للتسليم المكرر. مع موفر يعيد المحاولة على عدم 2xx أو انقطاع الاتصال، يمكن تحويل نسخة مكررة واحدة إلى عدة نسخ.

تشغيل “Always Output Data” في إعدادات عقدة Postgres يحل المشكلة: تمرر العقدة بعد ذلك عنصراً فارغاً على نسخة مكررة، يكتشفه شرط If على event_id، ويمكنك إرسال فرع النسخة المكررة مباشرة إلى عقدة Respond to Webhook الخاصة به مع استجابة 200 بحيث يتوقف الموفر عن إعادة المحاولة.

نعم، هذا نمط قوي وشائع الاستخدام.

الطريقة الموصى بها

INSERT INTO webhook_events (event_id, received_at)
VALUES ($1, NOW())
ON CONFLICT (event_id) DO NOTHING
RETURNING event_id;

ثم في n8n:

• إذا أرجعت الاستعلام صفًا → هذه هي المرة الأولى التي ترى فيها الحدث → متابعة سير العمل.

• إذا لم يرجع شيئًا → إنه نسخة مكررة → إيقاف سير العمل (أو التوجيه إلى فرع “مكرر”).

مرحباً @Sophia_Bennett نعم، هذا نمط قوي للإنتاج. سأجعل PostgreSQL مصدر الحقيقة لإزالة التكرارات بدلاً من الاعتماد على سجل تنفيذ n8n.

الجزء المهم هو قيد التفرد:
CREATE UNIQUE INDEX ux_webhook_events_event_id
ON webhook_events (event_id);

ثم يصبح الإدراج الخاص بك ذرياً:

INSERT INTO webhook_events (event_id, received_at)
VALUES ($1, NOW())
ON CONFLICT (event_id) DO NOTHING
RETURNING event_id;

إذا أرجع RETURNING لك event_id، فهذا التنفيذ قد طالب بالحدث بنجاح ويمكنه المتابعة.

إذا لم يرجع أي شيء، فإن تنفيذاً آخر قد عالج أو طالب بهذا الحدث بالفعل، لذا يجب أن تتوقف سير العمل قبل إنشاء السجلات أو تفعيل الإجراءات اللاحقة.

Webhook
↓
إدراج معرّف الحدث
↓
تم إرجاع صف؟
├─ نعم → معالجة الحدث
└─ لا → تكرار → إيقاف

تجنب أيضاً وضع فحص إزالة التكرارات والعملية الفعلية للعمل في خطوات منفصلة “تحقق ثم أدرج”. يمكن لتنفيذي n8n أن يمرا كلاهما هذا الفحص في نفس الوقت. اترك قيد التفرد في PostgreSQL للتعامل مع حالة السباق.

نقطة مهمة أخرى: إزالة التكرارات لا تجعل الإجراءات اللاحقة آمنة تلقائياً. إذا كان سير العمل يرسل دفعة أو بريداً إلكترونياً أو طلب API، إلخ، استخدم نفس معرّف الحدث كمفتاح عدم التكرار حيث يدعمه النظام اللاحق.