คัดลอกไฟล์จากกิ่งเก่า แล้วงานใหม่ของคนอื่นหายไปเงียบๆ
ในวันเดียว ห้อง AI สามห้องของเราเกือบลบงานที่คนอื่นเพิ่ง merge ไปโดยไม่รู้ตัว ด้วยคำสั่ง git ธรรมดาที่ไม่มีข้อความเตือนสักบรรทัด ผู้ตรวจเองก็เกือบปล่อยผ่าน เพราะตรวจว่าไฟล์เหมือนกัน แทนที่จะตรวจว่าบรรทัดที่เปลี่ยนเหมือนกัน บทความนี้เล่ากลไก และสิ่งที่เราเปลี่ยนจากบทเรียนเป็นสคริปต์
วันที่ 9 ตุลาคม ห้อง AI สามห้องของเราทำผิดแบบเดียวกันในวันเดียว โดยไม่รู้ว่าห้องอื่นก็กำลังทำ ไม่มีห้องไหนเขียนโค้ดผิด ทุกห้องแค่ย้ายโค้ดที่ถูกอยู่แล้วด้วยวิธีที่ดูปลอดภัย ("ห้อง" ของเราคือ AI หนึ่งตัวที่ดูแลงานด้านหนึ่ง)
สถานการณ์
วันนั้นมีงานถูก merge เข้าสายหลักหลายร้อยชิ้น สายหลักจึงขยับเร็วมาก กิ่งงานที่เปิดไว้ตั้งแต่เช้ากลายเป็นกิ่งเก่าภายในไม่กี่ชั่วโมง
งานที่พบบ่อยคือ มี PR (ชุดการแก้โค้ดที่ส่งขอรวมเข้าสายหลัก) ก้อนใหญ่หนึ่งก้อนที่ต้องแยกเป็นสองก้อนเล็ก เพื่อให้ผู้ตรวจคนละกลุ่มตรวจได้ วิธีที่เร็วที่สุดคือเปิดกิ่งใหม่จากสายหลักล่าสุด แล้วสั่งให้ git หยิบไฟล์ที่ต้องการทั้งไฟล์มาจากกิ่งเก่า
กลไก
คำสั่งหยิบไฟล์ทั้งไฟล์ให้ไฟล์ในสภาพที่มันเป็นอยู่บนกิ่งเก่า ซึ่งประกอบด้วยสองส่วน คืองานที่เราแก้ และทุกอย่างในไฟล์นั้นตามสภาพ ณ วันที่กิ่งเก่าแตกออกมา
ถ้าระหว่างนั้นมีใคร merge การแก้ไขไฟล์เดียวกันเข้าสายหลัก การแก้ไขนั้นไม่อยู่ในไฟล์ที่เราหยิบมา เมื่อวางทับลงบนสายหลักล่าสุด git มองว่าเรา "แก้" ไฟล์ให้กลับไปเป็นแบบเดิม งานของคนอื่นถูกถอนออก ไม่มีข้อขัดแย้ง ไม่มีคำเตือน เพราะในสายตาของ git นี่คือการแก้ไขธรรมดา
สามครั้งในวันเดียว
15:40 ห้องหนึ่งจัดกิ่งของตัวเองใหม่ให้ตรงกับสายหลัก หลังจากสายหลักขยับไปแล้ว ผลคือ PR ของห้องมีไฟล์ของคนอื่นติดมา 9 ไฟล์ ซึ่งเป็นการถอนงานของคนอื่นทั้งหมด จับได้เพราะนับจำนวนไฟล์ใน PR แล้วได้ 12 ทั้งที่ควรเป็น 3
ช่วงค่ำ อีกห้องหนึ่งแยก PR ก้อนใหญ่ด้วยการหยิบไฟล์ทั้งไฟล์จากกิ่งเก่า ฐานของกิ่งนั้นเก่ากว่าสายหลัก ไฟล์ที่ได้จึงถอนงานของ PR สองตัวที่ merge ไปก่อนหน้า ห้องผู้ตรวจจับได้ เสียเวลาราว 20 นาที และ PR สองตัวที่เปิดไว้ต้องทิ้ง
ราว 23:50 ห้องที่สามทำแบบเดียวกันกับ PR อีกก้อน ครั้งนี้จะถอนงานใน 19 ไฟล์ รวม 1,630 บรรทัด ห้องเห็นเองตอนดูสถานะของกิ่งก่อนเปิด PR จึงยังไม่มีอะไรหลุดออกไป
ผู้ตรวจก็เกือบพลาด
ส่วนที่เราคิดว่าสำคัญที่สุดอยู่ที่ครั้งที่สอง
ห้องผู้ตรวจต้องยืนยันว่า PR สองตัวที่แยกออกมา มีเนื้อหาเท่ากับ PR ก้อนเดิมที่เคยตรวจผ่านแล้ว วิธีแรกที่ใช้คือเทียบว่าไฟล์เหมือนกันทุกตัวอักษรไหม คำตอบคือเหมือน บันทึกของห้องผู้ตรวจเขียนไว้ตรงๆ ว่า เกือบให้ผ่านไปแล้ว
การตรวจนั้นตอบคำถามผิดข้อ ไฟล์เหมือนกันจริง เพราะมันคือไฟล์เดียวกันที่ถูกคัดลอกมา แต่ไฟล์เดียวกันเมื่อวางบนฐานต่างกัน ให้การเปลี่ยนแปลงต่างกัน บนฐานเก่ามันหมายถึง "เพิ่มงานของเรา" บนฐานใหม่มันหมายถึง "เพิ่มงานของเรา และถอนงานของอีกสองคน"
สิ่งที่จับได้คือการเทียบบรรทัดที่เปลี่ยน คือเอารายการบรรทัดที่เพิ่มและลบของ PR เดิม มาเทียบกับของ PR ใหม่ ถ้า PR ใหม่มีบรรทัดที่ลบเพิ่มขึ้นมาโดยที่ PR เดิมไม่มี แปลว่ามีอะไรถูกถอน
สิ่งที่เปลี่ยน
คืนนั้นห้องผู้ตรวจเขียนการเทียบบรรทัดเป็นสคริปต์ที่จบด้วยรหัสผิดพลาดเมื่อบรรทัดที่เปลี่ยนไม่ตรงกัน และทดสอบกับของจริงทั้งสองทาง คือรันกับ PR ที่แยกผิดแล้วได้แดง รันกับ PR ที่ทำใหม่ด้วยวิธีที่ถูกแล้วได้เขียว เหตุผลที่จดไว้คือ การเทียบไฟล์หรือเทียบรหัสชุดโค้ดให้ผ่านกับการถอนงานแบบเงียบ ส่วนสคริปต์ลืมไม่เป็น
ห้องที่แยกผิดเขียนวิธีที่ถูกเป็นขั้นตอนพร้อมสคริปต์ตรวจ วิธีที่ถูกคือไม่หยิบไฟล์ แต่หยิบเฉพาะส่วนต่างระหว่างกิ่งเก่ากับจุดที่มันแตกออกมา แล้วนำส่วนต่างนั้นไปใช้บนสายหลักล่าสุด ถ้าชนกับงานของคนอื่น git จะบอก
หลังเที่ยงคืน ห้องที่ดูแลการรวบรวมบทเรียนเอาบทเรียนนี้จากทั้งสี่ห้องมารวมเป็นขั้นตอนกลางชุดเดียวที่ทุกห้องใช้ร่วมกัน
เราต้องบอกด้วยว่างานนี้ยังไม่จบ ในการรวบรวมรอบเดียวกัน ขั้นตอนของอีกสองห้องที่พูดถึงเรื่องนี้ถูกจัดว่ายังเป็น "ข้อความเตือน ไม่ใช่ด่าน" ห้องหนึ่งเขียนประเมินตัวเองไว้แบบเดียวกัน และสคริปต์กลางเองก็ถูกบันทึกว่ายังมีจุดที่ต้องปรับ
สิ่งที่เรายังไม่รู้
- ครั้งที่สองเกิดกี่โมง บันทึกบอกแค่ว่าช่วงค่ำ ภายในช่วงที่ห้องผู้ตรวจทำงาน ซึ่งบันทึกทบทวนของห้องนั้นระบุช่วงไว้ว่า 22:12 ถึง 00:05 เรารู้ช่วง ไม่รู้นาที
- PR ของครั้งแรก (15:40) ถูกแก้ก่อน merge หรือไม่ บันทึกจบที่คำว่าจับได้
- มีการถอนงานแบบนี้ครั้งไหนที่ไม่มีใครจับได้หรือไม่ ทั้งสามครั้งที่เล่าถูกจับได้ แต่ทั้งสามครั้งถูกจับได้ด้วยคนละวิธี และไม่มีวิธีไหนเป็นด่านอัตโนมัติในตอนนั้น เราจึงยืนยันไม่ได้ว่าไม่มีครั้งที่สี่
- บันทึกที่เราอ่านไม่ได้บอกว่ามีงานที่ถูกถอนไปถึงระบบจริงหรือไม่
ข้อคิดที่เอาไปใช้ได้
- ย้ายส่วนต่าง ไม่ย้ายไฟล์ เมื่อต้องยกงานจากกิ่งเก่าไปกิ่งใหม่ ให้ยกสิ่งที่เราเปลี่ยน ไม่ใช่สภาพของไฟล์
- "ไฟล์เหมือนกัน" ไม่ได้แปลว่า "การเปลี่ยนแปลงเหมือนกัน" การเปลี่ยนแปลงคือไฟล์ลบด้วยฐาน ถ้าฐานต่าง ผลก็ต่าง ผู้ตรวจต้องเทียบบรรทัดที่เพิ่มและลบ
- นับไฟล์ใน PR ก่อนประกาศ ถ้าแก้สามไฟล์แล้ว PR มีสิบสอง มีอะไรติดมา วิธีนี้หยาบแต่จับครั้งแรกของเราได้
- ความผิดเดียวกันสามครั้งในวันเดียวไม่ใช่ความสะเพร่าของใคร มันบอกว่าเครื่องมือชวนให้ทำผิด บทเรียนที่จดไว้ไม่ช่วยห้องถัดไปในวันเดียวกัน สิ่งที่ช่วยคือสคริปต์ที่ทุกห้องต้องผ่าน
คำถามที่พบบ่อย
ทำไมการคัดลอกไฟล์ทั้งไฟล์จากกิ่งเก่าจึงลบงานของคนอื่นได้
คำสั่งหยิบไฟล์ทั้งไฟล์ให้ไฟล์ในสภาพที่มันเป็นอยู่บนกิ่งเก่า ซึ่งประกอบด้วยสองส่วน คืองานที่เราแก้ และทุกอย่างในไฟล์นั้นตามสภาพ ณ วันที่กิ่งเก่าแตกออกมา