یک استقرار PostgreSQL در محیط عملیاتی (production) پس از اینکه یک توسعه‌دهنده یک آرگومان اختیاری به یک تابع موجود با استفاده از CREATE OR REPLACE FUNCTION اضافه کرد، دچار فروپاشی شد. این تغییر باعث شد دو تابع با نام یکسان باقی بمانند که منجر به بازگشت خطای “function is not unique” از سوی پایگاه داده و ارسال خطای 400 از سوی API شد. این حادثه نشان می‌دهد که چگونه یک اشتباه ساده در مهاجرت (migration) می‌تواند به صورت بی‌صدا یک شمای (schema) زنده را خراب کند و چرا بررسی‌های سطح کد به تنهایی کافی نیستند.

چه مشکلی پیش آمد

تیم نیاز داشت یک رویه ذخیره‌شده (stored procedure) را با یک پارامتر اضافی و اختیاری گسترش دهد. آن‌ها دستور CREATE OR REPLACE FUNCTION … را اجرا کردند، با این فرض که تعریف قبلی را بازنویسی می‌کند. PostgreSQL تنها زمانی یک تابع را جایگزین می‌کند که لیست کامل آرگومان‌ها دقیقاً مطابقت داشته باشد. تغییر در امضا (signature)، در حالی که نسخه اصلی را دست‌نخورده باقی می‌گذارد، یک ورودی تابع کاملاً جدید ایجاد می‌کند.

از آنجایی که آرگومان جدید دارای یک مقدار پیش‌فرض بود، فراخوان‌کننده‌هایی که تعداد آرگومان‌های قدیمی را ارسال می‌کردند، می‌توانستند با هر دو تعریف مطابقت داشته باشند. PostgreSQL نمی‌توانست تصمیم بگیرد کدام یک را فراخوانی کند و خطای “function is not unique” را صادر کرد که به صورت یک پاسخ 400 از سوی API ظاهر شد.

مخزن کد تنها یک تعریف را نشان می‌داد و یک اسکریپت سفارشی که درخت منبع (source tree) را اسکن می‌کرد، هیچ مورد تکراری گزارش نکرد. مورد تکراری فقط در پایگاه داده وجود داشت که هنگام اجرای مجدد یک فایل مهاجرت قدیمی روی سرور ایجاد شده بود.

چرا مهاجرت از زیر دست در رفت

مهاجرتی که پارامتر اختیاری را اضافه می‌کرد، صرفاً دستور CREATE OR REPLACE FUNCTION را اجرا کرد. وقتی مهاجرت برای بار دوم اجرا شد — شاید پس از یک بازگشت (rollback) یا در طول یک استقرار مجدد — پایگاه داده دستور را به جای «جایگزینی نسخه موجود»، به عنوان «افزودن یک اورلود (overload) جدید» در نظر گرفت. مهاجرت وضعیت حاصل را تأیید نکرد، بنابراین نسخه تکراری بدون اینکه متوجه شوند باقی ماند.

اسکریپتی که مخزن را بررسی می‌کرد، فایل‌های منبع را بررسی می‌کرد، نه شمای زنده را. اسکریپت داشت از در اصلی نگاه می‌کرد، در حالی که باگ از در پشتی وارد شده بود.

مخاطرات

یک تابع مبهم می‌تواند هر سرویسی را که به آن وابسته است، از کار بیندازد. راه حل شامل بازگشت (rollback) تراکنش در صورت اشتباه بودن تعداد توابع و اطلاع‌رسانی به کشِ شمای (schema cache) برای بارگذاری مجدد بود.

چگونه از مهاجرت‌ها محافظت کنیم

تیم مهاجرت را با بررسی‌های صریح بازسازی کرد و آن را به یک عملیات خود-تأییدکننده (self-asserting) تبدیل کرد:

  • یک تراکنش (transaction) شروع کنید تا در صورت بروز هرگونه خطا، کل تغییر بازگشت (rollback) داده شود.
  • تابع قدیمی را به طور صریح حذف (Drop) کنید قبل از ایجاد نسخه جدید، تا تضمین شود که تنها یک تعریف وجود دارد.
  • تابع جدید را با امضای (signature) مورد نظر ایجاد کنید.
  • تعداد توابع را با نام داده شده در pg_catalog بشمارید و تأیید کنید که تعداد دقیقاً یک باشد.
  • اگر تعداد متفاوت بود، تراکنش را بازگشت (Roll back) دهید تا از باقی ماندن نسخه تکراری جلوگیری شود.
  • به کشِ شمای (schema cache) اطلاع دهید تا دوباره بارگذاری شود و اطمینان حاصل شود که پرس‌وجوهای بعدی، تعریف به‌روز شده را مشاهده می‌کنند.

با پرسیدن از پایگاه داده که «چه توابعی وجود دارند؟» به جای فرض بر اینکه کد صحیح است، مهاجرت در برابر اجراهای مکرر، استقرار‌های ناقص یا ویرایش‌های دستی قابل اعتماد می‌شود.

استدلال مخالف: راحتی در مقابل ایمنی

دستور CREATE OR REPLACE FUNCTION جذاب است زیرا به توسعه‌دهندگان اجازه می‌دهد بدون نوشتن دستورات جداگانه DROP به سرعت پیشرفت کنند. در محیط‌هایی که مهاجرت‌ها فقط یک بار اجرا می‌شوند و هرگز تکرار نمی‌شوند، این میان‌بر به خوبی کار می‌کند. ریسک زمانی ظاهر می‌شود که مهاجرت‌ها دوباره اجرا شوند — چه به دلیل خط لوله‌های CI که پایگاه‌های داده تست را بازنشانی می‌کنند، چه بازگشت‌های خودکار یا اعمال مجدد دستی در محیط عملیاتی.

نتیجه‌گیری

تغییر امضای یک تابع با CREATE OR REPLACE جایگزینی را تضمین نمی‌کند — اگر لیست آرگومان‌ها متفاوت باشد، PostgreSQL به صورت بی‌صدا یک اورلود (overload) ایجاد می‌کند. محیط‌های عملیاتی که به مهاجرت‌ها متکی هستند، باید شمای حاصل را تأیید کنند، نه فقط کد منبع را. گنجاندن دستورات حذف صریح، بررسی‌های تراکنشی و تأییدیه‌های پس از مهاجرت، یک میان‌بر راحت را به یک فرآیند قابل اعتماد و تکرارپذیر تبدیل می‌کند.