บอทเริ่มส่งคำตอบเดิมซ้ำสองหรือสามครั้งทุกครั้งที่ผู้ใช้กดปุ่มส่งรัวๆ การส่งข้อมูลซ้ำจะเกิดขึ้นเฉพาะกับผู้ที่พิมพ์เร็วพอที่จะส่งข้อความหลายข้อความก่อนที่ AI จะเริ่มประมวลผล และปัญหานี้ก็ซ่อนอยู่ในระบบ production เป็นเวลานานเนื่องจากรูปแบบการเกิดนั้นพบได้ยาก การล็อกฐานข้อมูลที่หลุดก่อนเวลาอันควร—ซึ่งถูกปล่อยออกไปเพียงไม่กี่มิลลิวินาทีหลังจากที่ล็อกไว้—ทำให้การสนทนาไม่ได้รับการป้องกัน ส่งผลให้มีหลายกระบวนการ (processes) สามารถตอบคำถามเดียวกันได้
ทำไมการล็อกถึงล้มเหลว
โค้ดทำการล็อกในคำสั่งฐานข้อมูลเพียงครั้งเดียว จากนั้นก็ส่งต่อการควบคุมกลับไปยังตัวจัดการคำขอ (request handler) ทันที อายุการใช้งานของล็อกนั้นวัดเป็นหน่วยมิลลิวินาที ซึ่งสั้นกว่าเวลาที่โมเดล AI ต้องใช้ในการสร้างคำตอบมาก กว่าที่โมเดลจะเริ่มทำงาน ล็อกก็หายไปแล้ว จึงไม่มีอะไรป้องกันไม่ให้คำขอที่สองเข้ามาดึงข้อมูลการสนทนาเดียวกันและส่งคำตอบออกมาอีกครั้ง
อาการที่ปรากฏมีสองรูปแบบ:
- คำตอบที่เหมือนกันถูกส่งออกมาต่อเนื่องกัน
- คำตอบที่มีการปรับเปลี่ยนสำนวนเล็กน้อยปรากฏขึ้นสำหรับคำถามเดียวกัน เนื่องจากแต่ละกระบวนการสร้าง prompt ของตัวเองจากข้อมูลนำเข้าของผู้ใช้ชุดเดียวกัน
เนื่องจากผู้ใช้ส่วนใหญ่จะหยุดเว้นระยะระหว่างข้อความ บั๊กนี้จึงไม่ถูกตรวจพบ จะมีเพียงผู้ที่พิมพ์เร็วเท่านั้นที่ทำให้เกิดสภาวะการแข่งขัน (race condition) และกรณีดังกล่าวก็นับว่าเกิดขึ้นได้น้อย
การแก้ไขแบบครึ่งๆ กลางๆ ที่ไม่ได้ผล
การตอบสนองครั้งแรกคือการเพิ่มการหน่วงเวลาสั้นๆ หลังจากข้อความมาถึง โดยหวังว่าจะช่วย "debounce" การป้อนข้อมูลที่รวดเร็ว วิธีนี้ช่วยได้เมื่อมีข้อความสองข้อความส่งมาติดๆ กัน แต่จะใช้ไม่ได้ผลหากมีข้อความที่สามปรากฏขึ้นในขณะที่ AI ยังคงสร้างข้อความไม่เสร็จ
ปัญหาที่สองเกิดขึ้นเมื่อตัวจับเวลา (timers) และข้อมูลการสนทนาถูกเก็บไว้ในพื้นที่จัดเก็บ (storage bucket) เดียวกัน เมื่อบอทประมวลผลคำขอเสร็จสิ้น มันจะเขียนทับข้อมูลตัวจับเวลา ซึ่งเท่ากับเป็นการลบการนับถอยหลังของตัวเองทิ้ง ระบบจึงสูญเสียการติดตามว่าข้อความใดได้รับการตอบไปแล้วบ้าง เปิดช่องให้เกิดการส่งข้อมูลซ้ำซ้อนมากขึ้น
การสร้างระบบป้องกันที่เชื่อถือได้: ตัวนับเวอร์ชัน, ตัวจับเวลาที่แยกส่วน และการเช่าสิทธิ์ (lease)
ทีมงานได้ออกแบบขั้นตอนการทำงานใหม่โดยยึดหลัก 3 ประการ:
- ตัวนับเวอร์ชัน (Version counter) – ข้อความที่เข้ามาแต่ละข้อความจะเพิ่มค่าตัวนับที่เก็บไว้พร้อมกับการสนทนา ตัวนับนี้จะบอกระบบว่ามีข้อความเข้ามาจำนวนเท่าใดนับจากการตอบกลับครั้งล่าสุด ทำให้ตรวจพบข้อมูลนำเข้าใหม่ได้ง่ายในขณะที่กำลังสร้างคำตอบ
- ช่วงเวลา debounce เฉพาะ (Dedicated debounce window) – ตัวจับเวลาจะถูกเก็บไว้ในพื้นที่จัดเก็บแยกต่างหาก เพื่อไม่ให้ปะปนกับข้อมูลการสนทนา (payloads) และมีการกำหนดเพดานสูงสุดของระยะเวลา debounce เพื่อป้องกันไม่ให้ผู้ใช้ทำให้บอทหยุดชะงักไปตลอดกาล
- การเช่าสิทธิ์เซสชัน (Session lease) – การล็อกแบบเดิมถูกแทนที่ด้วยการเช่าสิทธิ์ (lease) ที่มีตราประทับเวลาหมดอายุที่ชัดเจน การเช่าสิทธิ์จะทำผ่านการดำเนินการแบบ compare-and-swap (CAS): กระบวนการจะอ่านค่าการเช่าสิทธิ์ปัจจุบัน และจะเขียนค่าใหม่ลงไปก็ต่อเมื่อค่าเดิมตรงกันเท่านั้น ซึ่งจะทำให้ได้รับสิทธิ์ขาดในการจัดการการสนทนานั้น หากกระบวนการเกิดล่ม (crash) การเช่าสิทธิ์จะหมดอายุโดยอัตโนมัติ เพื่อคืนสิทธิ์การสนทนาให้กับตัวจัดการถัดไป
ขั้นตอนการทำงานของ pipeline ใหม่
- เมื่อข้อความมาถึง – ระบบจะเพิ่มค่าตัวนับเวอร์ชันและ (ตั้งค่าใหม่) ตัวจับเวลา debounce จากนั้นจะส่งข้อมูลกลับไปยังไคลเอนต์ทันทีโดยยังไม่เริ่มการทำงานของ AI
- เมื่อตัวจับเวลาหมดเวลา – ตัวจัดการตัวจับเวลาจะพยายามขอเช่าสิทธิ์ (lease) หากการทำ CAS สำเร็จ ตัวจัดการจะดำเนินการต่อ มิฉะนั้นจะหยุดรอ (back off) เนื่องจากทราบว่ามีกระบวนการอื่นเป็นเจ้าของการสนทนานั้นอยู่แล้ว
- ตรวจสอบข้อมูลนำเข้าใหม่ – ตัวจัดการจะเปรียบเทียบตัวนับเวอร์ชันปัจจุบันกับค่าที่บันทึกไว้เมื่อเริ่มทำงานตัวจับเวลา หากตัวนับมีการเปลี่ยนแปลง จะทำการรวมข้อความที่ค้างอยู่ทั้งหมดเข้าเป็น prompt เดียว
- สร้างคำตอบ – โมเดล AI จะทำงานเพียงครั้งเดียว เพื่อสร้างคำตอบเดียวที่ครอบคลุมข้อมูลนำเข้าล่าสุดทั้งหมดของผู้ใช้
- การตรวจสอบความถูกต้องขั้นสุดท้าย – ก่อนที่จะส่งคำตอบออกไป ตัวจัดการจะอ่านค่าตัวนับเวอร์ชันอีกครั้ง หากมีข้อความใหม่เข้ามาในระหว่างการสร้างคำตอบ คำตอบนั้นจะถูกทิ้งไปและกระบวนการจะเริ่มนับเวลาตัวจับเวลาใหม่ เพื่อให้มั่นใจว่าจะไม่มีคำตอบที่ล้าสมัยส่งไปถึงผู้ใช้
แนวทางนี้ช่วยกำจัดการส่งคำตอบซ้ำ จำกัดเวลาที่การสนทนาจะถูกหน่วงไว้ และสามารถกู้คืนระบบได้โดยอัตโนมัติหากกระบวนการล่ม เนื่องจากสิทธิ์การเช่า (lease) จะหมดอายุไปเอง
บทเรียนที่ได้รับ
การล็อกที่หายไปก่อนที่ส่วนวิกฤต (critical section) จะเริ่มทำงานนั้นไม่สามารถให้การป้องกันใดๆ ได้เลย การแทนที่การล็อกฐานข้อมูลที่เกิดขึ้นเพียงชั่วคราวด้วยการเช่าสิทธิ์ (lease) ที่มีกำหนดเวลาหมดอายุที่ชัดเจน และการแยกตัวจับเวลาออกจากข้อมูลการสนทนา ทำให้ตอนนี้บอทสามารถรับประกันการส่งคำตอบที่ถูกต้องและเป็นปัจจุบันได้เพียงครั้งเดียว แม้ว่าผู้ใช้จะพิมพ์ด้วยความเร็วแสงก็ตาม เหตุการณ์นี้ตอกย้ำบทเรียนที่เหนือกาลเวลาว่า: ระบบป้องกันการทำงานพร้อมกัน (concurrency safeguards) จะต้องมีอายุการใช้งานที่ยาวนานกว่างานที่พวกมันกำลังปกป้องอยู่ มิฉะนั้นพวกมันจะกลายเป็นเพียงปราการที่มองไม่เห็นซึ่งปล่อยให้บั๊กหลุดรอดผ่านไปได้
