الضغط على نشر مرتين أصبح ينشر مرة واحدة
وصلت ثلاثة طلبات نشر لتطبيق واحد إلى واجهة البرمجة الخاصة بنا بفارق 84 ملّي ثانية، وبدأ كل منها نشراً كاملاً لذلك التطبيق. تسابقت على الاستضافة نفسها، وفشل عدة إصدارات لأن غيرها وصل أولاً، وكانت العلامة الظاهرة الوحيدة رسالة بريد «فشل الإصدار»، أما التطبيق نفسه فانتهى سليماً. الآن، لا يبدأ طلب نشر يصل أثناء تشغيل نشر آخر للتطبيق نفسه بناءً ثانياً. تقرر قاعدة البيانات أي طلب يفوز، ويُخبر الآخرون بالحقيقة: هناك نشر قيد التنفيذ بالفعل.
ما الذي حدث
عندما تنشر تطبيقاً على Mythex، تسجل واجهة البرمجة نشراً للتطبيق وتطلب من المنسّق (orchestrator)، وهو الخدمة التي تشغّل عمليات البناء، أن يبني التطبيق ويُصدره. يمكن أن يبدأ النشر من زر النشر، أو من تبويب متصفح آخر، أو من الوكيل عندما تطلب منه النشر. والنقر المزدوج، أو تبويبان، أو طلب وكيل وصل مرتين، كلها يمكن أن تنتج أكثر من نشر للتطبيق نفسه في اللحظة نفسها تقريباً.
في هذه الحالة وصلت ثلاثة طلبات خلال 84 ملّي ثانية. وشغّل كل منها نشراً كاملاً. وعلى منصة الاستضافة، أُنشئت إصدارات للتطبيق نفسه في الثانية نفسها، وفشل ثلاثة منها لأن غيرها كان قيد التنفيذ. ونجح إصدار متأخر، وعمل التطبيق بعد ذلك.
وهذا ما جعل الأمر سهل الفوات. رأى المستخدم تطبيقاً واحداً يعمل. وظهرت الإخفاقات فقط في رسائل البريد عن فشل الإصدارات.
لماذا لم يوقفه شيء
كان لكود النشر خطوة تنظر إلى النشرات الجارية للمشروع. لكنها كانت تعلّم كل نشر جارٍ بأنه فشل قبل البدء، بعبارة «استُبدل بنشر جديد». كان الهدف تنظيف عمليات البناء التي تتركها عملية متعطلة وراءها.
عملياً، كان ذلك يعني أن طلب نشر ثانياً يصل في منتصف البناء يعلن الأول ميتاً ويبدأ بناءه الخاص. وكان الاثنان يكملان ضد التطبيق نفسه. لم يكن هناك قفل؛ كانت هناك خطوة تفسح الطريق بنشاط لسباق.
كانت هناك أعراض ثانية وأغرب للنشر الأول لتطبيق. مع عدم وجود سجل نشر يُعاد استخدامه بعد، كان نشران أوليان متزامنان يحاولان إدراج السجل نفسه. وفهرس الاسم الفريد على اسم رابط التطبيق (slug، مثل your-app في your-app.mythex.ai) يُفشل الإدراج الثاني. وكانت واجهة البرمجة تترجم ذلك إلى «اسم الرابط مأخوذ»، فتنصح المستخدم بإعادة تسمية تطبيق كان في تلك اللحظة يُنشر بنجاح تحت ذلك الاسم.
الحل: الحجز هو القفل
أصبح حجز سجل النشر تحديثاً شرطياً واحداً في قاعدة البيانات: خذ هذا الصف وعلّمه قيد البناء فقط إذا لم يكن أحد ينشره. ولأنه عبارة واحدة، تحسم قاعدة البيانات الطلبات التي تصل في الملّي ثانية نفسها. يغيّر أحدها الصف فقط. أما الباقون فيرون أن صفاً واحداً لم يتغير، فيتوقفون ولا يصلون إلى المنسّق أبداً.
يحتاج القفل أيضاً إلى طريقة للخروج عندما يموت صاحبه. فإذا أُعيد تشغيل واجهة البرمجة في منتصف النشر، سيبقى الصف مُعلَّماً «قيد البناء» إلى الأبد ولن يُنشر التطبيق مجدداً. لذلك يمكن إعادة حجز الصف الذي توقف مالكه عن إرسال نبضات الحياة، أي أن النشر الجاري يحدّث الصف كل 30 ثانية، وخمس دقائق من الصمت تُعدّ ميتاً. هذا الحد مأخوذ من النبضة نفسها، وليس من تخمين لمدة البناء. فبناء Dockerfile يملكه المستخدم قد يستغرق وقتاً طويلاً بشكل مشروع، وبناء بطيء ما زال يرسل نبضاته يحتفظ بحجزه.
خطوة فشل كل النشرات الجارية لم تعد تعمل قبل النشر. تُنظَّف فقط الصفوف التي تركتها عملية ميتة وراءها.
إخبار الخاسر بالحقيقة
الطلب الذي يخسر يحصل الآن على جواب واضح: publish_in_progress، مع معرّف النشر الذي يعمل. ويتعامل كل مكان يستطيع بدء النشر مع ذلك:
- مساحة العمل تتابع البناء الذي يعمل بالفعل بدلاً من عرض خطأ فوقه.
- الوكيل يُخبر أن نشراً لهذا التطبيق يعمل بالفعل، وعليه أن ينتظره، بدلاً من أن يُخبر بأن شيئاً فشل، وهو ما كان قد يدفعه سابقاً إلى «إصلاح» Dockerfile والمحاولة من جديد.
- تصادم النشر الأول على اسم الرابط يُفحص: إذا كان الصف الذي يحمل الاسم هو النشر الجاري لهذا المشروع نفسه، فالجواب «جارٍ النشر» لا «الاسم مأخوذ». أما التصادم الحقيقي مع مشروع آخر فيظل يحصل على رسالة إعادة التسمية.
كيف تحققنا منه
كتبنا نص إعادة إنتاج يشغّل ثلاثة نشرات لتطبيق واحد في الوقت نفسه، على قاعدة بيانات حقيقية ومنسّق وهمي، ويحسب عدد عمليات النشر التي يُطلبها المنسّق. يغطي ذلك النشر الأول وإعادة نشر تطبيق حي. قبل الإصلاح، حسب النص عمليتي نشر. وبعده، عملية واحدة.
كما يغطي النص مخرج الطوارئ: يعلّم صف التطبيق كقيد البناء دون نبضات لعشر دقائق، ثم ينشر من جديد، ويتحقق من أن النشر الجديد يحجز الصف ويعمل.
تضع اختبارات الوحدة القفل عند الشيئين اللذين يجب أن يفعلهما القفل: من بين ثلاث إعادات نشر متزامنة لتطبيق حي، يعبر واحد فقط، ويُخبر الآخران بأن نشراً جارٍ؛ وبناء مات مالكه يمكن حجزه. واختبار آخر يتحقق من أن المحاولة الجديدة لا تحمل رسالة خطأ المحاولة السابقة.
ما استخلصناه
- كود التنظيف يمكن أن يكون سباقاً متنكراً. «افشل كل شيء قيد التنفيذ، ثم ابدأ» يبدو نظافة. ومع اثنين من المستدعين، يلغي كل منهما الآخر.
- دع قاعدة البيانات تحكم. القراءة متبوعة بالكتابة تترك فجوة لطلب آخر. أما التحديث الشرطي الواحد فلا يترك فجوة.
- كل قفل يحتاج إلى انتهاء صلاحية مرتبط بالحياة. بدونه يتحول الانهيار إلى تطبيق عالق.
- رسائل الخطأ تعليمات. «ذلك الاسم مأخوذ» دفع الناس إلى الفعل الخاطئ. كانت الرسالة الصحيحة أبسط وصادقة.
مزيد عن النشر في الوثائق: كيفية النشر. وإصلاح ذو صلة: تبقى تطبيقك المنشور حياً أثناء إعادة نشره.