Workshop Bun 1.4 Cron: นัดงาน 9 โมงกรุงเทพฯ โดยไม่ฝากไว้กับ OS
งาน “ส่งรายงานทุกวันทำงานตอน 9 โมง” ฟังง่ายจน container ย้าย region, timezone กลายเป็น UTC หรือ process สอง replica ตื่นพร้อมกันครับ Cron ที่กระจายอยู่ใน crontab ของเครื่องยังทำให้ code review ไม่เห็นว่า schedule เปลี่ยนเมื่อไร และ local dev จำลองยาก
Bun 1.4 เพิ่ม Bun.cron ให้ประกาศ schedule กับ callback ในโปรแกรมเดียวกัน พร้อม parser สำหรับถามว่าเวลาถัดไปคือเมื่อไร Workshop นี้เริ่มที่ Bun.cron.parse เพราะเป็นส่วนที่ deterministic และทดสอบได้โดยไม่สร้าง job จริงหรือแก้ crontab ของเครื่อง
WHY: Cron Expression ไม่มี Timezone อยู่ในตัว
นิพจน์ 0 9 * * MON-FRI บอกนาที ชั่วโมง และวัน แต่ไม่บอกว่า 9 โมงของเมืองไหน ถ้า runtime ตีความด้วย UTC งานสำหรับกรุงเทพฯ จะมาถึง 16:00 น. เหมือนเขียนเวลานัดบนกระดาษโดยไม่ระบุสนามบิน
timezone ยังมีผลกับประเทศที่เปลี่ยน Daylight Saving Time ด้วย แม้กรุงเทพฯ ไม่มี DST เราก็ควรระบุ Asia/Bangkok ให้ intent อยู่ข้าง code และ test ได้ชัด เมื่อระบบขยายไปเมืองอื่นจะไม่ต้องเดาว่า schedule เดิมตั้งใจอิงที่ใด
HOW: พิสูจน์เวลาถัดไปก่อน Register Job
ตั้งเวลาอ้างอิงเป็นเที่ยงคืน UTC ของวันพุธที่ 2 กันยายน 2026 เวลา 9 โมงกรุงเทพฯ คือ 02:00 UTC ส่วนเวลา 9 โมง UTC คือ 09:00 UTC เรา assert ทั้งสองค่าเพื่อจับการลืม timezone
const schedule = "0 9 * * MON-FRI";
const from = new Date("2026-09-02T00:00:00.000Z");
const bangkok = Bun.cron.parse(schedule, from, {
tz: "Asia/Bangkok",
});
const utc = Bun.cron.parse(schedule, from, { tz: "UTC" });
if (bangkok?.toISOString() !== "2026-09-02T02:00:00.000Z") {
throw new Error(`unexpected Bangkok time: ${bangkok?.toISOString()}`);
}
if (utc?.toISOString() !== "2026-09-02T09:00:00.000Z") {
throw new Error(`unexpected UTC time: ${utc?.toISOString()}`);
}
console.log({ bangkok: bangkok.toISOString(), utc: utc.toISOString() });
ตัวอย่างนี้มีความหมายเพราะตรวจ instant ที่แน่นอน ไม่ใช่เพียงเช็กว่า parser คืน Date ได้ เมื่อนำไปใช้จริงค่อย register callback พร้อมชื่อ job, expression และ timezone ตามเอกสาร Bun
Scheduler ใน Process ยังไม่ใช่ Distributed Lock
ถ้า deploy สาม replica ทั้งสาม process จะมี scheduler ของตัวเองและอาจทำงานพร้อมกัน Bun.cron ไม่ได้รู้ว่าอีกเครื่องกำลังทำอะไร งานที่ต้องเกิดครั้งเดียวทั่วระบบจึงยังต้องมี leader election, database advisory lock, queue ที่ deduplicate หรือ scheduler ภายนอก
ออกแบบ callback ให้ idempotent ด้วย เช่น key ของรายงานเป็นวันที่ธุรกิจและผู้รับ หากถูกเรียกซ้ำให้ upsert หรือเห็นว่าส่งแล้วแล้วจบ ไม่ใช่ส่งอีเมลซ้ำ การป้องกันนี้ช่วยทั้งจากหลาย replica, process restart และการ retry หลัง network timeout
อย่าให้ cron callback ทำงานยาวโดยไม่มี timeout และ observability ควร log ชื่อ job, scheduled time, actual start, duration และ outcome พร้อม metric ความล่าช้า หาก process ปิดก่อนเวลานัด scheduler ใน memory จะไม่ย้อนกลับมาทำงานที่พลาดเอง เว้นแต่เราสร้าง catch-up policy
Checklist ก่อนย้ายจาก OS Cron
- ระบุ IANA timezone ให้ทุก schedule และมี test ด้วยเวลาคงที่
- ตัดสินใจว่า job รันหนึ่งครั้งต่อ process หรือหนึ่งครั้งต่อระบบ
- สร้าง idempotency key และ lock ตามขอบเขตนั้น
- มี timeout, retry policy และสัญญาณเตือนเมื่อเวลาจริงช้ากว่าเวลานัด
- ทดสอบ shutdown/restart ว่างานที่กำลังรันและงานที่พลาดจะถูกจัดการอย่างไร
Recap
Bun.cron ทำให้ schedule อยู่ข้าง code และ review ได้ แต่หัวใจของงานตามเวลายังเป็น timezone, ownership และ idempotency เริ่มด้วย Bun.cron.parse เพื่อพิสูจน์ instant ก่อนเปิด callback จริง
พรุ่งนี้ลองนำ schedule หนึ่งรายการจากเอกสารระบบมาเขียนเป็น test แบบนี้ ถ้าทีมตอบไม่ได้ว่าเวลาใดใน UTC, ใครเป็นเจ้าของงานเมื่อมีหลาย replica และทำซ้ำแล้วเกิดอะไรขึ้น นั่นคือสามคำถามที่ควรตอบก่อน migrate ครับ