執行環境套件管理器打包器測試執行器程式碼檢查格式化指南參考部落格意見回饋

Cron

使用 Bun 排程和解析 cron 任務

Bun 內建了對 cron 的支援——解析表示式,在行程內部按計劃執行回撥,或註冊能夠跨重啟存活的作業系統級任務。

快速開始

在當前行程中按計劃執行回撥:

TypeScript
Bun.cron("0 * * * *", async () => {
  await cleanupTempFiles();
});

解析 cron 表示式以查詢下一個匹配時間:

TypeScript
// 下一個工作日當地時間上午 9:30
const next = Bun.cron.parse("30 9 * * MON-FRI");

註冊一個作業系統級 cron 任務,在指定計劃下執行指令碼:

TypeScript
await Bun.cron("./worker.ts", "30 2 * * MON", "weekly-report");

Bun.cron.parse()

解析 cron 表示式,並傳回系統本地時區中下一個匹配的 Date。

TypeScript
const next = Bun.cron.parse("*/15 * * * *");
console.log(next); // => 下一個一刻鐘邊界

引數

引數型別描述
expressionstring5 欄位 cron 表示式或預定義別名
relativeDateDate | number搜尋的起始點(預設為 Date.now())
options{ tz?: string }用於解釋計劃的 IANA 時區名稱(預設為系統時區)

傳回值

Date | null — 下一個匹配的時間;如果在 8 年內不存在匹配項,則傳回 null(例如 2 月 30 日)。

鏈式呼叫

重複呼叫 parse() 以獲取即將到來的時間序列:

TypeScript
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 分鐘

命名值

月份和星期欄位接受不區分大小寫的名稱:

TypeScript
// 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 / @annually0 0 1 1 *每年一次(1 月 1 日)
@monthly0 0 1 * *每月一次(每月 1 日)
@weekly0 0 * * 0每週一次(週日)
@daily / @midnight0 0 * * *每天一次(午夜)
@hourly0 * * * *每小時一次
TypeScript
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):

TypeScript
// 無論伺服器的時區是什麼,均為 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 標準。

TypeScript
// 在每月 15 日或每週五觸發
Bun.cron.parse("0 0 15 * FRI");

如果只指定其中一個欄位(另一個為 *),Bun 只使用指定的欄位進行匹配。


Bun.cron(schedule, handler) — 行程內

在當前行程內以 cron 計劃執行回撥。

TypeScript
const job = Bun.cron("*/5 * * * *", async () => {
  await syncToDatabase();
});

行程內排程是面向長時間執行的伺服器和工作行程的輕量級選項。它不需要系統 cron 守護行程,在每個平臺上的工作方式都相同,並且呼叫之間共享狀態(資料庫連線池、快取、模組級變數)。

行程內作業系統級
行程退出/重啟後是否保留否是
多次執行之間是否共享狀態是否(每次都是全新行程)
平臺要求無crontab / launchd / Task Scheduler
Windows 表示式限制無48 個觸發器上限
傳回型別CronJobPromise<void>

引數

引數型別描述
schedulestringcron 表示式,或 "@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 退出。設定了監聽器時,任務會繼續執行——不會在第一次失敗後停止。

TypeScript
process.on("unhandledRejection", err => log.error("cron failed:", err));

Bun.cron("* * * * *", async () => {
  await mightThrow(); // 已記錄日誌,並在下一分鐘重試
});

bun --hot

在 bun --hot 模式下,Bun 會在模組圖重新求值之前立即停止所有行程內 cron 任務。隨後,原始碼中仍存在的每個 Bun.cron() 呼叫都會重新註冊。無論是編輯計劃、編輯處理程序,還是完全刪除該行,儲存後都會立即生效,且不會洩漏定時器。

CronJob 控制代碼

TypeScript
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 模組。

TypeScript
await Bun.cron("./worker.ts", "30 2 * * MON", "weekly-report");

引數

引數型別描述
pathstring指令碼路徑(相對於呼叫者解析)
schedulestringCron 表示式或別名
titlestring唯一任務識別符號(字母數字、連字元、下劃線)

使用相同的 title 重新註冊會就地覆蓋現有任務。Bun 會替換舊計劃,而不是建立重複任務。

TypeScript
await Bun.cron("./worker.ts", "0 * * * *", "my-job"); // 每小時
await Bun.cron("./worker.ts", "*/15 * * * *", "my-job"); // 替換為:每 15 分鐘

scheduled() 處理程序

註冊的指令碼必須匯出一個帶有 scheduled() 方法的預設物件,遵循 Cloudflare Workers Cron Triggers API:

worker.ts
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 條目如下所示:

Code
<schedule> '<bun-path>' run --cron-title=<title> --cron-period='<schedule>' '<script-path>'

當 cron 守護行程觸發任務時,Bun 會匯入您的模組並呼叫 scheduled() 處理程序。

檢視已註冊的任務:

shell
crontab -l

日誌: 在 Linux 上,cron 輸出進入系統日誌。使用以下命令檢視:

shell
# 基於 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() 處理程序內新增日誌記錄。

無需程式碼手動解除安裝:

shell
# 編輯您的 crontab 並刪除 "# bun-cron: <title>" 註釋
# 及其下方的命令列
crontab -e

# 或通過過濾一次性刪除所有 bun cron 任務:
crontab -l | grep -v "# bun-cron:" | grep -v "\-\-cron-title=" | crontab -

macOS

Bun 使用 launchd 註冊任務。Bun 會將每個任務安裝為 plist 檔案,路徑為:

Code
~/Library/LaunchAgents/bun.cron.<title>.plist

plist 使用 StartCalendarInterval 定義計劃。支援帶範圍、列表或步長的複雜模式。Bun 會將它們展開為多個 StartCalendarInterval 字典,形成笛卡爾積。

檢視已註冊的任務:

shell
launchctl list | grep bun.cron

日誌: stdout 和 stderr 寫入到:

Code
/tmp/bun.cron.<title>.stdout.log
/tmp/bun.cron.<title>.stderr.log

例如,一個標題為 weekly-report 的任務:

shell
cat /tmp/bun.cron.weekly-report.stdout.log
tail -f /tmp/bun.cron.weekly-report.stderr.log

無需程式碼手動解除安裝:

shell
# 從 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一個列出全部五個工作日的 CalendarTrigger1
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 × 2496
0,30 * 15 * FRIOR 分割使觸發器翻倍:2 × 24 × 296

關鍵因素在於表示式能否使用 Repetition 間隔(單個觸發器),還是必須展開為單獨的 CalendarTrigger 元素。能夠整除 60 的分鐘步長(*/1、*/2、*/3、*/4、*/5、*/6、*/10、*/12、*/15、*/20、*/30)只有在其他四個欄位均未受限制(*)時才會僅使用 Repetition。不能整除 60 的步長(*/7、*/8、*/9、*/11、*/13 等)必須展開;當 24 小時都處於啟用狀態時,觸發器數量會迅速超過 48 個。

要解決此問題,請簡化表示式或限制小時範圍:

TypeScript
// ❌ 在 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 容器

檢視已註冊的任務:

PowerShell
schtasks /query /tn "bun-cron-<title>"

# 列出所有 bun cron 任務
schtasks /query | findstr "bun-cron-"

無需程式碼手動解除安裝:

PowerShell
schtasks /delete /tn "bun-cron-<title>" /f

# 範例:
schtasks /delete /tn "bun-cron-weekly-report" /f

或者開啟任務計劃程式(taskschd.msc),找到名為 bun-cron-<title> 的任務,右鍵單擊並刪除它。


Bun.cron.remove()

通過其標題移除先前註冊的定時任務。適用於所有平臺。

TypeScript
await Bun.cron.remove("weekly-report");

這會撤銷 Bun.cron() 的操作:

平臺remove() 的操作
Linux編輯 crontab 以移除該條目及其標記註釋
macOS執行 launchctl bootout 並刪除 plist 檔案
Windows執行 schtasks /delete 以移除計劃任務

移除一個不存在的任務會無錯誤地完成。