Продакшн-развертывание PostgreSQL упало после того, как разработчик добавил необязательный аргумент в существующую функцию с помощью CREATE OR REPLACE FUNCTION. Это изменение привело к появлению двух функций с одинаковым именем, из-за чего база данных стала возвращать ошибку «function is not unique», а API — ошибку 400. Этот инцидент показывает, как одна ошибка в миграции может незаметно повредить рабочую схему и почему проверок только на уровне кода недостаточно.
Что пошло не так
Команде нужно было расширить хранимую процедуру дополнительным необязательным параметром. Они выполнили CREATE OR REPLACE FUNCTION …, полагая, что это перезапишет старое определение. Однако PostgreSQL заменяет функцию только в том случае, если полный список аргументов совпадает в точности. Изменение сигнатуры создает совершенно новую запись функции, оставляя оригинал нетронутым.
Поскольку у нового аргумента было значение по умолчанию, вызывающие стороны, передававшие прежнее количество аргументов, могли соответствовать любому из двух определений. PostgreSQL не смог решить, какую именно функцию вызвать, и выдал ошибку «function is not unique», которая проявилась как 400-й ответ от API.
В репозитории кода было только одно определение, а кастомный скрипт, сканирующий дерево исходного кода, не обнаружил дубликатов. Дубликат существовал только в базе данных — он появился, когда старый файл миграции был повторно выполнен на сервере.
Почему миграция проскочила незамеченной
Миграция, добавлявшая необязательный параметр, просто выполняла CREATE OR REPLACE FUNCTION. Когда миграция запустилась второй раз — возможно, после отката или во время повторного развертывания — база данных восприняла команду не как «заменить существующую», а как «добавить новую перегрузку». Миграция не проверяла итоговое состояние, поэтому дубликат остался незамеченным.
Скрипт, проверявший репозиторий, изучал исходные файлы, а не живую схему. Он смотрел в парадную дверь, в то время как баг проник через заднюю.
Ставки
Одна неоднозначная функция может вывести из строя любой сервис, который от нее зависит. Исправление заключалось в том, чтобы откатывать транзакцию, если количество функций было неверным, и уведомлять кэш схемы о необходимости перезагрузки.
Как обезопасить миграции
Команда переработала миграцию, добавив явные проверки, превратив ее в самопроверяющуюся операцию:
- Начните транзакцию, чтобы при любой ошибке откатились все изменения.
- Явно удалите старую функцию перед созданием новой версии, гарантируя наличие только одного определения.
- Создайте новую функцию с нужной сигнатурой.
- Подсчитайте количество функций с данным именем в
pg_catalogи убедитесь, что их ровно одна. - Откатите транзакцию, если количество функций отличается от ожидаемого, чтобы не допустить появления дубликата.
- Уведомьте кэш схемы о необходимости перезагрузки, чтобы последующие запросы видели обновленное определение.
Вместо того чтобы полагаться на предположение, что код верен, и спрашивать базу данных: «какие функции присутствуют?», миграция становится надежной при повторных запусках, частичных развертываниях или ручных правках.
Контраргумент: удобство против безопасности
CREATE OR REPLACE FUNCTION привлекателен тем, что позволяет разработчикам быстро вносить изменения, не прописывая отдельные инструкции DROP. В средах, где миграции запускаются один раз и никогда не повторяются, такой подход работает нормально. Риск возникает, когда миграции воспроизводятся повторно — будь то из-за CI-конвейеров, сбрасывающих тестовые базы данных, автоматических откатов или ручного повторного применения в продакшне.
Вывод
Изменение сигнатуры функции с помощью CREATE OR REPLACE не гарантирует замену — PostgreSQL незаметно создаст перегрузку, если список аргументов отличается. Продакшн-среды, полагающиеся на миграции, должны проверять итоговую схему, а не только исходный код. Использование явных DROP, транзакционных проверок и утверждений после миграции превращает удобный сокращенный путь в надежный и воспроизводимый процесс.
