跳轉到

ADR 0015 — 拆分 TWORK_SKD_NOTIFYEVENT 為兩支單一職責 SP,順手修兩個積累 bug

  • 日期:2026-05-04
  • 狀態:Accepted
  • 決策者:使用者

Context

TWORK_SKD_NOTIFYEVENT 是 SchedulerWorker 撈待辦事件的 SP,但它包了兩個完全無關的查詢,靠 @QueryType 分支:

  • @QueryType=1:撈「待 Email 通知」事件(NotifyJob 用)
  • @QueryType=2:撈「待 AutoPilot 自動執行」事件(AutoPilotJob 用)

C# 端 (TsERP.SchedulerWorker/SchedulerJob.cs:109,176) 也用 lp_p1="1" / "2" 兩個魔術數字觸發這兩條路徑。

Code review 時發現除了「一支 SP 包兩查詢」這個 anti-pattern 外,QueryType=2 分支內還累積了兩個沒被測出來的 bug:

Bug 1:停用 vs 是否停用

-- @QueryType=1 — 正確
where ... 是否停用 <> 'Y' ...

-- @QueryType=2 — 寫成 `停用` 是 bug
where ... 停用 <> 'Y' ...

對照 Common/Model/S.SKD/Twork_skd_vw_eventitemModel.cs:148-149,eventitem view 的欄位是 是否停用;只有 Twork_skd_vw_eventtemplatedModel 才有 停用twork_vw_calendarevent 是基於 eventitem 的 view,所以 停用 這個 column 在 SP 內可能根本不存在(或是 view 巧合有同名 column),實際行為不可預期。

Bug 2:殭屍判定還在用退役狀態碼 'X'

AND (已完成 <> 'X' OR 執行開始時間 < DATEADD(MINUTE, -10, GETDATE()))

ADR 0013(2026-05-03)已把「執行中」狀態碼從 'X' 改為 'R',且 production 端確認沒有 caller 還寫 'X'。所以這條 已完成 <> 'X' 永遠為真(沒人寫 X,所以條件等於沒擋),殭屍回收機制等於失效——任何卡在 'R' 的事件都不會被當殭屍重撈。

Bug 3(順手):SchedulerJob 自己也還在寫 'X'

SchedulerJob.cs:133:193 在跑事件前 bll.Update已通知/Update已完成(pkid, "X", DateTime.Now) 標執行中——也是 ADR 0013 漏掉的 caller。AutoPilotHelper.ExecuteMethod 已改為寫 'R',但 SchedulerJob 內 standalone 的這兩處被遺漏。

Decision

D1:把 SP 拆成兩支,名字反映實際語意

新 SP 取代 用途
TWORK_SKD_GET_PENDING_NOTIFY(@datetime) TWORK_SKD_NOTIFYEVENT @QueryType=1 NotifyJob 撈待 Email 通知事件
TWORK_SKD_GET_PENDING_AUTOEXEC(@datetime) TWORK_SKD_NOTIFYEVENT @QueryType=2 AutoPilotJob 撈待自動執行事件

兩支 SP 各自單一職責,未來改一邊不會誤碰另一邊。命名 GET_PENDING_* 動詞 + 名詞清楚表達「撈待處理 X」。

D2:在拆 SP 同時修掉 Bug 1 + 2

  • 停用是否停用(與 model schema 對齊)
  • 已完成 <> 'X'已完成 <> 'R'(殭屍判定恢復作用)
  • DATEADD(MINUTE, -10, GETDATE())DATEADD(MINUTE, -10, @datetime)(讓殭屍判定基準時間可由 caller 控制,便於外部測試)

D3:SchedulerJob 內兩處 "X" 改成 "R"

NotifyAsyncAutoPilotAsync 在跑前 mark 執行中時,由 "X""R"。AutoPilotHelper 內的 R 寫入是雙保險(idempotent)。

D4:舊 SP enum 標 [Obsolete],DB 端漸進淘汰

Common/DataBaseObjectEnum/Procedure.cs 內: - 加入 TWORK_SKD_GET_PENDING_NOTIFYTWORK_SKD_GET_PENDING_AUTOEXEC - 舊的 TWORK_SKD_NOTIFYEVENT[Obsolete],但不刪除——避免 DB 升級期間 schema/code 不同步時 production crash

Consequences

Migration

DBA 端,對每個目標 DB順序執行:

  1. CREATE PROCEDURE TWORK_SKD_GET_PENDING_NOTIFY(見 SqlBI/TWORK_SKD_GET_PENDING_NOTIFY.sql
  2. CREATE PROCEDURE TWORK_SKD_GET_PENDING_AUTOEXEC(見 SqlBI/TWORK_SKD_GET_PENDING_AUTOEXEC.sql
  3. Build & deploy 新版 TsERP.SchedulerWorker
  4. 觀察 1-2 輪正常運轉(看 c:\temps\SCHED\ log 內 Notify pkid=... / AutoPilot pkid=...)後,可以 DROP PROCEDURE TWORK_SKD_NOTIFYEVENT

順序很重要:先建新 SP、再升級 worker、最後砍舊 SP。反過來會在升級 gap 期間 SchedulerWorker 找不到 SP 而 crash。

Breaking Changes

  • DB schema 必動:兩支新 SP 必須建立,否則升級後 worker 跑不起來
  • 舊 SP 在新 worker 中不再被呼叫TWORK_SKD_NOTIFYEVENT 留在 DB 不會壞,但也不會被新 SchedulerWorker 觸碰。任何外部腳本 / 排程仍直呼舊 SP 者需自行評估遷移
  • 殭屍判定真的會生效了:先前因為 'X' bug 等於沒判,現在 'R' 超過 10 分鐘的事件會被視為殭屍重撈。若 production 真的有跑超過 10 分鐘的事件,會被重複撈 → 重複跑。建議先觀察 執行開始時間 分布,必要時調大 ZombieTimeoutMinutes

對使用者影響

  • 終端使用者無感:UI 不變、API 不變
  • DBA:必跑兩個 CREATE PROCEDURE script
  • 開發者:撈待辦事件不再走 @QueryType 魔術數字,新增 caller 時 SP 名字直接對到用途

Alternatives Considered

  1. (拒絕) 只 rename SP:表面整齊但兩個 bug 與 anti-pattern 還在。低收益。
  2. (拒絕) 只修兩個 bug、不拆 SP:bug 解了但下一次有人新增第三種查詢(例如「待回應」),又會再加一個 @QueryType=3 分支。anti-pattern 持續滋長。
  3. (拒絕) 把 @QueryType=2 直接改名、@QueryType=1 留原 SP:折中但仍有 SP 名字 vs 行為不一致問題(NOTIFYEVENT 留下卻只做通知)。同樣 confusing。
  4. (拒絕) 把舊 SP enum 直接刪掉:DB 升級和 code 升級不一定同步;保留 [Obsolete] enum 提供 grace period。