การกำหนดเวลางานพื้นหลังโดยไม่ต้องปวดหัวกับ cron
เรียนรู้รูปแบบการกำหนดเวลางานพื้นหลังโดยใช้ workflow และ jobs table เพื่อรันการเตือน สรุประหว่างวัน และการล้างข้อมูลอย่างเชื่อถือได้

ทำไม cron ดูเรียบง่ายจนกว่าจะไม่ใช่
Cron ดีตรงวันแรก: เขียนบรรทัด เลือกเวลา แล้วลืมมันไป สำหรับเซิร์ฟเวอร์หนึ่งเครื่องและงานหนึ่งอย่าง มันมักจะใช้ได้
ปัญหาจะปรากฏเมื่อคุณพึ่งพาการกำหนดเวลาเป็นพฤติกรรมของผลิตภัณฑ์จริงๆ: การเตือน ความสรุปประจำวัน งานล้างข้อมูล หรือการซิงก์ เรื่องราวของ “งานที่พลาด” ส่วนใหญ่ไม่ใช่ cron ล้มเหลวโดยตรง แต่เป็นสิ่งรอบ ๆ มัน: เซิร์ฟเวอร์บูตใหม่ การดีพลอยที่เขียนทับ crontab งานที่รันนานกว่าที่คาด หรือตัวนาฬิกา/ข้อแตกต่างโซนเวลา และเมื่อคุณรันหลายอินสแตนซ์ของแอป ก็อาจได้ความล้มเหลวอีกแบบ: งานซ้ำ เพราะสองเครื่องคิดว่าตัวเองต้องรันงานเดียวกัน
การทดสอบเป็นจุดอ่อนอีกจุด บรรทัด cron ไม่ได้ให้วิธีที่สะอาดสำหรับการทดสอบว่า “จะเกิดอะไรขึ้นเวลา 09:00 น. พรุ่งนี้” แบบทำซ้ำได้ ดังนั้นการกำหนดเวลามักกลายเป็นการตรวจด้วยมือ เหตุการณ์ที่น่าประหลาดใจในโปรดักชัน และการตามหาในล็อก
ก่อนเลือกวิธี ให้ชัดเจนว่าคุณกำลังกำหนดเวลาอะไร งานพื้นหลังส่วนใหญ่จะแบ่งได้เป็นกลุ่มเล็ก ๆ:
- การเตือน (ส่งตอนเวลาที่กำหนด เพียงครั้งเดียว)
- สรุปประจำวัน (รวบรวมข้อมูลแล้วส่ง)
- งานล้างข้อมูล (ลบ, เก็บถาวร, หมดอายุ)
- การซิงก์เป็นช่วง (ดึงหรือผลักการอัปเดต)
บางครั้งคุณอาจข้ามการกำหนดเวลาได้เลย หากบางสิ่งสามารถเกิดขึ้นทันทีเมื่อเหตุการณ์เกิดขึ้น (ผู้ใช้สมัคร จ่ายเงินสำเร็จ ตั๋วเปลี่ยนสถานะ) งานที่ขับเคลื่อนด้วยเหตุการณ์มักจะง่ายกว่าและน่าเชื่อถือกว่าการขับเคลื่อนด้วยเวลา
เมื่อคุณต้องการเวลา ความเชื่อถือได้ส่วนใหญ่ขึ้นอยู่กับการมองเห็นและการควบคุม คุณต้องการที่เก็บบันทึกว่าสิ่งใดควรรัน อะไรที่รันแล้ว และอะไรที่ล้มเหลว พร้อมวิธีปลอดภัยในการลองใหม่โดยไม่สร้างซ้ำสอง
รูปแบบพื้นฐาน: scheduler, jobs table, worker
วิธีง่าย ๆ เพื่อลดปัญหา cron คือแยกความรับผิดชอบ:
- scheduler ตัดสินใจว่าสิ่งใดควรรันและเมื่อใด
- worker ทำงานจริง
การแยกบทบาทช่วยในสองทาง คุณเปลี่ยนเวลาได้โดยไม่แตะตรรกะธุรกิจ และเปลี่ยนตรรกะธุรกิจได้โดยไม่ทำให้ตารางเวลาพัง
jobs table กลายเป็นแหล่งความจริง แทนที่จะซ่อนสถานะไว้ในโปรเซสเซิร์ฟเวอร์หรือบรรทัด cron ทุกหน่วยงานของงานเป็นแถว: ทำอะไร ให้ใคร เมื่อใด และสิ่งที่เกิดขึ้นครั้งล่าสุด เมื่อมีปัญหาคุณสามารถตรวจสอบ มาลองใหม่ หรือยกเลิกได้โดยไม่ต้องเดา
โฟลว์ทั่วไปเป็นแบบนี้:
- scheduler สแกนหางานที่ครบกำหนด (เช่น
run_at \u003c= nowและstatus = queued) - มันอ้างสิทธิ์งานเพื่อให้มีเพียง worker เดียวรับงานนั้น
- worker อ่านรายละเอียดงานและทำการกระทำ
- worker บันทึกผลกลับไปที่แถวเดียวกัน
แนวคิดสำคัญคือทำให้งานสามารถกลับมาทำต่อได้ ไม่ใช่เวทมนตร์ หาก worker ล้มครึ่งทาง แถวงานก็ควรบอกว่าทำอะไรแล้วและต้องทำอะไรต่อ
ออกแบบ jobs table ให้ใช้งานได้ยาวนาน
jobs table ควรตอบสองคำถามได้เร็ว: อะไรควรรันต่อไป และครั้งที่แล้วเกิดอะไรขึ้น
เริ่มด้วยชุดฟิลด์เล็กๆ ที่ครอบคลุมตัวตน เวลา และความคืบหน้า:
- id, type: id ที่ไม่ซ้ำกับ type สั้น ๆ เช่น
send_reminderหรือdaily_summary - payload: JSON ที่ตรวจสอบแล้ว มีเฉพาะสิ่งที่ worker ต้องการ (เช่น
user_idไม่ใช่วัตถุผู้ใช้ทั้งหมด) - run_at: เวลาที่งานมีสิทธิ์จะรัน
- status:
queued,running,succeeded,failed,canceled - attempts: เพิ่มขึ้นทุกครั้งที่ลอง
จากนั้นเพิ่มคอลัมน์เชิงปฏิบัติการที่ช่วยให้ concurrency ปลอดภัยและจัดการเหตุการณ์ง่ายขึ้น locked_at, locked_by, และ locked_until ช่วยให้ worker เดียวอ้างสิทธิ์งานเพื่อไม่ให้รันซ้ำ last_error ควรเป็นข้อความสั้น (และอาจมีรหัสข้อผิดพลาด) ไม่ใช่ stack trace ทั้งหมดที่ทำให้แถวใหญ่เกินไป
สุดท้าย เก็บ timestamps ที่ช่วยฝ่ายสนับสนุนและรายงาน: created_at, updated_at, และ finished_at เพื่อให้ตอบคำถามเช่น “วันนี้มีการเตือนกี่รายการที่ล้มเหลว?” โดยไม่ต้องขุดผ่านล็อก
ดัชนีสำคัญเพราะระบบของคุณถามบ่อยว่า “ต่อไปคืออะไร?” สองดัชนีที่มักคุ้มค่า:
(status, run_at)เพื่อดึงงานครบกำหนดอย่างรวดเร็ว(type, status)เพื่อดูหรือหยุดตระกูลงานหนึ่งในช่วงปัญหา
สำหรับ payload ให้ชอบ JSON ขนาดเล็ก โฟกัส และตรวจสอบก่อนใส่ ควรเก็บไอดีและพารามิเตอร์ ไม่ใช่ snapshot ของข้อมูลธุรกิจ ปฏิบัติกับโครง payload เหมือนสัญญา API เพื่อให้งานที่คิวอยู่เก่ากว่ายังคงรันได้หลังจากคุณเปลี่ยนแอป
วงจรชีวิตงาน: สถานะ การล็อก และ idempotency
ตัวรันงานจะเชื่อถือได้เมื่อทุกงานปฏิบัติตามวงจรชีวิตเล็กๆ ที่คาดเดาได้ วงจรนี้เป็นตาข่ายนิรภัยเมื่อสอง worker เริ่มพร้อมกัน เซิร์ฟเวอร์รีสตาร์ทกลางทาง หรือคุณต้องลองใหม่โดยไม่สร้างซ้ำ
เครื่องจักรสถานะง่ายๆ มักเพียงพอ:
- queued: พร้อมรันเมื่อ
run_atถึงหรือเลย - running: ถูกอ้างสิทธิ์โดย worker
- succeeded: เสร็จและไม่ควรรันอีก
- failed: เสร็จด้วยข้อผิดพลาดและต้องการความสนใจ
- canceled: หยุดโดยเจตนา (เช่น ผู้ใช้ยกเลิก)
อ้างสิทธิ์งานโดยไม่ทำซ้ำงาน
เพื่อป้องกันการซ้ำ การอ้างสิทธิ์งานต้องเป็นแบบอะตอม มักใช้ลักษณะล็อกพร้อม timeout (lease): worker อ้างสิทธิ์งานโดยตั้ง status=running และเขียน locked_by พร้อม locked_until หาก worker ล้ม ลีสหมดอายุและ worker อื่นสามารถอ้างสิทธิ์ใหม่ได้
กฎปฏิบัติสำหรับการอ้างสิทธิ์:
- อ้างสิทธิ์เฉพาะงาน queued ที่
run_at \u003c= now - ตั้ง
status,locked_by, และlocked_untilในการอัปเดตเดียวกัน - อ้างสิทธิ์งานที่กำลังรันอีกครั้งเมื่อ
locked_until \u003c nowเท่านั้น - ทำให้ลีสสั้นและต่ออายุถ้างานใช้เวลานาน
Idempotency (นิสัยที่ช่วยชีวิต)
Idempotency หมายความว่า: ถ้างานเดียวกันรันสองครั้ง ผลลัพธ์ยังถูกต้อง
เครื่องมือที่ง่ายที่สุดคือตัวคีย์ที่ไม่ซ้ำกัน ตัวอย่างเช่น สำหรับสรุประหว่างวัน คุณอาจบังคับหนึ่งงานต่อผู้ใช้ต่อวันด้วยคีย์เช่น summary:user123:2026-01-25 หากมีการแทรกซ้ำ มันจะชี้ไปยังงานเดียวกันแทนที่จะสร้างอันใหม่
ทำเครื่องหมายว่าสำเร็จเมื่อเอฟเฟกต์ข้างเคียงเสร็จจริง (อีเมลส่งแล้ว ระเบียนอัปเดตแล้ว) หากลองใหม่ เส้นทาง retry ต้องไม่สร้างอีเมลสองฉบับหรือเขียนซ้ำ
การ retry และการจัดการความล้มเหลวโดยไม่วุ่นวาย
การ retry คือจุดที่ระบบงานจะเชื่อถือได้หรือเปลี่ยนเป็นเสียงรบกวน เป้าหมายชัดเจน: ลองใหม่เมื่อข้อผิดพลาดน่าจะชั่วคราว หยุดเมื่อไม่ใช่
นโยบาย retry เริ่มต้นมักรวม:
- จำนวนครั้งสูงสุด (เช่น พยายามทั้งหมด 5 ครั้ง)
- ยุทธศาสตร์หน่วงเวลา (หน่วงคงที่หรือถดถอยแบบเอ็กซ์โพเนนเชียล)
- เงื่อนไขหยุด (อย่า retry กับข้อผิดพลาดประเภท “ข้อมูลไม่ถูกต้อง”)
- jitter (การกระจายสุ่มขนาดเล็กเพื่อหลีกเลี่ยงการกระโดดพร้อมกัน)
แทนที่จะคิดสถานะใหม่สำหรับการ retry คุณมักใช้ queued ได้: ตั้ง run_at เป็นเวลาเรียกครั้งต่อไปแล้วใส่กลับเข้าไปในคิว นั่นทำให้เครื่องจักรสถานะเล็ก
เมื่องานสามารถก้าวหน้าเป็นบางส่วน ให้ถือว่านั่นปกติ เก็บ checkpoint เพื่อให้การ retry สามารถต่อได้อย่างปลอดภัย บันทึกไว้ใน payload ของงาน (เช่น last_processed_id) หรือในตารางที่เกี่ยวข้อง
ตัวอย่าง: งานสรุประหว่างวันสร้างข้อความสำหรับผู้ใช้ 500 คน ถ้ามันล้มที่ผู้ใช้คนที่ 320 ให้เก็บไอดีผู้ใช้ล่าสุดที่สำเร็จแล้วและลองใหม่จาก 321 ถ้าคุณเก็บระเบียน summary_sent ต่อผู้ใช้ต่อวันอยู่แล้ว การรันซ้ำจะข้ามผู้ใช้ที่ทำเสร็จแล้วได้
การล็อกที่ช่วยแก้ปัญหาจริง
บันทึกพอที่จะดีบักในไม่กี่นาที:
- job id, type, และหมายเลข attempt
- อินพุตสำคัญ (user/team id, ช่วงวันที่)
- เวลาที่ใช้ (started_at, finished_at, next run time)
- สรุปข้อผิดพลาดสั้น ๆ (บวก stack trace ถ้ามี)
- จำนวนผลข้างเคียง (อีเมลส่ง, แถวที่อัปเดต)
ขั้นตอนทีละขั้น: สร้างลูป scheduler ง่าย ๆ
ลูป scheduler เป็นโปรเซสขนาดเล็กที่ตื่นตามจังหวะคงที่ ค้นหางานครบกำหนด แล้วมอบให้ worker เป้าหมายคือความเชื่อถือได้ที่น่าเบื่อ ไม่ใช่ความแม่นยำสมบูรณ์ สำหรับแอปจำนวนมาก “ตื่นทุกนาที” ก็เพียงพอ
เลือกความถี่ตามความสำคัญของเวลาและโหลดฐานข้อมูล ถ้าการเตือนต้องใกล้เวลาจริง ให้รันทุก 30–60 วินาที ถ้าสรุปประจำวันเลื่อนได้บ้าง ให้รันทุก 5 นาทีถูกกว่า
ลูปง่าย ๆ:
- ตื่นและรับเวลาปัจจุบัน (ใช้ UTC)
- เลือกงานครบกำหนดที่
status = 'queued'และrun_at \u003c= now - อ้างสิทธิ์งานอย่างปลอดภัยให้มี worker เพียงคนเดียว
- ส่งงานที่อ้างสิทธิ์ให้ worker แต่ละตัว
- นอนจนกว่าจะถึงติ๊กถัดไป
ขั้นตอนการอ้างสิทธิ์คือจุดที่หลายระบบพัง คุณต้องทำเครื่องหมายงานเป็น running (และเก็บ locked_by และ locked_until) ในทรานแซคชันเดียวกับการเลือก หลายฐานข้อมูลรองรับการอ่านแบบ “skip locked” เพื่อให้ scheduler หลายตัวรันโดยไม่ชนกัน
-- concept example
BEGIN;
SELECT id FROM jobs
WHERE status='queued' AND run_at \u003c= NOW()
ORDER BY run_at
LIMIT 100
FOR UPDATE SKIP LOCKED;
UPDATE jobs
SET status='running', locked_until=NOW() + INTERVAL '5 minutes'
WHERE id IN (...);
COMMIT;
เก็บขนาดแบตช์ให้เล็ก (เช่น 50–200) แบตช์ใหญ่เกินไปจะชะลอฐานข้อมูลและทำให้การชนกันอันเจ็บปวดขึ้น
หาก scheduler พังกลางแบตช์ ลีสจะช่วย งานที่ติดอยู่ใน running จะมีสิทธิ์อีกครั้งหลัง locked_until worker ของคุณควร idempotent เพื่อให้การอ้างสิทธิ์ใหม่ไม่สร้างอีเมลซ้ำหรือเรียกเก็บเงินสองครั้ง
รูปแบบสำหรับการเตือน สรุปประจำวัน และล้างข้อมูล
ทีมส่วนใหญ่ลงท้ายด้วยงานพื้นหลังสามประเภทเดียวกัน: ข้อความที่ต้องส่งตรงเวลา รายงานรันตามตาราง และงานล้างข้อมูลที่รักษาเก็บและประสิทธิภาพให้ดี ตาราง jobs และลูป worker เดียวกันสามารถจัดการทั้งหมดนี้ได้
การเตือน
สำหรับการเตือน ให้เก็บทุกอย่างที่จำเป็นในการส่งข้อความไว้ในแถวงาน: ใคร ช่องทาง (email, SMS, Telegram, in-app) เทมเพลต และเวลาส่งที่แน่นอน Worker ควรสามารถรันงานโดยไม่ต้อง “หา context เพิ่ม”
ถ้ามีการเตือนจำนวนมากครบกำหนดพร้อมกัน ให้เพิ่มการจำกัดอัตรา จำกัดข้อความต่อชั่วโมงต่อช่องทาง และให้งานส่วนเกินรอรันครั้งถัดไป
สรุปประจำวัน
สรุปประจำวันล้มเหลวเมื่อหน้าต่างเวลากำกวม เลือกเวลา cutoff ที่มั่นคง (เช่น 08:00 ตามเวลาท้องถิ่นของผู้ใช้) และกำหนดหน้าต่างอย่างชัดเจน (เช่น “เมื่อวาน 08:00 ถึงวันนี้ 08:00”) เก็บ cutoff และโซนเวลาผู้ใช้กับงานเพื่อให้การรันใหม่ให้ผลเหมือนเดิม
ให้แต่ละงานสรุปมีขนาดเล็ก หากต้องประมวลผลรายการจำนวนมาก แยกเป็นชิ้น ๆ (ตามทีม ตามบัญชี หรือช่วงไอดี) แล้วใส่ follow-up jobs
งานล้างข้อมูล
งานล้างข้อมูลปลอดภัยเมื่อแยก “ลบ” ออกจาก “เก็บถาวร” ตัดสินใจว่าสิ่งใดลบได้ถาวร (โทเค็นชั่วคราว เซสชันหมดอายุ) และอะไรควรเก็บถาวร (ล็อกตรวจสอบ ใบแจ้งหนี้) รันการล้างข้อมูลเป็นแบตช์คงที่เพื่อหลีกเลี่ยงล็อกยาวและการโหลดพุ่ง
เวลาและโซนเวลา: แหล่งบั๊กที่ซ่อนอยู่
ความล้มเหลวหลายอย่างมาจากบั๊กเวลา: การเตือนส่งผิดชั่วโมง สรุปประจำวันพลาดวันจันทร์ หรือการล้างข้อมูลรันสองครั้ง
ค่าเริ่มต้นที่ดีคือเก็บ timestamp การกำหนดเวลาเป็น UTC และเก็บโซนเวลาผู้ใช้แยกต่างหาก run_at ควรเป็นช่วงเวลาเดียวใน UTC เมื่อผู้ใช้บอกว่า “09:00 ตามเวลาฉัน” ให้แปลงเป็น UTC ตอนสร้างตาราง
Daylight saving เป็นจุดที่การตั้งค่าลวกๆ พัง “ทุกวันเวลา 09:00” ไม่เท่ากับ “ทุก 24 ชั่วโมง” ในการเปลี่ยน DST เวลา 09:00 จะแม็ปไปยัง UTC ต่างกัน และบางเวลาท้องถิ่นอาจไม่มีจริง (spring forward) หรือเกิดขึ้นสองครั้ง (fall back) วิธีที่ปลอดภัยคือคำนวณการเกิดครั้งถัดไปในเวลาท้องถิ่นทุกครั้งที่คุณรีสเค줄 แล้วแปลงเป็น UTC ใหม่
สำหรับสรุปประจำวัน ให้ตัดสินใจก่อนว่า “หนึ่งวัน” หมายถึงอะไร วันปฏิทิน (เที่ยงคืนถึงเที่ยงคืนในโซนเวลาผู้ใช้) ตรงกับความคาดหวังของมนุษย์ ช่วง 24 ชั่วโมงท้ายง่ายกว่าแต่เลื่อนและทำให้คนงงได้
ข้อมูลมาช้าคือสิ่งหลีกเลี่ยงไม่ได้: เหตุการณ์มาถึงหลัง retry บ้าง หรือโน้ตถูกเพิ่มหลังเที่ยงคืนไม่กี่นาที ตัดสินใจว่าข้อมูลช้านั้นจะเข้าข่าย “เมื่อวาน” (ด้วยช่วงเวลายกเว้น) หรือ “วันนี้” และรักษากฎนั้นให้สอดคล้อง
บัฟเฟอร์ปฏิบัติได้ช่วยป้องกันการพลาด:
- สแกนหางานครบกำหนดย้อนหลัง 2–5 นาที
- ทำให้งาน idempotent เพื่อให้การรันซ้ำปลอดภัย
- บันทึกช่วงเวลาที่คลุมใน payload เพื่อให้สรุปสม่ำเสมอ
ความผิดพลาดทั่วไปที่ทำให้พลาดหรือรันซ้ำ
ความเจ็บปวดส่วนใหญ่เกิดจากสมมติฐานที่คาดเดาได้
ข้อใหญ่ที่สุดคือสมมติว่า “รันหนึ่งครั้งเท่านั้น” ในระบบจริง worker รีสตาร์ท การเรียกเครือข่ายหมดเวลา และล็อกอาจหลุด คุณมักจะได้การส่งแบบ “อย่างน้อยหนึ่งครั้ง” ซึ่งหมายความว่าการซ้ำเป็นเรื่องปกติ and โค้ดของคุณต้องทนได้
อีกข้อคือทำผลข้างเคียงก่อน (ส่งอีเมล เก็บเงิน) โดยไม่มีการตรวจ dedupe การป้องกันง่ายๆ มักแก้ได้: sent_at timestamp คีย์ไม่ซ้ำ เช่น (user_id, reminder_type, date) หรือโทเค็น dedupe เก็บไว้
การมองเห็นเป็นช่องว่างถัดมา หากคุณตอบไม่ได้ว่า “อะไรติดค้าง มาตั้งแต่เมื่อไร และทำไม” คุณจะเดา ข้อมูลขั้นต่ำที่ต้องเก็บคือ status, attempt count, next scheduled time, last error, และ worker id
ข้อผิดพลาดที่พบบ่อยที่สุด:
- ออกแบบงานเหมือนมันจะรันเพียงครั้งเดียว แล้วแปลกใจเมื่อเกิดการซ้ำ
- ทำผลข้างเคียงโดยไม่ตรวจ dedupe
- รันงานใหญ่ชุดเดียวที่พยายามทำทุกอย่างแล้วหมดเวลาไปกลางทาง
- retry ไม่มีขีดจำกัด
- ขาดการมองเห็นคิวพื้นฐาน (ไม่มีมุมมอง backlog, failures, งานรันยาว)
ตัวอย่างที่เป็นรูปธรรม: งานสรุประหว่างวันวนผ่านผู้ใช้ 50,000 คน แล้วหมดเวลาเมื่อถึงผู้ใช้คนที่ 20,000 เมื่อ retry มันเริ่มใหม่และส่งสรุปซ้ำให้ผู้ใช้ 20,000 คนแรก เว้นแต่ว่าคุณติดตามการเสร็จแต่ละผู้ใช้หรือแยกเป็นงานต่อผู้ใช้
เช็คลิสต์ด่วนสำหรับระบบงานที่เชื่อถือได้
ตัวรันงานถือว่า “เสร็จ” เมื่อคุณวางใจได้ตอนตี 2
ตรวจให้แน่ใจว่าคุณมี:
- การมองเห็นคิว: จำนวน queued vs running vs failed และงาน queued ที่เก่าแก่ที่สุด
- Idempotency เป็นค่าเริ่มต้น: คาดว่าทุกงานอาจรันสองครั้ง ใช้คีย์ไม่ซ้ำหรือเครื่องหมาย “ประมวลผลแล้ว”
- นโยบาย retry แยกตามประเภทงาน: retry, backoff, และเงื่อนไขหยุดที่ชัดเจน
- การเก็บเวลาอย่างสอดคล้อง: เก็บ
run_atเป็น UTC; แปลงเฉพาะตอนรับเข้าและแสดงผล - ล็อกที่กู้คืนได้: ลีสเพื่อไม่ให้ crash ทำให้งานค้างตลอดไป
จำกัด ขนาดแบตช์ (จำนวนงานที่อ้างสิทธิ์พร้อมกัน) และ ความพร้อมทำงานพร้อมกันของ worker (จำนวนที่รันพร้อมกัน) ถ้าไม่จำกัด การพุ่งของงานอาจทำให้ฐานข้อมูลล่มหรือทำให้งานอื่นขาดแคลน
ตัวอย่างที่สมจริง: การเตือนและสรุปสำหรับทีมเล็กๆ
เครื่องมือ SaaS ขนาดเล็กมีบัญชีลูกค้า 30 รายแต่ละบัญชีต้องการสองอย่าง: การเตือนเวลา 09:00 น. สำหรับงานที่เปิดอยู่ และสรุปเวลา 18:00 น. ของสิ่งที่เปลี่ยนแปลงวันนี้ พวกเขายังต้องการการล้างข้อมูลรายสัปดาห์เพื่อไม่ให้ฐานข้อมูลเต็มด้วยล็อกเก่าและโทเค็นหมดอายุ
พวกเขาใช้ jobs table บวก worker ที่โพลหางานครบกำหนด เมื่อมีลูกค้าใหม่ backend จะกำหนดเวลาการเตือนและการสรุปรายวันแรกตามโซนเวลาของลูกค้า
งานถูกสร้างในบางช่วงเวลาทั่วไป: ตอนสมัคร (สร้างตารางการเกิดซ้ำ), ตอนเหตุการณ์บางอย่าง (enqueue การแจ้งเตือนแบบครั้งเดียว), ตอนติ๊กตาราง (insert รอบการทำงานที่กำลังจะมาถึง), และวันบำรุงรักษา (enqueue งานล้างข้อมูล)
วันอังคารหนึ่ง ผู้ให้บริการอีเมลมีปัญหาชั่วคราวเวลา 08:59 น. worker พยายามส่งการเตือน เกิด timeout และตั้งเวลาใหม่ด้วย backoff (เช่น 2 นาที, 10 นาที, 30 นาที) เพิ่ม attempts ทุกครั้ง เนื่องจากแต่ละงานเตือนมีคีย์ idempotency เช่น account_id + date + job_type การ retry จะไม่สร้างซ้ำหากผู้ให้บริการฟื้นตัวกลางทาง
การล้างข้อมูลรันเป็นรายสัปดาห์เป็นแบตช์เล็ก ๆ ดังนั้นมันจึงไม่บล็อกงานอื่น แทนที่จะลบล้านแถวในงานเดียว มันลบสูงสุด N แถวต่อรัน แล้วตั้งเวลาเองจนเสร็จ
เมื่อมีลูกค้าร้องว่า “ฉันไม่ได้รับสรุป” ทีมตรวจสอบ jobs table สำหรับบัญชีและวันนั้น: status ของงาน จำนวน attempt ฟิลด์ล็อกปัจจุบัน และ last error จากผู้ให้บริการ นั่นเปลี่ยนจาก “มันควรส่งแล้ว” เป็น “นี่คือสิ่งที่เกิดขึ้นจริง ๆ”
ขั้นตอนถัดไป: ทำจริง สังเกต แล้วสเกล
เริ่มด้วยงานประเภทเดียวแล้วทำให้เรียบร้อยก่อนเพิ่มงานอื่น งานเตือนชิ้นเดียวเป็นจุดเริ่มต้นที่ดีเพราะมันแตะทุกส่วน: การกำหนดเวลา การอ้างสิทธิ์งาน การส่งข้อความ และการบันทึกผล
เริ่มด้วยเวอร์ชันที่คุณวางใจได้:
- สร้าง jobs table และ worker ตัวเดียวที่ประมวลผลงานประเภทเดียว
- เพิ่มลูป scheduler ที่อ้างสิทธิ์และรันงานครบกำหนด
- เก็บ payload ให้พอรันงานโดยไม่ต้องเดา
- บันทึกทุก attempt และผลลัพธ์เพื่อให้คำถาม “มันรันไหม?” ตอบได้ภายใน 10 วินาที
- เพิ่มทาง rerun แบบแมนนวลสำหรับงานที่ล้มเพื่อการกู้คืนไม่ต้อง deploy
เมื่อมันรัน ให้ทำให้สามารถสังเกตได้สำหรับมนุษย์ มุมมองแอดมินพื้นฐานก็คุ้มค่าอย่างรวดเร็ว: ค้นหางานตาม status กรองตามเวลา ตรวจ payload ยกเลิกงานติดค้าง และ rerun งาน id ที่เฉพาะเจาะจง
ถ้าคุณต้องการสร้าง flow scheduler และ worker แบบนี้ด้วยตรรกะแบ็กเอนด์แบบภาพ AppMaster (appmaster.io) สามารถโมเดล jobs table ใน PostgreSQL และนำลูป claim-process-update ไปทำเป็น Business Process ในขณะที่ยังสร้างโค้ดต้นฉบับจริงสำหรับการดีพลอย


