إعادة المحاولة واللاتكرارية
ما يُعاد، وما لا يُعاد عن قصد، ولماذا لا يمكن لإرسال مُعاد أن يتضاعف.
الإرسالات
يرفق العميل ترويسة Idempotency-Key بكل إرسال (emails.send وemails.send_batch وtemplates.send وbroadcasts.send)، تُولَّد مرة واحدة لكل **استدعاء** على شكل oe- متبوعة بـ UUID عشوائي، وتعيد محاولات ذلك الاستدعاء استخدامها. ويحجز API ذلك المفتاح قبل أن يرسل أي شيء، فتعيد المحاولة تشغيل الرسالة الأصلية بدل إرسال رسالة ثانية، بينما يرسل استدعاءان مقصودان لـ send مرتين. فهاتان نيّتان مختلفتان وتبقيان مختلفتين.
مرّر idempotency_key: الخاص بك لمدّ ذلك الضمان عبر العمليات، فتُعيد مهمة انهارت ثم عملت من جديد تشغيل إرسالاتها بدل تكرارها. وتجيب إعادة التشغيل بـ replayed مضبوطًا على true وبالرسالة المخزَّنة كما هي الآن.
invoice_id = "inv_2026_09_4192" sent = client.emails.send( from: "[email protected]", to: "[email protected]", subject: "Your September invoice", text: "Invoice attached.", idempotency_key: "invoice:#{invoice_id}") puts sent[:id], sent[:replayed]اشتقّه مما جعل الإرسال ضروريًا. ولا تشتقّه من ساعة أبدًا. وإعادة استخدام مفتاح بمتن مختلف تُرفض بالخطأ 422 idempotency_key_reuse بدل أن تُعاد صامتةً. والمفتاح من 1 إلى 255 حرفًا من الحروف أو الأرقام أو _ أو . أو : أو -، وأي شيء آخر يعطي 400 invalid_idempotency_key.
كل ما عدا ذلك
كل GET تُعاد محاولته. أما الكتابة فلا تُعاد محاولتها إلا حيث لا يمكن لطلب ثانٍ مطابق أن يعني شيئًا مختلفًا عن الأول، والإرسال مؤهل لذلك لأن مفتاح اللاتكرارية الخاص به يحوّل التكرار إلى إعادة تشغيل.
| الاستدعاء | يُعاد | السبب |
|---|---|---|
| كل GET | نعم | لا شيء يتغير. |
| emails.send, emails.send_batch, templates.send, broadcasts.send | نعم | مفتاح اللاتكرارية يجعل التكرار إعادة تشغيل. |
| emails.cancel, emails.reschedule, broadcasts.cancel, forms.pause, forms.resume, threads.restore | نعم | ضبط خالص لحالة مسمّاة. |
| threads.update، threads.trash | نعم | ضبط تسميات. وتطبيقه مرتين هو تطبيقه مرة واحدة. |
| threads.snooze، threads.unsnooze | نعم | لحظة الاستيقاظ موجودة في المتن، وليست مشتقة من وقت الوصول. |
| emails.update, labels.update, webhooks.update, settings.update, roles.update, members.update, domains.update, domains.update_address, domains.update_address_forward, contacts.update, audiences.update, keys.update, forms.update, branding.update, threads.update_note, chats.rename, account.set_email_notification, account.set_push_muted, app_host.set, workspaces.set_active | نعم | ضبط خالص لحقول مسمّاة. |
| members.grant_address, members.grant_domain, rules.reorder, threads.reorder_notes | نعم | المنح عملية إدراج أو تحديث، والترتيب يُذكر كاملًا. |
| templates.publish, forms.publish, imports.start | نعم | نشر ما هو منشور أصلًا، أو بدء استيراد بدأ بالفعل، يعيده دون تغيير. |
| templates.preview، templates.render، broadcasts.preview، rules.test | نعم | تعرض أو تعدّ أو تقيّم، ولا تكتب شيئًا. |
| domains.verify, app_host.verify, senders.research | نعم | الفحص المكرّر لا يغيّر شيئًا سوى وقت إجرائه. |
| contacts.save, contacts.set_audiences, contacts.remove_photo, contacts.block, contacts.unblock, contacts.delete_many, keys.revoke, files.revoke_link, files.revoke_all_links, app_host.delete, account.remove_photo, branding.remove_image, domains.remove_logo, domains.remove_logo_certificate, domains.remove_address_photo, account.accept_invitation, account.decline_invitation, forms.approve_submission, subscriptions.move | نعم | كلٌّ منها يصرّح بالنتيجة النهائية، فيترك النداء الثاني ما تركه الأول. |
| audiences.add_contact, audiences.add_contacts, audiences.remove_contacts, audiences.import_contacts, suppressions.add, domains.create_address | نعم | يجد التكرار عمل النداء الأول منجَزًا فيبلّغ عنه بدل أن يؤديه مرتين. |
| contacts.set_photo, account.set_photo, branding.upload_image, domains.set_logo, domains.set_logo_certificate, domains.set_address_photo, imports.upload_chunk | نعم | البايتات المرسلة مجددًا تستبدل ما خزّنته المحاولة الأولى. |
| drafts.create, labels.create, webhooks.create, templates.create, rules.create, roles.create, temp_mail.create, files.upload | لا | إعادة المحاولة تترك كائنين. |
| drafts.update | لا | اقرأ المعرّف من نتيجة كل كتابة بدل إعادة استخدام المعرّف الذي أرسلته. |
| drafts.delete, labels.delete, webhooks.delete, templates.delete, rules.delete, roles.delete, members.remove, members.revoke_address, temp_mail.delete, temp_mail.delete_message | لا | إعادة المحاولة بعد استجابة ضائعة تبلّغ بإخفاق عن عمل قد نجح. |
| webhooks.rotate_secret | لا | التدوير الثاني يبطل السر الذي أعادته المحاولة الأولى. |
| webhooks.test | لا | سيرسل تسليمًا اصطناعيًا ثانيًا. |
| webhooks.replay_delivery | لا | سترسل الحدث إلى مستقبِلك مرة ثانية. |
| emails.translate, emails.compose, emails.rewrite, emails.suggest_subject | لا | كل واحد منها يستهلك استدعاءات للنموذج، فإعادة المحاولة بعد طلب لم يُجَب تشتري الإجابة نفسها مرتين. |
| security.begin_step_up, security.verify_step_up | لا | قد ترسل إعادة المحاولة بريدًا ثانيًا أو تستهلك محاولة ثانية للرمز. |
| كل استدعاء آخر ليس GET | لا | تُرسل مرة واحدة، ويُبلَّغ عن الإخفاق بدل تكراره. |
التراجع التدريجي
- محدودة بـ
max_retries:على العميل، والقيمة الافتراضية محاولتان إضافيتان. وmax_retries: 0توقف إعادة المحاولة. - فقط بعد فشل في الشبكة أو
408أو500أو502أو503أو504. ولا تُعاد محاولة429إلا حين يحملRetry-After، وهذه الواجهة لا ترسله، فيرفع تجاوز حد المعدل خطأً فورًا. وأي حالة أخرى ترفع خطأً على الفور. - أسّية من نصف ثانية حتى ثماني ثوانٍ، مع تشويش زمني: كل انتظار نقطة عشوائية بين نصف ذلك الحد الأقصى وكامله، كي لا يعيد أسطول من العملاء مزامنة نفسه عند التعافي.
- تُضبط وتيرتها بـ
Retry-Afterبأي من صيغتيها، delay-seconds وHTTP-date. وحين يحدّد الخادم مدة انتظار، ينتظر العميل تلك المدة بالضبط بدل التراجع التدريجي. - الخادم الذي يطلب أكثر من دقيقة يُعامَل كمن يقول للعميل توقّف لا انتظر، فيُرفع الخطأ ومعه
retry_after_seconds. فالعودة قبل ما طلب ليست احترامًا لطلبه. - انتهاء المهلة فشل في الشبكة كأي فشل آخر، فالاستدعاء الذي يمكن تكراره بأمان يُجرَّب مجددًا بعده، وتنطبق
timeout:على كل محاولة من جديد. - فترات الانتظار استدعاءات
sleepداخل الاستدعاء، فينتظر خيط التنفيذ المستدعي أيضًا، ولا يعود الاستدعاء أو يرفع خطأً إلا بعد انتهاء محاولته الأخيرة.