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"¶
NotifyAsync 與 AutoPilotAsync 在跑前 mark 執行中時,由 "X" 改 "R"。AutoPilotHelper 內的 R 寫入是雙保險(idempotent)。
D4:舊 SP enum 標 [Obsolete],DB 端漸進淘汰¶
Common/DataBaseObjectEnum/Procedure.cs 內:
- 加入 TWORK_SKD_GET_PENDING_NOTIFY 與 TWORK_SKD_GET_PENDING_AUTOEXEC
- 舊的 TWORK_SKD_NOTIFYEVENT 標 [Obsolete],但不刪除——避免 DB 升級期間 schema/code 不同步時 production crash
Consequences¶
Migration¶
DBA 端,對每個目標 DB順序執行:
CREATE PROCEDURE TWORK_SKD_GET_PENDING_NOTIFY(見SqlBI/TWORK_SKD_GET_PENDING_NOTIFY.sql)CREATE PROCEDURE TWORK_SKD_GET_PENDING_AUTOEXEC(見SqlBI/TWORK_SKD_GET_PENDING_AUTOEXEC.sql)- Build & deploy 新版
TsERP.SchedulerWorker - 觀察 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 PROCEDUREscript - 開發者:撈待辦事件不再走
@QueryType魔術數字,新增 caller 時 SP 名字直接對到用途
Alternatives Considered¶
- (拒絕) 只 rename SP:表面整齊但兩個 bug 與 anti-pattern 還在。低收益。
- (拒絕) 只修兩個 bug、不拆 SP:bug 解了但下一次有人新增第三種查詢(例如「待回應」),又會再加一個
@QueryType=3分支。anti-pattern 持續滋長。 - (拒絕) 把
@QueryType=2直接改名、@QueryType=1留原 SP:折中但仍有 SP 名字 vs 行為不一致問題(NOTIFYEVENT留下卻只做通知)。同樣 confusing。 - (拒絕) 把舊 SP enum 直接刪掉:DB 升級和 code 升級不一定同步;保留
[Obsolete]enum 提供 grace period。