Cái ticket
Một ticket support, chuyển lời đối tác: search điểm đến này không ra khách sạn nào. Các vùng xung quanh vẫn bình thường. Cùng tài khoản, cùng ngày, cùng mọi thứ — dịch cái search sang vùng bên cạnh là kết quả lại về đầy đủ.
Phản xạ đầu tiên: chắc có gì đó sập. Không có gì sập cả. Cái region row mà search resolve ra là hợp lệ. Câu query dựng từ nó là hợp lệ. Mọi service trong chuỗi — resolve region, chạy search, đọc cache, dựng response — đều trả 200 và làm đúng phần việc của mình. Kết quả rỗng vì dữ liệu rỗng, mà một mảng rỗng thì là một câu trả lời hoàn toàn hợp lệ.
Hỏng thật sự ở đâu
Điểm đến đó tồn tại hai lần. Cùng một nơi ngoài đời thật, hai region row trong dữ liệu mapping — và khách sạn thì treo ở row kia. Search resolve trúng cái row không có gì bám vào, join với một tập rỗng, rồi trả về đúng những gì dữ liệu nói: không có gì. Cùng họ với nó còn một biến thể mình cũng đã gặp: chỉ có một row thôi, nhưng tập mapping khách sạn của nó rỗng trơn.
Từng row đứng riêng thì chẳng row nào sai. Row nào cũng qua được validation. Cái bug không nằm trong row nào cả — nó nằm ở chỗ có hai row, và mấy bài toán danh tính kiểu này chính là lý do vì sao resolve nhầm một danh tính có thể làm trắng cả một vùng tìm kiếm.
Vì sao không chuông nào kêu
0 khách sạn không ném ra exception nào. Không alert nào nổ, vì mọi tín hiệu mình theo dõi đều được chỉnh để bắt thất bại — tỉ lệ non-2xx, timeout, exception — còn đây là một thành công. Trên mọi metric đang có, một vùng bị hỏng trông y hệt một lượt tìm ở hòn đảo thật sự không có cái khách sạn nào.
Cái checklist design review ở trang decisions của site này đã gọi tên đúng lớp lỗi đó: "Trong lúc tính năng đang hỏng, client nhìn thấy gì — một lỗi, hay một kết quả rỗng trông y như một câu trả lời?" Câu hỏi đó nằm trong danh sách chính vì những cái ticket như thế này. Ở đây, thứ client nhìn thấy là một câu trả lời.
Và cái giá thì mang hình dạng tệ nhất có thể: mỗi lượt search vào vùng đó trong lúc nó hỏng là một khách đọc dòng "không có khách sạn" rồi đi đặt chỗ khác. Bao nhiêu lượt, kéo dài bao nhiêu ngày — mình không biết. Không có gì đếm số lượt search ra 0 kết quả theo từng vùng so với mức nền cả, và đó đúng là cái khe mà lớp lỗi này chui vào trốn. Không có con số doanh thu mất đi nào để đăng lên đây, và chính sự vắng mặt đó là cái phát hiện.
Cách chữa trước đây
Một engineer từng gặp rồi sẽ chép tay mapping từ row có dữ liệu sang row đang bị search trúng, flush cache search, chạy lại search trên trình duyệt, rồi nhìn bằng mắt. Cách này chạy được — miễn là engineer đó đang có mặt và nhớ đủ các bước. Nó cũng đồng thời là một chuỗi thao tác ghi vào dữ liệu mapping production được giữ với nhau bằng trí nhớ, kết thúc bằng một bước kiểm tra mà điều kiện đạt là "nhìn có vẻ ổn".
Cái đã thay đổi
Quy trình đó thành một lệnh runbook, và hình dạng của cái lệnh chính là bài học:
- Clone tập mapping sang row chuẩn. Dán nhãn là thao tác ghi vào dữ liệu production, ngay ở mục lục, và chạy theo đúng cái quy trình mà mọi thao tác ghi trong bộ lệnh phải theo: dry run → in kế hoạch → xin xác nhận → thực thi.
- Flush đúng những cache key bị ảnh hưởng. Đúng những key đó, không phải cả cụm cache — và cũng phải dán nhãn, vì flush là một thao tác ghi có mức sát thương, không phải việc dọn dẹp vặt.
- Verify: chạy lại search và đếm kết quả. Lệnh không kết thúc bằng chữ "done". Nó kết thúc bằng một con số, và con số đó phải lớn hơn 0 thì mới được phép tuyên bố vùng này đã sửa xong.
Bước 3 là bước đáng tiền nhất. Bước cuối của quy trình cũ là một con người nhìn vào màn hình; bước cuối của quy trình mới là một assertion. "Đã sửa" thôi không còn là một cảm giác, mà thành một số đếm.
Điều rút ra
Một kết quả rỗng là loại thành công đắt nhất. Nó không ném exception, không hiện trên dashboard lỗi nào, không đánh thức ai — và mỗi lượt search nó chạm vào là một đơn hàng mất đi trong im lặng. Một thất bại biết tự la lên thì tốn của bạn một cái incident. Một thành công không chở theo gì thì tốn của bạn doanh thu, cho tới chừng nào tình cờ có người nhìn vào.
Status code không phải là một khẳng định về nội dung. 200 nghĩa là "tôi đã làm xong phần việc của tôi", và service nào trong chuỗi này cũng làm xong thật. Nếu kết quả rỗng là một vấn đề kinh doanh, thì phải có một thứ gì đó đứng ra khẳng định sự không-rỗng — một phép đếm, một mức nền, một chốt chặn ở biên. Mặc định thì không có gì làm việc đó, ở bất kỳ stack nào mình từng làm qua.
Sửa xong chưa tính là xong, chừng nào chính cái tool đi sửa chưa tự kiểm tra lại. Bước verify tốn đúng một lượt search. Bỏ qua nó là cách một bản sửa được tuyên bố hoàn tất trong khi đâu đó vẫn còn một cache đang giữ nguyên câu trả lời rỗng — và là cách đúng vùng đó quay lại thành cái ticket của tuần sau.