Натискането на Publish два пъти вече публикува веднъж
Три заявки за публикуване на едно и също приложение стигнаха до нашия API на 84 милисекунди една от друга и всяка стартира пълен деплой на това приложение. Те се състезаваха за един и същи хостинг, няколко от получените релийзи се провалиха, защото друг вече е бил там първи, а единственият видим знак беше имейл „release failed“ — самото приложение в крайна сметка беше наред. Сега публикуване, което пристига, докато друго за същото приложение вече работи, не стартира втори билд. Базата данни решава коя заявка печели, а останалите се информират честно: публикуване вече върви.
Какво се случи
Когато публикувате приложение в Mythex, API записва деплой за него и моли нашия оркестратор — услугата, която пуска билдовете — да го изгради и пусне. Публикуването може да започне от бутона Publish, от друг таб на браузъра или от агента, когато го помолите да публикува. Двоен клик, два таба или заявка на агент, която пристига два пъти, могат да създадат повече от едно публикуване за едно и също приложение почти едновременно.
В този случай три пристигнаха в рамките на 84 ms. Всяко стартира пълен деплой. В хостинг платформата релийзи за едно и също приложение се създадоха в една и съща секунда и три от тях се провалиха, защото другите вече бяха в ход. По-късен релийз се заклещи, но приложението работеше след това.
Това беше лесно да се пропусне. Потребителят виждаше едно работещо приложение. Провалите се показваха само като имейли за неуспешен релийз.
Защо нищо не спираше това
Кодът за публикуване имаше стъпка, която гледаше деплоите в ход за проекта. Но тя правеше следното: маркираше всеки деплой в ход като неуспешен преди стартиране — „заменен от ново публикуване“. Трябваше да почиства билдове, останали след срив на процес.
На практика това значеше, че второ публикуване, пристигнало по средата на билд, обявяваше първия за мъртъв и стартираше собствен. И двата вървяха докрай срещу едно и също приложение. Нямаше заключване; имаше стъпка, която активно разчистваше пътя за състезание.
Имаше и втори, по-странен симптом при първото публикуване на приложение. Без още запис за деплой, който да се използва повторно, две едновременни първи публикувания се опитваха да вмъкнат един и същ запис. Базата данни има уникален индекс върху името на линка на приложението (неговия slug, като your-app в your-app.mythex.ai), така че второто вмъкване се провали. API превеждаше това в „това име на линк е заето“ — и съветваше потребителя да преименува приложение, което точно в този момент се публикуваше успешно под това име.
Поправката: претенцията е заключването
Поемането на запис за деплой вече е едно условно UPDATE в базата данни: вземи този ред и го маркирай като building само ако никой не публикува. Тъй като е един оператор, базата данни решава заявките, които пристигат в една и съща милисекунда. Точно една от тях променя реда. Останалите виждат, че нула реда са променени, спират и никога не стигат до оркестратора.
Заключването също трябва да има изход, когато държателят му умре. Ако API се рестартира по средата на публикуване, редът би останал маркиран като „building“ завинаги и приложението не би могло да се публикува. Затова ред, чийто собственик е спрял да изпраща heartbeat — работещото публикуване обновява реда на всеки 30 секунди, а пет минути мълчание се смята за мъртвост — може да бъде поет отново. Този праг идва от heartbeat, а не от предположение колко трае билдът. Изграждането на собствен Dockerfile на потребителя може законно да трае дълго, а бавен билд, който още изпраща heartbeat, запазва претенцията си.
Стъпката, която маркираше всеки деплой в ход като неуспешен, вече не се изпълнява преди публикуване. Чистят се само редовете, които е оставил мъртъв процес.
Казването на губещия истината
Заявката, която губи, сега получава ясен отговор: publish_in_progress, заедно с ID-то на деплоя, който върви. Всяко място, което може да стартира публикуване, се справя с това:
- Работното пространство следва вече работещия билд вместо да показва грешка върху него.
- Агентът получава съобщение, че публикуване на това приложение вече върви, и да изчака него, вместо да му се казва, че нещо се е провалило — което преди можеше да го накара да отиде да „поправя“ Dockerfile и да опита пак.
- Сблъсък при първо публикуване по име на линк се проверява: ако редът, който държи името, е собственото работещо публикуване на този проект, отговорът е „вече се публикува“, а не „това име е заето“. Истинският сблъсък с друг проект все още получава съобщението за преименуване.
Как го проверихме
Написахме скрипт за възпроизвеждане, който пуска три публикувания на едно приложение едновременно срещу истинска база данни и заглушен оркестратор, и брои за колко деплоя оркестраторът е помолен. Покрива първо публикуване и повторно публикуване на работещо приложение. Преди поправката броеше два деплоя. След нея — един.
Скриптът покрива и изхода за спешни случаи: маркира реда на приложението като building без heartbeat за десет минути, публикува отново и проверява, че новото публикуване поема реда и се изпълнява.
Unit тестовете държат заключването до двете неща, които едно заключване трябва да прави: от три едновременни повторни публикувания точно едно минава, а другите две получават съобщение, че публикуване върви; и билд, чийто собственик е умрял, може да бъде поет. Друг тест проверява, че нов опит не пренася съобщението за грешка от предишния опит.
Какво взехме от това
- Кодът за почистване може да е състезание в маскировка. „Провали всичко, което е в ход, после започни“ изглежда като хигиена. При двама извикващи това е всеки да отменя другия.
- Оставете базата данни да арбитрира. Четене, последвано от запис, оставя празнина за друга заявка. Едно условно UPDATE не оставя.
- Всяко заключване се нуждае от срок на валидност, свързан с жизнеността. Без него срив се превръща в заседнало приложение.
- Съобщенията за грешки са инструкции. „Това име е заето“ казваше на хората да направят грешното нещо. Правилното съобщение беше по-просто и вярно.
Повече за публикуването в документацията: как да публикувате. И свързана поправка: публикуваното ви приложение остава онлайн, докато го публикувате отново.