程式碼走讀:一個 jobs handler 的解剖
jobs queue 的觀念篇講了鐵律,這篇拿 newsletter 確認信的 handler 當標本,看鐵律在程式碼裡長什麼樣。
註冊與進入點
newsletter-mail.handler.ts(節錄)ts
onApplicationBootstrap(): void {
this.jobs.registerHandler<ConfirmMailPayload>(
JOB_NEWSLETTER_CONFIRM_MAIL,
(payload) => this.sendConfirmMail(payload),
);
}
handler 在應用啟動時向佇列登記「這種 job 歸我」。payload 型別是泛型參數——enqueue 端與 handler 端共用同一個 interface,序列化邊界兩側的型別由編譯器對齊。
本體:每一行都是一條鐵律的化身
ts
async sendConfirmMail(payload: ConfirmMailPayload): Promise<void> {
const subscriber = await this.subscribers.findById(payload.subscriberId);
if (!subscriber || subscriber.status !== 'pending') return; // ①冪等防線
const token = subscriber.rotateConfirmToken(); // ②祕密現產
await this.subscribers.save(subscriber);
await this.mailTemplates.sendTemplated('newsletter.confirm', subscriber.email, {
email: subscriber.email,
confirmUrl: `${this.siteUrl()}/newsletter/confirm?token=${token}`,
}); // ③失敗自然 throw
}
① 冪等防線:開頭先驗目標狀態。job 可能重跑(at-least-once 的本義),也可能 enqueue 之後世界變了——訂閱者已確認、已退訂。狀態不對就靜默放棄,這不是錯誤,是佇列的日常。
② 祕密在 handler 內才產生:payload 只有 subscriberId。確認 token 在執行當下輪替——因為 job 的 payload 會落 background_jobs 表、後台看得到;同時「重跑=作廢舊票發新票」,任何時刻只有一張有效票,重試不會累積出一堆能用的連結。
③ 失敗就讓它 throw:寄信炸了不接、不吞。佇列接住例外標記失敗、按退避策略排下一次重試,七次後進死信等人工處置。handler 裡寫 try/catch 把錯誤吞掉,等於對佇列謊報成功——這封信就永遠消失了。
對照:enqueue 端瘦得像一行
ts
await this.jobs.enqueue(JOB_NEWSLETTER_CONFIRM_MAIL,
{ subscriberId: subscriber.id },
{ dedupKey: `newsletter-confirm-${subscriber.id}` });
HTTP 請求的職責到 enqueue 為止——快路徑只驗證與排隊,慢而不可靠的世界(SMTP)整個關進 worker。dedupKey 擋住短時間的重複排隊,行為的可靠性分層,每層只操心自己那份。
讀到這裡若有一點收穫,替這顆種子澆一次水——匿名、一人一次。