ข้าม​ไป​ยัง​เนื้อหา

Fail Fast

ถ้า​จะ​พัง ให้​พัง​ให้​เร็ว​ที่สุด

หลักการ fail fast เสนอ​ว่า หาก​ข้อ​ผิดพลาด​หรือ​ปัญหา​กำลัง​จะ​เกิด​ขึ้น ทาง​ที่​ดี​ที่สุด​คือ​ตรวจ​จับ​และ​รายงาน​มัน​ให้​เร็ว​ที่สุด​เท่า​ที่​จะ​ทำได้ ไม่ใช่​ปล่อย​ให้​ระบบ​พยายาม​ทำงาน​ต่อ​ไป​ทั้ง​ที่ state ของ​มัน​น่า​สงสัย​อยู่​แล้ว input ที่​ไม่​เหมาะสม​หรือ​ไม่​ถูกต้อง ทั้ง​ต่อ​ระบบ​โดย​รวม​หรือ​ต่อ method แต่ละ​ตัว เป็น​แหล่ง​ปัญหา​ที่​พบ​บ่อย​ที่สุด​ใน​ซอฟต์แวร์

Jim Shore ผู้​เขียน​บทความ “Fail Fast” ใน IEEE Software (column ที่ Martin Fowler เป็น​บรรณาธิการ) นิยาม​มัน​สั้น ๆ ว่า “เขียน​ซอฟต์แวร์​ให้​พัง​ทันที​และ​พัง​อย่าง​เห็น​ได้​ชัด​เมื่อ​เกิด​ข้อ​ผิดพลาด” ตรง​ข้าม​กับ​แนวทาง​ที่​พยายาม “หา​ทาง​เลี่ยง” ปัญหา​แบบ​เงียบ ๆ แล้ว​ปล่อย​ให้​ระบบ​ทำงาน​ต่อ ซึ่ง​ฟัง​ดูเหมือน​จะ​ทำให้​ซอฟต์แวร์​เปราะบาง​กว่า แต่​ใน​ความ​เป็น​จริง​กลับ​ทำให้​มัน​แข็งแรง​กว่า เพราะ bug ถูก​มอง​เห็น​ง่าย​ขึ้น ถูก​แก้ไข​ได้​เร็ว​กว่า​และ​ถูก​กว่า และ bug จำนวน​น้อย​ลง​ที่​จะ​หลุด​รอด​ไป​ถึง production

Wikipedia อธิบาย​ใน​มุม systems design ว่า fail-fast system คือ​ระบบ​ที่ “รายงาน​เงื่อนไข​ที่​น่า​จะ​บ่ง​ชี้​ความ​ล้มเหลว​ออก​มา​ที่ interface ของ​มัน​ทันที” แทนที่​จะ​พยายาม​ทำงาน​ต่อ​ไป​ทั้ง​ที่​กระบวนการ​อาจ​มี​ข้อ​บกพร่อง​อยู่​แล้ว ระบบ​แบบ​นี้​มัก​ตรวจสอบ state ของ​ตัวเอง​ที่​หลาย​จุด​ระหว่าง​การ​ทำงาน เพื่อ​ให้​ตรวจ​พบ​ความ​ล้มเหลว​ได้​แต่​เนิ่น ๆ และ​หน้าที่​ของ module fail-fast ก็​เพียง​แค่​ตรวจ​จับ​ข้อ​ผิดพลาด แล้ว​ส่ง​ต่อ​ให้​ระดับ​ที่​สูง​กว่า​ของ​ระบบ​เป็น​คน​จัดการ ไม่ใช่​พยายาม​ซ่อมแซม​มัน​เอง

แนวคิด​นี้​มี​ราก​มา​ไกล​กว่า​การ​เขียน code เสีย​อีก — คำ​ว่า “fail fast” ปรากฏ​ครั้ง​แรก ๆ ใน​รายงาน​ของ Jim Gray เรื่อง “Why Do Computers Stop and What Can Be Done About It?” (1985) ซึ่ง​พูด​ถึง​ระบบ​ที่​ต้อง fault-tolerant ใน​ระดับ​ฮาร์ดแวร์/OS

บาง​ครั้ง​ปัญหา​ที่​ไม่​ได้​ถูก​จับ​ตั้งแต่​ต้น​จะ​ถูก​บันทึก​ลง​ฐาน​ข้อมูล กลาย​เป็น​ข้อมูล​ที่​เสียหาย​หรือ​ไม่​ถูกต้อง​ซึ่ง​ยาก​จะ​แยกแยะ​และ​แก้ไข​ภายหลัง บาง​ครั้ง​ปัญหา​ก็​ทำให้​ระบบ​ค้าง​ไม่​ตอบ​สนอง กิน​ทรัพยากร​ไป​เรื่อย ๆ และ​อาจ​กีดกัน​ไม่​ให้​ผู้​ใช้​คน​อื่น​เข้าถึง​ระบบ​ได้

หลักการ fail fast ช่วย​ลด​วงจร​ป้อน​กลับ (feedback loop) เมื่อ input ที่​มี​ปัญหา​เข้า​สู่​ระบบ ยิ่ง​ผู้​ใช้​หรือ​ระบบ​อื่น​ได้​รับ​แจ้ง​เร็ว​เท่าไร​ว่า​มี​ปัญหา​ใน​การ​ประมวล​ผล input ก็​ยิ่ง​ดี ระบบ​จะ​ตอบ​สนอง​ดี​ขึ้น​และ​ใช้​ทรัพยากร​น้อย​ลง​ใน​การ​จัดการ input ที่สุดท้าย​จะ​นำ​ไป​สู่​ข้อ​ผิดพลาด​หรือ​ความ​ล้มเหลว​ของ​ระบบ​อยู่ดี

แนว​ปฏิบัติ​อย่าง defensive programming และ pattern อย่าง guard clause มัก​ถูก​นำ​มา​ใช้​เพื่อ​ทำ​ตาม​หลักการ fail fast — assertion และ precondition check คือ​กลไก​หลัก​ที่​ทำให้​มัน​เกิด​ขึ้น​จริง​ใน code

fail fast ไม่​ได้​ใช้​กับ​การ​เขียน code ระดับ​ล่าง​เท่านั้น แต่​ยัง​นำ​ไป​ใช้​กับ​การ​พัฒนา​ผลิตภัณฑ์​และ​การ​ทดลอง​ทางการ​ตลาด​ได้​ด้วย แทนที่​จะ​ทุ่ม​ทรัพยากร​จำนวน​มาก​ให้​กับ​ผลิตภัณฑ์​หรือ​แคมเปญ​ที่​ถูก​กำหนด​ให้​ล้มเหลว​อยู่​แล้ว fail fast ใน​ความหมาย​ทาง​ธุรกิจ​เสนอ​ให้​ทำการ​ทดลอง​ที่​เร็ว​ที่สุด​เท่า​ที่​จะ​เป็น​ไป​ได้ เพื่อ​พิจารณา​ว่า​ผลิตภัณฑ์ แคมเปญ หรือ​ไอเดีย​นั้น​มี​คุณค่า​พอ​หรือ​ไม่ ก่อน​จะ​ทุ่ม​ทรัพยากร​เต็ม​ที่​ลง​ไป — เป็น​แนวคิด​เดียว​กับ fail fast ใน code เพียง​แค่​ขยับ scope จาก​บรรทัด code ไป​เป็น​ทั้ง​โครงการ

ระบบ​ที่​ปล่อย​ให้ error เดินทาง​ไกล​ก่อน​จะ​ถูก​จับ​ได้ มี​ต้นทุน​สูง​กว่า​มาก​ใน​หลาย​มิติ:

  1. ระยะ​ห่าง​ระหว่าง​สาเหตุ​กับ​อาการ — ยิ่ง code ปล่อย​ให้ state ที่​ผิดพลาด​เดินทาง​ไป​ไกล​แค่​ไหน​ก่อน​จะ​พัง exception หรือ stack trace ที่​ได้​ก็​ยิ่ง​ห่าง​จาก​จุด​ที่​ปัญหา​เกิด​ขึ้น​จริง​มาก​ขึ้น​เท่านั้น ทำให้การ debug ยาก​และ​ใช้​เวลา​นาน​ขึ้น​มาก
  2. ป้องกัน​ข้อมูล​เสียหาย — ถ้า method ยัง​คง​ประมวล​ผล​ต่อ​ไป​ทั้ง​ที่​ได้​รับ input ที่​ไม่​ถูกต้อง มัน​อาจ​เขียน​ข้อมูล​ที่​ไม่​สมบูรณ์​หรือ​ผิดพลาด​ลง database ซึ่ง​การ​แก้ไข​ข้อมูล​ย้อนหลัง​นั้น​แพง​กว่า​การ​ป้องกัน​ไม่​ให้​มัน​เกิด​ขึ้น​มาก
  3. ความ​มั่นใจ​ว่า​โปรแกรม​ทำงาน​ถูกต้อง — Enterprise Craftsmanship (Vladimir Khorikov) ชี้​ว่า​เมื่อ​ระบบ fail fast อย่าง​สม่ำเสมอ ถ้า​โปรแกรม​ทำงาน​ได้​จน​จบ​โดย​ไม่​พัง ก็​มี​ความ​น่า​เชื่อถือ​สูง​ว่า​มัน​ทำงาน​ถูกต้อง เพราะ​ไม่มี error ที่​ถูก​กลืน​หาย​ไป​เงียบ ๆ ระหว่าง​ทาง
  4. สวน​ทาง​กับ fail-silently — รูปแบบ​ที่​อันตราย​ที่สุด​คือ catch แบบ​กว้าง ๆ ที่ log แล้ว​ปล่อย​ผ่าน​โดย​ไม่ throw ต่อ ทำให้​โปรแกรม​เดิน​หน้า​ต่อ​ไป​ทั้ง​ที่ state ผิด​ปกติ​แล้ว วิธี​แก้​ง่าย ๆ ตาม​ที่ Khorikov แนะนำ​คือ throw ต่อ​หลัง log เสมอ เว้น​แต่​จะ​มี​เหตุผล​ที่​ชัดเจน​จริง ๆ ว่า​ทำไม operation นั้น​ควร recover ได้
  5. ลด internal software entropy — ตาม​ที่ Wikipedia สรุป​ไว้ code ที่​ออกแบบ​แบบ fail-fast ช่วย​ลด​ความ​ยุ่งเหยิง​ภายใน​และ​ลด​ความ​พยายาม​ใน​การ debug ลง​ใน​ระยะ​ยาว เพราะ bug ถูก​จับ​ตั้งแต่​จุด​กำเนิด​ของ​มัน ไม่ใช่​จุด​ที่​มัน​แสดง​อาการ
  6. มี​ข้อ​ยกเว้น​ที่​ต้อง​พิจารณา — สำหรับ​ระบบ​ที่ stateless เช่น web server แต่ละ request หนึ่ง​ตัว​ที่ fail ไม่​จำเป็น​ต้อง​ทำให้​ทั้ง application หยุด​ทำงาน เพราะ request นั้น​ไม่​ได้​ทิ้ง state ค้าง​ไว้​ใน memory ระยะ​ยาว — fail fast ใน​ระดับ operation เดียว ไม่ใช่​ทั้ง​กระบวนการ

หลักการ​นี้​มัก​ถูก​นำ​ไป​ใช้​ผ่าน guard clause ที่​ตรวจสอบ precondition ตั้งแต่​ต้น method และ​ผ่าน​การ validate state ให้​ครบ​ตั้งแต่​ตอน construct object แทนที่​จะ​ปล่อย​ให้​มี object ที่ initialize ไม่​สมบูรณ์​หลุด​รอด​ออก​ไป

ตัวอย่าง​ด้าน​ล่าง​เปรียบเทียบ method ที่​ปล่อย​ให้ input ที่​ผิดพลาด “เดินทาง​ลึก” เข้าไป​ใน​ระบบ​ก่อน​จะ​พัง กับ method ที่ fail fast ตั้งแต่​ต้น:

// แบบ "ไม่" fail fast — ปล่อยให้ input ที่ผิดพลาดเดินทางลึกเข้าไปในระบบ
public class OrderProcessor
{
public decimal CalculateDiscount(Order order, decimal discountPercentage)
{
// ไม่ตรวจสอบ discountPercentage หรือ order ก่อนใช้งาน
// ถ้า discountPercentage เป็นค่าติดลบหรือเกิน 100
// ผลลัพธ์จะผิดเงียบ ๆ และอาจถูกบันทึกลงฐานข้อมูลไปแล้ว
var total = order.Items.Sum(i => i.Price * i.Quantity);
return total * (discountPercentage / 100m);
// NullReferenceException ที่ order.Items เป็น null จะเกิดขึ้น
// ที่นี่ ห่างไกลจากจุดที่ order ถูกสร้างขึ้นมาแบบไม่สมบูรณ์
}
}
// แบบ fail fast — ตรวจสอบ precondition ทันทีด้วย guard clause
public class OrderProcessor
{
public decimal CalculateDiscount(Order order, decimal discountPercentage)
{
// Guard clause: fail ทันทีพร้อมข้อความที่บอกสาเหตุชัดเจน
if (order is null)
throw new ArgumentNullException(nameof(order));
if (order.Items is null || order.Items.Count == 0)
throw new ArgumentException("Order must contain at least one item.", nameof(order));
if (discountPercentage is < 0 or > 100)
throw new ArgumentOutOfRangeException(
nameof(discountPercentage),
discountPercentage,
"Discount percentage must be between 0 and 100.");
var total = order.Items.Sum(i => i.Price * i.Quantity);
return total * (discountPercentage / 100m);
}
}

หลักการ​เดียวกัน​นี้​ใช้ได้​กับ constructor เช่น​กัน — object ที่ construct เสร็จ​ควร​อยู่​ใน state ที่​ถูกต้อง​เสมอ แทนที่​จะ​ปล่อย​ให้​มัน​ถูก​สร้าง​แบบ “ครึ่ง ๆ กลาง ๆ” แล้วไป​พัง​ทีหลัง​ตอน​ถูก​ใช้งาน​จริง:

public sealed class EmailAddress
{
public string Value { get; }
public EmailAddress(string value)
{
// fail ทันทีที่จุดสร้าง object ไม่ปล่อยให้ EmailAddress
// ที่ไม่ valid หลุดรอดไปให้ code ส่วนอื่นใช้งานต่อ
if (string.IsNullOrWhiteSpace(value) || !value.Contains('@'))
throw new ArgumentException($"'{value}' is not a valid email address.", nameof(value));
Value = value;
}
}

รูปแบบ fail fast อีก​แบบ​ที่​พบ​บ่อย​คือ validate ค่า configuration ทั้งหมด​ตอน application เริ่ม​ทำงาน (startup) แทนที่​จะ​รอ​ให้​แต่ละ code path มา​เจอ​ค่า config ที่​หาย​ไป​เอา​ตอน​ที่​ระบบ deploy ไป​แล้ว​และ​มี​ผู้​ใช้งาน​จริง — ความ​ล้มเหลว​เรื่อง configuration ควร​ทำให้ application start ไม่​ขึ้น​เลย ดี​กว่า​ปล่อย​ให้ start ได้​แล้ว​ค่อย​พัง​กลาง​ดึก​ตอน​มี​คน trigger code path ที่​ต้อง​ใช้ config ตัว​นั้น

  • method รับ parameter เข้า​มา​โดย​ไม่​ตรวจสอบ null, ค่า​ติดลบ, หรือ​ค่า​นอก​ช่วง​ที่​ถูกต้อง​เลย แล้ว​ปล่อย​ให้​มัน​ไหล​ลึก​เข้าไป​ใน​ระบบ
  • ใช้ try/catch ที่ log exception แล้ว​ปล่อย​ผ่าน​โดย​ไม่ throw ต่อ (fail-silently) ทำให้​โปรแกรม​ทำงาน​ต่อ​ทั้ง​ที่ state ผิด​ปกติ​แล้ว
  • object ถูก construct ใน​สถานะ​ที่​ไม่​สมบูรณ์ (เช่น field สำคัญ​ยัง​เป็น null) แล้ว​ค่อย​พัง​ตอน​ถูก​เรียก​ใช้​ทีหลัง ไม่ใช่​พัง​ตอน​สร้าง
  • ค่า configuration ที่​หาย​ไป​หรือ​ผิด​รูปแบบ​ไม่​ถูก​ตรวจ​จับ​ตอน application เริ่ม​ทำงาน แต่​กลับ​ทำให้​ระบบ crash แบบ​ไม่​คาด​คิด​ตอน​มี traffic จริง​เข้า​มา
  • error message ที่​โยน​ออก​มา​ไม่มี​บริบท​เพียงพอ (เช่น “Invalid input” เฉย ๆ) ทำให้​ต้อง​เดา​ว่า​อะไร​ผิดพลาด​จริง ๆ แทนที่​จะ​รู้ทันที​จาก exception message
  • ใช้ return code พิเศษ (เช่น -1, null) แทน exception เพื่อ​สื่อ​ว่า​เกิด​ข้อ​ผิดพลาด แล้ว​ปล่อย​ให้ caller ลืม​เช็ค​ค่า​นั้น ทำให้ error หาย​ไป​เงียบ ๆ
  • code พยายาม “เดา” หรือ “ซ่อมแซม” ค่าที่​ผิดพลาด​ให้​ดู​สม​เหตุ​สม​ผล (เช่น แทน​ค่า default แบบ​เงียบ ๆ) แทนที่​จะ​รายงาน​ว่า​มัน​ผิด​ตั้งแต่​ต้น