同一條 crontab,測試頁和線上跑在不同日子
接手一台閒置機器時見過這樣一行:0 0 13 * 5,註釋寫著「每月 13 號週五跑一次 demo-report.csv」。把這行丟進解析頁,第一條預測時間是 2024-09-13;可執行日誌裡每個週五都躺著一行記錄。同一個表達式,一邊說 2024 年只有 2 次,一邊真實跑了 62 次——中間差的 60 次不是運氣,是兩套語義。
這一頁的輸入只有一行文字。我把 Crontab解析 範本裡的 nextRun 與 parseField 逐字抽出來,起點釘死在 2024-01-01 00:00:00 逐分鐘往後走,量到兩個語義空洞:日和週同時限定時的交集與聯集,以及短月、永不存在日期下的空返回。
空洞一:五個欄位被 && 串成一串
命中判定寫在範本 240–244 行,條件 f.minute && f.hour && f.day && f.month && f.week 是五段全交集,而真實 vixie-cron 在「日」與「週」都不是 * 時走「或」。給 2024 年算一筆帳:13 號有 12 天,週五有 52 天,既是 13 號又是週五只有 2 天,聯集是 12 + 52 - 2 = 62,交集只剩那 2 天,頁面等於把 62 次壓成 2 次、差 60 次。
描述文字也站在交集這邊:它給出的自然語言是「在每小時的第0分鐘0點每月13日週五執行一次」,讀起來是「且」,與 nextRun 自洽,卻一起偏離 cron 本身。五個欄位各自是什麼、介面怎麼示範,站內 Crontab 表達式看不懂?線上解析並預測下次執行時間 講透了,這裡只補它沒量過的分支。
圖 1:兩種讀法在同一年窗口裡的命中數。
只有一個欄位受限時,兩套語義合流
| 表達式 | 本頁給出的前幾次(上海時區) | 歧義 |
|---|---|---|
0 0 * * 1 | 2024-01-08、01-15、01-22、01-29、02-05,全是週一 00:00 | 無,只有週受限 |
0 0 1 * * | 2024-02-01、03-01、04-01、05-01、06-01 | 無,只有日受限 |
0 0 13 * 5 | 2024-09-13、2024-12-13、2025-06-13、2026-02-13、2026-03-13、2026-11-13、2027-08-13 | 有,第 8 次返回空 |
前兩行只限定一個欄位,交集聯集重合、鏈條規矩;日和週同時收窄時,第一次命中直接推到 2024-09-13,而線上每個週五照跑。
空洞二:日曆本身不給那一天
0 0 31 * * 從 2024-01-01 起鏈 12 次,只命中 7 個月:1、3、5、7、8、10、12 月,2、4、6、9、11 月被直接跳過。從 2 月往後問下一次,它答 2024-03-31,不報錯也不解釋;閏年 2024 同樣是 7 個含 31 號的月份。
更極端的是 2 月 30 日。0 0 30 2 * 欄位值與欄位數都合法,解析一路通過,nextRun 老老實實掃滿 366 × 24 × 60 = 527040 分鐘再返回空,實測單次約 58 毫秒,不死迴圈也不拋異常,介面只留一句「未找到下次執行時間(一年內)」。0 0 31 2 * 走同一條路。
這個上限還會誤傷活著的排程:0 0 13 * 5 跑到第 7 次 2027-08-13 之後,下一個「既是週五又是 13 號」落在 2028-10-13,距上一次超過 366 天,於是第 8 次同樣顯示未找到——稀疏不等於不存在,但頁面上這兩種情況文案一模一樣。另一條靜默路徑是反向區間 8-1:欄位被解析成空集合且不報錯,此後無論怎麼問都只返回空。
報錯文案:英文名一律不是名字
| 輸入 | 頁面拋出的原文 |
|---|---|
SUN/sun/Sat/FRI | 無效的值: SUN(大小寫都一樣,沒有名稱表) |
週欄位寫 8 | 值 8 超出範圍 [0-7] |
61 * * * * | 值 61 超出範圍 [0-59] |
0 0 0 * * | 值 0 超出範圍 [1-31](日的下限是 1) |
*/0 * * * * | 無效的步長: */0 |
| 四段或六段 | 表達式必須為5個欄位(分 時 日 月 週) |
週欄位裡 7 與 0 等價,parseCron 第 145 行把 7 歸成 0,所以 0 0 * * 7 與 0 0 * * 0 同解;可 FRI 這種真實 cron 認得的縮寫,在這裡只是 parseInt 出來的一個 NaN。欄位數校驗排在拆分之後,於是 0 0 1, * * * 這種「逗號後多打一個空格」被拆成六段,報的仍是那句五欄位提示。
預測還隨環境漂
nextRun 讀的是本地時鐘(getHours、getDate、getDay),第 229 行先把秒歸零,輸入裡帶的秒被丟掉;起點還是獨占的,after 正好壓在命中時刻上時不會把自身再報一遍。我把起點固定成上海時區的 2024-01-01 00:00,換算成毫秒就是 時間戳轉換 給的 1704038400000,免得每跑一次換個結果。
日光節約時間是最直觀的一刀:本機解析出的時區是 Asia/Shanghai,沒有日光節約時間,必須強制 TZ=America/New_York 才觸發得到。那一檔裡 0 2 * * * 從 2024-03-08 起命中 2024-03-08、2024-03-09、2024-03-11、2024-03-12——春季前撥把 3 月 10 日的 02:00 整個抹掉,預測從來不是純表達式決定的。
這一頁自己的體量
順帶量一下:範本 337 行、13855 位元組,7 個 id、14 個 button、1 個 input,邏輯全在前端。解析按鈕固定循環 10 趟去產生「未來 10 次執行時間」,每趟都自帶那 527040 分鐘上限,任何一趟返回空就當場收尾,所以稀疏的 0 0 13 * 5 只列出 7 條。
怎麼把兩套語義落回同一條
要「每月 13 號或每週五」,別指望一行表達式兩全,拆成 0 0 13 * * 與 0 0 * * 5 兩行,把聯集交給排程器;要「13 號恰好是週五」,就讓定時只管觸發、腳本自己判日期,此時那 2 次預測正好對得上。驗證的方法很笨但有效:把頁面前幾次命中的星期和日子逐個唸一遍。
常見疑問
Q:這一頁給的下次執行時間能直接照抄線上嗎?
只限定日或只限定週時可以,兩種讀法在那時重合;兩段同時收窄就不行,本頁算交集,線上算聯集。
Q:出現「未找到下次執行時間(一年內)」是不是任務永遠不跑?
不一定。永不存在日期 0 0 30 2 * 與合法的稀疏排程都會走到這句話,前者是日曆裡沒有那天,後者只是下一個命中點超出 366 天的遍歷上限。
Q:週日寫 7 還是寫 0?
在這頁等價,第 145 行把 7 歸 0;但 SUN、sat 一律按無效值拋錯,別把真實 cron 的縮寫習慣帶進來。