

งานเบื้องหลังมักถูกออกแบบให้กลับมาทำใหม่ได้ เพราะระหว่างทางมีทั้งบริการที่ตอบช้า เครื่องที่หยุดทำงาน และคนที่กดสั่งซ้ำเมื่อยังไม่เห็นผล แต่การรันโค้ดเดิมได้อีกรอบกับการทำงานเดิมซ้ำโดยไม่สร้างผลเพิ่ม เป็นคนละเรื่องที่ต้องดูให้ครบ
อย่าง workflow ที่รับคำขอแล้วสร้างใบงาน ถ้ารอบแรกสร้างใบงานสำเร็จ แต่การเชื่อมต่อขาดก่อนรับคำตอบ ฝั่งผู้เรียกอาจเห็นเพียง error แล้วเริ่มใหม่ รอบถัดมาจึงมีโอกาสสร้างใบงานอีกใบ ทั้งที่ผู้ใช้ต้องการส่งคำขอเดียวกัน
เรื่องนี้เป็นเหตุผลที่ต้องคิดถึง idempotency ครับ เป้าหมายคือให้การทำซ้ำของคำขอเดิมยังคงผลทางธุรกิจตามที่ตั้งใจไว้ โดยต้องกำหนดก่อนว่าคำขอไหนถือเป็นงานเดิม และจะตรวจหลักฐานการทำงานจากที่ใด ไม่ใช่เพียงเพิ่มเงื่อนไขว่าถ้าเจอ error ให้ลองใหม่
ลองสมมติระบบรับคำขอซ่อมอุปกรณ์หนึ่งรายการ รหัสคำขอนี้ควรตามไปกับการสร้างใบงานทุกครั้งที่เป็นการลองทำคำขอเดิม ส่วน execution ID หรือ attempt ID ใช้แยกว่ากำลังตรวจการรันรอบไหน ถ้าสร้างรหัสคำขอใหม่ทุกครั้งที่ retry ปลายทางก็จะมองทุกครั้งเป็นงานใหม่ตามรหัสที่ได้รับ
ขณะเดียวกัน การที่ผู้ใช้คนเดิมส่งข้อมูลหน้าตาเหมือนกันก็ไม่ได้แปลว่าเป็นคำขอเดียวกันเสมอไป เขาอาจตั้งใจแจ้งซ่อมอีกครั้งหลังจากงานก่อนจบแล้ว การใช้ข้อมูลทั้งหมดมาทำ hash เพื่อเดาว่าซ้ำจึงต้องระวัง เพราะความเหมือนของข้อมูลยังไม่บอกเจตนาของคำสั่ง
จุดเริ่มที่ชัดกว่าคือกำหนดรหัสของการดำเนินการจากต้นทางที่รู้บริบท แล้วส่งรหัสเดิมไปกับการลองใหม่ หากระบบรองรับ idempotency key ก็ต้องกำหนดขอบเขตด้วยว่าใช้กับผู้ใช้รายใด ประเภทงานใด และเก็บผลไว้เป็นเวลานานเท่าไร เพื่อไม่ให้รหัสจากคนละบริบทมาชนกัน

ถ้ารหัสเดิมถูกส่งมากับข้อมูลที่เปลี่ยนไป ต้องมีข้อตกลงว่าปฏิเสธหรือจัดการอย่างไร การคืนผลครั้งเก่าให้กับคำขอที่เปลี่ยนรายละเอียดแล้วอาจทำให้ผู้เรียกเข้าใจผิดว่ารายละเอียดใหม่ถูกนำไปใช้ ส่วนการยอมทำใหม่เงียบๆ ก็ทำลายความหมายของการป้องกันงานซ้ำได้เหมือนกัน
โค้ดที่ค้นก่อนว่ามีใบงานหรือยัง แล้วค่อยสร้างเมื่อไม่พบ ดูเหมือนจะกันรายการซ้ำได้ แต่ถ้าสอง worker ทำพร้อมกัน ทั้งคู่สามารถค้นไม่พบในช่วงเวลาเดียวกัน ก่อนจะต่างคนต่างสร้างใบงานของตัวเองได้
ถ้าข้อมูลอยู่ในฐานเดียวกัน ควรใช้กลไกที่บังคับเงื่อนไขได้จริง เช่น unique constraint ของรหัสการดำเนินการ และจัดการการสร้างกับบันทึกผลภายใน transaction ตามความเหมาะสม เมื่อมีสองคำขอชนกัน ฐานข้อมูลจึงเป็นผู้รักษาเงื่อนไขว่ารหัสเดียวกันสร้างรายการได้ตามข้อตกลง แทนการฝากไว้กับลำดับโค้ดใน worker แต่ละตัว
หลังพบว่ารหัสมีอยู่แล้ว ยังต้องดูสถานะของรายการเดิมด้วย หากสำเร็จแล้วอาจคืนรหัสใบงานเดิมได้ แต่ถ้ายังทำงานอยู่ก็ควรตอบสถานะที่ต่างกัน ส่วนรายการที่ผิดพลาดระหว่างทางอาจต้องตรวจหรือกู้คืนก่อน ไม่ควรตีความว่าพบรหัสเท่ากับทำครบแล้วทุกกรณี
วิธีจัดเก็บขึ้นอยู่กับฐานข้อมูลและขอบเขตของงาน แต่หลักที่ต้องรักษาคือการรับสิทธิ์ทำงานหนึ่งครั้งกับการบันทึกสถานะต้องทนต่อการเข้ามาพร้อมกันได้ การล็อกเฉพาะในหน่วยความจำของ process เดียวอาจไม่ครอบคลุม worker อีกเครื่องที่กำลังรับงานเดียวกัน
transaction ของฐานข้อมูลเราไม่ได้ครอบคลุมการสร้างรายการในบริการอื่นโดยอัตโนมัติ สมมติบันทึกว่าเตรียมสร้างใบงานไว้แล้ว จากนั้นเรียก API ภายนอกสำเร็จ แต่ process หยุดก่อนบันทึกรหัสใบงานกลับมา ฐานข้อมูลฝั่งเรายังไม่รู้ผลสุดท้าย ทั้งที่ปลายทางอาจมีรายการอยู่จริง
ถ้าปลายทางรองรับ idempotency key ก็ควรส่งรหัสการดำเนินการเดิมไปตามรูปแบบที่บริการกำหนด แล้วตรวจทั้งอายุการเก็บ key เงื่อนไขข้อมูลเปลี่ยน และพฤติกรรมเมื่อมีคำขอพร้อมกัน การใส่ header ที่ตั้งชื่อเองไม่ได้ทำให้บริการรองรับพฤติกรรมนี้ขึ้นมา ต้องมีข้อตกลงจาก API จริงด้วย
ถ้าไม่มีความสามารถดังกล่าว อาจใช้รหัสอ้างอิงที่ค้นกลับได้หรือจัดให้มีขั้นตรวจเทียบผลภายหลัง โดยต้องยอมรับข้อจำกัดของช่วงเวลาที่ผลยังคลุมเครือ หากเป็นการกระทำที่มีผลมาก การพักรอให้ตรวจยืนยันอาจเหมาะกว่าการลองสร้างใหม่ทันที

แนวทาง outbox ช่วยให้บันทึกข้อมูลธุรกิจกับความตั้งใจส่งงานออกไปอยู่ใน transaction ฝั่งเราได้ แล้วมี worker มารับไปส่งต่อ แต่การส่งออกจาก outbox ก็ยังอาจเกิดซ้ำได้หากหยุดระหว่างรับผลกับบันทึกสถานะ จึงต้องออกแบบฝั่งรับหรือการตรวจซ้ำร่วมกัน ไม่ถือว่ามี outbox แล้วได้ exactly-once ตลอดทุกระบบทันที
เครื่องมืออย่าง Celery ช่วยจัดคิวและเรียก task แต่ยังต้องเขียน task ให้รับมือกับการทำซ้ำตามลักษณะงานเอง การตั้งค่า acknowledgment หรือ retry มีผลต่อการส่งและรับข้อความ แต่ไม่ได้เปลี่ยนโค้ดสร้างใบงานให้ตรวจรายการซ้ำโดยอัตโนมัติ
จึงต้องอ่านพฤติกรรมเมื่อ worker หยุดและเงื่อนไขการส่งข้อความกลับมาทำใหม่ของระบบที่ใช้ด้วย ถ้าเข้าใจแค่ว่าคิวรับข้อความไว้แล้ว ก็อาจมองข้ามช่วงที่งานเริ่มทำไปแต่ยังไม่ยืนยันผล หรือเข้าใจผิดว่ามีการรับรองผลธุรกิจเสร็จแล้วตั้งแต่ข้อความถูกตอบรับ
ในการตามงาน ควรบันทึกรหัสงาน รหัส attempt รหัสผลจากปลายทาง และสถานะที่ตรวจยืนยันได้ โดยมีความสัมพันธ์ให้เปิดดูต่อกันได้ ข้อมูลนี้ช่วยให้แยกว่ากำลังเห็นงานใหม่จริงๆ หรือกำลังเห็นคำขอเดิมกลับมาอีกครั้ง ไม่ต้องอาศัยการจับเวลาใกล้กันเพื่อเดาว่าอันไหนซ้ำ
ถ้าต้องส่งข้อความแจ้งคน ก็ต้องคิดแยกเช่นกันว่ารายการธุรกิจที่สร้างหนึ่งครั้งอาจมีการแจ้งหลายครั้งโดยตั้งใจหรือไม่ เช่น แจ้งครั้งแรกกับแจ้งเตือนงานค้างเป็นคนละการดำเนินการ การใช้ key ควรสะท้อนความหมายเหล่านี้ ไม่กว้างจนไปกันงานที่ควรเกิดคนละครั้ง
การทดสอบควรใช้ข้อมูลและปลายทางทดสอบ แล้วส่งคำขอรหัสเดียวกันทั้งแบบต่อเนื่องและพร้อมกัน จากนั้นลองตัดการทำงานหลังปลายทางรับคำขอแต่ก่อนฝั่งเราบันทึกผล เพื่อดูว่ารอบกู้คืนสามารถหาใบงานเดิมได้หรือสร้างเพิ่มขึ้นมา
อีกกรณีคือใช้ key เดิมกับ payload ที่เปลี่ยน ลองส่งหลังพ้นระยะเก็บ key และลองให้งานอยู่สถานะกำลังทำเป็นเวลานาน สิ่งที่ต้องตรวจคือจำนวนรายการปลายทางกับคำตอบที่ผู้เรียกได้รับ ไม่ใช่ดูเพียงว่าฟังก์ชันไม่โยน exception ตัวอย่างเหล่านี้เป็นแผนตรวจ ยังไม่มีผลทดสอบระบบจริงประกอบบทนี้
การทำ workflow ซ้ำให้ได้ผลตามเดิมเริ่มจากนิยามงานเดียวกันให้ชัด แล้วรักษารหัสนั้นข้ามทุก attempt กลไกฐานข้อมูลช่วยกันการชนกันภายในระบบ ส่วน API ภายนอกต้องมีข้อตกลงหรือวิธีตรวจยืนยันเพิ่มเติม เมื่อแยกสองขอบเขตนี้ออกจากความสามารถของคิว เราก็จะออกแบบการลองใหม่ได้ตรงกับผลที่ต้องรักษา และรู้ว่าจุดไหนยังต้องรอตรวจแทนการสร้างงานซ้ำ