18 ธ.ค. 2568·อ่าน 2 นาที

การกำหนดเวลางานพื้นหลังโดยไม่ต้องปวดหัวกับ cron

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

การกำหนดเวลางานพื้นหลังโดยไม่ต้องปวดหัวกับ cron

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

ง่ายต่อการเริ่มต้น
สร้างบางสิ่งที่ น่าทึ่ง

ทดลองกับ AppMaster ด้วยแผนฟรี
เมื่อคุณพร้อม คุณสามารถเลือกการสมัครที่เหมาะสมได้

เริ่ม