Помилка PostgreSQL «cannot execute CREATE TABLE in a read-only transaction» зараз стає причиною збоїв міграцій у багатьох командах, залишаючи схеми наполовину застосованими, а таблиці історії міграцій — заблокованими. Помилка зазвичай виникає, коли операція запису потрапляє на репліку замість основного вузла (primary), і це зупиняє будь-який інструмент, що покладається на зміни схеми — Flyway, Liquibase, Django ORM та подібні.
Чому виникає ця помилка
PostgreSQL вимикає всі операції запису, зміни схеми та оновлення послідовностей (sequences), коли транзакція позначена як read-only. Найпоширеніші причини, через які міграція опиняється в такому стані:
- Пулери з'єднань (наприклад, PgBouncer), які ненавмисно спрямовують з'єднання для міграції на репліку для читання.
- Ролі, у яких параметр
default_transaction_read_onlyза замовчуванням встановлено в значення on. - Хмарні кінцеві точки (endpoints), що надають окремі URL-адреси для читання та запису; використання URL-адреси для читання (що є звичним для AWS RDS або Aurora) спрямовує запис на репліку.
За будь-якої з цих умов процес міграції може створювати таблиці, додавати стовпці або оновлювати послідовності лише для того, щоб натрапити на перешкоду, залишаючи базу даних у стані часткової міграції.
Негайне виправлення всередині транзакції
Якщо ви вже отримали цю помилку, ви можете перевизначити прапорець read-only для поточної транзакції, не впливаючи на решту сесії — це критично важливо, коли пулер з'єднань повторно використовує ту саму сесію для інших завдань.
BEGIN;
SET LOCAL default_transaction_read_only = off;
SET TRANSACTION READ WRITE;
CREATE TABLE orders (id SERIAL PRIMARY KEY, total NUMERIC);
COMMIT;
SET LOCAL змінює налаштування лише на час тривалості транзакції. Використання звичайного SET збереже зміну на всю сесію, що може порушити роботу інших операцій, яким дійсно потрібен режим read-only за замовчуванням.
Заходи профілактики
1. Переконайтеся, що ви на основному вузлі (primary node)
Додайте швидку перевірку перед запуском будь-якої міграції:
SELECT CASE WHEN pg_is_in_recovery() THEN 'REPLICA' ELSE 'PRIMARY' END;
Якщо результат — REPLICA, перервіть міграцію. Функція pg_is_in_recovery() повертає true на резервному (standby) сервері, що гарантує, що ви не намагаєтеся здійснювати запис на копію лише для читання.
2. Використовуйте спеціальні ролі для міграцій
Створіть роль, у якої режим транзакції за замовчуванням дозволяє запис, і надайте їй лише необхідні привілеї:
CREATEна цільовій базі даних.CONNECT, щоб дозволити інструменту міграції відкрити сесію.
Уникайте надання цій ролі атрибута default_transaction_read_only = on, який іноді встановлюється для користувачів загального призначення.
3. Налаштуйте інфраструктуру на кінцеву точку запису (writer endpoint)
В IaC-скриптах (Terraform, CloudFormation тощо) налаштуйте виконавців міграцій (migration runners) на використання writer endpoint кластера, а не reader endpoint. Writer endpoint вказує на основний вузол, тоді як reader endpoint вказує на репліку, яка відхилятиме записи.
4. Додайте перевірку в CI/CD
Додайте shell-крок у ваш пайплайн (GitHub Actions, GitLab CI тощо), який виконує запит pg_is_in_recovery(). Якщо він повертає true, завершіть завдання зі статусом, відмінним від нуля, щоб зупинити розгортання на ранньому етапі.
if psql $DATABASE_URL -c "SELECT pg_is_in_recovery()" | grep -q t; then
echo "Connected to replica – aborting migration"
exit 1
fi
5. Налаштуйте повторні спроби інструментів міграції
Такі інструменти, як Flyway, часто автоматично повторюють спроби підключення. Для Flyway 9+ встановіть flyway.connectRetries=0. Це запобігає багаторазовим зверненням інструменту до репліки, що в іншому випадку може збільшити затримку реплікації (replication lag) та витратити ресурси.
На що звернути увагу далі
- Метрики затримки реплікації (replication lag): Збільшення затримки може вказувати на те, що міграція ненавмисно була спрямована на репліку, через що спроби запису потрапили в чергу.
- Правила маршрутизації пулера з'єднань: Переконайтеся, що конфігурації пулера явно спрямовують трафік міграцій на основний хост.
- Параметри за замовчуванням для ролей після оновлень: Оновлення бази даних іноді скидають параметри ролей; проведіть повторний аудит
default_transaction_read_onlyпісля зміни основних версій.
Підсумок: помилка read-only транзакції рідко є багом PostgreSQL; це симптом того, що трафік надсилається на неправильний вузол або роль налаштована некоректно. Перевіряючи роль вузла, використовуючи спеціальні облікові записи для міграцій та зміцнюючи ваш CI/CD пайплайн, ви зможете забезпечити безперебійну роботу міграцій і уникнути наполовину застосованих схем, які гальмують подальшу розробку.
