บทความทั้งหมด

Retry อย่างไรเมื่อเครือข่ายล้ม โดยไม่ทำให้เกิดผลซ้ำ

อ่าน 2 นาที

ไม่มีรูปภาพ
Retry อย่างไรเมื่อเครือข่ายล้ม โดยไม่ทำให้เกิดผลซ้ำ

เครือข่ายล้มเหลวเป็นเรื่องปกติ คำขอบางครั้งหมดเวลาโดยที่คุณไม่รู้ว่าเซิร์ฟเวอร์ได้รับและประมวลผลไปแล้วหรือยัง เมื่อเป็นเรื่องที่เกี่ยวกับการตรวจสลิปและการอนุมัติออเดอร์ การลองใหม่อย่างไม่ระวังอาจทำให้หักโควต้าซ้ำหรืออนุมัติออเดอร์สองครั้ง บทความนี้เสนอแนวคิดที่ฝั่งผู้เรียกควรออกแบบไว้ โดยอิงสิ่งที่คู่มือ 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 ด้วยหลักง่าย ๆ

  1. ผูกผลการตรวจกับออเดอร์ด้วย constraint ในฐานข้อมูล เช่นหนึ่งออเดอร์มีสถานะชำระเงินได้ครั้งเดียว
  2. เก็บเลขอ้างอิงธนาคารของสลิป (slip.reference) ที่ใช้อนุมัติไปแล้ว และปฏิเสธถ้าซ้ำกับอีกออเดอร์ ดูแนวคิดเพิ่มเติมที่ การนำสลิปเดิมไปใช้ซ้ำข้ามร้าน
  3. ใช้การเปลี่ยนสถานะแบบมีเงื่อนไข เช่น อัปเดตเป็น "ชำระแล้ว" เฉพาะแถวที่ยังเป็น "รอชำระ" แล้วดูจำนวนแถวที่ถูกแก้
// 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 และรหัสผลลัพธ์

ลองตรวจสลิปอัตโนมัติกับร้านของคุณ

สมัครแล้วเริ่มใช้งานได้ภายในไม่กี่นาที

บทความอื่น ๆ