PostgreSQL का “cannot execute CREATE TABLE in a read-only transaction” एरर अब कई टीमों के माइग्रेशन (migrations) में बाधा डाल रहा है, जिससे स्कीमा (schemas) आधे-अधूरे लागू हो जाते हैं और माइग्रेशन हिस्ट्री टेबल लॉक हो जाते हैं। यह विफलता आमतौर पर तब आती है जब कोई राइट ऑपरेशन (write operation) प्राइमरी के बजाय रेप्लिका (replica) पर चला जाता है, और यह Flyway, Liquibase, Django ORM और इसी तरह के किसी भी टूल को रोक देता है जो स्कीमा बदलावों पर निर्भर करता है।

यह एरर क्यों आता है

जब किसी ट्रांजेक्शन (transaction) को read-only मार्क किया जाता है, तो PostgreSQL सभी राइट्स (writes), स्कीमा बदलावों (schema alterations) और सीक्वेंस अपडेट्स (sequence updates) को डिसेबल कर देता है। माइग्रेशन के उस स्थिति में पहुँचने के सबसे सामान्य तरीके ये हैं:

  • Connection poolers (जैसे, PgBouncer) जो अनजाने में माइग्रेशन कनेक्शन को रीड रेप्लिका (read replica) पर भेज देते हैं।
  • Roles जिनमें default_transaction_read_only पैरामीटर डिफ़ॉल्ट रूप से on सेट होता है।
  • Cloud endpoints जो अलग-अलग रीडर (reader) और राइटर (writer) URLs प्रदान करते हैं; रीडर URL का उपयोग करना (जैसा कि AWS RDS या Aurora में सामान्य है) राइट्स को रेप्लिका की ओर रूट कर देता है।

जब इनमें से कोई भी स्थिति लागू होती है, तो माइग्रेशन प्रक्रिया टेबल बना सकती है, कॉलम जोड़ सकती है, या सीक्वेंस अपडेट कर सकती है, लेकिन अंततः वह रुक जाती है, जिससे डेटाबेस आंशिक रूप से माइग्रेटेड (partially migrated) स्थिति में रह जाता है।

ट्रांजेक्शन के भीतर तत्काल समाधान

यदि आप पहले ही इस एरर का सामना कर चुके हैं, तो आप बाकी सेशन को प्रभावित किए बिना वर्तमान ट्रांजेक्शन के लिए 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. सत्यापित करें कि आप प्राइमरी नोड पर हैं

किसी भी माइग्रेशन को चलाने से पहले एक त्वरित जाँच जोड़ें:

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

यदि परिणाम REPLICA है, तो माइग्रेशन को रोक दें। pg_is_in_recovery() फंक्शन स्टैंडबाय सर्वर पर true लौटाता है, जिससे यह सुनिश्चित होता है कि आप रीड-ओनली कॉपी में लिखने का प्रयास नहीं कर रहे हैं।

2. समर्पित माइग्रेशन रोल्स का उपयोग करें

एक ऐसा रोल बनाएँ जिसका डिफ़ॉल्ट ट्रांजेक्शन मोड राइट-इनेबल्ड (write-enabled) हो, और उसे केवल वही विशेषाधिकार (privileges) दें जिनकी उसे आवश्यकता है:

  • टारगेट डेटाबेस पर CREATE अधिकार।
  • माइग्रेशन टूल को सेशन खोलने की अनुमति देने के लिए CONNECT अधिकार।

इस रोल को default_transaction_read_only = on एट्रिब्यूट देने से बचें, जो कभी-कभी सामान्य उपयोग वाले यूजर्स के लिए सेट किया जाता है।

3. इंफ्रास्ट्रक्चर को राइटर एंडपॉइंट पर पॉइंट करें

IaC स्क्रिप्ट्स (Terraform, CloudFormation, आदि) में, माइग्रेशन रनर्स को क्लस्टर के writer एंडपॉइंट का उपयोग करने के लिए कॉन्फ़िगर करें, न कि रीडर एंडपॉइंट का। राइटर एंडपॉइंट प्राइमरी नोड पर रिज़ॉल्व होता है, जबकि रीडर एंडपॉइंट एक रेप्लिका पर रिज़ॉल्व होता है जो राइट्स को रिजेक्ट कर देगा।

4. CI/CD गेट जोड़ें

अपने पाइपलाइन (GitHub Actions, GitLab CI, आदि) में एक शेल स्टेप (shell step) डालें जो pg_is_in_recovery() क्वेरी चलाता है। यदि यह true लौटाता है, तो डिप्लॉयमेंट को जल्दी रोकने के लिए नॉन-ज़ीरो (non-zero) स्टेटस के साथ जॉब से बाहर निकल जाएँ।

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 जैसे टूल्स अक्सर कनेक्शन को स्वचालित रूप से रिट्राइ (retry) करते हैं। Flyway 9+ के लिए flyway.connectRetries=0 सेट करें। यह टूल को बार-बार रेप्लिका पर हिट करने से रोकता है, जिससे अन्यथा रेप्लिकेशन लैग (replication lag) बढ़ सकता है और संसाधन बर्बाद हो सकते हैं।

आगे क्या ध्यान रखें

  • Replication lag metrics: बढ़ता हुआ लैग यह संकेत दे सकता है कि माइग्रेशन ने अनजाने में किसी रेप्लिका को टारगेट किया है, जिससे राइट अटेंप्ट्स (write attempts) कतार (queue) में लग गए हैं।
  • Connection-pooler routing rules: सुनिश्चित करें कि पूलर कॉन्फ़िगरेशन स्पष्ट रूप से माइग्रेशन ट्रैफिक को प्राइमरी होस्ट पर रूट करते हैं।
  • Role defaults after upgrades: डेटाबेस अपग्रेड कभी-कभी रोल पैरामीटर्स को रीसेट कर देते हैं; मेजर वर्जन बदलावों के बाद default_transaction_read_only का पुन: ऑडिट करें।

सार यह है: रीड-ओनली ट्रांजेक्शन एरर शायद ही कभी PostgreSQL का बग होता है; यह गलत नोड पर ट्रैफिक भेजे जाने या रोल के गलत कॉन्फ़िगरेशन का लक्षण है। नोड रोल की जाँच करके, समर्पित माइग्रेशन अकाउंट का उपयोग करके और अपने CI/CD पाइपलाइन को मजबूत करके, आप माइग्रेशन को सुचारू रूप से चला सकते हैं और आधे-अधूरे लागू स्कीमा से बच सकते हैं जो डाउनस्ट्रीम डेवलपमेंट को बाधित करते हैं।