Laravel 13.31 เพิ่มความสามารถ interruptible jobs ซึ่งช่วยให้ queue workers สามารถรับสัญญาณ SIGTERM ที่ส่งมาในระหว่างการ deploy และปิดตัวลงได้อย่างราบรื่น แทนที่จะต้องยกเลิกงานกลางคัน ทีมที่ต้องรันงานที่ใช้เวลานาน เช่น การประมวลผลข้อมูลหลายหมื่นแถว, การส่งอีเมลจำนวนมาก หรือการสร้างรายงาน จะได้รับประโยชน์อย่างมาก เพราะข้อมูลจะยังคงความถูกต้องแม่นยำ (consistent) เมื่อมีการปล่อยเวอร์ชันใหม่
ทำไมการ deploy ถึงทำให้งานหยุดชะงัก
เมื่อมีการติดตั้งเวอร์ชันใหม่ ตัวจัดการโปรเซส (process managers) อย่าง Supervisor, Docker และ Kubernetes จะสั่งให้ worker ที่ทำงานอยู่หยุดทำงานโดยการส่ง SIGTERM ซึ่งเป็นคำขอให้ยุติการทำงานอย่างสุภาพ โดยปกติแล้ว worker ส่วนใหญ่จะรอให้งานปัจจุบันเสร็จสิ้นก่อนจึงจะปิดตัวลง แต่ปัญหาจะเกิดขึ้นเมื่อตัวจัดการอนุญาตให้มีเวลาเพียงไม่กี่วินาที หากงานนั้นต้องใช้เวลาหลายนาที ตัวจัดการจะยกระดับไปใช้ SIGKILL ซึ่งเป็นการสั่งฆ่าโปรเซสทันที ส่งผลให้งานไม่สามารถไปถึงสถานะที่สมบูรณ์ได้ ทิ้งไว้เพียงข้อมูลที่อัปเดตไม่เสร็จ, ไฟล์ที่ถูกทิ้งไว้ (orphaned files) หรือการทำงานที่ซ้ำซ้อน
คำตอบของ Laravel: การขัดจังหวะแบบร่วมมือกัน (cooperative interruption)
Laravel 13.31 ได้แนะนำ contract ที่ชื่อว่า Interruptible ซึ่งคลาสของ job สามารถนำไป implement เพื่อให้รับรู้ถึงคำขอให้ยุติการทำงานได้ เมื่อ SIGTERM มาถึง Laravel จะเรียกใช้เมธอด interrupted() ของ job นั้นๆ ทั้งนี้ framework จะ ไม่ หยุดการทำงานของ job โดยอัตโนมัติ แต่ตัว job เองจะต้องตรวจสอบ flag ที่ Laravel ตั้งไว้และสั่ง exit ด้วยตัวเอง โมเดลแบบร่วมมือกัน (cooperative model) นี้ช่วยให้นักพัฒนาสามารถจบการทำงานใน loop รอบปัจจุบัน, ทำการ roll back การเปลี่ยนแปลงที่ทำไปบางส่วน หรือบันทึก checkpoint ก่อนที่จะปิดตัวลงได้
วิธีทำให้ job รองรับการขัดจังหวะ
- Implement contract – เพิ่ม
implements Interruptibleในการนิยามคลาส job - ตรวจสอบ flag ภายใน work loop – ใส่การตรวจสอบในแต่ละรอบการทำงาน (iteration) เพื่อดูว่า job ควรหยุดทำงานหรือไม่
- ทำการ cleanup – ในเมธอด
interrupted()ให้บันทึกสถานะใดๆ ที่จะช่วยให้ job สามารถกลับมาทำงานต่อได้ในภายหลัง หรือบันทึก log การขัดจังหวะไว้เพื่อการวิเคราะห์ในภายหลัง
กับดักที่ใหญ่ที่สุด: อย่าใช้ --once
การรัน queue worker ด้วยคำสั่ง php artisan queue:work --once จะเป็นการปิดการจัดการสัญญาณ (signal handling) ทั้งหมด โดย worker จะประมวลผลเพียงงานเดียวแล้วจบการทำงาน แต่จะไม่เคยลงทะเบียน SIGTERM handler ไว้เลย ส่งผลให้ interruptible job ใดๆ ก็ตามจะเพิกเฉยต่อคำขอให้ยุติการทำงานและถูกฆ่าทิ้งกลางคัน รูปแบบนี้มักพบในคอนเทนเนอร์ที่ทำงานผ่าน cron และการ deploy แบบครั้งเดียว (one-shot deployments) วิธีแก้ไขคือให้รันในโหมด daemon (php artisan queue:work) ทุกครั้งที่คุณต้องการใช้งาน interruptible jobs
การเฝ้าติดตามการขัดจังหวะ
Laravel จะส่ง event ออกมาสองอย่างที่คุณสามารถ hook เข้าไปได้:
- WorkerInterrupted – จะถูกส่งออกมาเมื่อมี worker ใดก็ตามได้รับ SIGTERM การบันทึก event นี้จะช่วยให้เห็นภาพรวมว่าการ deploy ส่งผลให้ worker ถูกขัดจังหวะบ่อยแค่ไหน
- JobInterrupted – จะถูกส่งออกมาเฉพาะเมื่อ job ปัจจุบันมีการ implement contract Interruptible เท่านั้น ใช้ event นี้เพื่อสั่งการ cleanup เฉพาะของแต่ละ job เช่น การลบไฟล์ชั่วคราว หรือการอัปเดตตัวระบุ "แถวสุดท้ายที่ประมวลผลแล้ว" (last processed row marker)
การฟัง event เหล่านี้ช่วยให้ทีมสามารถสร้าง dashboard เพื่อแสดงผลกระทบจากการ deploy และตรวจพบ job ที่มักจะถูกตัดจบอยู่บ่อยๆ ได้
การรายงานขนาดคิวได้รับการปรับปรุง
ในเวอร์ชันนี้มีการเพิ่มเมธอด totalSize() ให้กับ queue manager ซึ่งจะคืนค่าจำนวนงานทั้งหมดที่ค้างอยู่ในทุกคิว ค่านี้จะมีความแม่นยำสำหรับ driver ที่ใช้ฐานข้อมูลและ Redis ซึ่งสามารถรายงานจำนวนที่แท้จริงได้ ส่วน driver อย่าง SQS, Sync และ Beanstalkd จะยังคงคืนค่าเป็นศูนย์ เนื่องจากไม่ได้เปิดเผยจำนวนผ่าน API ของ Laravel ทีมที่ใช้ SQS ควรใช้ metrics จาก CloudWatch ในการตรวจสอบความลึกของคิวต่อไป
สิ่งนี้หมายถึงอะไรสำหรับสภาพแวดล้อมที่แตกต่างกัน
- Docker/Kubernetes – ตอนนี้สามารถใช้ช่วงเวลาการปิดตัวอย่างราบรื่น (graceful shutdown period) ได้อย่างมีประสิทธิภาพมากขึ้น โดยควรขยายระยะเวลาการปิดตัวเพื่อให้ job มีเวลาไม่กี่วินาทีในการตรวจพบ flag และปิดตัวลงอย่างสะอาด
- Supervisor – ตั้งค่า
stopwaitsecsให้สูงกว่าเวลาที่คุณคาดว่า job จะทำงานเสร็จหลังจากได้รับ interrupt flag - Legacy jobs – ตรวจสอบงานเก่าๆ ที่ทำงานนานกว่า timeout ของตัวจัดการ และหากเป็นไปได้ ให้ปรับปรุงให้เป็นแบบ interruptible
อีกมุมหนึ่ง: ความซับซ้อนที่เพิ่มขึ้น
ฟีเจอร์นี้ไม่ได้มาแทนที่การตั้งค่า timeout ที่เหมาะสม หาก loop ของ job ไม่เคยตรวจสอบ interrupt flag เลย ตัวจัดการก็จะยังคงส่ง SIGKILL อยู่ดี ทีมงานจึงต้องตรวจสอบเส้นทางการทำงานของโค้ดที่ใช้เวลานาน (long-running code paths) และแทรกการตรวจสอบไว้ในจุดที่เหมาะสม นักพัฒนาบางคนอาจมองว่าการต้องเขียนโค้ดเพิ่ม (boilerplate) เช่น การ implement contract และการเพิ่มการตรวจสอบ flag เป็นเรื่องที่ไม่คุ้มค่าสำหรับ job สั้นๆ ที่ทำงานเสร็จภายในช่วงเวลาการปิดตัวอยู่แล้ว
รายการสิ่งที่ต้องทำ (Action checklist)
- ตรวจสอบ deployment scripts เพื่อหา flag
--onceและเปลี่ยนไปใช้ daemon mode ในกรณีที่มีการใช้ interruptible jobs - ระบุ jobs ที่ใช้เวลาทำงานนานที่สุด (เช่น jobs ที่ต้องจัดการกับข้อมูลหลายหมื่นแถวหรือใช้เวลาทำงานหลายนาที) และเพิ่ม Interruptible contract เข้าไป
- แทรกการตรวจสอบ flag ไว้ภายในแต่ละรอบของการวนลูป และย้ายโค้ดส่วนที่จำเป็นสำหรับการจัดการขั้นตอนสุดท้าย (finalisation code) ไปไว้ใน method
interrupted() - เชื่อมต่อ listeners เข้ากับ
WorkerInterruptedและJobInterruptedเพื่อเก็บข้อมูล metrics ว่าการ deployment ส่งผลกระทบต่อการประมวลผลบ่อยเพียงใด - สำหรับ Redis หรือ database queues ให้ใช้
totalSize()เพื่อตรวจสอบ backlog ส่วนสำหรับ SQS ให้คงการตั้งค่า CloudWatch alarms ไว้ตามเดิม
ให้มองว่าการ shutdown ที่เกิดจากการ deployment เป็นขั้นตอนที่ต้องมีการประสานงานกัน ไม่ใช่การสั่งปิดแบบกะทันหัน (abrupt kill) Laravel 13.31 จะช่วยรักษาความถูกต้องของข้อมูลและช่วยลดปัญหาในการจัดการงานที่ทำค้างไว้ (half-finished jobs) แม้จะต้องแลกมาด้วยความรับผิดชอบในการเขียนโค้ดที่เพิ่มขึ้นเล็กน้อย แต่ทีมที่ต้องพึ่งพาการประมวลผลเบื้องหลัง (background processing) จำนวนมาก จะได้รับสภาพแวดล้อมบน production ที่มีความน่าเชื่อถือมากขึ้น
