Cron
使用 Bun 排程和解析 cron 任務
Bun 內建了對 cron 的支援——解析表示式,在行程內部按計劃執行回撥,或註冊能夠跨重啟存活的作業系統級任務。
快速開始
在當前行程中按計劃執行回撥:
Bun.cron("0 * * * *", async () => {
await cleanupTempFiles();
});
解析 cron 表示式以查詢下一個匹配時間:
// 下一個工作日當地時間上午 9:30
const next = Bun.cron.parse("30 9 * * MON-FRI");
註冊一個作業系統級 cron 任務,在指定計劃下執行指令碼:
await Bun.cron("./worker.ts", "30 2 * * MON", "weekly-report");
Bun.cron.parse()
解析 cron 表示式,並傳回系統本地時區中下一個匹配的 Date。
const next = Bun.cron.parse("*/15 * * * *");
console.log(next); // => 下一個一刻鐘邊界
引數
| 引數 | 型別 | 描述 |
|---|---|---|
expression | string | 5 欄位 cron 表示式或預定義別名 |
relativeDate | Date | number | 搜尋的起始點(預設為 Date.now()) |
options | { tz?: string } | 用於解釋計劃的 IANA 時區名稱(預設為系統時區) |
傳回值
Date | null — 下一個匹配的時間;如果在 8 年內不存在匹配項,則傳回 null(例如 2 月 30 日)。
鏈式呼叫
重複呼叫 parse() 以獲取即將到來的時間序列:
let cursor: Date | number = Date.now();
for (let i = 0; i < 3; i++) {
cursor = Bun.cron.parse("0 * * * *", cursor)!;
console.log(cursor.toLocaleString()); // 接下來三個整點邊界
}
Cron 表示式語法
標準 5 欄位格式:minute hour day-of-month month day-of-week
| 欄位 | 取值 | 特殊字元 |
|---|---|---|
| 分鐘 | 0–59 | * , - / |
| 小時 | 0–23 | * , - / |
| 月中的日期 | 1–31 | * , - / |
| 月份 | 1–12 或 JAN–DEC | * , - / |
| 星期幾 | 0–7 或 SUN–SAT | * , - / |
特殊字元
| 字元 | 說明 | 範例 |
|---|---|---|
* | 所有值 | * * * * * — 每分鐘 |
, | 列表 | 1,15 * * * * — 第 1 和第 15 分鐘 |
- | 範圍 | 9-17 * * * * — 第 9 到 17 分鐘 |
/ | 步長 | */15 * * * * — 每 15 分鐘 |
命名值
月份和星期欄位接受不區分大小寫的名稱:
// 3 字母縮寫
Bun.cron.parse("0 9 * * MON-FRI"); // 工作日
Bun.cron.parse("0 0 1 JAN,JUN *"); // 一月和六月
// 完整名稱
Bun.cron.parse("0 9 * * Monday-Friday");
Bun.cron.parse("0 0 1 January *");
在星期欄位中,0 和 7 都表示星期日。
預定義別名
| 別名 | 等效表示式 | 說明 |
|---|---|---|
@yearly / @annually | 0 0 1 1 * | 每年一次(1 月 1 日) |
@monthly | 0 0 1 * * | 每月一次(每月 1 日) |
@weekly | 0 0 * * 0 | 每週一次(週日) |
@daily / @midnight | 0 0 * * * | 每天一次(午夜) |
@hourly | 0 * * * * | 每小時一次 |
const next = Bun.cron.parse("@daily");
console.log(next); // => 下一個本地午夜
時區
Bun 會按照系統的本地時區解釋計劃,方式與 crontab、launchd 和 Windows Task Scheduler 讀取計劃的方式相同。作業系統級形式和行程內回撥形式會在同一牆上時鐘時間觸發。
如需覆蓋預設時區,請將 IANA 時區名稱作為 { tz } 傳遞給 Bun.cron.parse() 或行程內的 Bun.cron(schedule, handler, options):
// 無論伺服器的時區是什麼,均為 09:00 UTC
Bun.cron.parse("0 9 * * *", Date.now(), { tz: "UTC" });
// 在紐約時間 09:00 觸發
Bun.cron("0 9 * * *", handler, { tz: "America/New_York" });
夏令時轉換:
- 春季調快 — 如果計劃任務落在缺失的小時內,則會在當天觸發,並因時間間隔而順延(例如,
30 2 * * *會在春季調快當天的 3:30 執行)。對於缺失小時內的多分鐘模式(*/15 2 * * *),只有第一個匹配項會觸發。 - 秋季調慢 — 在重複小時內的固定時間計劃(
30 1 * * *)只會觸發一次,即在第一次出現時觸發。分鐘或小時欄位為*的計劃(0 * * * *、* * * * *)會經過兩次出現的時間——每個實際時間分鐘觸發一次,與 Linux 上的 crontab 行為一致。
月份中的日期與星期互動
當同時指定月中的日期和星期幾(兩者都不是 *)時,只要任一條件成立,表示式就會匹配。這遵循 POSIX cron 標準。
// 在每月 15 日或每週五觸發
Bun.cron.parse("0 0 15 * FRI");
如果只指定其中一個欄位(另一個為 *),Bun 只使用指定的欄位進行匹配。
Bun.cron(schedule, handler) — 行程內
在當前行程內以 cron 計劃執行回撥。
const job = Bun.cron("*/5 * * * *", async () => {
await syncToDatabase();
});
行程內排程是面向長時間執行的伺服器和工作行程的輕量級選項。它不需要系統 cron 守護行程,在每個平臺上的工作方式都相同,並且呼叫之間共享狀態(資料庫連線池、快取、模組級變數)。
| 行程內 | 作業系統級 | |
|---|---|---|
| 行程退出/重啟後是否保留 | 否 | 是 |
| 多次執行之間是否共享狀態 | 是 | 否(每次都是全新行程) |
| 平臺要求 | 無 | crontab / launchd / Task Scheduler |
| Windows 表示式限制 | 無 | 48 個觸發器上限 |
| 傳回型別 | CronJob | Promise<void> |
引數
| 引數 | 型別 | 描述 |
|---|---|---|
schedule | string | cron 表示式,或 "@hourly" 這類暱稱。 |
handler | (this: CronJob) => unknown | 每次觸發時呼叫。可以傳回 Promise;Bun 會等它完成後才安排下一次觸發。在 function 回撥內部,this 是 CronJob(因此 this.stop() 可以正常工作)。 |
options | { tz?: string } | 用於解釋計劃的 IANA 時區名稱(預設為系統時區)。 |
同步傳回一個 CronJob。如果表示式無效、時區名稱未知,或者表示式沒有未來的發生時間(例如 "0 0 30 2 *",即 2 月 30 日),則丟擲 TypeError。
不重疊保證
Bun 僅在處理程序及其傳回的 Promise 完成後才計算下一次觸發時間。如果你的處理程序需要 90 秒,而計劃是 * * * * *,那麼第二次觸發會在處理程序完成後的第一個分鐘邊界發生,而不是在第一次觸發後 60 秒發生。呼叫不會堆疊。
錯誤處理
錯誤行為與 setTimeout 保持一致:
- 同步
throw會觸發process.on("uncaughtException")。 - 被拒絕的傳回
Promise會觸發process.on("unhandledRejection")。
沒有監聽器時,行程會以程式碼 1 退出。設定了監聽器時,任務會繼續執行——不會在第一次失敗後停止。
process.on("unhandledRejection", err => log.error("cron failed:", err));
Bun.cron("* * * * *", async () => {
await mightThrow(); // 已記錄日誌,並在下一分鐘重試
});
bun --hot
在 bun --hot 模式下,Bun 會在模組圖重新求值之前立即停止所有行程內 cron 任務。隨後,原始碼中仍存在的每個 Bun.cron() 呼叫都會重新註冊。無論是編輯計劃、編輯處理程序,還是完全刪除該行,儲存後都會立即生效,且不會洩漏定時器。
CronJob 控制代碼
using job = Bun.cron("0 * * * *", () => {});
job.cron; // => "0 * * * *"
job.stop(); // 取消 — handler 將不再觸發
job.unref(); // 即使仍處於計劃狀態,也允許行程退出
job.ref(); // 保持行程執行(預設)
CronJob 是 Disposable —— using job = Bun.cron(...) 會在作用域退出時自動停止。stop()、ref() 和 unref() 都會傳回該 job 以便鏈式呼叫。
假定時器
行程內 cron 支援 jest.useFakeTimers()。setSystemTime()、advanceTimersByTime() 和 runAllTimers() 控制其觸發時間,因此你可以無需等待真即時鍾即可測試計劃回撥。
Bun.cron(path, schedule, title) — 作業系統級別
註冊一個作業系統級別的 cron 任務,按計劃執行 JavaScript/TypeScript 模組。
await Bun.cron("./worker.ts", "30 2 * * MON", "weekly-report");
引數
| 引數 | 型別 | 描述 |
|---|---|---|
path | string | 指令碼路徑(相對於呼叫者解析) |
schedule | string | Cron 表示式或別名 |
title | string | 唯一任務識別符號(字母數字、連字元、下劃線) |
使用相同的 title 重新註冊會就地覆蓋現有任務。Bun 會替換舊計劃,而不是建立重複任務。
await Bun.cron("./worker.ts", "0 * * * *", "my-job"); // 每小時
await Bun.cron("./worker.ts", "*/15 * * * *", "my-job"); // 替換為:每 15 分鐘
scheduled() 處理程序
註冊的指令碼必須匯出一個帶有 scheduled() 方法的預設物件,遵循 Cloudflare Workers Cron Triggers API:
export default {
scheduled(controller: Bun.CronController) {
console.log(controller.cron); // "30 2 * * 1"
console.log(controller.type); // "scheduled"
console.log(controller.scheduledTime); // 1737340201847(呼叫時的 Date.now())
},
};
處理程序可以是 async。Bun 會等待傳回的 promise 完成後再退出。
各平臺的工作原理
Linux
Bun 使用 crontab 註冊任務。Bun 會將每個任務作為一行寫入使用者的 crontab,並在其上方新增 # bun-cron: <title> 標記註釋。
crontab 條目如下所示:
<schedule> '<bun-path>' run --cron-title=<title> --cron-period='<schedule>' '<script-path>'
當 cron 守護行程觸發任務時,Bun 會匯入您的模組並呼叫 scheduled() 處理程序。
檢視已註冊的任務:
crontab -l
日誌: 在 Linux 上,cron 輸出進入系統日誌。使用以下命令檢視:
# 基於 systemd 的系統(Ubuntu、Fedora、Arch 等)
journalctl -u cron # 或在某些發行版上為 crond
journalctl -u cron --since "1 hour ago"
# 基於 syslog 的系統(舊系統)
grep CRON /var/log/syslog
要將 stdout/stderr 捕獲到檔案,可直接在 crontab 條目中重定向輸出,或在 scheduled() 處理程序內新增日誌記錄。
無需程式碼手動解除安裝:
# 編輯您的 crontab 並刪除 "# bun-cron: <title>" 註釋
# 及其下方的命令列
crontab -e
# 或通過過濾一次性刪除所有 bun cron 任務:
crontab -l | grep -v "# bun-cron:" | grep -v "\-\-cron-title=" | crontab -
macOS
Bun 使用 launchd 註冊任務。Bun 會將每個任務安裝為 plist 檔案,路徑為:
~/Library/LaunchAgents/bun.cron.<title>.plist
plist 使用 StartCalendarInterval 定義計劃。支援帶範圍、列表或步長的複雜模式。Bun 會將它們展開為多個 StartCalendarInterval 字典,形成笛卡爾積。
檢視已註冊的任務:
launchctl list | grep bun.cron
日誌: stdout 和 stderr 寫入到:
/tmp/bun.cron.<title>.stdout.log
/tmp/bun.cron.<title>.stderr.log
例如,一個標題為 weekly-report 的任務:
cat /tmp/bun.cron.weekly-report.stdout.log
tail -f /tmp/bun.cron.weekly-report.stderr.log
無需程式碼手動解除安裝:
# 從 launchd 解除安裝任務
launchctl bootout gui/$(id -u)/bun.cron.<title>
# 刪除 plist 檔案
rm ~/Library/LaunchAgents/bun.cron.<title>.plist
# 範例:標題為 "weekly-report" 的任務:
launchctl bootout gui/$(id -u)/bun.cron.weekly-report
rm ~/Library/LaunchAgents/bun.cron.weekly-report.plist
Windows
Bun 使用 Windows Task Scheduler 和基於 XML 的任務定義。Bun 會將每個任務註冊為名為 bun-cron-<title> 的計劃任務,並使用 CalendarTrigger 元素和 Repetition 模式。
大多數 cron 表示式都得到完全支援,包括 @daily、@weekly、@monthly、@yearly、範圍 (1-5)、列表 (1,15)、命名日/月和日期模式。
使用者上下文
Bun 使用 S4U(Service-for-User)登入型別註冊任務,即使使用者未登入,任務也會以註冊使用者的身份執行。Linux crontab 的行為相同。不會儲存密碼。
TCP/IP 網路(fetch()、HTTP、WebSocket、資料庫連線)正常工作。唯一的限制是 S4U 任務無法訪問使用 Windows 身份驗證的網路資源(SMB 檔案共享、對映驅動器、Kerberos/NTLM 服務)。
在某些無頭伺服器和 CI 環境中,無法解析當前使用者的安全識別符號(SID),例如由 NSSM 或類似工具建立的服務帳戶。在這種情況下,Bun.cron() 會失敗並顯示說明問題的錯誤。要解決此問題,可以將 Bun 作為普通使用者帳戶執行,或者使用 schtasks /create /xml <file> /tn <name> /ru SYSTEM /f 手動建立計劃任務。
觸發器限制
在所有平臺上都有效的表示式:
| 模式 | 觸發器策略 | 數量 |
|---|---|---|
*/5 * * * * | 使用 Repetition(PT5M)的單個觸發器 | 1 |
*/15 * * * * | 使用 Repetition(PT15M)的單個觸發器 | 1 |
0 9 * * MON-FRI | 一個列出全部五個工作日的 CalendarTrigger | 1 |
0,30 9-17 * * * | 2 分鐘 × 9 小時 | 18 |
@daily、@weekly、@monthly、@yearly | 單個觸發器 | 1 |
在 Windows 上失敗的表示式(但在 Linux 和 macOS 上有效):
| 模式 | 原因 | 觸發器數量 |
|---|---|---|
*/7 * * * * | 9 個分鐘值 × 24 小時 | 216 |
*/8 * * * * | 8 個分鐘值 × 24 小時 | 192 |
*/9 * * * * | 7 個分鐘值 × 24 小時 | 168 |
*/11 * * * * | 6 個分鐘值 × 24 小時 | 144 |
*/13 * * * * | 5 個分鐘值 × 24 小時 | 120 |
*/15 * * 6 * | 月份限制阻止了 Repetition:4 × 24 | 96 |
0,30 * 15 * FRI | OR 分割使觸發器翻倍:2 × 24 × 2 | 96 |
關鍵因素在於表示式能否使用 Repetition 間隔(單個觸發器),還是必須展開為單獨的 CalendarTrigger 元素。能夠整除 60 的分鐘步長(*/1、*/2、*/3、*/4、*/5、*/6、*/10、*/12、*/15、*/20、*/30)只有在其他四個欄位均未受限制(*)時才會僅使用 Repetition。不能整除 60 的步長(*/7、*/8、*/9、*/11、*/13 等)必須展開;當 24 小時都處於啟用狀態時,觸發器數量會迅速超過 48 個。
要解決此問題,請簡化表示式或限制小時範圍:
// ❌ 在 Windows 上失敗:*/7 配合全天 = 216 個觸發器
await Bun.cron("./job.ts", "*/7 * * * *", "my-job");
// ✅ 有效:限制為特定小時(9 個值 × 5 小時 = 45 個觸發器)
await Bun.cron("./job.ts", "*/7 9-13 * * *", "my-job");
// ✅ 有效:改用 60 的約數(Repetition,1 個觸發器)
await Bun.cron("./job.ts", "*/5 * * * *", "my-job");
Windows 容器
檢視已註冊的任務:
schtasks /query /tn "bun-cron-<title>"
# 列出所有 bun cron 任務
schtasks /query | findstr "bun-cron-"
無需程式碼手動解除安裝:
schtasks /delete /tn "bun-cron-<title>" /f
# 範例:
schtasks /delete /tn "bun-cron-weekly-report" /f
或者開啟任務計劃程式(taskschd.msc),找到名為 bun-cron-<title> 的任務,右鍵單擊並刪除它。
Bun.cron.remove()
通過其標題移除先前註冊的定時任務。適用於所有平臺。
await Bun.cron.remove("weekly-report");
這會撤銷 Bun.cron() 的操作:
| 平臺 | remove() 的操作 |
|---|---|
| Linux | 編輯 crontab 以移除該條目及其標記註釋 |
| macOS | 執行 launchctl bootout 並刪除 plist 檔案 |
| Windows | 執行 schtasks /delete 以移除計劃任務 |
移除一個不存在的任務會無錯誤地完成。