Skip to content

Scheduled Jobs (Cron) ​

Cron đang chạy ​

Bảng này là toàn bộ triggers.crons của mọi wrangler.jsonc trong repo; thêm cron mà không thêm dòng ở đây là vi phạm luật 5 bên dưới. (Audit #013 T-3 bắt được trang này khai 1 cron khi worker chạy 4, và cổng freshness không thấy vì chỉ đo ngày commit của chính trang.)

WorkerCron (UTC)Giờ VNJobKhoá / trầnNguồn
nemo12-api*/5 * * * *mỗi 5 phútNhịp AI: xưởng curriculum (runCurriculumAuthoring) → xưởng IELTS (runIeltsAuthoring) → dịch khung b21 (runB21Translation). Một ngân sách lượt gọi model chung cho cả nhịp (shared/aiBudget.ts)withLock("b21-translate", 240 s); trần 30 lượt/nhịp, chưa có trần USD/ngày (Audit #013 P-1)SRC-642, SRC-634
nemo12-api10 * * * *phút 10 mỗi giờNâng candidate sạch lên published (promoteScreenedCandidates, tối đa 4 lô × 500)withLock("promote-candidates", 3000 s)SRC-538
nemo12-api0 19 * * *02:00Thư tổng kết hằng ngày cho phụ huynh (runDailyDigest, WF digest)withLock("parent-digest", 6 h); chống gửi trùng bằng PK (learner_id, digest_date)SRC-452, SRC-529
nemo12-api0 19 * * * (thứ Hai)02:00Báo cáo tuần cho phụ huynh (runWeeklyReports) — chạy kèm nhịp trên, chỉ thứ Hai giờ VNwithLock("parent-weekly", 6 h); dedupe learner:email:tuầnSRC-683
nemo12-api0 12 * * *19:00Lượt quét thư trạng thái (runParentMailPass): nhắc dựng hồ sơ · mốc kỹ năng · nghỉ dài · đếm ngược thi · nhắc buổi học thử ngày maiwithLock("parent-mail-pass", 6 h); mỗi loại thư try/catch riêng; chống trùng bằng email_dispatchesSRC-683
nemo12-api0 12 * * *19:00Lượt quét thư cho IELTS Learner (runIeltsLearnerPass, SRC-737): 8 loại thư gửi thẳng cho người học — xác nhận mục tiêu · xác nhận điểm xuất phát · xong cụm hồ sơ · lộ trình sẵn sàng · qua mốc · kỹ năng lên nửa band · đếm ngược · im lặng đã lâu. Chạy SAU lượt của phụ huynh trong cùng nhánh cronwithLock("ielts-learner-mail", 6 h); mỗi loại try/catch riêng; trần nhóm 2 thư/7 ngày ngoài trần chung 3 thư/ngàySRC-737
nemo12-api0 21 * * *04:00WF-17 Retention Refresh (runRetentionRefresh, ≤200 learner/lượt)withLock("retention-refresh", 6 h)SRC-104
nemo12-foundry*/15 * * * *mỗi 15 phútKéo việc xưởng nội dung theo content_priority (WF-19/WF-20); tắt bằng FOUNDRY_AUTO_DRAIN ≠ "1"5 chốt của startRun + FOUNDRY_DAILY_USD_CAPSRC-632, SRC-636

Khai báo: "triggers": { "crons": ["0 21 * * *", "0 19 * * *", "0 12 * * *", "10 * * * *", "*/5 * * * *"] } trong workers/api/wrangler.jsonc; ["*/15 * * * *"] trong workers/foundry/wrangler.jsonc. Tất cả đi qua scheduled() của worker tương ứng và rẽ nhánh theo controller.cron.

⏰ Cron của Cloudflare chạy theo UTC. Việt Nam là UTC+7 — 0 21 * * * là 04:00 ICT, đúng giờ ít người dùng nhất.

Chi phí đáng nhớ: nhịp */5 là job duy nhất tự gọi model mà không ai bấm nút (288 nhịp/ngày); tháng 8/2026 nó là phần lớn của hoá đơn Workers AI, và các câu SELECT dò việc của nó quét cả bảng là phần lớn của hoá đơn D1 (hoá đơn IN-77746678). Thêm việc vào nhịp này thì phải trả lời trước: hết việc thì nhịp rỗng tốn bao nhiêu dòng đọc, và target hỏng có bị chọn lại mãi không (P-2).


WF-17 — Retention Refresh ​

Vì sao phải có cron cho việc này ​

Đây là job duy nhất trong hệ thống bắt buộc phải chạy khi không ai online, vì lý do bản chất:

Trí nhớ phai theo THỜI GIAN, không theo HÀNH ĐỘNG.

Mọi model khác chỉ đổi khi có việc xảy ra — học sinh nộp bài, phụ huynh khai mục tiêu. Retention thì ngược lại: chính lúc học sinh không làm gì mới là lúc trí nhớ phai đi. Không có cron thì học sinh nghỉ ba tuần sẽ không có bất kỳ tín hiệu nào cho biết em ấy đang quên dần.

Chạy gì ​

runRetentionRefresh(env, { trigger: "cron:0 21 * * *" })
  ├── startWorkflowRun(code: "WF-17")
  ├── chọn tối đa 200 learner (RETENTION_REFRESH_LIMIT)
  │     ORDER BY last_snapshot ASC — learner CHƯA từng có snapshot (NULL) lên đầu
  ├── mỗi learner → runRetentionEngine()
  │     ├── refreshRetentionProjections()   ← chỉ trường phái sinh
  │     └── saveModelVersion(model_kind: "retention")
  │           → mỗi ngày có snapshot "hôm đó trí nhớ em thế nào"
  └── finishWorkflowRun()

Mỗi learner là một step riêng trong run (refresh:{learnerId}), nên một learner lỗi không làm hỏng cả lượt.

Giới hạn 200 learner/lượt ​

Không quét toàn bộ. Learner vượt cap sẽ tự lên đầu hàng đợi hôm sau vì thứ tự là last_snapshot ASC — cơ chế công bằng tự cân bằng, không cần bảng hàng đợi.

Ở quy mô 10.000 learner, cap này cần nâng hoặc chia lượt — theo dõi deferred trong output của run.

An toàn khi chạy trùng ​

refreshRetentionProjections() chỉ ghi trường phái sinh (review_urgency, next_review_*, retention_confidence), không bao giờ đụng anchor (current_retention, stability_days, last_exposure_at).

Vì hàm mũ là memoryless, re-anchor tại now với giá trị đã decay cho ra đúng cùng các mốc tuyệt đối. Chạy 1 lần hay 100 lần trong ngày đều ra cùng một lịch ôn. Bất biến này được khoá bằng test modules/retention/periodic.test.ts.

Chi tiết: retention-model §5.

Chạy tay ​

POST /v1/admin/retention-refresh (role admin) — cùng workflow WF-17, dùng khi cần kiểm chứng ngay thay vì đợi tới 4 giờ sáng.

Quan sát ​

Tín hiệuỞ đâu
RETENTION_REFRESH_DONE + { processed, deferred, changed }Workers logs
RETENTION_REFRESH_FAILEDWorkers logs
Run + từng stepGET /v1/admin/workflow-runs (lọc WF-17)

Việc chưa có cron ​

#JobLịch đề xuấtVì sao cầnChặn bởi
1Nhắc lịch thi — còn ≤14 / ≤3 ngàyHằng ngàyGoal operational chỉ đổi hành vi khi có người mở app; deadline không tự nhắccần hệ thống notification
2Dọn session hết hạnHằng ngàysessions chỉ lọc expires_at lúc đọc — bảng phình vô hạnkhông
3Cảnh báo DLQMỗi 15 phútMessage chết im lặng (queues §4)QG-009
4Freshness tài liệu/nội dungTuầnQG-001/QG-006 đang kiểm thủ côngkhông

Luật khi thêm cron ​

  1. Idempotent. Cron sẽ chạy trùng (retry, deploy chồng). Hai lần phải cho cùng kết quả — WF-17 đạt được bằng tính memoryless, không phải bằng khoá.
  2. Có giới hạn. Không job nào quét toàn bộ learner không cap. Có cap thì phải có cơ chế cho phần bị hoãn được ưu tiên lượt sau.
  3. Ghi run log qua startWorkflowRun() với trigger: "cron:…". Job đêm không có log là job không ai biết đã hỏng.
  4. Bọc ctx.waitUntil() + try/catch — cron nổ không được làm chết worker.
  5. Khai vào bảng trên trong cùng PR. Không có cron ẩn (REQ-DOC-02). Bảng trên phải khớp triggers.crons của mọi wrangler.jsonc — kiểm bằng grep -h '"crons"' workers/*/wrangler.jsonc.
  6. Nói rõ nhịp rỗng tốn gì. Cron chạy mãi kể cả khi hết việc; câu "hết việc thì không tốn gì" chỉ đúng khi câu SELECT dò việc đi qua chỉ mục. Ghi ước lượng dòng đọc/nhịp ngay trong comment handler.
ts
// workers/api/src/index.ts
async scheduled(controller: ScheduledController, env: Env, ctx: ExecutionContext) {
  ctx.waitUntil((async () => {
    try { /* dispatch theo controller.cron */ }
    catch (e) { console.error("…_FAILED", e); }
  })());
}

Trace ​

  • REQ-INT-30 (retention refresh định kỳ — SRC-104), REQ-PAR-11/12 (digest phụ huynh — SRC-452/529), SRC-538 (nâng candidate), SRC-642 (dịch b21), SRC-632/636 (xưởng nội dung), SRC-674 (Audit #013 T-3).
  • Thiết kế: SDD-017 §15, SDD-006.
  • Kiểm chứng: QG-005 (periodic.test.ts), QG-009.

Thư gửi từ các cron này mô tả ở Email.