المعالجة المكررة عند إعادة محاولة سير عمل n8n

مرحبا
أملك سير عمل n8n يستقبل أحداث webhook ثم ينشئ سجلات في PostgreSQL ويستدعي واجهة برمجية خارجية.
المشكلة هي أنه أحياناً تنتهي مهلة انتظار واجهة برمجية خارجية بعد معالجة الطلب بالفعل. يرى n8n انتهاء المهلة كفشل ويعيد محاولة التنفيذ، مما قد يسبب معالجة العملية نفسها مرتين.
التدفق المبسط هو:
Webhook
↓
تحقق من البيانات
↓
PostgreSQL
↓
واجهة برمجية خارجية
↓
تحديث الحالة
أنا أفكر في استخدام مفتاح إدراك التكرار بناءً على معرّف الحدث وتخزين حالة المعالجة في PostgreSQL.
على سبيل المثال: INSERT INTO processed_events (event_id, status)
VALUES ($1, ‘processing’)
ON CONFLICT (event_id) DO NOTHING;
كيف ستتعامل مع الحالة التي تنجح فيها واجهة برمجية خارجية لكن n8n ينهار قبل تحديث قاعدة البيانات؟
هل ستتحقق من واجهة برمجية خارجية للعملية الموجودة قبل إعادة المحاولة؟
هل هناك نمط n8n أفضل لجعل سير العمل هذا آمناً وقابلاً للإعادة بأمان؟

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

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

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

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

شارك الناتج الذي أرجعته العقدة الأخيرة

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

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

مرحباً @Decoure_Ryan أنا سأستخدم مفتاح idempotency مدعوم بقيد فريد في PostgreSQL، لكنني لن أعتمد على حالة قاعدة البيانات وحدها.

الجزء المهم هو أن نفس event_id ينتج دائماً نفس operation key:event-123-payment

ثم يمكن للسير العملي أن يتبع شيئاً مثل

Webhook
↓
Create/Check Idempotency Key
↓
PostgreSQL
↓
Check External API Status
↓
Process Only If Needed
↓
Update Status


إذا تعطل n8n بعد نجاح API لكن قبل تحديث PostgreSQL، تحاول إعادة المحاولة مع event_id الموجود وتتحقق من حالة API الخارجي باستخدام مفتاح idempotency هذا.

إذا أكد API أنه نجح بالفعل، يمكن لـ n8n ببساطة تحديث PostgreSQL والمتابعة بدلاً من إنشاء عملية أخرى.

سأجعل أيضاً قيد PostgreSQL يفرض الفرادة:CREATE UNIQUE INDEX idx_processed_events_event_id
ON processed_events(event_id);

@Niffzy لديه الشكل الصحيح والمفتاح الحتمي هو الجزء المهم. هناك شيئان أود إضافتهما، لأن هناك سباقًا متبقيًا في هذا التدفق، وأحدهما يحدد ما إذا كنت تحتاج إلى فحص الحالة على الإطلاق.

سباق الفحص-ثم-العمل

في هذا التسلسل:

فحص حالة واجهة برمجية خارجية → المعالجة فقط إذا لزم الأمر

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

ما يغلقها هو المطالبة بالعمل بشكل ذري، مع عقد إيجار بحيث لا يتمكن الانهيار من إيقاف الحدث إلى الأبد:

INSERT INTO processed_events (event_id, status, claimed_at, attempts)
VALUES ($1, 'processing', now(), 1)
ON CONFLICT (event_id) DO UPDATE
   SET claimed_at = now(),
       attempts   = processed_events.attempts + 1
 WHERE processed_events.status = 'processing'
   AND processed_events.claimed_at < now() - interval '5 minutes'
RETURNING *;

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

عقد الإيجار يصحح أيضًا شيئًا في النسخة الأصلية. علامة ON CONFLICT DO NOTHING عادية تضع الصف في حالة ‘processing’ ولم يتم إطلاقه مطلقًا، لذا فإن الانهيار الدقيق الذي تسأل عنه يترك هذا الصف في حالة ‘processing’ إلى الأبد. تصل إعادة المحاولة إلى التضارب وتفعل شيئًا، والحدث يُسقط بصمت. هذا عادة أسوأ من النسخة المكررة، لأنه لا شيء ينبهك. اضبط الفاصل الزمني براحة أطول من أسوأ زمن استجابة واجهة برمجية واقعي لديك.

قد لا تحتاج إلى فحص الحالة على الإطلاق

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

شيئان بخصوص جانب n8n

أوقف retryOnFail على عقدة واجهة برمجية. إعادة محاولة على مستوى العقدة تعيد التشغيل داخل نفس التنفيذ وستتجاوز بسعادة المنطق الذي لفته حول العقدة.

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

شيء واحد أخير

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