Một đợt triển khai PostgreSQL trên môi trường production đã bị sập sau khi một lập trình viên thêm một đối số tùy chọn vào một hàm hiện có bằng lệnh CREATE OR REPLACE FUNCTION. Thay đổi này đã để lại hai hàm có cùng tên, khiến cơ sở dữ liệu trả về lỗi “function is not unique” và API trả về lỗi 400. Sự cố này cho thấy một sai lầm duy nhất trong quá trình migration có thể âm thầm làm hỏng schema đang hoạt động như thế nào, và tại sao các bước kiểm tra ở cấp độ mã nguồn là chưa đủ.

Chuyện gì đã xảy ra

Nhóm phát triển cần mở rộng một stored procedure với một tham số tùy chọn bổ sung. Họ đã chạy lệnh CREATE OR REPLACE FUNCTION …, với giả định rằng nó sẽ ghi đè lên định nghĩa cũ. Tuy nhiên, PostgreSQL chỉ thay thế một hàm khi danh sách đối số đầy đủ khớp chính xác. Việc thay đổi signature (chữ ký hàm) sẽ tạo ra một mục hàm hoàn toàn mới trong khi vẫn giữ nguyên hàm gốc.

Vì đối số mới có giá trị mặc định, nên các bên gọi hàm với số lượng đối số cũ có thể khớp với cả hai định nghĩa. PostgreSQL không thể quyết định nên gọi hàm nào và đã ném ra lỗi “function is not unique”, lỗi này sau đó hiển thị dưới dạng phản hồi 400 từ API.

Kho lưu trữ mã nguồn chỉ hiển thị một định nghĩa duy nhất, và một script tùy chỉnh quét cây thư mục nguồn cũng báo cáo không có sự trùng lặp. Bản trùng lặp này chỉ tồn tại trong cơ sở dữ liệu, được tạo ra khi một file migration cũ bị thực thi lại trên máy chủ.

Tại sao migration lại lọt qua được

Bản migration thêm tham số tùy chọn chỉ đơn giản là chạy lệnh CREATE OR REPLACE FUNCTION. Khi bản migration này chạy lần thứ hai — có thể là sau khi rollback hoặc trong một đợt triển khai lặp lại — cơ sở dữ liệu đã xử lý lệnh này như một thao tác "thêm một overload mới" thay vì "thay thế cái hiện có". Bản migration đã không xác minh trạng thái kết quả, khiến bản trùng lặp tồn tại mà không bị phát hiện.

Script kiểm tra kho lưu trữ chỉ kiểm tra các tệp nguồn, chứ không kiểm tra schema đang hoạt động. Nó giống như việc đang canh gác ở cửa trước trong khi lỗi lại lọt vào từ cửa sau.

Hệ quả

Một hàm không rõ ràng có thể làm sập bất kỳ dịch vụ nào phụ thuộc vào nó. Cách khắc phục bao gồm việc rollback transaction nếu số lượng hàm không chính xác và thông báo cho schema cache để tải lại.

Cách bảo vệ các bản migration

Nhóm đã xây dựng lại bản migration với các bước kiểm tra tường minh, biến nó thành một thao tác tự xác nhận (self-asserting):

  • Bắt đầu một transaction để bất kỳ lỗi nào cũng sẽ rollback toàn bộ thay đổi.
  • Xóa hàm cũ một cách tường minh (explicitly drop) trước khi tạo phiên bản mới, đảm bảo chỉ có duy nhất một định nghĩa tồn tại.
  • Tạo hàm mới với signature mong muốn.
  • Đếm số lượng hàm có cùng tên trong pg_catalog và xác minh số lượng đó chính xác là một.
  • Rollback transaction nếu số lượng không khớp, ngăn chặn việc tồn tại hàm trùng lặp.
  • Thông báo cho schema cache để tải lại, đảm bảo các truy vấn tiếp theo sẽ thấy định nghĩa đã được cập nhật.

Bằng cách hỏi cơ sở dữ liệu "đang có những hàm nào?" thay vì mặc định rằng mã nguồn là đúng, bản migration sẽ trở nên đáng tin cậy trước các lần chạy lặp lại, triển khai từng phần hoặc các chỉnh sửa thủ công.

Ý kiến phản biện: sự tiện lợi đối với sự an toàn

CREATE OR REPLACE FUNCTION rất hấp dẫn vì nó cho phép các lập trình viên phát triển nhanh chóng mà không cần viết các câu lệnh drop riêng biệt. Trong các môi trường mà migration chỉ chạy một lần và không bao giờ chạy lại, lối tắt này hoạt động tốt. Rủi ro chỉ xuất hiện khi các bản migration được chạy lại — dù là do các pipeline CI thực hiện reset database kiểm thử, các lệnh rollback tự động, hay việc áp dụng lại thủ công trên môi trường production.

Bài học rút ra

Thay đổi signature của một hàm bằng CREATE OR REPLACE không đảm bảo việc thay thế — PostgreSQL sẽ âm thầm tạo ra một overload nếu danh sách đối số khác biệt. Các môi trường production dựa vào migration phải xác minh schema kết quả, chứ không chỉ kiểm tra mã nguồn. Việc lồng ghép các lệnh drop tường minh, các bước kiểm tra giao dịch và các xác nhận sau migration sẽ biến một lối tắt tiện lợi thành một quy trình đáng tin cậy và có thể lặp lại.