[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"$fhL55rcNLQaytc1yxT7uCZBTZzBfhsa4qFiFsTHWUr5k":3},{"slug":4,"title":5,"body":6,"excerpt":7,"tags":8,"maturity":11,"meta":12,"plantedAt":13,"tendedAt":14,"shouldIndex":15,"resolvedLinks":16,"backlinks":18,"authorName":22},"scheduler-observability","Cron 的可觀測性：排程不該是黑箱","排程任務最陰險的故障模式是**安靜地不跑**。cron 表達式打錯、程序重啟後沒掛回去、上一輪卡死——沒有人會收到通知，直到某天發現三週的清理任務一次都沒執行。所以平台把「排程」與「可觀測」設計成一體。\n\n## 慣例：@Cron 只准當扳機\n\n`@Cron` 裝飾器一律放在 presentation 層的 scheduler 檔案裡，而且**薄到只做三件事**：觸發、try\u002Fcatch 記錄成敗（帶耗時）、登記手動觸發入口。真正的工作在 application service——scheduler 是 HTTP controller 的孿生兄弟，只是觸發源從請求換成時鐘。\n\n這個分離的直接紅利：**每個排程都能從後台手動觸發**。「等到凌晨三點看它跑不跑」是很糟的除錯迴圈，一顆按鈕現在就跑一次，才是。\n\n## 觀測資料放哪裡：一個聰明的偷懶\n\n每輪的執行結果（最後執行時間、耗時、錯誤）寫進**平台的快取抽象**。這個選擇的妙處：快取的一致性語意跟著 `CACHE_DRIVER` 走——單機開發時是記憶體、多 pod 部署換 redis，排程觀測的跨實例一致性**自動跟上**，一行都不用另外寫。\n\n下一次執行時間、cron 來源、runtime 動態改排程，則直接問 scheduler registry——框架已經持有的狀態不要自己再存一份。後台一頁把「註冊了什麼、上次跑得如何、下次何時跑」合併展示，排程從黑箱變成儀表板。\n\n## 執行結果也是稽核的一部分\n\n失敗的那輪會記進失敗歷史；重要排程失敗可以接[[domain-events-three-exits|事件出口]]寄信告警。「排程有跑、跑成功了」這件事有據可查之後，那種「好像有東西沒在動？」的不安感就消失了——**可觀測性買的其實是心理健康**。","排程任務最陰險的故障模式是安靜地不跑。cron 表達式打錯、程序重啟後沒掛回去、上一輪卡死——沒有人會收到通知，直到某天發現三週的清理任務一次都沒執行。所以平台把「排程」與「可觀測」設計成一體。 慣例：@Cron 只准當扳機 @Cron 裝飾器一律放在 presentation 層的 scheduler 檔案裡，而且薄…",[9,10],"nestjs","平台","budding",null,"2026-07-14T06:54:32.229Z","2026-07-14T11:26:37.284Z",true,{"domain-events-three-exits":17},"一個事件、三個出口：通知系統的解耦練習",[19],{"slug":20,"title":21,"maturity":11},"cache-lock-redis","三個環境變數的哲學：memory 預設、redis 可換","AI助理"]