PostgreSQL’s “cannot execute CREATE TABLE in a read-only transaction” error is now tripping migrations for many teams, leaving schemas half-applied and migration history tables locked. The failure usually appears when a write operation lands on a replica instead of the primary, and it stalls any tool that relies on schema changes—Flyway, Liquibase, Django ORM, and similar.

Mengapa ralat ini muncul

PostgreSQL menyahaktifkan semua operasi penulisan, pengubahsuaian skema, dan kemas kini urutan (sequence) apabila transaksi ditandakan sebagai baca-sahaja (read-only). Cara paling biasa migrasi berakhir dalam keadaan tersebut adalah:

  • Connection pooler (cth., PgBouncer) yang secara tidak sengaja menghantar sambungan migrasi ke replika baca.
  • Peranan (Roles) yang mempunyai parameter default_transaction_read_only ditetapkan kepada on secara lalai.
  • Titik akhir awan (Cloud endpoints) yang mendedahkan URL pembaca (reader) dan penulis (writer) yang berasingan; penggunaan URL pembaca (seperti yang biasa dengan AWS RDS atau Aurora) akan menghalakan penulisan ke replika.

Apabila mana-mana keadaan ini berlaku, proses migrasi mungkin berjaya mencipta jadual, menambah lajur, atau mengemas kini urutan, namun akhirnya terhenti, meninggalkan pangkalan data dalam keadaan migrasi separa.

Penyelesaian segera dalam transaksi

Jika anda sudah terkena ralat ini, anda boleh mengatasi (override) flag baca-sahaja untuk transaksi semasa tanpa menjejaskan baki sesi tersebut—ini sangat penting apabila connection pool menggunakan semula sesi yang sama untuk tugasan lain.

BEGIN;
SET LOCAL default_transaction_read_only = off;
SET TRANSACTION READ WRITE;
CREATE TABLE orders (id SERIAL PRIMARY KEY, total NUMERIC);
COMMIT;

SET LOCAL mengubah tetapan hanya untuk tempoh transaksi tersebut. Penggunaan SET biasa akan mengekalkan perubahan untuk keseluruhan sesi, yang boleh merosakkan operasi lain yang sememangnya memerlukan tetapan lalai baca-sahaja.

Langkah pencegahan

1. Sahkan anda berada pada nod utama (primary node)

Tambahkan semakan pantas sebelum sebarang migrasi dijalankan:

SELECT CASE WHEN pg_is_in_recovery() THEN 'REPLICA' ELSE 'PRIMARY' END;

Jika hasilnya adalah REPLICA, batalkan migrasi tersebut. Fungsi pg_is_in_recovery() mengembalikan nilai true pada pelayan sandaran (standby server), sekali gus menjamin anda tidak cuba menulis ke salinan baca-sahaja.

2. Gunakan peranan (roles) migrasi khusus

Cipta peranan yang mod transaksi lalainya adalah diaktifkan untuk penulisan, dan berikan hanya keistimewaan (privileges) yang diperlukan:

  • CREATE pada pangkalan data sasaran.
  • CONNECT untuk membenarkan alatan migrasi membuka sesi.

Elakkan memberikan atribut default_transaction_read_only = on kepada peranan ini, yang kadangkala ditetapkan untuk pengguna tujuan umum.

3. Halakan infrastruktur ke titik akhir penulis (writer endpoint)

Dalam skrip IaC (Terraform, CloudFormation, dll.), konfigurasikan pelari migrasi (migration runners) untuk menggunakan titik akhir writer kluster, bukannya titik akhir reader. Titik akhir writer akan merujuk kepada nod utama, manakala titik akhir reader akan merujuk kepada replika yang akan menolak sebarang penulisan.

4. Tambahkan gerbang (gate) CI/CD

Masukkan langkah shell dalam saluran paip (pipeline) anda (GitHub Actions, GitLab CI, dll.) yang menjalankan pertanyaan pg_is_in_recovery(). Jika ia mengembalikan nilai true, tamatkan tugasan dengan status bukan sifar untuk menghentikan penggunaan (deployment) lebih awal.

if psql $DATABASE_URL -c "SELECT pg_is_in_recovery()" | grep -q t; then
  echo "Connected to replica – aborting migration"
  exit 1
fi

5. Laraskan cubaan semula (retries) alatan migrasi

Alatan seperti Flyway sering melakukan cubaan semula sambungan secara automatik. Untuk Flyway 9+, tetapkan flyway.connectRetries=0. Ini menghalang alatan daripada berulang kali menghubungi replika, yang jika tidak dicegah, boleh meningkatkan lengah replikasi (replication lag) dan membazirkan sumber.

Perkara yang perlu diperhatikan seterusnya

  • Metrik lengah replikasi (Replication lag metrics): Lengah yang semakin meningkat mungkin menunjukkan bahawa migrasi secara tidak sengaja menyasarkan replika, menyebabkan cubaan penulisan dimasukkan ke dalam barisan (queued).
  • Peraturan penghalaan connection-pooler: Pastikan konfigurasi pooler menghalakan trafik migrasi secara eksplisit ke hos utama.
  • Tetapan lalai peranan selepas naik taraf: Naik taraf pangkalan data kadangkala menetapkan semula parameter peranan; audit semula default_transaction_read_only selepas perubahan versi utama.

Kesimpulannya: ralat transaksi baca-sahaja jarang sekali merupakan pepijat (bug) PostgreSQL; ia adalah simptom trafik dihantar ke nod yang salah atau peranan yang salah dikonfigurasi. Dengan menyemak peranan nod, menggunakan akaun migrasi khusus, dan memperkukuh saluran paip CI/CD anda, anda boleh memastikan migrasi berjalan lancar dan mengelakkan skema separa yang melumpuhkan pembangunan hiliran (downstream).