Retry อย่างไรเมื่อเครือข่ายล้ม โดยไม่ทำให้เกิดผลซ้ำ
อ่าน 2 นาที
ไม่มีรูปภาพ
เครือข่ายล้มเหลวเป็นเรื่องปกติ คำขอบางครั้งหมดเวลาโดยที่คุณไม่รู้ว่าเซิร์ฟเวอร์ได้รับและประมวลผลไปแล้วหรือยัง เมื่อเป็นเรื่องที่เกี่ยวกับการตรวจสลิปและการอนุมัติออเดอร์ การลองใหม่อย่างไม่ระวังอาจทำให้หักโควต้าซ้ำหรืออนุมัติออเดอร์สองครั้ง บทความนี้เสนอแนวคิดที่ฝั่งผู้เรียกควรออกแบบไว้ โดยอิงสิ่งที่คู่มือ API ของ PPay ระบุจริง
เริ่มจากรู้ว่าอะไรลองใหม่ได้
คู่มือระบุไว้ชัดว่าควรลองซ้ำเฉพาะเมื่อได้ result: "error" (ระบบคืนโควต้าหรือเครดิตให้แล้ว) หรือ HTTP 5xx หรือ 429 ส่วนผล invalid ไม่ควรลองซ้ำอัตโนมัติ เพราะการส่งสลิปใบเดิมอีกครั้งจะได้ DUPLICATE_SLIP และถูกหักเพิ่ม
| สถานการณ์ | ลองใหม่อัตโนมัติ? | เหตุผล |
|---|---|---|
| HTTP 429 RATE_LIMITED | ได้ หลังรอตาม Retry-After | เรียกถี่เกินไป ไม่หักเงิน |
| HTTP 503 | ได้ แบบหน่วงเวลา | ระบบจัดเก็บหรือคิวขัดข้องชั่วคราว ไม่หักเงิน |
| result error | ได้ แบบหน่วงเวลา | คืนโควต้าหรือเครดิตแล้ว |
| result invalid | ไม่ | สลิปไม่ผ่านจริง ส่งซ้ำโดนหักอีก |
| HTTP 400, 401, 402, 413 | ไม่ | ปัญหาอยู่ที่ข้อมูลหรือสิทธิ์ ลองซ้ำก็ไม่หาย |
ปัญหาของ timeout ที่ไม่รู้ผล
กรณียากคือคำขอหมดเวลาฝั่งคุณ (timeout) หรือการเชื่อมต่อหลุดระหว่างรอคำตอบ คุณไม่รู้ว่าคำขอถึงเซิร์ฟเวอร์หรือไม่ ในเอกสารที่เปิดเผยไม่มีกลไก idempotency key สำหรับ endpoint ตรวจสลิป ดังนั้นอย่าสมมติว่าการส่งซ้ำจะถูกรวมเป็นรายการเดียว ฝั่งผู้เรียกจึงต้องออกแบบเองให้ปลอดภัย แนวทางที่ทำได้มีดังนี้
- ตั้ง timeout ให้เหมาะสม ตัวอย่างในคู่มือใช้ 30 วินาที การตั้งสั้นเกินไปทำให้เกิดกรณีไม่รู้ผลบ่อยขึ้น
- ใช้แบบคิวเมื่อระบบของคุณกลัวการค้างรอ ส่งด้วย
POST /api/v1/verify/asyncพร้อมreferenceเช่นเลขออเดอร์ ได้jobIdกลับมาเก็บไว้ แล้วรับผลทาง Webhook หรือดึงด้วยGET /api/v1/jobs/{jobId} - บันทึกสถานะของคุณเองก่อนเรียก เช่นสร้างแถว "กำลังตรวจสลิป" ผูกกับออเดอร์ก่อน ถ้าเรียกซ้ำจะรู้ว่ามีความพยายามค้างอยู่
- เมื่อไม่รู้ผล ให้ตรวจประวัติก่อนส่งใหม่ คู่มือแนะนำให้เก็บ
idและjobIdไว้ใช้ร่วมกับหน้า "ประวัติ" ในพื้นที่สมาชิก เพื่อดูว่าสลิปใบนั้นถูกตรวจไปแล้วหรือยัง
ป้องกันซ้ำฝั่งระบบของคุณเอง
ความเสี่ยงที่แท้จริงคือ "การอนุมัติออเดอร์ซ้ำ" ไม่ใช่การเรียก API ซ้ำ ให้ทำให้ขั้นตอนอนุมัติเป็น idempotent ด้วยหลักง่าย ๆ
- ผูกผลการตรวจกับออเดอร์ด้วย constraint ในฐานข้อมูล เช่นหนึ่งออเดอร์มีสถานะชำระเงินได้ครั้งเดียว
- เก็บเลขอ้างอิงธนาคารของสลิป (
slip.reference) ที่ใช้อนุมัติไปแล้ว และปฏิเสธถ้าซ้ำกับอีกออเดอร์ ดูแนวคิดเพิ่มเติมที่ การนำสลิปเดิมไปใช้ซ้ำข้ามร้าน - ใช้การเปลี่ยนสถานะแบบมีเงื่อนไข เช่น อัปเดตเป็น "ชำระแล้ว" เฉพาะแถวที่ยังเป็น "รอชำระ" แล้วดูจำนวนแถวที่ถูกแก้
// pseudo-code: conditional update makes approval idempotent
const changed = await db.run(
"UPDATE orders SET status = 'paid', slip_ref = ? WHERE id = ? AND status = 'pending'",
[slip.reference, orderId]
);
if (changed === 0) { /* already handled: do nothing */ }
Backoff ที่เหมาะสม
เมื่อต้องลองใหม่ ให้เว้นระยะเพิ่มขึ้นเรื่อย ๆ และสุ่มเวลาเล็กน้อยเพื่อไม่ให้ทุกคำขอกลับมาพร้อมกัน ถ้าได้ 429 ให้ใช้ค่า Retry-After ที่ระบบส่งมาเป็นหลัก จำกัดจำนวนครั้งสูงสุดเสมอ เช่นไม่เกินสามถึงห้าครั้ง แล้วส่งเรื่องเข้ากระบวนการตรวจด้วยคนหรือแจ้งลูกค้าให้ลองใหม่ ระบบคิวของ PPay เองก็ลองใหม่สูงสุด 3 ครั้งเมื่อผู้ให้บริการขัดข้อง ก่อนปิดงานเป็น SERVICE_UNAVAILABLE และคืนโควต้า คุณไม่ต้องซ้อน retry ทับในส่วนนั้น
สรุปเช็กลิสต์
- ลองใหม่เฉพาะ 429, 503 และ result error
- ไม่ลองซ้ำเมื่อได้ invalid
- ตั้ง timeout สมเหตุสมผล และเก็บ jobId หรือ id ทุกครั้ง
- ทำขั้นตอนอนุมัติออเดอร์ให้ idempotent ในฐานข้อมูลของคุณ
- จำกัดจำนวนครั้ง retry และมีทางออกสำรองให้คนตรวจ
ดูรหัสข้อผิดพลาดทั้งหมดใน คู่มือ API และอ่านวิธีแยกผลลัพธ์ในโค้ดได้ที่ จัดการ error และรหัสผลลัพธ์