يتسبب خطأ PostgreSQL "cannot execute CREATE TABLE in a read-only transaction" حالياً في تعثر عمليات الهجرة (migrations) للعديد من الفرق، مما يترك المخططات (schemas) مطبقة جزئياً ويؤدي إلى قفل جداول سجل الهجرة. يظهر هذا الفشل عادةً عندما تقع عملية كتابة على نسخة احتياطية (replica) بدلاً من العقدة الأساسية (primary)، مما يعطل أي أداة تعتمد على تغييرات المخطط مثل Flyway وLiquibase وDjango ORM وأدوات مماثلة.
لماذا يظهر هذا الخطأ
يقوم PostgreSQL بتعطيل جميع عمليات الكتابة، وتعديلات المخطط، وتحديثات التسلسل (sequences) عندما يتم تحديد المعاملة (transaction) على أنها للقراءة فقط. وأكثر الطرق شيوعاً لوصول عملية الهجرة إلى هذه الحالة هي:
- أدوات إدارة تجمعات الاتصالات (Connection poolers) (مثل PgBouncer) التي ترسل اتصال الهجرة عن غير قصد إلى نسخة احتياطية للقراءة.
- الأدوار (Roles) التي تم ضبط معلمة
default_transaction_read_onlyفيها على الوضع on بشكل افتراضي. - نقاط النهاية السحابية (Cloud endpoints) التي توفر روابط (URLs) منفصلة للقراءة والكتابة؛ حيث يؤدي استخدام رابط القارئ (كما هو شائع في AWS RDS أو Aurora) إلى توجيه عمليات الكتابة إلى نسخة احتياطية.
عند تحقق أي من هذه الشروط، يمكن لعملية الهجرة إنشاء جداول أو إضافة أعمدة أو تحديث التسلسلات، لتصطدم فجأة بالفشل، مما يترك قاعدة البيانات في حالة هجرة جزئية.
الإصلاح الفوري داخل المعاملة
إذا واجهت هذا الخطأ بالفعل، يمكنك تجاوز علامة القراءة فقط للمعاملة الحالية دون التأثير على بقية الجلسة — وهو أمر بالغ الأهمية عندما تعيد أداة إدارة الاتصالات استخدام نفس الجلسة لأعمال أخرى.
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 العادي فسيجعل التغيير مستمراً طوال الجلسة، مما قد يؤدي إلى تعطيل العمليات الأخرى التي تتطلب افتراضياً وضع القراءة فقط.
التدابير الوقائية
1. تحقق من أنك على العقدة الأساسية (primary node)
أضف فحصاً سريعاً قبل تشغيل أي عملية هجرة:
SELECT CASE WHEN pg_is_in_recovery() THEN 'REPLICA' ELSE 'PRIMARY' END;
إذا كانت النتيجة REPLICA، قم بإلغاء عملية الهجرة. تعيد الدالة pg_is_in_recovery() القيمة true على الخادم الاحتياطي (standby server)، مما يضمن أنك لا تحاول الكتابة في نسخة للقراءة فقط.
2. استخدم أدوار هجرة مخصصة
قم بإنشاء دور (role) يكون وضع المعاملة الافتراضي فيه مفعلاً للكتابة، وامنحه فقط الصلاحيات التي يحتاجها:
- صلاحية
CREATEعلى قاعدة البيانات المستهدفة. - صلاحية
CONNECTللسماح لأداة الهجرة بفتح جلسة.
تجنب منح هذا الدور خاصية default_transaction_read_only = on التي يتم ضبطها أحياناً للمستخدمين العامين.
3. وجه البنية التحتية إلى نقطة نهاية الكتابة
في سكربتات البنية التحتية كبرمجية (IaC) مثل (Terraform وCloudFormation وغيرها)، قم بتكوين مشغلات الهجرة لاستخدام نقطة نهاية الكتابة (writer) الخاصة بالعنقود (cluster)، وليس نقطة نهاية القارئ. تشير نقطة نهاية الكتابة إلى العقدة الأساسية، بينما تشير نقطة نهاية القارئ إلى نسخة احتياطية سترفض عمليات الكتابة.
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 metrics): قد يشير التأخر المتزايد إلى أن عملية الهجرة استهدفت نسخة احتياطية عن غير قصد، مما تسبب في وضع محاولات الكتابة في قائمة الانتظار.
- قواعد توجيه أدوات إدارة الاتصالات: تأكد من أن تكوينات أدوات إدارة الاتصالات توجه حركة مرور الهجرة صراحةً إلى المضيف الأساسي.
- القيم الافتراضية للأدوار بعد الترقية: تؤدي ترقيات قواعد البيانات أحياناً إلى إعادة ضبط معاملات الأدوار؛ لذا أعد مراجعة
default_transaction_read_onlyبعد تغيير الإصدارات الرئيسية.
الخلاصة: نادراً ما يكون خطأ المعاملة للقراءة فقط (read-only transaction) خللاً في PostgreSQL؛ بل هو عرض لإرسال حركة المرور إلى العقدة الخاطئة أو سوء تكوين أحد الأدوار. من خلال التحقق من دور العقدة، واستخدام حسابات هجرة مخصصة، وتحصين خط أنابيب CI/CD الخاص بك، يمكنك ضمان استمرار عمليات الهجرة بسلاسة وتجنب المخططات المطبقة جزئياً التي تعيق عمليات التطوير اللاحقة.
