
การหลุดของการเชื่อมต่อครั้งเดียวไม่ควรทำให้ทั้งแอปพัง — แต่ของเราพัง
เมื่อแบ็กเอนด์ของเราหลุดออฟไลน์กลางการประชุม มันไม่ได้แค่หยุดการถอดเสียง — มันทำให้ทั้งแอปพัง สาเหตุคือเหตุการณ์ที่ไม่ถูกจัดการเพียงตัวเดียว และการแก้ไขมีแค่สิบบรรทัด นี่คือชุดรีลีสที่ทำให้ GeekBye ยังคงล็อกอินอยู่และยังคงเชื่อมต่ออยู่ ผ่านสิ่งที่เคยฆ่ามันมาก่อน
บั๊กบางตัวขัดจังหวะฟีเจอร์หนึ่ง ตัวที่แย่ที่สุดล้มทุกอย่างรอบ ๆ มันลง ในบิลด์ยุคแรกของ GeekBye ถ้าแบ็กเอนด์ของเราหลุดออฟไลน์ขณะที่คุณอยู่กลางการประชุม การถอดเสียงไม่ได้แค่หยุด — ทั้งแอปพัง การเชื่อมต่อหลุดครั้งเดียว แล้วทั้งหน้าต่างก็หายไป
ชุดรีลีสที่แก้เรื่องนี้ — v1.6.8 ถึง v1.6.11 — เป็นตำราชั้นครูของงานที่ไม่หวือหวาอย่าง "คงล็อกอิน คงการเชื่อมต่อ" การแก้ไขพาดหัวมีแค่สิบบรรทัด นี่คือสิ่งที่สิบบรรทัดนั้นเป็น และทุกอย่างที่ออกมาพร้อมกับมัน
สิบบรรทัดระหว่างการหลุดกับการพัง
การพังอาศัยอยู่ในโค้ดเสียง เมื่อเซสชันถอดเสียงรื้อ WebSocket ของมันลง มันเรียก removeAllListeners() บนพวกมัน — การเก็บกวาดที่สมเหตุสมผล แต่นี่คือรายละเอียดของ Node.js ที่เปลี่ยนการเก็บกวาดให้กลายเป็นหายนะ: เหตุการณ์ error ที่ไม่มีตัวรับฟังไม่ได้ถูกเพิกเฉย มันถูกโยนซ้ำเป็นข้อยกเว้นที่ไม่ถูกจับ
ลองนึกภาพลำดับเหตุการณ์เมื่อ แบ็กเอนด์หลุดออฟไลน์: ซ็อกเก็ตมีเหตุการณ์ error ต่อคิวรอ ที่จะยิงออกมา การเก็บกวาดรัน removeAllListeners() ถอดตัวจัดการที่ควรจะจับมันไว้ออกไป อีกชั่วอึดใจ error ที่ต่อคิวอยู่ก็ยิง — เข้าสู่ความว่างเปล่า Node เห็นเหตุการณ์ error ที่ไม่มีใครฟัง และทำสิ่งเดียวที่สัญญาของมันอนุญาต: มันโยนออกมาเป็นข้อยกเว้นที่ไม่ถูกจับ ซึ่งทำให้โปรเซสพัง แบ็กเอนด์ของผู้ใช้สะอึกไปสองวินาที แล้วแอปของพวกเขาก็หายวับ
การแก้ไข (v1.6.8) แทบจะน่าผิดหวังในความเรียบง่าย ทันทีหลังจากถอดตัวรับฟังออก ให้ผูก ตัวจัดการ error ที่ตั้งใจไม่ทำอะไร กลับเข้าไป:
this.micWebSocket.removeAllListeners()
this.micWebSocket.on('error', () => {}) // absorb late error events
ฟังก์ชันว่างเปล่านั้นคือประเด็นทั้งหมด มันให้ที่ลงเอยแก่ error ที่ค้างอยู่ — จุดรับ — เพื่อให้ Node มีตัวรับฟังและไม่มีวันโยนซ้ำ คอมมิตเดียวกันยังเก็บกวาดซ็อกเก็ตที่เปิดค้างครึ่งเดียวเมื่อความพยายามเชื่อมต่อล้มเหลวกลางคัน เพื่อไม่ให้การเชื่อมต่อที่พลาดทิ้งซ็อกเก็ตที่ยังมีชีวิตห้อยอยู่พร้อม error รอยิง ความคิดเห็นในคอมมิตพูดตรง ๆ ว่า: "ถ้าไม่มีสิ่งนี้ error ที่ค้างอยู่จะกลายเป็นข้อยกเว้นที่ไม่ถูกจับและทำให้แอปพัง" สิบบรรทัดเปลี่ยนการพังทั้งระบบให้กลายเป็นการหลุดที่เงียบและกู้คืนได้
บทเรียนมีค่ามากกว่าตัวการแก้ไข: ทุกครั้งที่คุณเรียก removeAllListeners() บนซ็อกเก็ตหรือสตรีมที่คุณอาจยังรื้อไม่เสร็จ ให้ผูกจุดรับ error ที่ไม่ทำอะไรกลับเข้าไปก่อน เหตุการณ์ error ที่ไม่ถูกจัดการคือการพัง ไม่ใช่บรรทัดล็อก
การคงล็อกอิน: รีเฟรชล่วงหน้า และรีเฟรชตอนเด้งกลับ
ครึ่งที่ว่า "คงการเชื่อมต่อ" มีฝาแฝดที่ว่า "คงล็อกอิน" ก่อนชุดนี้ โทเคนยืนยันตัวตนของ GeekBye ก็แค่หมดอายุไปเฉย ๆ — สักพักคุณก็ถูกโยนกลับไปหน้าล็อกอินอย่างไม่ไยดี กลางเวิร์กโฟลว์ โดยไม่มีเหตุผลที่คุณมองเห็นได้
v1.6.8 แก้เรื่องนี้จากทั้งสองทิศทาง:
- เชิงรุก: ตัวจับเวลารีเฟรชโทเคน ก่อน ที่มันจะหมดอายุ กำหนดไว้ที่เวลาหมดอายุลบด้วยบัฟเฟอร์อันชาญฉลาด (เศษเสี้ยวของอายุโทเคน มีขั้นต่ำและเพดานกำหนดไว้) คุณถูกรีเฟรชก่อนที่คุณจะทันสังเกตด้วยซ้ำ
- เชิงรับ: ถ้าคำขอกลับมาเป็น
401อยู่ดี — นาฬิกาคลาดเคลื่อน ตัวจับเวลาที่พลาดไป แล็ปท็อปที่เพิ่งตื่น — ไคลเอนต์รีเฟรชหนึ่งครั้งและลองส่งคำขอเดิมใหม่เพียงครั้งเดียวเป๊ะ แฟล็กป้องกันเพียงตัวเดียวจำกัดการลองใหม่นั้น เพื่อให้ 401 ที่ดื้อรั้นไม่มีวันหมุนเป็นลูปไม่รู้จบ
ส่วนที่ละเอียดอ่อนแต่สำคัญยิ่ง: เส้นทางทั้งสองอยู่หลัง มิวเท็กซ์ ถ้าสิบคำขอชน 401 ในเสี้ยววินาทีเดียวกัน พวกมันไม่ยิงสิบการรีเฟรช — พวกมันทั้งหมดรอการรีเฟรช ครั้งเดียว ที่กำลังดำเนินอยู่แล้วจึงลองใหม่ และเมื่อการรีเฟรชกู้ไม่ได้จริง ๆ แอปทำให้เซสชันหมดอายุ อย่างนุ่มนวล — ทอสต์ชัดเจนสิบวินาทีว่า "เซสชันของคุณหมดอายุแล้ว โปรดล็อกอินอีกครั้ง" — แทนที่จะเป็นการเตะกลับไปหน้าล็อกอินอย่างเงียบ ๆ
การเชื่อมต่อใหม่: เพิ่มที่ไคลเอนต์ แล้วย้ายไปแบ็กเอนด์
v1.6.9 ทำให้การเชื่อมต่อถอดเสียงที่หลุดกลายเป็นสิ่งที่ กู้คืนได้ แทนที่จะเป็นความตาย เมื่อแบ็กเอนด์รายงานว่าการเชื่อมต่อเสียงต้นทางหลุด ไคลเอนต์เลิกแสดง error ร้ายแรงและเชื่อมต่อใหม่แทน — ด้วยประสบการณ์ที่จงใจให้สงบ: ทอสต์สีเหลืองอำพันอันเดียวว่า "กำลังเชื่อมต่อใหม่…" (ไม่ใช่อันละหนึ่งต่อความพยายาม) ทอสต์สีเขียว "เชื่อมต่อใหม่แล้ว" เมื่อมันกลับมา และ error ปลายทางว่า "โปรดเริ่มใหม่" จะปรากฏ ก็ต่อเมื่อ ทุกความพยายามถูกใช้จนหมด แล้วเท่านั้น
แล้วสิ่งที่ผมชอบเกี่ยวกับชุดนี้: อีกหนึ่งรีลีสถัดมา ใน v1.6.10 เราลบตรรกะการเชื่อมต่อใหม่ฝั่งไคลเอนต์นั้นทิ้ง — ราว 113 บรรทัด — และย้ายการเชื่อมต่อใหม่ไปที่แบ็กเอนด์ ซึ่งตอนนี้ส่งข้อความสถานะ "กำลังเชื่อมต่อใหม่" อย่างเงียบ ๆ ให้ไคลเอนต์แค่สะท้อนออกมา เราสร้างการกู้คืนที่ไคลเอนต์ ตัดสินใจว่าแบ็กเอนด์คือเจ้าของที่ถูกต้อง แล้วย้ายมัน นี่ไม่ใช่การโลเล นี่คือวงจรชีวิตอันซื่อตรงของปัญหาที่ยาก ตรรกะความน่าเชื่อถือที่แยกกันอยู่คนละปลายของการเชื่อมต่อเป็นกลิ่นที่ไม่ดี — เลือกเจ้าของสักหนึ่ง (ท้ายที่สุดแบ็กเอนด์กลายเป็นเจ้าของที่แน่นอนของเรื่องนี้ ซึ่งคือเรื่องราวการเชื่อมต่อใหม่ที่เล่าไว้ใน ทำไมโปรแกรมจดโน้ต AI ของคุณถึงหยุดเมื่อ Wi-Fi แย่)
รีลีสเดียวกันทำให้ขีดจำกัดเวลาบันทึกเสียงมีความเป็นมนุษย์: แทนที่จะเป็น error ดิบ ๆ กลับเป็นทอสต์เตือนที่ยอมให้บันทึกต่อไปได้ และข้อความชัดเจนว่า "ถึงขีดจำกัดการบันทึกแล้ว" เมื่อมันหยุดจริง — โผล่ประโยคที่เป็นมิตรออกมา ถอดคำนำหน้าภายในทิ้งไป
การลองใหม่หนึ่งเดียวที่ปกครองทั้งหมด
พอถึง v1.6.11 ตรรกะลองใหม่พร้อมแบ็กออฟชุดเดียวกันถูกก็อปวางไปในสามที่ต่างกัน — การยืนยันตัวตน การอัปเดต การรื้อเซสชัน มันจึงถูกรวมเข้าเป็นโมดูลเดียวพร้อมกฎร่วมข้อหนึ่งว่าด้วย อะไรกันแน่ที่คุ้มจะลองใหม่: ความล้มเหลวของเครือข่ายชั่วคราว (การเชื่อมต่อรีเซ็ต ไทม์เอาต์ DNS สะอึก ซ็อกเก็ตหลุด) จะถูกลองใหม่ด้วยแบ็กออฟแบบเอ็กซ์โพเนนเชียล ส่วน error ของเซิร์ฟเวอร์จริง ๆ จะล้มเหลวทันที เพราะการลองใหม่กับ 4xx ของแท้ก็แค่เปลืองเวลาของผู้ใช้ ตัวแยกประเภทหนึ่งตัว พรีเซ็ตที่มีชื่อไม่กี่ตัว และผู้เรียกสามรายย้ายมาใช้มัน
สิ่งที่ชุดนี้สอน
- เหตุการณ์
errorที่ไม่ถูกจัดการคือการพัง ไม่ใช่บรรทัดล็อก Node โยนเหตุการณ์ error ที่ไม่มีตัวรับฟังซ้ำ ถ้าคุณถอดตัวรับฟังบนซ็อกเก็ตที่คุณอาจยังรื้อไม่เสร็จ คุณต้องผูกจุดรับ error ที่ไม่ทำอะไรกลับเข้าไป — ไม่อย่างนั้นการหลุดครั้งเดียวจะพาทั้งแอปไปด้วย - รีเฟรชข้อมูลรับรองทั้งก่อนหมดอายุ และ เชิงรับเมื่อเจอ 401 — พร้อมมิวเท็กซ์ทั้งคู่ เชิงรุกทำให้คุณคงล็อกอิน เชิงรับจับกรณีขอบ ๆ มิวเท็กซ์หมายความว่าผู้เรียกพร้อมกัน N รายก่อให้เกิดการรีเฟรชเพียงครั้งเดียวเป๊ะ และเพดานการลองใหม่หมายความว่าโทเคนเสียจะวนไปตลอดกาลไม่ได้
- ลองใหม่เฉพาะกับ error ชั่วคราว และรวมศูนย์ตัวแยกประเภท "อันนี้คุ้มจะลองใหม่ไหม?" เป็นการตัดสินใจหนึ่งเดียวที่ควรอยู่ที่เดียว การลองใหม่กับ error ของจริงก็แค่การล้มเหลวที่ช้ากว่า
- ตัดสินใจว่าใครเป็นเจ้าของการเชื่อมต่อใหม่ เราเพิ่มมันที่ไคลเอนต์ แล้วย้ายไปแบ็กเอนด์ ตรรกะการกู้คืนที่อยู่ทั้งสองปลายคือเครื่องผลิตบั๊ก ให้บ้านเดียวแก่มัน
นี่คือบทที่สามของเรื่องราวความน่าเชื่อถือที่กลายมาเป็น GeekBye v2 สำหรับบทก่อนหน้า ดู เราลบโค้ดเสียง 5,000 บรรทัด แล้วการถอดเสียงเริ่มโผล่มาสองครั้ง (v1.6.0); สำหรับที่ที่การเชื่อมต่อใหม่ลงเอยในที่สุด ดู ทำไมโปรแกรมจดโน้ต AI ของคุณถึงหยุดเมื่อ Wi-Fi แย่; และสำหรับเส้นโค้งทั้งเส้น ดู กายวิภาคของการส่งมอบซอฟต์แวร์สู่ความสมบูรณ์แบบ