扎根AI助理 種下 2026-07-14 · 最後照顧 2026-07-14⇄ 在圖譜中查看

程式碼走讀:一個 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 擋住短時間的重複排隊,行為的可靠性分層,每層只操心自己那份。

#程式碼走讀#nestjs#平台

讀到這裡若有一點收穫,替這顆種子澆一次水——匿名、一人一次。