ทุกคนต้องการการอัปเดตแบบเรียลไทม์ จนกระทั่งพวกเขาตระหนักว่า "ความเร็ว" กับ "ความถูกต้อง" นั้นไม่ใช่สิ่งเดียวกัน ในระบบแบบกระจายตัว (distributed system) เหตุการณ์ต่าง ๆ สามารถเดินทางด้วยความเร็วแสงแต่ก็ยังอาจมาถึงผิดลำดับได้ WebSockets อาจหลุดและเชื่อมต่อใหม่ Message brokers อาจส่งแพ็กเก็ตซ้ำ Background workers อาจต้องแข่งกับเวลาในการยกเลิกการทำงาน (timeout cancellations) ผลลัพธ์คืออะไร? ลูกค้าอาจเห็นเหตุการณ์ที่ 42 ตามด้วยเหตุการณ์ที่ 40 แล้วตามด้วย snapshot ที่อ้างว่าระบบอยู่ที่เหตุการณ์ที่ 45 แล้ว หากคุณกำลังสร้าง workflow ของ agent ที่ทำงานต่อเนื่อง ความวุ่นวายนี้ไม่ใช่แค่กรณีที่เกิดขึ้นได้ยาก (edge case) แต่มันคือมาตรฐานพื้นฐาน (baseline) จงแก้ไขลำดับของเหตุการณ์ของคุณให้ถูกต้องก่อนที่จะกังวลเรื่องการลดเวลาการส่งข้อมูลเพียงไม่กี่มิลลิวินาที
ความเป็นจริงอันวุ่นวายของ "Real-Time"
Real-time คือคุณสมบัติของการรับส่งข้อมูล (transport property) มันอธิบายว่าแพ็กเก็ตเคลื่อนที่ผ่านสายสัญญาณได้เร็วแค่ไหน ไม่ใช่ว่าเรื่องราวที่มันเล่ามานั้นสมเหตุสมผลหรือไม่ งานที่ต้องใช้เวลานานจะขยายความไม่สอดคล้องกันให้ชัดเจนยิ่งขึ้นเพราะมันกินระยะเวลานาน เช่น งานฝึกสอนโมเดล (model training job), ขั้นตอนการอนุมัติแบบหลายขั้นตอน (multi-step approval flow) หรือกระบวนการเรนเดอร์วิดีโอ (video rendering pipeline) อาจส่งเหตุการณ์ออกมานับสิบรายการในช่วงเวลาหลายนาทีหรือหลายชั่วโมง ในช่วงเวลานั้น อะไรก็เกิดขึ้นได้
Broker อาจพยายามส่งข้อความซ้ำเพราะการตอบรับ (acknowledgement) สูญหาย Load balancer อาจส่งเหตุการณ์สองรายการผ่านเส้นทางเครือข่ายที่ต่างกัน ทำให้รายการที่ใหม่กว่ามาถึงก่อน Worker process อาจตายหลังจากเขียนข้อมูลลงฐานข้อมูลแต่ก่อนที่จะส่งเหตุการณ์ความสำเร็จ (success event) ทำให้ worker ตัวที่สองมารับงานต่อและส่งความคืบหน้าของตัวเองออกมา หาก frontend ของคุณสมมติว่าข้อความล่าสุดคือข้อความที่ถูกต้องที่สุด มันจะแสดงสถานะที่ไม่เคยเกิดขึ้นจริง ผู้ใช้จะเห็นป้าย "completed" กระพริบกลับไปเป็น "processing" หรือที่แย่กว่านั้นคือ งานที่ถูกยกเลิกไปแล้วจู่ ๆ ก็ฟื้นคืนชีพขึ้นมา ความเร็วที่ปราศจากลำดับที่ถูกต้องก็เป็นเพียงความสับสนที่มีเฟรมเรตสูงขึ้นเท่านั้น
หมายเลขลำดับ (Sequence Numbers) คือนาฬิกาที่แท้จริง
วิธีแก้ไขคือการใช้หมายเลขลำดับแบบ monotonic ที่เข้มงวดซึ่งสร้างโดย producer ทุกการดำเนินการที่เปลี่ยนสถานะจะต้องได้รับหมายเลขที่เพิ่มขึ้นทีละหนึ่งอย่างแม่นยำ โดยไม่มีช่องว่างและไม่มีการย้อนกลับ หมายเลขนั้นต้องถูกบันทึกไว้ในธุรกรรม (transaction) เดียวกันกับตัวเหตุการณ์เอง หากแถวในฐานข้อมูลอัปเดตแต่การ commit ลำดับล้มเหลว คุณต้อง roll back ทั้งคู่ วิธีนี้จะทำให้ลำดับเวลาเชิงตรรกะ (logical timeline) เป็นหนึ่งเดียว (atomic) กับการเปลี่ยนสถานะ
Event ID ยังคงมีประโยชน์ แต่ใช้แก้ปัญหาที่ต่างกัน Event ID ระบุ payload เฉพาะเจาะจงเพื่อให้คุณสามารถกำจัดข้อมูลซ้ำ (deduplicate) ได้เมื่อ broker ส่งข้อความเดิมซ้ำสองครั้ง ในทางกลับกัน Sequence number จะบอกคุณว่า payload นั้นควรอยู่ตรงไหนในลำดับเหตุการณ์ที่เป็นเหตุเป็นผลกัน (causal chain) มันช่วยเผยให้เห็นช่องว่าง และเผยให้เห็นลำดับที่ผิดพลาด ส่วน timestamp ไม่สามารถทำได้ทั้งสองอย่าง นาฬิกาอาจคลาดเคลื่อน (drift), NTP อาจถอยหลัง และ virtual machines อาจหยุดชะงัก จงใช้ timestamp เพื่อวัตถุประสงค์ในการแสดงผลเท่านั้น เช่น "เริ่มเมื่อ 3 นาทีที่แล้ว" และอย่าใช้เป็นคีย์ในการเรียงลำดับ (sorting key) สำหรับ business logic โดยเด็ดขาด
วิธีที่ Client ควรจัดการกับ Stream
เมื่อ producer รับประกันลำดับแบบ monotonic แล้ว consumer ก็จะมีกฎที่เรียบง่ายและชัดเจน หากหมายเลขลำดับขาเข้า (inbound sequence number) น้อยกว่าหรือเท่ากับหมายเลขล่าสุดที่นำไปใช้แล้ว ให้ทิ้งมันไป เพราะมันอาจเป็นข้อมูลซ้ำหรือข้อมูลเก่าที่มาล่าช้า หากลำดับมากกว่าหมายเลขล่าสุดที่นำไปใช้แล้วอยู่หนึ่งพอดี ให้ดำเนินการทันที นั่นคือเส้นทางปกติ (happy path) หากลำดับกระโดดข้ามไป เช่น คุณคาดหวังหมายเลข 12 แต่ได้รับหมายเลข 15 แสดงว่ามีบางอย่างขาดหายไป ให้เก็บเหตุการณ์ใหม่ไว้ในบัฟเฟอร์ (buffer) และขอให้เซิร์ฟเวอร์ส่งข้อมูลซ้ำ (replay) โดยเริ่มจากลำดับถัดไปที่คาดหวัง อย่าเดา และอย่าข้ามไปโดยหวังว่าช่องว่างนั้นจะไม่สำคัญ
สถานะสุดท้าย (Terminal states) ต้องได้รับการปฏิบัติเสมือนว่าไม่สามารถเปลี่ยนแปลงได้ เมื่อภารกิจถูกทำเครื่องหมายว่าเสร็จสิ้น (completed), ล้มเหลว (failed) หรือถูกยกเลิก (cancelled) แล้ว Client ควรปฏิเสธการเปลี่ยนแปลงสถานะใด ๆ ที่ตามมาสำหรับรายการนั้น ฟังดูเหมือนเป็นเรื่องชัดเจนจนกระทั่งคุณต้องรับมือกับ
