Четыреста тридцать миграций и семь номеров, занятых дважды
Номер выбирают в момент создания файла, а применяют через день. Две ветки берут один свободный номер, и коллизия всплывает не у автора, а на чужой машине при накате.
У нас 430 миграций. Семь номеров из них заняты дважды: 0001, 0017, 0018,
0067, 0109, 0320, 0327.
Ни одна из этих коллизий не была замечена автором. Все всплыли на чужой машине.
Откуда они берутся
Номер выбирают в момент создания файла, а применяют — через час, день или после ревью. Между этими двумя моментами кто-то ещё создаёт файл и берёт тот же свободный номер: на его ветке он действительно свободен.
Дальше обе ветки вливаются, и в каталоге оказываются 0320_a.sql и
0320_b.sql. Порядок применения между ними становится неопределённым — он
зависит от сортировки, то есть от имени файла после номера, то есть от
случайности.
Пока миграции независимы, ничего не происходит. Как только одна ждёт результата другой — накат падает на чужой машине, и падает не там, где ошибка.
Почему это не ловится обычными способами
Ревью не ловит. Рецензент смотрит на SQL, а не на соседний файл в каталоге; да и второго файла в его ветке ещё нет.
Тесты не ловят. На машине автора накат проходит: у него в каталоге один файл с этим номером.
Слияние не ловит. Git видит два разных файла с разными именами — конфликта нет, сливается молча.
Единственное место, где коллизия становится видимой, — каталог после слияния. То есть уже в общей ветке.
Что помогает
Выбирать номер перед созданием файла, а не после написания SQL. Звучит мелко, но меняет окно: если номер занят сразу, ветки расходятся на минуты, а не на дни.
Смотреть в каталог, а не в свою ветку. Свободный номер — это свободный в
main, а не в том, что у вас под рукой.
Держать порядок наката явным. Мы применяем миграции раннером и от нужной роли, а состояние читаем из базы — по максимальной применённой версии, а не по выводу команды. Вывод говорит, что раннер сделал; база говорит, что получилось.
Как проверить у себя
Одна команда покажет все задвоенные номера:
ls migrations/*.sql | sed 's|.*/||' | cut -d_ -f1 | sort | uniq -d
Если вывод не пуст — посмотрите на каждую пару и ответьте на один вопрос: зависит ли одна от другой? Если да, порядок у вас сейчас случайный, и он совпал с нужным по удаче.
Что из этого следует
Это общий класс: идентификатор, который выбирают по состоянию общего ресурса в момент начала работы, а фиксируют в момент её конца. Номера миграций, имена файлов, ключи в общем справочнике, номера версий.
Между выбором и фиксацией лежит время, и в этом времени состояние меняется. Единственные надёжные способы — либо сокращать это окно до минимума, либо брать идентификатор, который не зависит от чужого состояния вовсе.
Мы держим полный слепок схемы в репозитории и пересобираем его самим накатом, а не руками. Как устроена документация проекта, которая переживает такие разборы — Документы.
Читайте также
Автор
Александр ЦапковОснователь Скоупворк
Веду платформу и её боевой контур сам: разработка, выкат, дежурство. Пишу о том, на чём мы обожглись, — с датами, замерами и ссылками на решения в репозитории.