專案

一般

配置概況

動作

Bug #1436

已結束

[Branch Bug] CT1 CP003 重連後 watchdog 以舊 Heartbeat 覆寫四槍狀態

是由 陳國瑋 於 約 2 個月 前加入. 於 30 天 前更新.

狀態:
Closed
優先權:
High
被分派者:
開始日期:
2026-08-13
完成日期:
預估工時:

概述

問題摘要

2026-08-13,CT1(寶台 A17)CP003 的四個 connectors 在 Branch/使用者畫面全部顯示無法使用;但 Charge Point edge 端四槍皆為 AVAILABLE,Modbus meter read 持續更新,設備並無實體故障。

調查確認:CP003 在 OCPP 斷線後已重新建立 WebSocket、完成 BootNotification 並重送初始 StatusNotification(Available);Branch heartbeat watchdog 隨後仍依重連前的舊 last_heartbeat,再次把四槍覆寫為 UNAVAILABLE / OCPP heartbeat stale。下一筆 Heartbeat 只恢復 charge point connection,不會偽造 connector operational status,因此 Branch 與 edge 狀態長期分裂,使用者無法啟動充電。

本單是 #1382 offline watchdog 的 production regression,與 #1421 Fortune command capability/real-load E2E 無關。

與既有 Issue 的關係

  • 原始功能:#1382。
  • #1393 曾記錄相同 reconnect/watchdog 缺口,但於 2026-08-12 被標為 Rejected,未留下拒絕理由。
  • CT1/CP003 於 2026-08-13 再次在 production 發生,證明缺口仍存在;因此建立本單追蹤本次復發與正式修補。
  • 不合併進 #1421,避免混入 Fortune vendor certification 範圍。

環境與影響

  • 案場:CT1(寶台 A17)
  • Charge Point:CP003
  • Connectors:CP003-001、CP003-004、CP003-005、CP003-008
  • 發生日期:2026-08-13(Asia/Taipei)
  • 使用者影響:四支槍全部顯示無法使用,無法開始充電
  • Branch watchdog:Heartbeat interval 300 秒、offline timeout 330 秒、scan fixed delay 60 秒

現場證據

Branch

  • e_charge_points.CP003:status = AVAILABLE、ocpp_connection_status = ONLINE,Heartbeat 後續持續更新。
  • 四筆 e_connectors:UNAVAILABLE / NOERROR / OCPP heartbeat stale,last_status_change = 2026-08-13 05:16:26。

Charge Point edge

  • 四筆 cp_connectors 均為 AVAILABLE。
  • Meter read 與 Modbus 輪詢持續成功。
  • Branch 與 edge 皆無進行中 transaction。

上述證據排除 connector 硬體、Modbus gateway 與 meter 故障。

事件時序

  1. 05:06:57:最後一筆舊 Heartbeat 成功。
  2. 05:13:26:watchdog 依 stale Heartbeat 將 CP003 判離線。
  3. 05:14:43:CP client 因 WebSocket pong timeout 關閉連線。
  4. 05:14:46:Branch 收到 broken pipe 並套用 WebSocket-close offline sync。
  5. 05:15:13:CP003 reconnect,BootNotification Accepted。
  6. 05:15:26:watchdog 仍使用舊 Heartbeat,再次判離線。
  7. 05:15:26~05:15:30:CP 初始 StatusNotification 將四槍短暫恢復 AVAILABLE。
  8. 05:16:26:第一個 post-reconnect Heartbeat 尚未到達,watchdog 再次把四槍覆寫成 UNAVAILABLE。
  9. 05:19:56:Heartbeat 恢復 charge point 為 ONLINE,但 connector 狀態仍卡在 UNAVAILABLE。

程式碼根因

  1. ChargePointRepository.findEnabledStaleHeartbeat() 只判斷 lastHeartbeat,不考慮剛成功的 lastBootNotification。
  2. ChargePointOfflineService.markOfflineIfHeartbeatStale() 的 locked recheck 同樣只重讀 lastHeartbeat。
  3. BootNotification 把 connection 設為 ONLINE,但依設計不應偽造 lastHeartbeat。
  4. Connector recovery 依實際 StatusNotification;Heartbeat 不會批次恢復 connectors。
  5. 因此「舊 Heartbeat + 新 Boot + 初始 StatusNotification + 第一個新 Heartbeat 尚未到」期間,watchdog 可覆寫剛恢復的 connector,之後狀態永久分裂。

已執行的暫時復原

復原前確認 Branch 與 edge 未結束交易皆為 0,四槍 current_transaction_id 皆為 null。

只受控重啟 CP003 edge service,未重啟 Branch/HQ/其他 CP,也未以 SQL 直接改 connector 狀態。標準 OCPP reconnect 完成 WebSocket、BootNotification、Heartbeat 與四筆 StatusNotification;Branch/edge 四槍恢復 AVAILABLE,下一輪 Heartbeat 與 meter read 持續更新。

此操作只恢復現場資料,尚未部署永久修補。

建議修正方向

  1. Watchdog candidate query 同時檢查 lastHeartbeat 與 lastBootNotification。
  2. Locked recheck 同時重讀兩者;任一時間不早於 cutoff,就略過離線覆寫。
  3. BootNotification 更新與 watchdog 共用 charge point row lock。
  4. BootNotification 只提供第一筆 Heartbeat 前、等同 offline timeout 的 reconnect grace;不可刷新或偽裝 lastHeartbeat。
  5. 兩者都過期時仍須正常判離線,不能以 live session 永久略過,以保留 TCP half-open 偵測。

驗收條件

  1. 舊 Heartbeat 早於 cutoff,但 fresh BootNotification 位於 cutoff 內時,不得覆寫為 OFFLINE/UNAVAILABLE。
  2. reconnect 初始 StatusNotification 已回報 AVAILABLE,在 grace 內不得被舊 Heartbeat 再次覆寫。
  3. BootNotification 超過 timeout 且仍無 Heartbeat 時,watchdog 必須正常判離線。
  4. Heartbeat 與 BootNotification timestamp 等於 cutoff 時不算 stale。
  5. BootNotification 不更新 lastHeartbeat,也不覆寫 operational status。
  6. TCP half-open 與無 Boot/無 Heartbeat 的既有離線偵測正常。
  7. currentTransactionId、Branch ACTIVE transaction、edge ACTIVE transaction 的保護不得退化。
  8. 補回歸測試並通過 Branch build。
  9. CT1 或等價 simulator 驗證 reconnect → Boot → StatusNotification → watchdog → first Heartbeat 全時序。

E2E Impact

  • 分類:Update
  • 案例:OCP-007
  • Priority:P1
  • Trigger:offline watchdog、OCPP reconnect liveness 或相關 production incident 修正
  • Release pack:Major / Charging / OCPP
  • Gap:需補可控制時間的 simulator/整合時序;production recovery 可作現場證據,但不能取代修補版部署後驗證

目前程式碼狀態

已有本機未提交草稿用於驗證修補方向;尚未 commit、尚未 push、尚未部署。建立本單後,必須依 Redmine SOP 在本單專屬 branch 重新走需求確認、實作規劃、測試計畫與文件 gate。

是由 陳國瑋 於 約 2 個月 前更新

  • 優先權 從 Normal 變更為 High

是由 陳國瑋 於 約 2 個月 前更新

是由 陳國瑋 於 約 2 個月 前更新

  • 狀態 從 New 變更為 In Progress
  • 被分派者 設定為 陳國瑋

收案啟動

  • 本單獨立於 #1421。
  • 已完成 production incident 的只讀診斷與 CP003 安全復原。
  • 永久修補尚未 commit、push 或部署。
  • 下一步依 Redmine SOP 進入需求確認與實作規劃 gate。

是由 陳國瑋 於 約 2 個月 前更新

需求確認 1:修補範圍

Ken 已確認:

  • 本單只處理 Branch heartbeat watchdog 的 BootNotification reconnect grace。
  • Charge Point 重連後立即補送 Heartbeat 不納入 #1436;若後續需要,另列為防禦性強化。
  • 不修改 #1421 範圍。

是由 陳國瑋 於 約 2 個月 前更新

需求確認 2:廠牌適用範圍

Ken 已確認:

  • reconnect grace 是 Branch 端、廠牌無關 的 OCPP liveness 判斷。
  • 任何 Charge Point 只有在 Branch 實際收到 fresh BootNotification 時,才取得 grace。
  • 不控制、不要求也不偽造 Fortune 或其他第三方充電樁的 Heartbeat 發送行為。
  • 若設備重連後沒有送 BootNotification,本次規則不提供 grace;既有 watchdog 行為不變。

是由 陳國瑋 於 約 2 個月 前更新

需求確認 3:grace 設定來源

Ken 已確認:

  • BootNotification reconnect grace 直接沿用既有 ems.branch.chargePoint.offlineTimeoutSeconds。
  • 目前設定值為 330 秒(300 秒 Heartbeat interval + 30 秒容錯)。
  • 本單不新增第二組 grace timeout 設定,避免兩套 OCPP liveness 時窗不一致。

需求確認階段完成,下一步進入實作規劃與 E2E Impact Review。

是由 陳國瑋 於 約 2 個月 前更新

階段 2:實作規劃確認

Ken 已確認規劃可接受。

核准範圍

  • Watchdog 候選查詢只有在 lastHeartbeat 與 lastBootNotification 都 stale/null 時才列入。
  • 寫入前 row-lock recheck 套用相同規則;任一證據仍在 cutoff 內即取消離線覆寫。
  • BootNotification 更新與 watchdog 使用同一把 charge point row lock。
  • 不更新或偽造 lastHeartbeat,不修改 operational status。
  • 不修改任何廠牌的 Heartbeat 發送行為。
  • 不新增 schema 或設定;grace 沿用 offlineTimeoutSeconds。

E2E Impact

  • Update:OCP-007
  • Priority:P1
  • Trigger:offline watchdog、OCPP reconnect liveness 或相關 production incident 修正
  • Release pack:Major / Charging / OCPP

下一步進入階段 3;多案例測試計畫仍須 Ken 另行明確啟動。

是由 陳國瑋 於 約 2 個月 前更新

階段 3:實作完成,等待正式測試啟動

已完成草稿

  • Branch watchdog candidate query 改為只有 lastHeartbeat 與 lastBootNotification 都 stale/null 才列入。
  • row-lock recheck 使用相同 cutoff;任一存活證據位於 cutoff 內即取消 OFFLINE/UNAVAILABLE 覆寫。
  • handleBootNotification() 改用與 watchdog 相同的 pessimistic row lock。
  • Boot 只更新 Boot timestamp 與 connection,不刷新/偽造 Heartbeat,也不覆寫 operational status。
  • 沿用既有 offlineTimeoutSeconds=330;無 schema/config/charge_point/vendor-specific 變更。
  • 已更新 AI knowledge、E2E inventory 的 OCP-007 與 #1436 草稿報告。

已完成、未執行測試的檢查

  • git diff --check:PASS。
  • E2E catalog validator:PASS(176 cases)。
  • java/ems_branch: mvn -DskipTests package:BUILD SUCCESS;主程式與 test source 編譯成功,Surefire 顯示 Tests are skipped。
  • 尚未 commit、push 或部署。

待 Ken 啟動的正式 Unit 測試計畫

Command:

cd java/ems_branch
mvn -Dtest=ChargePointOfflineServiceTest,ChargePointOfflineWatchdogTaskTest,ChargePointServiceOcppConnectionStatusTest test

涵蓋範圍:

  1. fresh Heartbeat cutoff 邊界不誤判。
  2. 舊 Heartbeat + fresh Boot 的 CP003 reconnect grace,不覆寫 connectors。
  3. Heartbeat + Boot 都過期時仍正常 OFFLINE/UNAVAILABLE。
  4. Boot 與 watchdog 共用 row lock,且 Boot 不改 Heartbeat/operational status。
  5. 既有 watchdog cutoff 傳遞與 offline transaction/queue/event 保護回歸。

環境與資料風險:

  • JUnit 5 + Mockito isolated unit tests。
  • 不載入 Spring integration/test profile。
  • 不連接任何外部服務、正式/共用資料庫或 OCPP Charge Point。
  • 不修改共用資料;只使用 process 內 mock objects。
  • 一次執行 3 個 test classes,依 SOP 必須等 Ken 明確回覆「開始測試」。

修補版部署與 E2E 另屬階段 4;Unit 授權不包含部署或 E2E。

是由 陳國瑋 於 約 2 個月 前更新

階段 3:正式 Unit 測試結果

Ken 已明確回覆「開始測試」,本次只執行先前列出的測試計畫:

cd java/ems_branch
mvn -Dtest=ChargePointOfflineServiceTest,ChargePointOfflineWatchdogTaskTest,ChargePointServiceOcppConnectionStatusTest test

結果

  • ChargePointOfflineWatchdogTaskTest:1 PASS。
  • ChargePointServiceOcppConnectionStatusTest:6 PASS。
  • ChargePointOfflineServiceTest:11 PASS。
  • 合計:18 tests,Failures 0,Errors 0,Skipped 0。
  • Maven:BUILD SUCCESS。
  • 完成時間:2026-08-13 11:31:44(Asia/Taipei)。

已驗證重點

  • 舊 Heartbeat + cutoff 內 fresh BootNotification:略過 OFFLINE/UNAVAILABLE 覆寫。
  • Heartbeat 與 BootNotification 都早於 cutoff:仍正常離線。
  • cutoff 等值邊界不誤判。
  • Boot 與 watchdog 共用 row lock。
  • Boot 不刷新/偽造 Heartbeat,不覆寫 operational status。
  • 既有 active transaction、queue、event 與快速重連保護測試未退化。

測試為 JUnit 5 + Mockito isolated Unit Tests,未載入 integration/test profile、未連接外部服務或任何資料庫,未修改共用資料。

階段 3 完成。目前尚未 commit、push 或部署;Unit 授權不包含部署與 E2E。修補版 E2E 報告仍維持 PARTIAL。

是由 陳國瑋 於 約 2 個月 前更新

已合併至 private

Ken 已明確指示 merge to private 並關閉 #1436 worktree。

Git

  • 修補 commit:6f09934 — fix(#1436): 修正重連後 watchdog 誤判離線
  • 測試/文件 commit:feb98c0 — test(#1436): 補充 CP003 重連回歸測試報告
  • private merge commit:fa9c18e — merge(#1436): 整合 CP003 重連 watchdog 修正
  • feature branch 與 private 均已推送至 origin。

最終驗證(最新 origin/private 基線)

  • 3 個 isolated Unit Test classes:18 tests,Failures 0,Errors 0,Skipped 0,BUILD SUCCESS。
  • mvn -DskipTests package:BUILD SUCCESS。
  • E2E catalog validator:PASS(176 cases)。
  • git diff --check:PASS。

尚待事項

  • 本次未部署。
  • 修補版 OCPP reconnect 完整 E2E 尚未執行,報告維持 PARTIAL,不得視為 PASS。
  • 因此目前只記錄程式已合併,不將 #1436 誤標為已完成部署/E2E。

是由 陳國瑋 於 約 1 個月 前更新

2026-08-22 Production 再次發生:CT1 CP001

本次為 #1436 相同根因在另一個 CloudLink instance 的 production recurrence,補充追蹤如下。

影響範圍

  • 案場:CT1(寶台 A17)
  • Charge Point:CP001
  • 受影響 connectors:CP001-001、CP001-003、CP001-007
  • 使用者影響:Branch/LIFF 顯示 UNAVAILABLE,無法開始充電
  • CP001-002 未受影響:事件當下有 active transaction,offline propagation 受到 currentTransactionId 保護;交易結束後又收到真實 AVAILABLE

現場證據與時序

  1. 2026-08-20 04:20:18:CP001 reconnect,BootNotification Accepted;001/003/007 均送出初始 StatusNotification(AVAILABLE)。
  2. 04:23:18:WebSocket 因未及時收到 pong,以 code 1006 關閉。
  3. 04:23:48:CP001 再次建立 WebSocket;因 BootNotification cooldown,這次跳過 Boot。
  4. 04:25:50.496:offline watchdog 以 cutoff 04:20:20.493 掃到 stale candidate。
  5. 04:25:50.498:Branch 依舊的 lastHeartbeat=04:07:15 將 CP001 判為 HEARTBEAT_STALE。
  6. 04:25:50.504~04:25:50.507:001/003/007 被 offline propagation 改成 UNAVAILABLE / OCPP heartbeat stale。
  7. 04:25:57.236:watchdog 完成。
  8. 04:25:57.709:下一筆 Heartbeat 隨即把 CP001 恢復 ONLINE,但未恢復 connector operational status。
  9. 至 2026-08-22 20:00,Branch 三支仍為 stale UNAVAILABLE;edge cp_connectors 三支皆為 AVAILABLE,Modbus polling/meter read 持續正常。

暫時復原

復原前交叉確認:

  • Branch CP001 現存 connector 的 current_transaction_id 全為 null。
  • Branch linked ACTIVE transaction:0。
  • Edge cp_transactions 沒有 CP001 ACTIVE transaction。
  • Edge 四支 connector 均為 AVAILABLE 且 meter read 新鮮。

只重啟 ems-cp-api(CP001),未重啟 Branch、CP002、CP003,也未直接修改 DB:

  • 20:01:28:WebSocket connected、BootNotification Accepted、Heartbeat #1 成功。
  • Modbus gateway recovery:1/1 成功;設備通訊驗證:1/1 成功。
  • 20:01:38~20:01:42:001/003/007 逐支送出初始 StatusNotification(AVAILABLE/NOERROR)。
  • 20:01:43:三支在 Branch 均已恢復 AVAILABLE。

追蹤結論

  • 本次證明 #1436 並非只限 CP003,而是 CP001 也會發生的 CP instance-independent race。
  • 現場已恢復,但永久修補仍須完成 A17 部署與 reconnect → Boot/Boot cooldown → watchdog → first Heartbeat 的 E2E 驗收。
  • E2E impact 不新增重複案例,沿用 Update:OCP-007,P1,Major / Charging / OCPP release pack。

是由 陳國瑋 於 30 天 前更新

  • 狀態 從 In Progress 變更為 Resolved

結案摘要

  • Branch watchdog 的 BootNotification reconnect grace 已完成並部署。
  • 修補 commit:6f09934;測試/文件 commit:feb98c0;既有整合 commit:fa9c18e。
  • Isolated Unit Test:18 PASS,0 failure/error/skip。
  • #1456 後續已補足 Boot cooldown reconnect 的 immediate Heartbeat 與 connector convergence,Ken environment E2E 為 5 PASS。
  • 綜合現場恢復、部署狀態與後續回歸驗證,本單調整為 Resolved / 100%。

是由 陳國瑋 於 30 天 前更新

  • 狀態 從 Resolved 變更為 Closed

結案狀態補正

Redmine 在 Resolved 狀態下未套用 100% 完成度,因此依已完成並驗證的結案決定,改用正式完成狀態關閉本單。

動作

匯出至 Atom PDF