專案

一般

配置概況

動作

Bug #1518

進行中

[Backend Bug] StopTransaction 補送失敗後保留殭屍交易,Connector 無法再次充電

是由 陳國瑋 於 4 天 前加入. 於 3 天 前更新.

狀態:
New
優先權:
High
被分派者:
-
開始日期:
2026-09-28
完成日期:
預估工時:
(總計: 0.00 小時)

概述

問題摘要

寶台 A17 的 CP003-005 實際已停止充電,Charge Point(CP)本機也已將交易 865 完成,但停止當下 OCPP WebSocket 斷線,StopTransaction 未能送達 Branch。

CP 後續雖然重新連線,但重連時同時補送大量累積的 MeterValues,重要的 StopTransaction 沒有在連線再次中斷前送達 Branch。CP 端重試三次後停止補送,導致 Branch 永久保留:

  • e_transactions.transaction_id=865、status=ACTIVE
  • e_connectors.id=CP003-005、current_transaction_id=865

因此車主畫面雖然取得原始 Connector 狀態 AVAILABLE、即時電流 0A,仍因 Branch 的舊交易證據而投影成「充電中」,只提供「停止充電」。停止操作又因 Connector 並非 CHARGING 而被後端拒絕,造成使用者既不能停止舊交易,也無法再次開始充電。

這是交易訊息收斂問題,應與 #1517 的 OCPP 存活判斷/Connector 狀態恢復分開處理。

使用者影響

使用者開啟 CP003-005 時會看到:

  • 原始狀態:AVAILABLE/待命
  • driverState:CHARGING/充電中
  • 即時電流:0A
  • active operation:無
  • 可用操作:只有「停止充電」

2026-09-27 22:35 與 22:50,使用者實際送出兩次停止操作,Branch 均記錄:

transactionId=865
connectorStatus=AVAILABLE
reason=not charging

因此正常車主操作流程已被阻斷。

事故時間軸

以下時間均為 Asia/Taipei。

時間 事件
2026-09-25 00:23:44 CP003-005 開始交易,Branch 配發交易 ID 865。
2026-09-25 05:11:40 CP 偵測電流降至 0.05A,判定 EVDisconnected。
2026-09-25 05:11:42 CP 本機完成交易 865,停止時間、meter stop、耗電量及 duration 均已保存。
2026-09-25 05:11:42 CP 發送 StopTransaction 時失敗,錯誤為 WebSocket not connected。
2026-09-25 05:12:01 CP 重新建立 OCPP WebSocket。
2026-09-25 05:12~05:16 CP 集中補送累積的 MeterValues;Branch 每筆處理最慢約 4.3 秒。此連線至少處理 56 筆 MeterValues。
2026-09-25 05:13:11 CP 重試 StopTransaction 865,但 Branch OCPP audit 沒有收到這筆訊息。
2026-09-25 05:14:01 WebSocket 因對端未及時回 pong 再次關閉。
2026-09-25 05:14:12 StopTransaction 在離線狀態重試失敗。
2026-09-25 05:16:44 第三次失敗,CP 保存 retry_count=3、stop_message_sent=false,之後即使重連也不再補送。
2026-09-27 Branch 交易 865 仍為 ACTIVE,造成車主畫面與停止流程互相矛盾。

兩端資料差異

事故查核時的資料如下。

CP 本機

欄位 值
transaction_id 865
connector_id CP003-005
status COMPLETED
stop_timestamp 2026-09-25 05:11:42
meter_start 2425300 Wh
meter_stop 2458770 Wh
energy_consumed 33470 Wh
duration_seconds 17278
reason EVDisconnected
stop_message_sent false
retry_count 3
last_retry_timestamp 2026-09-25 05:16:44

CP 的 Connector 為 AVAILABLE、current_transaction_id=NULL,近期量測為 0A/0W,電表總量維持 2458770 Wh。

Branch

欄位 異常值
transaction_id 865
status ACTIVE
stop_timestamp NULL
meter_stop NULL
reason NULL
connector.current_transaction_id 865

根因

本事故包含三個需要一起修正的缺口:

  1. 交易訊息沒有優先權
    重連後,StopTransaction 與大量歷史 MeterValues 共用發送流程。MeterValues 塞車時,交易結束訊息無法優先送達。

  2. StopTransaction 重試有上限,但達上限後沒有後續收斂機制
    retry_count=3 後即永久停止補送;後續 WebSocket 恢復、BootNotification 成功或 Connector 已為 AVAILABLE,都不會重新處理這筆未送達的完成交易。

  3. Branch 沒有針對「AVAILABLE + 舊 ACTIVE transaction」做安全對帳
    Branch 收到 Connector 的 AVAILABLE 狀態後,只更新 raw status,沒有判斷 CP 本機交易是否已完成,因此保留 current transaction。畫面將舊交易視為仍在充電,而 RemoteStop 又依 raw status 拒絕,形成不可自行恢復的死結。

  4. Branch 在 OCPP 同步回覆路徑重算完整 Connector 指標,MeterValues 被慢查詢阻塞
    Branch 保存每一筆帶 transactionId 的 MeterValues 後,會在 transaction commit callback 內同步發布 Connector 更新事件。事件組裝又會重新讀取交易資料、計算最早與最新能源讀值、載入整筆交易的能源歷史並重算預估費用;上述工作全部完成後,Branch 才回覆 OCPP CALLRESULT。

    A17 的 e_meter_values 已有約 169 萬筆資料,但只有分開的 transaction_id、value_type、timestamp 索引。能源差額查詢條件同時使用這三欄,MySQL 卻選擇 timestamp 索引。2026-09-27 對進行中的交易 877 執行 EXPLAIN ANALYZE,查找最早能源讀值實際掃描 1,691,077 筆索引資料並耗時 4,204 ms;強制使用現有 transaction_id 索引後,相同結果約 6.8 ms。這證明主要瓶頸是索引不符合查詢條件,不是 MeterValue insert,也不是 log 中出現在回覆前的 HistoricalTariff 查詢。

    現場 CP 又把同一採樣時間的 Current、Voltage、Power、Energy 分成四個 MeterValues request;每一個 request 都重跑相同的 Connector 完整計算。CP003-001 近期四筆 response 分別約 3.753、4.100、3.900、4.012 秒,一個採樣週期合計約 15~16 秒。多 Connector 同時充電或重連補送 backlog 時,會形成 WebSocket head-of-line blocking,連 Heartbeat、StatusNotification、StopTransaction 與 pong 都可能延後。

分階段永久修正方案

修正必須依下列順序執行。先消除資料庫主瓶頸,再縮短 OCPP 同步路徑;若先做非同步,只會把四秒慢查詢移到背景 queue,資料庫負擔仍會持續累積。

第一階段:新增 MeterValue 複合索引(Branch/CP 均不需重啟)

  • 共用 migration 檔案:java/ems_branch/patch/patch_20260928_redmine_1518_meter_value_lookup_index.sql。
  • 在 e_meter_values 新增共用複合索引 (transaction_id, value_type, timestamp),讓最早/最新能源讀值可直接由同一索引頭尾取得。
  • 以 ALGORITHM=INPLACE, LOCK=NONE 明確要求 MySQL 使用線上 DDL;若該案場版本或資料表無法支援,patch 必須失敗,不可自動退回會長時間鎖表的演算法。
  • Patch 必須可重跑:缺少索引才建立、完全相符時 no-op、同名但欄位或順序不符時 fail-fast。
  • 套用前確認沒有等待中的 metadata lock、沒有長時間 transaction、磁碟空間足夠;session 設定短 lock_wait_timeout,取得不到必要 metadata lock 時應中止,不能等待並阻塞正式流量。
  • 套用後以 information_schema.statistics 與 EXPLAIN ANALYZE 驗證索引及實際掃描筆數,並持續觀察 MeterValues processing time、Heartbeat、WebSocket 與現行交易。
  • 這是唯一可直接改善 A17 現況、又不需要重新啟動 Branch 或 CP 的 production 項目;線上 DDL 不等於零負載,仍須按上述 guard 執行。

第二階段:將 Connector event 移出 OCPP 同步回覆路徑(需部署並重啟 Branch)

  • MeterValues 完成驗證與資料庫 commit 後即可建立 CALLRESULT;RabbitMQ、Admin WebSocket snapshot、即時費用等非 OCPP 必要工作改由 commit 後背景流程處理。
  • 背景事件只傳遞 connectorId、transactionId、meter timestamp 等不可變識別資訊,不可把已脫離 transaction 的 JPA entity 直接交給背景執行緒。
  • 同一 Connector 的事件必須保序;背景發布失敗需有 retry/metric/告警,不能因改成非同步而靜默遺失。

第三階段:即時指標改成索引查詢或增量計算(需部署並重啟 Branch)

  • Voltage、Current、Power、累積 Energy 只查各類型最新一筆,不再每次載入整筆交易全部 MeterValue。
  • 即時預估費用改為依相鄰能源讀值增量更新,或維護交易摘要;完整歷史重算保留給 StopTransaction 最終結算、月結、人工重算與稽核。
  • 最終結算結果必須與現有歷史費率及跨時段規則一致,不能用效能優化改變帳務結果。

第四階段:合併 Connector 更新事件(需部署並重啟 Branch)

  • 目前程式對每個 sampledValue 註冊一次 after-commit event;應改為整個 MeterValues PDU 完成後最多發布一次。
  • 對同一 Connector 短時間連續到達的多個 PDU 再做 keyed coalescing/debounce,只發布包含最新資料的 snapshot,並保留最大等待時間,避免管理畫面長時間不更新。
  • MeterValue 原始資料仍逐筆保存;合併的是衍生事件,不可丟棄計費與稽核讀值。

第五階段:CP 將同一時間點的 measurand 合併為一個 OCPP MeterValues.req(需部署並重啟 CP)

  • OCPP 1.6J 的 MeterValue.sampledValue 本來就是陣列;同一 timestamp 的 Current、Voltage、Power、Energy 可以放在同一個 MeterValues.req。
  • 合併後可減少 OCPP request/response、audit insert 與 database transaction 數量;Branch 仍須相容尚未升級、繼續分筆上報的 CP。
  • 目前 Branch 即使收到一個含四個 sampledValue 的 PDU,仍會註冊四個 Connector event,因此必須配合第四階段,不能只改 CP 就宣稱事件已減少。

現場有進行中交易時的部署界線

項目 是否需重啟 有車充電時的處理
建立、審查、測試 SQL patch 否 可先完成,不影響案場。
A17 線上新增複合索引 否 可在 guard 通過後執行;需設定短 lock timeout、保留中止條件並監看 OCPP。
Branch 第二~四階段程式開發與離線測試 否 可先完成程式與測試,但暫不部署到 A17。
部署第二~四階段 Branch JAR 是 等現行交易結束並安排維護時段。
部署第五階段 CP JAR 是 等所有受影響 Connector 無交易,再逐 CP 更新與驗收。

共用 SQL patch 日期與跨案場規則

  • java/ems_branch/patch/ 是所有案場共用的 migration sequence;A17 只是先行驗證案場,不得把 patch 放在 A17 專用的 documents/sql/。
  • Patch 檔名日期代表 migration 建立/release 排序日期,不是各案場實際執行日期。檔名在合併後保持不變;其他案場日後仍套用同一檔案,不得為每個案場複製並改日期。
  • 每次案場部署必須依 release manifest 明確列出 #1517、#1518 所需的完整 patch 檔名,並逐案場記錄已套用檔案;不能只用「最近一次部署日期」推測舊日期 patch 已經執行。
  • 本次 release 使用 patch_20260928_redmine_1517_last_ocpp_message.sql 與 patch_20260928_redmine_1518_meter_value_lookup_index.sql,同日依票號維持執行順序;兩者均須具備首次套用、重跑、partial/incompatible schema guard 及 postcondition query。
  • Schema patch 一律先於相依 Branch JAR。A17 先行套用成功不代表其他案場已具備 schema;其他案場部署時仍須逐一執行並保存完整 SQL output。

修正目標

Charge Point

  • StartTransaction/StopTransaction 必須優先於歷史 MeterValues 補送。
  • WebSocket 未連線時,不應消耗會造成永久放棄的最終重試次數。
  • 重新連線並完成 BootNotification 後,重新掃描尚未成功送達的 Start/StopTransaction。
  • StopTransaction 的重試必須具備退避、可追蹤狀態及告警,但不能只因固定次數失敗就永久遺失。
  • 重送相同 transaction ID 時需維持 idempotent,避免建立重複交易或重複結算。

Branch

  • StopTransaction 重送必須能安全處理;已完成交易收到相同結果時應回覆成功且不得重複計費。
  • 當 Connector 為 AVAILABLE,但 current_transaction_id 或最新交易仍為 ACTIVE 時,必須辨識為狀態矛盾,不應對車主顯示可執行但必定失敗的「停止充電」。
  • 若能取得可信任的 CP runtime transaction,應提供有 guard 的自動收斂流程;至少要產生可監控告警,避免殭屍交易永久存在。
  • 收斂動作必須保留 stop time、meter stop、reason、energy、duration 與 audit,不得直接刪除交易。
  • 交易收斂不得誤清除真正仍在供電或仍有電流的交易。

驗收條件

  1. CP 離線期間完成交易,重連後可將 StopTransaction 成功補送至 Branch。
  2. 重連時即使有大量 MeterValues backlog,StopTransaction 仍優先處理。
  3. WebSocket 反覆斷線超過三次後,未送達的 StopTransaction 不會永久停止處理。
  4. Branch 收到重送的 StopTransaction 只完成同一筆交易,不建立重複交易、不重複結算。
  5. Branch 最終交易狀態為 COMPLETED,Connector 的 current_transaction_id 被清除。
  6. Connector 為 AVAILABLE、0A、無現行交易時,driverState 回 READY,提供「開始充電」。
  7. Connector 確實仍在充電或仍有電流時,不得因自動對帳誤結束交易。
  8. 重連、補送、重試達門檻及自動收斂均有足夠 log/metric/告警可追查。
  9. 加入事故回歸案例,涵蓋斷線完成交易、訊息 backlog、重連補送、重複 StopTransaction 與 Branch/CP 狀態收斂。

2026-09-28 A17 第一階段線上改善

在 CP003-005 的交易 878 仍為 ACTIVE/CHARGING 時,已先完成不需要重啟 Branch 或 CP 的第一階段:

  • 正式套用 patch_20260928_redmine_1518_meter_value_lookup_index.sql。
  • 新增 idx_mv_transaction_type_timestamp (transaction_id, value_type, timestamp)。
  • DDL 明確使用 ALGORITHM=INPLACE, LOCK=NONE,session lock_wait_timeout=2。
  • 套用前確認 e_meter_values 沒有既有或等待中的 metadata lock;無關的 CP2 sleeping transaction 沒有使用或鎖定此表,因此未列為阻擋條件。
  • Branch 與 CP3 container 全程未重啟,原 StartedAt 保持不變。
  • 交易 878 套用後仍為 ACTIVE、Connector 仍為 CHARGING,CP003 保持 OCPP ONLINE 且 Heartbeat 持續更新。

套用後 EXPLAIN ANALYZE 已改用新的複合索引:

查詢 修正前最差實測 修正後實測
最新能源讀值 依資料分布而異 0.371 ms
最早能源讀值 4,204 ms;掃描 1,691,077 筆 0.169 ms;索引直接定位 1 筆

套用後 CP003-005 同一採樣時間的 Current、Voltage、Power、Energy 四筆 MeterValues response 為 17、9、8、9 ms。Patch 重跑結果為 APPLIED_OR_ALREADY_APPLIED,索引欄位與順序維持一致。

部署證據保存在 A17:

/opt/ems/deployments/20260928-000111-redmine-1518-online-index/

此結果只代表 A17 已完成第一階段。其他案場仍須在各自部署時套用同一個共用 patch;第二~第五階段尚未部署,因此 Branch/CP 重啟仍延後到無現行交易的維護時段。

2026-09-28 第二~第五階段程式實作

第二至第五階段已在主要 workspace 完成程式與單一隔離 unit test,但尚未部署到 A17:

  • 外層 EMS repository branch:feature/redmine-1517-ocpp-liveness。
  • java/charge_point 是獨立 Git repository,也使用相同 branch 名稱;兩邊均直接在既有 workspace
    作業,沒有建立 worktree。
  • Branch 的 MeterValueService 改為每個 PDU 只解析一次 ChargePoint/Connector/Transaction,
    原始 sampledValue 仍逐筆保存,但整個 PDU 最多註冊一次 commit-after Connector refresh。
  • 新增 MeterValueConnectorEventDispatcher:OCPP transaction commit 後只排入不可變 ID/timestamp,
    由背景 transaction 重新載入 Connector;同 Connector 以 250ms debounce 合併、最多等待 2 秒,
    transport 發布最多嘗試 3 次並輸出 requested/coalesced/published/failed/retry 計數。
  • 即時 Voltage、Current、Power、Energy 改用
    (transaction_id, value_type, timestamp) 索引查各類型最新一筆,不再透過
    Transaction.meterValues 載入完整交易歷史。
  • 即時預估費用改為 process-local transaction summary:首次、Branch 重啟後或亂序讀值時才由完整
    ENERGY 歷史重建;正常新讀值只計算最後兩點的增量。此摘要不作帳務依據,StopTransaction、
    月結與人工重算仍使用完整資料庫歷史。
  • CP queue 依 ChargePoint/Connector/Transaction/timestamp 分組;同一時間點的
    Current、Voltage、Power、Energy 放入一個 OCPP 1.6 MeterValues.req,整組一起標記成功或重試。
    Branch parser 仍相容尚未升級、繼續分筆上報的 CP。

目前已通過 Branch 與 CP 的 mvn -DskipTests package,以及六個單一、無 Spring profile、
無外部服務、無資料庫的 unit test method。完整 module suite、隔離 DB integration 與部署後
OCPP/充電 E2E 仍須依測試計畫另行啟動;在 A17 現行交易結束及維護窗口前,不部署 JAR、不重啟服務。

2026-09-28 Astra 程式審查發現與待修正項目(交由 Sol 接手)

審查結論與適用範圍

本章五項均尚未修正。第二~第五階段已有程式實作,但審查未通過,目前不宜部署。 先前六個正常情境 Unit Test 與編譯通過,只代表該部分驗證成立,不能解讀為沒有回歸。本章補充並修正前述實作紀錄對「保序、亂序重建、不遺失事件」的完成判斷。

本次針對 #1518 的 Branch/CP MeterValues 效能改善修改,檢查新增機制是否破壞既有邏輯。R2~R5 已分別以單一隔離案例重現;R1 以發布至 HQ/前端的程式路徑與併發順序分析確認,尚未執行 MQ/現場 E2E。下列金額與失敗次數均為本機合成案例,並非 A17 實際帳單或現場事件。

本章不是新的 production 事故,也不否定 A17 已套用複合索引的效益。本章五項修完,亦不代表本票原有 StopTransaction 優先補送、重試耗盡後恢復及殭屍交易安全對帳等目標已全部完成,仍須分別追蹤。

接手位置與作業限制

  • 主要 workspace:/Users/ken/Documents/workspace/workspace_cloudlink/ems。
  • 外層 EMS repository 與內層獨立 java/charge_point repository 均使用 feature/redmine-1517-ocpp-liveness。
  • 審查對象是尚未提交的 working tree,包含新增且尚未追蹤的服務檔;不能只看 HEAD 或已追蹤檔案的 diff。
  • 直接在主要 workspace 接續,未經 Ken 允許不得建立 worktree,也不得使用 browser/computer-use。
  • Workspace 同時有 #1517 與其他修改;#1518 的修正、註解、測試及文件需依 ticket 一起整理。因為是兩個 Git repository,提交時各自建立 #1518 commit,不得混入其他 ticket 或無關修改。
  • 本章是交接說明,不授權對案場部署/重啟。現場充電中的服務維持運作,程式部署仍需依原有維護窗口安排。
  • #1517/#1518 共用 SQL patch 的 release 日期與跨案場套用規則沿用本票既有章節,不因這次審查或接手日期而重命名。

問題總覽

編號 子票 優先級 問題 證據 狀態
R1 #1519 P1 停止後可能收到舊充電中快照 程式路徑/併發順序分析 待修正及時序測試
R2 #1520 P1 增量計價繞過負增量調和而高估費用 0.18 元預期、0.21 元實際 已重現、待修正
R3 #1521 P1 補送舊讀值不使快取失效 0.45 元預期、0.00 元實際 已重現、待修正
R4 #1522 P2 pending 移除與 enqueue 競爭導致漏送 排程 2 次、只發布 1 次 已重現、待修正
R5 #1523 P2 CP 單組載入失敗中斷後續 Connector 上報 健康組預期送出 1 次、實際 0 次 已重現、待修正

以下路徑均以 EMS workspace 根目錄為基準;行號是審查當下的位置,修正後可能移動。修正方向是設計建議,驗收條件則是接手修正必須保留的行為。

R1 — P1:舊 MeterValues 快照可能晚於停止/狀態更新事件送出

位置:java/ems_branch/src/main/java/com/sylksoft/ems/branch/service/MeterValueConnectorEventDispatcher.java:244-251。

背景 dispatcher 重新載入 Connector 後,在同一個 read-only transaction 內組裝及發布快照。
但 TransactionService.stopTransaction() 與 StatusNotificationHandler 仍直接在各自 afterCommit
發布 ConnectorEvent,不經 dispatcher 的 per-connector 序列控制。

可發生的順序:背景工作先讀到 charging 狀態,組裝指標時延遲;此時 StopTransaction/
StatusNotification 提交並先送出停止後狀態;背景工作恢復後仍以先前載入的 entity 發出舊快照。
ConnectorEventPublisher 又在組裝完成時使用 LocalDateTime.now() 作 event timestamp,因此
舊快照甚至具有較新的發送時間。HQ subscriber 不檢查資料版本,LIFF store 直接合併收到的
connectorData,畫面可能回退為充電中或帶回舊操作狀態。

證據:靜態路徑及併發順序分析,未執行外部 MQ/現場 E2E。
建議:狀態與 meter refresh 共用 Connector 發布序列,或採用從 authoritative state 產生的
單調版本,讓所有接收端丟棄舊版本。只用「發送時間」不能解決。

修正後驗收條件:

  • 用可控制的 barrier/latch,讓 MeterValues 背景工作停在「已讀取舊狀態、尚未發布」,再完成 StopTransaction/StatusNotification;釋放背景工作後,HQ/LIFF 及 Admin 最終快照必須保持最新狀態。
  • 已完成交易的 currentTransactionId、driverState、可用操作不能被舊快照帶回;新交易開始後,也不能被前一交易的背景工作覆蓋。
  • 同一 Connector 的一般 refresh、重試與業務狀態事件都必須適用同一套保序/版本規則;不同 Connector 可獨立處理。
  • 背景事件不得重新阻塞 OCPP CALLRESULT 的同步路徑。

R2 — P1:增量計價繞過既有負增量調和,導致預估費用偏高

位置:java/ems_branch/src/main/java/com/sylksoft/ems/branch/service/LiveTransactionCostService.java:97-100;
service/billing/MeterValueTariffCalculationService.java:114-119。

原本完整計價會將 611010 → 611000 → 611070 Wh 的 10 Wh 短暫下降調和成 60 Wh。
新增量流程將負區段的金額視為 0,但仍把 cache 的前一筆讀值推進到 611000,下一段便計入
70 Wh。以每 kWh 3 元計算,既有流程為 0.18 元,新快取為 0.21 元。

證據:negative-spike 使用真實計價服務與真實 live service,只有資料來源是 mock。
預期一致的斷言失敗,已確認回歸。影響是即時 costPredict;本次沒有改寫正式月結主流程。
建議:增量狀態必須保存足夠的尾端點,沿用同 timestamp/負增量調和;不完整或異常區段不能
在忽略 issue 的同時永久推進已結算 cursor。重建後 cursor 也必須對應調和後的標準點。

修正後驗收條件:

  • 同樣的三筆讀值依序到達、一次補送,以及中途清除快取後重建,最終預估均須與完整計价一致:60 Wh、0.18 元。
  • 保留原本同 timestamp 調和、10 Wh 容許範圍、負增量、跨費率/跨日期區段與 Wh/kWh 換算規則。
  • 增量計算有 ERROR/PENDING_DATA 時,要有明確的重建/待補資料策略;不能把未能計價的區段當作已完整處理。
  • 正式帳務仍以既有完整歷史計算為準,不能為了讓測試通過而改掉原本調和規則。

R3 — P1:補送舊讀值無法使即時計費快取失效

位置:java/ems_branch/src/main/java/com/sylksoft/ems/branch/service/LiveTransactionCostService.java:70-86。

呼叫端只傳入按 meter timestamp 查到的最新 ENERGY。若後來補送較早的資料,最新讀值 ID
沒有改變,sameReading() 直接回傳舊總額。即使下一個新 timestamp 到達,range query 也只查
cache timestamp 之後,先前補送的區段仍被略過。沒有 ingest invalidation 通知可讓它重建。

重現:先收到 07:59:30 的 10000 Wh 與 08:01:00 的 10090 Wh;跨費率的缺漏區段原先不可計價。
再補入 08:00:00 的 10030 Wh 與 08:00:30 的 10060 Wh。完整計價已能算出 0.45 元,
但新快取仍回傳 0.00 元。

證據:late-reading 的一致性斷言失敗。
建議:成功 commit 的 ENERGY 寫入須通知 cache 新資料的時間範圍/版本;凡影響已計算區間,
就重建或局部重算。只比較最新一筆資料不能偵測历史插入或修正。

修正後驗收條件:

  • 執行本章 late-reading 案例,補齊資料後必須從 0.00 元更新為 0.45 元,即使最新 timestamp/ID 完全沒有改變。
  • 補送早於 cursor 的讀值、補送同 timestamp 修正值,以及舊資料與較新資料混在同一個 PDU,均須正確失效/重算。
  • 補送後再收到新週期資料,結果仍須與完整歷史計價一致;不能僅靠未來的新讀值碰巧觸發重建。
  • 快取失效應與成功 commit 對齊;資料回滾不能發布基於未提交資料的費用。

R4 — P2:pending 移除與新 enqueue 競爭,導致 refresh 靜默遺失

位置:java/ems_branch/src/main/java/com/sylksoft/ems/branch/service/MeterValueConnectorEventDispatcher.java:129-130。

computeIfAbsent() 取得 pending reference 與取得 pending monitor 不是原子操作。新 enqueue
可能先取得舊 pending,尚未進入 synchronized 時,dispatch 已完成並把該 pending 從 map 移除。
enqueue 隨後仍對舊物件排程;task 執行時因 map 不再含該物件,在 line 179 直接退出。

證據:enqueue-race 以 monitor 控制上述交錯,使用真實 dispatch 完成移除;成功建立兩個
task,實際只發布一次,第二個已提交更新遺失。無需外部服務或機率式壓測。
建議:map membership 與 pending lifecycle 必須一起原子管理,或取得 monitor 後重新驗證
membership 並重試取得當前物件;同時檢查失敗重試上限移除的分支。

修正後驗收條件:

  • 本章 enqueue-race 案例須從發布 1 次恢復為預期 2 次;第二次請求不能因舊 pending 被移除而消失。
  • 覆蓋發布成功移除、重試達上限移除,以及 publish 執行期間又收到新 request 三種交錯。
  • 保留同 Connector 合併、保序、最大等待時間的排程意圖與重試上限;不能用刪除版本檢查的方式讓舊工作反向覆蓋新工作。
  • 用可重現的同步控制測試,避免只靠隨機執行或長時間 sleep 判定。

R5 — P2:CP 單一 batch 載入失敗會中斷其餘 Connector 的上報

位置:java/charge_point/src/main/java/com/sylksoft/ocppmodbus/service/processor/queue/TransactionMessageQueue.java:299-310,
無交易 batch 的迴圈也有相同缺口。

原本每個 MeterValue 的重新載入/發送有獨立 try/catch;新 batch 的 loadInRequestedOrder()
移到例外隔離之外。任何 entity 讀取/轉換失敗會直接離開整輪 processMeterValuesQueue(),
後續健康 batch 及無交易資料不再處理,失敗 batch 也沒有進入 retry 計數流程。若同一筆壞資料
持續失敗,下一輪仍會卡在相同位置。

證據:cp-batch-failure 模擬第一組載入失敗、第二個 Connector 完全正常;原本應送出第二組
一次,實際 0 次,DataRetrievalFailureException 直接逸出。
建議:每組的 reload/send/retry 更新都需有明確例外隔離與記錄,不讓單一壞組阻斷後續組。

修正後驗收條件:

  • 第一組載入失敗、第二組正常時,第二個 Connector 仍能送出;同一輪無交易 MeterValues 也必須繼續處理。
  • 有交易與無交易 batch 兩條路徑都須覆蓋;單組失敗不得使整輪佇列永久卡住。
  • 整組成功才一起標記 sent;發送或資料處理失敗有可追蹤紀錄與適當 retry 行為,不能標記假成功。
  • 檢查非同步 callback 中更新 sent/retry 本身失敗的情境,避免例外只落在無人觀察的 CompletableFuture;此項為修正時一併驗證的相鄰路徑,不列為本次已重現的第六項缺陷。

重現工具、測試證據與執行方式

本機報告:test_report/20260928_002_redmine-1518-code-review.md。原實作報告 test_report/20260928_001_redmine-1518-implementation.md 已加上後續審查警示。

重現程式:

  • Branch:test_report/evidence/1518-review/Review1518.java。
  • CP:test_report/evidence/1518-review/Review1518Cp.java。
  • 執行器:test_report/evidence/1518-review/run_one.py。

在 EMS workspace 根目錄,每次執行一個命令:

python3 test_report/evidence/1518-review/run_one.py negative-spike
python3 test_report/evidence/1518-review/run_one.py late-reading
python3 test_report/evidence/1518-review/run_one.py enqueue-race
python3 test_report/evidence/1518-review/run_one.py cp-batch-failure

審查時以上四個命令分別執行,均因「預期正確行為與實際結果不一致」而以 AssertionError/exit code 1 結束,這是缺陷的重現證據,不是環境啟動失敗。測試以 mock 隔離 repository、scheduler 或 transport,使用真實 production 計價/dispatcher/queue 類別;沒有啟動 Spring context,也沒有連接共用 DB、RabbitMQ 或實機 CP。

執行器依賴各模組 target/surefire-reports 內既有 Unit Test XML 的 classpath 與 target/classes。修正 Java 後,必須先更新相應模組的編譯產物,不能直接測到舊 classes;若在另一個 workspace/乾淨環境接手,需先準備對應單一隔離測試的依賴與報告,或將重現案例移入正式 JUnit 測試。R2~R5 修正後應通過原本的正確性斷言,不能改掉預期值來消除失敗。

建議修正順序與交付條件

  1. 先處理 R1/R4 的 Connector 發布順序與 pending 生命週期,兩者共用 dispatcher,需一起檢視,避免解決漏送卻引入舊狀態覆蓋。
  2. 再處理 R2/R3 的計價狀態與失效策略,以既有完整歷史計價作為比較基準;正常增量、亂序補送、重複 timestamp、重建與跨費率邊界須一致。
  3. 處理 R5 的 CP batch 例外隔離,並驗證整組 sent/retry 更新與其他 Connector 的持續處理。
  4. 將四個現有重現案例納入正式回歸測試,補上 R1 的可控制時序案例;保留原有六個正常情境與舊版 CP 分筆上報相容性。
  5. 更新程式註解、測試報告及 E2E Catalog 的受影響案例。CHG-047/CHG-048 需補足異常與併發覆蓋,計價一致性缺口亦需正式追蹤;FAIL 未修正、未執行或 Skip 均不可寫為 PASS。
  6. 依專案 SOP,單一且不連外的隔離 Unit Test 可個別驗證;多案例/完整 suite、DB integration、MQ/OCPP E2E 須另列計畫,等 Ken 明確啟動。這次交接不等於授權開始所有測試或部署。
  7. 交付時逐一回報 R1~R5 的修改位置、根因如何消除、實際驗證結果及仍未驗證的範圍;在這些回歸問題修正前,不把第二~第五階段標示為可部署。

2026-09-27 現場暫時復原

為先恢復 CP003-005 的可用狀態,已依 CP 本機的 COMPLETED 交易資料,對 Branch 進行一次有條件的資料收斂:

  • Branch 交易 865:ACTIVE → COMPLETED
  • 補入 stop_timestamp=2026-09-25 05:11:42
  • 補入 meter_stop=2458770
  • 補入 reason=EVDisconnected
  • 補入 energy_consumed=33470
  • 補入 duration_seconds=17278
  • 設定 stop_message_sent=true
  • 清除 CP003-005 的 current_transaction_id

執行前 guard 同時確認:

  • CP003 OCPP ONLINE 且近期有 Heartbeat。
  • CP003-005 兩端狀態均為 AVAILABLE。
  • CP 本機 current transaction 為 NULL。
  • 最新電流為 0A、功率為 0W。
  • 交易 865 在 CP 本機已完整結束。
  • 沒有 SENT/ACCEPTED 的遠端操作。
  • 沒有可變動的輪充 queue record。

原始資料備份位於 A17 主機:

/opt/ems/backups/20260927-2307-cp003-005-tx865/

這次資料修復只解除現場阻擋,無法防止其他交易再次發生相同問題,仍須完成本票的永久修正。

與 #1517 的關係

  • #1517:Branch 的 OCPP 存活判斷,以及離線後 Connector 狀態恢復。
  • 本票:Start/StopTransaction 在斷線、backlog 與重試耗盡後的可靠補送與跨服務交易收斂。

兩者會在同一段 OCPP 重連流程交會,但資料模型、失敗後果與驗收案例不同,因此獨立追蹤。


子任務 5 (5 進行中 — 0 已結束)

Bug #1519: [Backend Bug][#1518-R1] Connector 快照跨事件來源失序,停止後可能回退為充電中In Progress2026-09-28動作
Bug #1520: [Backend Bug][#1518-R2] MeterValues 增量計費繞過負增量調和,預估費用偏高New2026-09-28動作
Bug #1521: [Backend Bug][#1518-R3] 補送舊 MeterValue 未使即時計費快取失效New2026-09-28動作
Bug #1522: [Backend Bug][#1518-R4] Connector dispatcher pending 競爭造成更新靜默遺失New2026-09-28動作
Bug #1523: [Backend Bug][#1518-R5] CP 單一 MeterValues batch 失敗會阻斷後續 ConnectorNew2026-09-28動作

是由 陳國瑋 於 4 天 前更新

是由 陳國瑋 於 4 天 前更新

是由 陳國瑋 於 4 天 前更新

是由 陳國瑋 於 4 天 前更新

是由 陳國瑋 於 4 天 前更新

2026-09-28 A17 已先完成不需重啟的第一階段改善:在線套用共用 patch patch_20260928_redmine_1518_meter_value_lookup_index.sql,以 ALGORITHM=INPLACE, LOCK=NONE 新增 idx_mv_transaction_type_timestamp (transaction_id, value_type, timestamp)。Branch/CP 未重啟,CP003-005 transaction 878 全程維持 ACTIVE/CHARGING,CP003 保持 OCPP ONLINE。

驗證結果:最早/最新能源讀值查詢均改用新索引,EXPLAIN ANALYZE 為 0.169 ms/0.371 ms;套用後 transaction 878 四種 MeterValues response 為 8~17 ms。Patch 重跑為 APPLIED_OR_ALREADY_APPLIED。

A17 證據:/opt/ems/deployments/20260928-000111-redmine-1518-online-index/。其他案場仍須在各自 deployment manifest 中套用同一共用 patch;第二~第五階段尚未部署,待無現行交易的維護時段再安排 Branch/CP 重啟。

是由 陳國瑋 於 4 天 前更新

是由 陳國瑋 於 4 天 前更新

是由 陳國瑋 於 4 天 前更新

已完成 #1518 MeterValues 效能改善第 2~5 階段的程式實作,尚未部署、未重啟 Branch/CP:

  1. CP 將同一 connector、transaction、timestamp 的 Current/Voltage/Power/Energy 合併成一個 MeterValues.req;整組收到 CALLRESULT 後一起標記成功,失敗時整組增加 retry。
  2. Branch 每個 MeterValues PDU 只查一次 ChargePoint/Connector/Transaction,仍逐筆保存原始 sampledValue;整包保存成功後只建立一次 Connector 更新工作。
  3. Connector snapshot 改在 DB commit 後由背景 scheduler 重新載入 authoritative state。相同 Connector 的短時間事件會 debounce/coalesce,最長 2 秒一定送出,transport 失敗最多嘗試 3 次,並記錄 requested/coalesced/published/failed/retry 計數。
  4. 即時 Energy/Voltage/Current/Power 改用 transaction_id + value_type + timestamp 複合索引查最新值,不再載入整筆 Transaction.meterValues。
  5. 進行中交易的畫面預估費用使用 process-local 增量摘要。首次、重啟或亂序資料才從完整 ENERGY 歷史重建;一般更新只查尚未處理的尾端 rows 並逐段計價,避免事件合併時跨過費率邊界。StopTransaction、月結與稽核仍使用完整 DB 歷史。

程式位置:

  • ems repository:feature/redmine-1517-ocpp-liveness
  • java/charge_point(獨立 repository):feature/redmine-1517-ocpp-liveness
    兩者均直接在主要 workspace 修改,未使用 worktree。因為是兩個獨立 Git repository,後續會各自建立一個 #1518 commit。

目前驗證:

  • Branch 與 CP 執行 mvn -DskipTests package:BUILD SUCCESS。
  • 6 個單一隔離 unit test method 全部通過,涵蓋 Branch 每 PDU 一次事件、背景合併、增量費用、索引最新值,以及 CP payload/queue 合併。
  • E2E Catalog 新增 CHG-047、CHG-048,validator 通過(181 cases)。

依專案測試 SOP,完整 unit suite、獨立 DB integration、production-size performance 與部署後 E2E 尚待列出計畫並由 Ken 明確啟動。現在不會對正在充電的案場做部署或重啟。

是由 陳國瑋 於 3 天 前更新

是由 陳國瑋 於 3 天 前更新

  • 子任務 #1519 已新增

是由 陳國瑋 於 3 天 前更新

  • 子任務 #1520 已新增

是由 陳國瑋 於 3 天 前更新

  • 子任務 #1521 已新增

是由 陳國瑋 於 3 天 前更新

  • 子任務 #1522 已新增

是由 陳國瑋 於 3 天 前更新

  • 子任務 #1523 已新增

是由 陳國瑋 於 3 天 前更新

是由 陳國瑋 於 3 天 前更新

已將 Astra 審查的五項待修正問題拆成 #1518 子票,便於分別追蹤修正與驗收:

  • #1519(R1,High):Connector 快照跨事件來源失序,停止後可能回退為充電中
  • #1520(R2,High):MeterValues 增量計費繞過負增量調和,預估費用偏高
  • #1521(R3,High):補送舊 MeterValue 未使即時計費快取失效
  • #1522(R4,Normal):Connector dispatcher pending 競爭造成更新靜默遺失
  • #1523(R5,Normal):CP 單一 MeterValues batch 失敗會阻斷後續 Connector

每張子票均包含問題說明、影響、相關程式、建議修正、驗收條件與重現方式。#1518 description 的問題總覽也已回填子票編號。

建議處理順序:#1519 與 #1522 一起設計 Connector 發布順序;接著處理 #1520 與 #1521 的即時計費一致性;最後處理 #1523 的 CP batch 例外隔離。五項修正完成前,第二~第五階段維持不可部署。

動作

匯出至 Atom PDF