השגיאה של PostgreSQL: "cannot execute CREATE TABLE in a read-only transaction" משבשת כעת מיגרציות עבור צוותים רבים, מה שמשאיר סכמות בחצי יישום וטבלאות היסטוריית מיגרציה נעולות. הכשל מופיע בדרך כלל כאשר פעולת כתיבה נוחתת על replica במקום על ה-primary, והוא מעכב כל כלי המסתמך על שינויי סכימה — Flyway, Liquibase, Django ORM וכדומה.

למה השגיאה מופיעה

PostgreSQL מבטלת את כל פעולות הכתיבה, שינויי הסכימה ועדכוני ה-sequences כאשר טרנזקציה מסומנת כ-read-only. הדרכים הנפוצות ביותר שבהן מיגרציה מגיעה למצב זה הן:

  • מנהלי מאגר חיבורים (Connection poolers) (למשל, PgBouncer) השולחים בטעות את חיבור המיגרציה ל-read replica.
  • תפקידים (Roles) שבהם הפרמטר default_transaction_read_only מוגדר כ-on כברירת מחדל.
  • נקודות קצה בענן (Cloud endpoints) החושפות כתובות URL נפרדות לקריאה וכתיבה; שימוש בכתובת ה-URL של הקורא (כפי שקורה ב-AWS RDS או Aurora) מפנה כתיבות ל-replica.

כאשר אחד מהתנאים הללו מתקיים, תהליך המיגרציה יכול ליצור טבלאות, להוסיף עמודות או לעדכן sequences, רק כדי להיתקל במחסום, מה שמשאיר את בסיס הנתונים במצב של מיגרציה חלקית.

תיקון מיידי בתוך הטרנזקציה

אם כבר נתקלתם בשגיאה, תוכלו לדרוס את דגל ה-read-only עבור הטרנזקציה הנוכחית מבלי להשפיע על שאר הסשן (session) — דבר קריטי כאשר מנהל מאגר חיבורים משתמש באותו סשן לעבודות אחרות.

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. השתמשו בתפקידי מיגרציה ייעודיים

צרו תפקיד (role) שבו מצב הטרנזקציה כברירת מחדל מאפשר כתיבה, והעניקו לו רק את ההרשאות שהוא צריך:

  • CREATE על בסיס הנתונים היעד.
  • CONNECT כדי לאפשר לכלי המיגרציה לפתוח סשן.

הימנעו ממתן התכונה default_transaction_read_only = on לתפקיד זה, כפי שלעיתים מוגדר למשתמשים לשימוש כללי.

3. כוונו את התשתית ל-writer endpoint

בסקריפטים של IaC (כמו Terraform, CloudFormation וכו'), הגדירו את מריצי המיגרציה להשתמש ב-writer endpoint של ה-cluster, ולא ב-reader endpoint. ה-writer endpoint מפנה לצומת ה-primary, בעוד ה-reader endpoint מפנה ל-replica שידחה כתיבות.

4. הוסיפו CI/CD gate

הוסיפו שלב shell ב-pipeline שלכם (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. כוונון ניסיונות חוזרים (retries) של כלי המיגרציה

כלים כמו Flyway מבצעים לעיתים קרובות ניסיונות חיבור חוזרים באופן אוטומטי. עבור Flyway 9+, הגדירו flyway.connectRetries=0. זה מונע מהכלי לפגוש שוב ושוב את ה-replica, מה שעלול להגדיל את השהיית הרפליקציה (replication lag) ולבזבז משאבים.

מה כדאי לעקוב אחריו בהמשך

  • מדדי השהיית רפליקציה (Replication lag metrics): השהיה גוברת עשויה להעיד על כך שמיגרציה כוונה בטעות ל-replica, מה שגורם לניסיונות כתיבה להצטבר בתור.
  • חוקי ניתוב של מנהלי מאגר חיבורים (Connection-pooler routing rules): ודאו שהגדרות ה-pooler מנתבות במפורש את תעבורת המיגרציה למארח ה-primary.
  • ברירות מחדל של תפקידים לאחר שדרוגים: שדרוגי בסיסי נתונים מאפסים לעיתים פרמטרים של תפקידים; בצעו ביקורת חוזרת על default_transaction_read_only לאחר שינויי גרסה משמעותיים.

השורה התחתונה: שגיאת טרנזקציה לקריאה בלבד היא לעיתים נדירות באג של PostgreSQL; זהו סימפטום של תעבורה שנשלחת לצומת (node) הלא נכון או של תפקיד שמוגדר לא נכון. על ידי בדיקת תפקיד הצומת, שימוש בחשבונות מיגרציה ייעודיים והקשחת ה-CI/CD pipeline שלכם, תוכלו לשמור על מיגרציות שרצות בצורה חלקה ולהימנע מסכמות בחצי יישום שמשתקות פיתוח בהמשך הדרך.