فشردن دوبارهی Publish حالا فقط یک بار منتشر میکند
سه درخواست انتشار برای یک برنامه با فاصلهی ۸۴ میلیثانیه به API ما رسیدند و هر کدام یک استقرار کامل از آن برنامه را شروع کرد. این درخواستها روی یک میزبانی مشترک با هم رقابت کردند، چند انتشار حاصل شکست خوردند چون دیگری زودتر رسیده بود، و تنها نشانهی قابلدیدن، یک ایمیل «انتشار ناموفق» بود — خود برنامه در نهایت سالم بود. حالا انتشاری که وقتی انتشار دیگری برای همان برنامه در حال اجراست میرسد، یک ساخت دوم شروع نمیکند. پایگاه داده تصمیم میگیرد کدام درخواست برنده شود، و بقیه حقیقت را میشنوند: یک انتشار از قبل در حال اجراست.
چه اتفاقی افتاد
وقتی یک برنامه را در Mythex منتشر میکنید، API یک استقرار برای آن ثبت میکند و از ارکستراتور ما — سرویسی که ساختها را اجرا میکند — میخواهد آن را بسازد و منتشر کند. انتشار میتواند از دکمهی Publish، از یک تب مرورگر دیگر، یا از عامل وقتی از آن میخواهید منتشر کند شروع شود. دوبار کلیک کردن، دو تب، یا یک درخواست عامل که دوبار برسد، میتوانند در تقریباً همان لحظه بیش از یک انتشار برای یک برنامه ایجاد کنند.
در این مورد، سه درخواست در ۸۴ میلیثانیه رسیدند. هر کدام یک استقرار کامل راه انداخت. روی پلتفرم میزبانی، انتشارهای همان برنامه در همان ثانیه ساخته شدند و سه تا از آنها شکست خوردند چون بقیه از قبل در حال پیشرفت بودند. یک انتشار بعدی گیر کرد، و بعد از آن برنامه کار میکرد.
همین باعث شد دیده نشود. کاربر یک برنامهی کارکننده دید. شکستها فقط در قالب ایمیلهای «انتشار ناموفق» ظاهر شدند.
چرا هیچچیز جلویش را نگرفت
کد انتشار یک گامی داشت که استقرارهای در حال اجرای پروژه را نگاه میکرد. اما کاری که میکرد این بود که قبل از شروع، هر استقرار در حال اجرا را ناموفق علامت بزند — «جایگزین شده با یک انتشار جدید». قرار بود ساختهایی را که یک فرایند کرشکرده جا گذاشته پاک کند.
در عمل، یعنی وقتی انتشار دومی وسط ساخت میرسید، اولی را مرده اعلام میکرد و ساخت خودش را شروع میکرد. هر دو تا آخر روی یک برنامه اجرا میشدند. قفلی در کار نبود؛ یک گامی بود که فعالانه راه را برای یک رقابت باز میکرد.
یک علامت دوم و عجیبتر برای اولین انتشار یک برنامه وجود داشت. چون هنوز رکورد استقراری برای استفادهی مجدد نبود، دو اولین انتشار همزمان هر دو سعی میکردند یکی را درج کنند. پایگاه داده روی نام لینک برنامه (slug آن، مثل your-app در your-app.mythex.ai) یک ایندکس یکتا دارد، پس درج دوم شکست میخورد. API آن را به «این نام لینک گرفته شده است» ترجمه میکرد — و به کاربر توصیه میکرد برنامهای را تغییر نام دهد که در همان لحظه با موفقیت زیر همان نام منتشر میشد.
رفع: ادعای رکورد همان قفل است
گرفتن رکورد استقرار حالا یک بهروزرسانی شرطی واحد روی پایگاه داده است: این ردیف را بگیر و آن را در حال ساخت علامت بزن فقط اگر کسی در حال انتشار آن نباشد. چون این یک دستور واحد است، پایگاه داده درخواستهایی را که در همان میلیثانیه میرسند حل میکند. دقیقاً یکی از آنها ردیف را تغییر میدهد. بقیه میبینند که صفر ردیف تغییر کرده، متوقف میشوند، و هرگز به ارکستراتور نمیرسند.
یک قفل هم به راهی برای خروج وقتی صاحبش میمیرد نیاز دارد. اگر API وسط یک انتشار ریاستارت میشد، ردیف برای همیشه «در حال ساخت» میماند و برنامه قابل انتشار نبود. پس ردیفی که صاحبش دیگر ضربان نمیفرستد — انتشار در حال اجرا هر ۳۰ ثانیه ردیف را بهروز میکند، و پنج دقیقه سکوت بهعنوان مرده شمرده میشود — دوباره قابل ادعا شدن است. این آستانه از ضربان میآید، نه از حدس زدن طول یک ساخت. ساختن یک Dockerfile خود کاربر ممکن است بهدرستی زمان زیادی ببرد، و یک ساخت کند که هنوز ضربان میفرستد ادعایش را نگه میدارد.
گامی که استقرارهای در حال اجرا را ناموفق میکرد دیگر پیش از انتشار اجرا نمیشود. فقط ردیفهایی که یک فرایند مرده جا گذاشته پاک میشوند.
به برندهی باخته حقیقت را گفتن
درخواستی که میبازد حالا پاسخ روشنی میگیرد: publish_in_progress، بههمراه شناسهی استقراری که در حال اجراست. هر جایی که میتواند انتشار را شروع کند این را مدیریت میکند:
- فضای کار دنبال ساختی را میگیرد که از قبل در حال اجراست، بهجای نشان دادن یک خطا روی آن.
- عامل به او گفته میشود که انتشاری از این برنامه از قبل در حال اجراست و باید منتظر آن بماند، نه اینکه گفته شود کاری شکست خورده — که پیشتر میتوانست او را به «تعمیر» یک Dockerfile و تلاش دوباره بفرستد.
- برخورد اولین انتشار روی نام لینک بررسی میشود: اگر رکوردی که نام را نگه داشته انتشار در حال اجرای همین پروژه باشد، پاسخ «در حال انتشار است» است، نه «این نام گرفته شده». برخورد واقعی با پروژهی دیگر هنوز پیام تغییر نام را میگیرد.
چطور آزمودیم
یک اسکریپت بازتولید نوشتیم که سه انتشار از یک برنامه را همزمان روی یک پایگاه دادهی واقعی و یک ارکستراتور ساختگی اجرا میکند، و میشمارد ارکستراتور چند استقرار خواسته است. این شامل یک اولین انتشار و یک انتشار مجدد از یک برنامهی زنده میشود. پیش از رفع، دو استقرار شمرده شد. بعد از رفع، یکی.
اسکریپت راه خروج را هم پوشش میدهد: ردیف برنامه را در حال ساخت بدون ضربان برای ده دقیقه علامت میزند، دوباره منتشر میکند، و بررسی میکند که انتشار جدید ردیف را ادعا کند و اجرا شود.
تستهای واحد قفل را به دو کاری که قفل باید انجام دهد محدود میکنند: از سه انتشار مجدد همزمان، دقیقاً یکی عبور میکند و دو تای دیگر گفته میشود که یک انتشار در حال اجراست؛ و ساختی که صاحبش مرده قابل ادعا شدن است. تست دیگری بررسی میکند که یک تلاش جدید پیام خطای تلاش قبلی را با خود نبرد.
آنچه از آن یاد گرفتیم
- کد پاکسازی میتواند یک رقابت در لباس مبدل باشد. «هر چیز در حال اجرا را شکست بده، سپس شروع کن» مثل نظافت به نظر میرسد. با دو فراخواننده، این میشود هر کدام دیگری را لغو کند.
- اجازه بدهید پایگاه داده داوری کند. خواندن و سپس نوشتن یک شکاف برای درخواست دیگر باقی میگذارد. یک بهروزرسانی شرطی واحد این شکاف را نمیگذارد.
- هر قفلی به انقضایی وابسته به زنده بودن نیاز دارد. بدون آن، یک کرش به یک برنامهی گیرکرده تبدیل میشود.
- پیامهای خطا دستورالعمل هستند. «آن نام گرفته شده است» مردم را به کار اشتباه هدایت کرد. پیام درست سادهتر بود و راست بود.
بیشتر دربارهٔ انتشار در مستندات: چطور منتشر کنیم. و یک رفع مرتبط: برنامهی منتشرشدهتان هنگام انتشار مجدد آنلاین میماند.