การปรับใช้ PostgreSQL บนระบบ Production ล่มหลังจากนักพัฒนาเพิ่มอาร์กิวเมนต์แบบเลือกได้ (optional argument) ให้กับฟังก์ชันที่มีอยู่เดิมด้วยคำสั่ง CREATE OR REPLACE FUNCTION การเปลี่ยนแปลงนี้ทำให้มีฟังก์ชันสองฟังก์ชันที่มีชื่อเดียวกัน ส่งผลให้ฐานข้อมูลคืนค่า “function is not unique” และ API ส่งข้อผิดพลาด 400 กลับมา เหตุการณ์นี้แสดงให้เห็นว่าความผิดพลาดในการทำ migration เพียงครั้งเดียวสามารถทำให้ schema ที่ใช้งานจริงเสียหายได้อย่างเงียบเชียบ และทำไมการตรวจสอบในระดับโค้ดเพียงอย่างเดียวจึงไม่เพียงพอ

สิ่งที่ผิดพลาดไป

ทีมงานจำเป็นต้องขยาย stored procedure โดยเพิ่มพารามิเตอร์แบบเลือกได้ (optional parameter) เข้าไป พวกเขาใช้คำสั่ง CREATE OR REPLACE FUNCTION … โดยสมมติว่ามันจะเขียนทับนิยามเดิม แต่ PostgreSQL จะแทนที่ฟังก์ชันก็ต่อเมื่อรายการอาร์กิวเมนต์ทั้งหมดตรงกันทุกประการเท่านั้น การเปลี่ยน signature จะเป็นการสร้างรายการฟังก์ชันใหม่ขึ้นมา ในขณะที่ฟังก์ชันเดิมยังคงอยู่เหมือนเดิม

เนื่องจากอาร์กิวเมนต์ใหม่มีค่าเริ่มต้น (default value) ผู้เรียกใช้งานที่ส่งอาร์กิวเมนต์ตามจำนวนเดิมจึงสามารถจับคู่กับนิยามใดก็ได้ PostgreSQL จึงไม่สามารถตัดสินใจได้ว่าจะเรียกใช้ฟังก์ชันไหน และส่งข้อผิดพลาด “function is not unique” ออกมา ซึ่งปรากฏเป็น response 400 จาก API

ใน repository ของโค้ดแสดงนิยามเพียงหนึ่งเดียว และสคริปต์ที่ใช้สแกน source tree ก็รายงานว่าไม่มีรายการที่ซ้ำกัน แต่รายการที่ซ้ำกันนั้นมีอยู่เฉพาะในฐานข้อมูล ซึ่งเกิดขึ้นเมื่อไฟล์ migration เก่าถูกรันซ้ำบนเซิร์ฟเวอร์

ทำไมการ migration ถึงหลุดรอดไปได้

การทำ migration ที่เพิ่มพารามิเตอร์แบบเลือกได้นั้นเพียงแค่รัน CREATE OR REPLACE FUNCTION เมื่อการ migration ถูกรันเป็นครั้งที่สอง—อาจจะหลังจาก rollback หรือระหว่างการ deploy ซ้ำ—ฐานข้อมูลจะมองว่าคำสั่งนี้คือ “การเพิ่ม overload ใหม่” แทนที่จะเป็น “การแทนที่ของเดิม” การ migration ไม่ได้ตรวจสอบสถานะที่เกิดขึ้นจริง ดังนั้นรายการที่ซ้ำกันจึงยังคงอยู่โดยไม่มีใครสังเกตเห็น

สคริปต์ที่ตรวจสอบ repository ตรวจสอบเฉพาะไฟล์ต้นฉบับ (source files) ไม่ใช่ schema ที่ใช้งานจริง มันเหมือนกับการเฝ้าดูที่ประตูหน้า ในขณะที่บั๊กแอบเข้ามาทางประตูหลัง

ความเสี่ยงที่ตามมา

ฟังก์ชันที่กำกวมเพียงฟังก์ชันเดียวสามารถทำให้บริการใดๆ ที่เรียกใช้งานมันล่มได้ วิธีการแก้ไขคือการ rollback transaction หากจำนวนฟังก์ชันไม่ถูกต้อง และแจ้งให้ schema cache ทำการ reload ใหม่

วิธีป้องกันการ migration

ทีมงานได้สร้างการ migration ใหม่พร้อมการตรวจสอบที่ชัดเจน เปลี่ยนให้มันเป็นการทำงานแบบ self-asserting:

  • เริ่ม transaction เพื่อให้หากเกิดความล้มเหลวใดๆ จะได้ rollback การเปลี่ยนแปลงทั้งหมด
  • สั่ง Drop ฟังก์ชันเดิมอย่างชัดเจน ก่อนที่จะสร้างเวอร์ชันใหม่ เพื่อรับประกันว่าจะมีนิยามเพียงหนึ่งเดียวเท่านั้น
  • สร้างฟังก์ชันใหม่ ด้วย signature ที่ต้องการ
  • นับจำนวนฟังก์ชัน ที่มีชื่อเดียวกันใน pg_catalog และตรวจสอบว่าจำนวนต้องเท่ากับหนึ่งพอดี
  • Roll back transaction หากจำนวนไม่ถูกต้อง เพื่อป้องกันไม่ให้รายการที่ซ้ำกันคงอยู่
  • แจ้งให้ schema cache ทำการ reload เพื่อให้มั่นใจว่า query หลังจากนั้นจะเห็นนิยามที่อัปเดตแล้ว

การถามฐานข้อมูลว่า “มีฟังก์ชันอะไรอยู่บ้าง?” แทนที่จะสมมติว่าโค้ดนั้นถูกต้อง จะทำให้การ migration มีความน่าเชื่อถือมากขึ้น ไม่ว่าจะถูกรันซ้ำ, มีการ deploy เพียงบางส่วน หรือมีการแก้ไขด้วยมือก็ตาม

ข้อโต้แย้ง: ความสะดวก vs ความปลอดภัย

CREATE OR REPLACE FUNCTION นั้นน่าดึงดูดเพราะช่วยให้นักพัฒนาทำงานได้อย่างรวดเร็วโดยไม่ต้องเขียนคำสั่ง drop แยกต่างหาก ในสภาพแวดล้อมที่การ migration รันเพียงครั้งเดียวและไม่เคยรันซ้ำ วิธีลัดนี้ก็ใช้งานได้ดี ความเสี่ยงจะปรากฏขึ้นเมื่อมีการรัน migration ซ้ำ (replay)—ไม่ว่าจะเป็นเพราะ CI pipelines ที่รีเซ็ต test databases, การทำ automated rollbacks หรือการรันซ้ำด้วยมือในระบบ production

บทสรุป

การเปลี่ยน signature ของฟังก์ชันด้วย CREATE OR REPLACE ไม่ได้รับประกันการแทนที่—PostgreSQL จะสร้าง overload ขึ้นมาอย่างเงียบเชียบหากรายการอาร์กิวเมนต์แตกต่างกัน สภาพแวดล้อมการทำงานจริง (Production) ที่ต้องพึ่งพาการ migration จะต้องตรวจสอบ schema ที่เกิดขึ้นจริง ไม่ใช่แค่ตรวจสอบโค้ดต้นฉบับ การใส่คำสั่ง drop ที่ชัดเจน, การตรวจสอบผ่าน transaction และการทำ assertion หลังการ migration จะเปลี่ยนวิธีลัดที่สะดวกให้กลายเป็นกระบวนการที่เชื่อถือได้และทำซ้ำได้