Vòng lặp thử lại một cách mù quáng thường là nguyên nhân khiến việc tích hợp với API bên ngoài không vượt qua được sự cố ngắn hạn mà còn làm tình hình tệ hơn: liên tục gọi bằng một khóa đã sai, đốt hạn mức yêu cầu vào những lệnh gọi chắc chắn thất bại, hoặc nghiêm trọng hơn là trừ tiền lặp lại cho cùng một giao dịch mua. Cách làm đúng không bắt đầu từ khoảng nghỉ giữa các lần thử, mà từ việc phân loại nguyên nhân: không phải lỗi nào cũng nên thử lại, và không phải lần thử lại nào cũng an toàn.
Ba nhóm lỗi API
Lỗi tạm thời không liên quan đến nội dung yêu cầu: đứt kết nối mạng, hết thời gian chờ kết nối, nhà cung cấp quá tải trong chốc lát. Cùng một yêu cầu với cùng tham số sẽ chạy bình thường sau vài giây, vì lỗi nằm ở đường truyền hoặc thời điểm gọi chứ không phải ở bản thân yêu cầu.
Lỗi vĩnh viễn thì ngược lại: khóa truy cập sai, tham số không hợp lệ, không có quyền thực hiện thao tác. Lệnh gọi như vậy sẽ thất bại như nhau ở lần thử đầu tiên và cả lần thứ một trăm, vì nguyên nhân nằm ngay trong yêu cầu hoặc cấu hình tài khoản. Thử lại mà không sửa nguyên nhân chỉ để lại thêm một dòng trong log.
Lỗi liên quan đến tiền thoạt nhìn giống lỗi tạm thời: không đủ tiền trong số dư, không còn số khả dụng cho quốc gia cần chọn. Yêu cầu lặp lại có thể thành công sau đó, khi đã nạp thêm tiền hoặc kho số được cập nhật, nhưng không được thử lại một cách mù quáng, nhất là chính thao tác trừ tiền — các phần sau sẽ giải thích lý do.
Nên thử lại lỗi nào và không nên thử lại lỗi nào
Quy tắc rất đơn giản: chỉ nên thử lại những lỗi có thể tự khắc phục theo thời gian. Lỗi mạng, hết thời gian chờ, phản hồi 5xx hoặc dấu hiệu quá tải là những ứng viên để tự động thử lại theo cơ chế backoff. Khóa sai, tham số không đúng, bị từ chối do thiếu quyền thì thử lại là vô nghĩa cho đến khi nguyên nhân được xử lý thủ công: cần sửa cấu hình rồi gửi lại lệnh gọi một lần, chứ không phải chạy vòng lặp. Việc thiếu số dư hoặc thiếu kho số nên được hiển thị cho bên gọi như một trạng thái riêng, và quyết định có thử lại hay không thuộc về logic nghiệp vụ hoặc người dùng, không phải bộ đếm thời gian. Thiếu kho số thường xảy ra ở đâu trong mô hình «mua theo lượt hoặc thuê theo thời hạn» được phân tích trong bài Kích hoạt OTP so với thuê số.
Exponential backoff và cái giá của việc thử lại mù quáng
Exponential backoff là khoảng nghỉ trước lần thử tiếp theo tăng dần sau mỗi lần: chẳng hạn một giây, rồi hai, bốn, tám — cho đến giới hạn vài lần thử, thường là ba đến năm lần, sau đó vòng lặp dừng lại và lỗi được chuyển lên tầng trên như một thất bại cuối cùng. Nên cộng thêm một độ lệch ngẫu nhiên nhỏ vào khoảng nghỉ, để nhiều client gặp cùng một sự cố của nhà cung cấp không đồng loạt dồn vào đúng lúc dịch vụ vừa hồi phục.
Nếu không phân loại ngay từ đầu thì backoff không cứu được gì: áp dụng cho nguyên nhân vĩnh viễn, nó chỉ kéo dài theo thời gian những lệnh gọi vô ích, mà mỗi lệnh đều chiếm một suất trong hạn mức chung của tài khoản. Trong lúc vòng lặp gọi vô ích vào khóa không hợp lệ, những yêu cầu có ích của cùng tài khoản — kích hoạt thật, kiểm tra trạng thái — cũng va vào cùng hạn mức đó và bị từ chối vì giới hạn tốc độ. Trong hệ thống tự động hóa có nhiều worker chạy song song, như đã phân tích trong bài về đăng ký qua API, chỉ một tiến trình kẹt trong vòng lặp thử lại cũng có thể làm cạn hạn mức của tất cả các tiến trình còn lại.
Log để phân tích sự cố
Sau sự cố, việc phân tích dựa vào log chứ không dựa vào trí nhớ, vì vậy cần ghi đủ để trả lời câu hỏi «lỗi do nhà cung cấp hay do phía mình» mà không phải tái hiện lại vấn đề. Bộ thông tin tối thiểu gồm: thời điểm gọi, endpoint, mã định danh của yêu cầu hoặc của lượt kích hoạt, mã và nội dung từ chối từ nhà cung cấp, số thứ tự lần thử và các tham số đã loại bỏ thông tin bí mật — khóa và token không được đưa vào log.
Ngoài phản hồi của nhà cung cấp, nên ghi riêng cả cách trình xử lý của bạn đã phân loại nguyên nhân: là tạm thời, vĩnh viễn hay liên quan đến tiền. Nếu bộ phân loại nhầm và thực tế một nguyên nhân vĩnh viễn đã bị thử lại như lỗi tạm thời, thì chỉ bản ghi như vậy mới cho thấy điều đó, còn riêng mã phản hồi thì không.
Idempotency cho thao tác liên quan đến tiền: cách tránh trừ tiền hai lần
Hết thời gian chờ không cho biết rõ thao tác đã được thực hiện trên máy chủ hay chưa: kết nối có thể đứt sau khi đã trừ tiền nhưng trước khi phản hồi kịp đến client. Nếu thử lại yêu cầu đó mà không có biện pháp bảo vệ, API sẽ coi đó là một thao tác mới — và dẫn đến việc trừ tiền lần hai cho cùng một giao dịch mua.
Khóa idempotency giải quyết vấn đề này: client tạo một mã định danh duy nhất một lần cho mỗi thao tác và gửi kèm trong mọi lần thử, kể cả các lần thử lại sau khi hết thời gian chờ. Máy chủ ghi nhớ kết quả đã trả cho khóa đó và khi nhận lại yêu cầu với cùng giá trị thì trả về kết quả cũ thay vì thực hiện lại. Đây là cách duy nhất để thử lại một lệnh gọi liên quan đến tiền một cách an toàn — thiếu khóa thì nguy cơ trừ tiền hai lần luôn còn đó.
Câu hỏi thường gặp
Có nên retry lỗi 429 giống như lỗi hết thời gian chờ không?
Có, nhưng với khoảng nghỉ dài hơn: 429 không phải là sự cố mà là hạn mức yêu cầu đã cạn, và thử lại ngay lập tức chỉ kéo dài thời gian chờ của tất cả mọi người. Hãy lấy giá trị từ header phản hồi nếu nhà cung cấp có trả về, hoặc bắt đầu backoff ngay từ khoảng nghỉ vài giây thay vì một giây.
Có thể tự động thử lại yêu cầu trừ số dư khi hết thời gian chờ không?
Không phải bản thân yêu cầu, mà chỉ là thao tác có khóa idempotency. Hết thời gian chờ không cho biết thao tác đã được thực hiện trên máy chủ hay chưa trước khi kết nối đứt; thử lại với cùng khóa là an toàn vì nhà cung cấp sẽ trả về kết quả của thao tác đã thực hiện thay vì trừ tiền lại. Không có khóa thì việc thử lại có nguy cơ làm thanh toán bị nhân đôi.
Nên đặt bao nhiêu lần thử cho lỗi tạm thời?
Thông thường là ba đến năm lần thử với khoảng nghỉ tăng theo cấp số nhân và tổng thời gian chờ tối đa khoảng 30-60 giây. Thêm nhiều lần thử hiếm khi thay đổi kết quả: nếu nhà cung cấp chưa hồi phục trong khoảng thời gian này thì vấn đề mang tính hệ thống, và nên báo lên cấp cao hơn thay vì tiếp tục vòng lặp.
Mã lỗi theo từng nhóm, header của khóa idempotency và tham số thử lại cho từng endpoint có trong tài liệu API. Việc nhận sự kiện qua webhook, cũng dựa trên cơ chế thử lại và idempotency, được phân tích trong bài webhook: nhận mã mà không cần polling, còn các kịch bản tích hợp cơ bản nằm trong tài liệu API thuê số.