Блог

Двойное нажатие «Опубликовать» теперь публикует один раз

Три запроса на публикацию одного и того же приложения дошли до нашего API с интервалом 84 миллисекунды, и каждый запустил полный деплой этого приложения. Они наперегонки шли на один и тот же хостинг, несколько получившихся релизов упали, потому что другой уже занял место, а единственным видимым признаком было письмо «релиз не удался» — само приложение в итоге работало нормально. Теперь публикация, которая приходит, пока для того же приложения уже выполняется другая, не запускает вторую сборку. База данных решает, какой запрос победит, а остальным честно сообщается: публикация уже выполняется.

Что произошло

Когда вы публикуете приложение на Mythex, API записывает для него деплой и просит наш оркестратор — сервис, который запускает сборки, — собрать и выпустить его. Публикация может начаться с кнопки «Опубликовать», из другой вкладки браузера или от агента, когда вы его об этом попросите. Двойной клик, две вкладки или запрос агента, который пришёл дважды, могут породить несколько публикаций одного приложения почти в один и тот же момент.

В этом случае три пришли в пределах 84 мс. Каждая запустила полный деплой. На платформе хостинга релизы одного приложения создавались в одну и ту же секунду, и три из них упали, потому что другие уже были в процессе. Один более поздний релиз застрял, а приложение впоследствии работало.

Именно это делало проблему незаметной. Пользователь видел одно работающее приложение. Сбои проявлялись только в письмах о неудачных релизах.

Почему ничто не останавливало это

У кода публикации был шаг, который смотрел на выполняющиеся деплои проекта. Но он делал так: перед стартом помечал каждый выполняющийся деплой как неудачный — «заменён новой публикацией». Задумывался он для очистки сборок, оставшихся после упавшего процесса.

На практике это означало, что вторая публикация, пришедшая во время сборки, объявляла первую мёртвой и запускала свою. Обе доводились до конца против одного и того же приложения. Блокировки не было; был шаг, который активно расчищал путь для гонки.

При первой публикации приложения проявлялся второй, более странный симптом. Записи о деплое ещё не было, поэтому две параллельные первые публикации обе пытались вставить одну. У базы данных есть уникальный индекс на имя ссылки приложения (его slug, например your-app в your-app.mythex.ai), так что вторая вставка падала. API переводил это в «такое имя ссылки занято», советуя пользователю переименовать приложение, которое в тот момент успешно публиковалось под этим именем.

Исправление: захват записи — это и есть блокировка

Захват записи о деплое теперь — одно условное обновление в базе данных: взять эту строку и пометить её как «сборка» только если никто её не публикует. Поскольку это один запрос, база данных разрешает запросы, которые приходят в одну и ту же миллисекунду. Ровно один из них меняет строку. Остальные видят, что изменено ноль строк, останавливаются и никогда не доходят до оркестратора.

Блокировка также должна иметь выход, когда её владелец умирает. Если API перезапустился в середине публикации, строка оставалась бы помеченной как «сборка» навсегда, и приложение нельзя было бы опубликовать. Поэтому строку, чей владелец перестал посылать сигналы жизни — выполняющаяся публикация обновляет строку каждые 30 секунд, а пять минут тишины считаются смертью, — можно захватить заново. Этот порог берётся из сигнала жизни, а не из догадки о том, сколько длится сборка. Сборка собственного Dockerfile пользователя может законно идти долго, и медленная сборка, которая продолжает подавать сигналы жизни, сохраняет свой захват.

Шаг, который помечал каждый выполняющийся деплой как неудачный, перед публикацией больше не запускается. Очищаются только строки, оставленные мёртвым процессом.

Честный ответ проигравшему

Запрос, который проиграл, теперь получает понятный ответ: publish_in_progress вместе с идентификатором выполняющегося деплоя. Каждое место, которое может начать публикацию, обрабатывает это:

  • Рабочее пространство следует за уже выполняющейся сборкой, вместо того чтобы показать поверх неё ошибку.
  • Агенту сообщают, что публикация этого приложения уже выполняется и нужно её дождаться, а не то, что что-то упало, — раньше это могло побудить его «исправлять» Dockerfile и пробовать снова.
  • Конфликт при первой публикации на имени ссылки проверяется: если строка, которая держит имя, — это собственная выполняющаяся публикация этого проекта, ответ «уже публикуется», а не «такое имя занято». Настоящий конфликт с другим проектом по-прежнему получает сообщение о переименовании.

Как мы проверили

Мы написали скрипт воспроизведения, который запускает три публикации одного приложения одновременно на настоящей базе данных и заглушке оркестратора и считает, сколько деплоев запросил оркестратор. Он охватывает первую публикацию и повторную публикацию живого приложения. До исправления он насчитал два деплоя, после — один.

Скрипт также покрывает аварийный выход: он помечает строку приложения как «сборка» без сигналов жизни в течение десяти минут, публикует снова и проверяет, что новая публикация захватывает строку и запускается.

Модульные тесты удерживают блокировку на двух вещах, которые она должна делать: из трёх одновременных повторных публикаций проходит ровно одна, а двум другим сообщают, что публикация выполняется; и сборку, чей владелец умер, можно захватить. Другой тест проверяет, что новая попытка не переносит сообщение об ошибке предыдущей.

Что мы из этого вынесли

  • Код очистки может быть гонкой под видом гигиены. «Провалить всё, что выполняется, потом начать» выглядит как порядок. При двух вызывающих это каждый отменяет другого.
  • Пусть арбитром будет база данных. Чтение, за которым следует запись, оставляет зазор для другого запроса. Одно условное обновление его не оставляет.
  • У каждой блокировки должен быть срок действия, привязанный к признаку жизни. Без него сбой превращается в застрявшее приложение.
  • Сообщения об ошибках — это инструкции. «Такое имя занято» подсказывало людям неверное действие. Правильное сообщение было проще и было правдой.

Больше о публикации в документации: как опубликовать. И связанное исправление: ваше опубликованное приложение остаётся доступным во время повторной публикации.

Mythex · Pricing · Создавайте что угодно с ИИ · Шаблоны сайтов и приложений · Сравнения Mythex · Глоссарий по созданию приложений с ИИ · Бесплатные инструменты для создания приложений с ИИ · English

English አማርኛ العربية Български বাংলা Bosanski Català Čeština Dansk Deutsch Ελληνικά Español Español (España) Eesti فارسی Suomi Français Français (Canada) ગુજરાતી हिन्दी Hrvatski Magyar Հայերեն Bahasa Indonesia Íslenska Italiano 日本語 ქართული Қазақ тілі ಕನ್ನಡ 한국어 Lietuvių Latviešu Македонски മലയാളം Монгол मराठी Bahasa Melayu မြန်မာ Norsk bokmål Nederlands ਪੰਜਾਬੀ Polski Português (Brasil) Português (Portugal) Română Slovenčina Slovenščina Soomaali Shqip Srpski Svenska Kiswahili தமிழ் తెలుగు ไทย Tagalog Türkçe Українська اردو Tiếng Việt 简体中文 繁體中文(香港) 繁體中文(台灣)