Skip to content

feat(relay-loop-rewrite): 依 DESIGN-03「relay 與迴圈」改寫 outbox relay(搶列→交易外整批送出→小交易條件式回寫)、迴圈小交易與滿批續做、broker 逾時與關機設定 - #99

Merged
Marcus778899 merged 4 commits into
developfrom
feat/relay-loop-rewrite
Oct 22, 2026
Merged

Marcus778899 merged 4 commits into
developfrom
feat/relay-loop-rewrite

Conversation

@Marcus778899

Copy link
Copy Markdown
Owner

Closes #94

派工單:tasks/relay-loop-rewrite.md(負責:backend)

Worker 自驗:cd api && ./mvnw -q spotless:check && ./mvnw -B verify → exit 0

變更檔案:

  • api/src/main/java/com/cdp/common/fault/FaultPoint.java
  • api/src/main/java/com/cdp/delivery/DeliveryConsumer.java
  • api/src/main/java/com/cdp/delivery/DeliveryListenerConfig.java
  • api/src/main/java/com/cdp/delivery/DeliveryProperties.java
  • api/src/main/java/com/cdp/delivery/LoopProperties.java
  • api/src/main/java/com/cdp/delivery/LoopStartupValidator.java
  • api/src/main/java/com/cdp/delivery/OutboxStore.java
  • api/src/main/java/com/cdp/delivery/relay/OutboxRelay.java
  • api/src/main/java/com/cdp/delivery/relay/RelayProperties.java
  • api/src/main/java/com/cdp/engine/runtime/AdvanceLoop.java
  • api/src/main/java/com/cdp/ingest/EventDispatcher.java
  • api/src/main/java/com/cdp/persistence/JdbcOutboxStore.java
  • api/src/main/resources/application.yml
  • api/src/test/resources/application-test.yml
  • api/src/test/java/com/cdp/delivery/DeliveryConsumerTest.java
  • api/src/test/java/com/cdp/ingest/EventDispatcherBackoffTest.java
  • api/src/test/java/com/cdp/ingest/EventDispatcherUnparsableRowTest.java

@Marcus778899

Copy link
Copy Markdown
Owner Author

QA:PASS(commit 921c51d)

PASS 921c51d

QA relay-loop-rewrite(B 驗證,HEAD 921c51d)

實際執行

  1. docker compose --env-file .env -f .worktrees/relay-loop-rewrite/compose.yaml run --rm test
    • 結果:Tests run: 204, Failures: 0, Errors: 0, Skipped: 0 / BUILD SUCCESS
    • 驗收測試:relayloop.RelayRewriteAcceptanceTest 7、RelayStuckTimersAcceptanceTest 1、RelayStartupValidationAcceptanceTest 5,全綠
    • 既有 acceptance/delivery:DeliveryPipelineAcceptanceTest 12、DeliveryResilienceAcceptanceTest 5,全綠;其餘 acceptance(ingest、engine、security、query、schema、common)全綠
    • 輸出中的 "APPLICATION FAILED TO START" 4 次來自啟動驗證的負向測試(DispatchPropertiesStartupAcceptanceTest 等),該些測試通過。
  2. cd api && ./mvnw -o -q spotless:check -> exit 0
  3. npm run lint -w web(eslint . && tsc --noEmit)-> exit 0(前端測試沒動,未跑)

驗收測試 lock

  • git diff --stat f6a908a HEAD -- api/src/test/java/com/cdp/acceptance/ 無輸出:自仲裁後 lock(f6a908a)起沒被改。
  • worker 分支對 acceptance 的變更只有 qa 的 111f981、f6a908a。

改動範圍

git diff --name-only 4aa0430 HEAD 全在負責檔案內:delivery/、AdvanceLoop、EventDispatcher、JdbcOutboxStore、common/fault/FaultPoint、application.yml、application-test.yml、對應單元測試(DeliveryConsumerTest、EventDispatcherBackoffTest、EventDispatcherUnparsableRowTest),其餘為 qa 的驗收測試。沒有 migration、web/、docs/、.env.example 變更。

驗收條件逐條

  1. OutboxStore:有 claimDue/markPublished/reschedule(boolean)/cancelPendingByEnrollment/ByJourney(另有 markPublishedAll/rescheduleAll 批次版),無 lockDue(OutboxStore 內已移除);relay 交易外 convertAndSend+CorrelationData、統一等 confirm、小交易 batchUpdate;FaultPoint 有 RELAY_BEFORE_WRITEBACK。通過(行為由 RelayRewriteAcceptanceTest 驗)。
  2. AdvanceLoop、EventDispatcher 每筆一交易、失敗者略過;滿批續做至 CDP_LOOP_MAX_DRAIN(3 倍批量一個 tick 清空之測試通過)。通過。
  3. spring.task.scheduling.pool.size=${CDP_SCHEDULER_POOL_SIZE}、connection-timeout=${RABBITMQ_CONNECTION_TIMEOUT}、shutdown-timeout=${DELIVERY_SHUTDOWN_TIMEOUT};LoopStartupValidator 驗 pool >= 迴圈數、lease > confirm 逾時(<= 即拒絕啟動);consumer 遇 outbox CANCELLED 寫 delivery_task.CANCELLED。測試 RelayStartupValidationAcceptanceTest 通過。通過。
  4. 驗收情境(兩處中斷恰好一則、lease 過期重發、遲到 confirm 不覆寫 publish_count=1、relay 卡住計時器照常、3 倍批量一 tick 清空):RelayRewriteAcceptanceTest/RelayStuckTimersAcceptanceTest 全綠。通過。
  5. 驗收與既有測試全綠:204/204。通過。

設定值

application.yml 新增項目全為 ${VAR},無預設值、無寫死;新變數 OUTBOX_RELAY_CONFIRM_TIMEOUT=PT5S、OUTBOX_RELAY_LEASE=PT30S、RABBITMQ_CONNECTION_TIMEOUT=PT10S、DELIVERY_SHUTDOWN_TIMEOUT=PT30S、CDP_SCHEDULER_POOL_SIZE=4、CDP_LOOP_MAX_DRAIN=PT10S 都在 .env.example(D-047 建議值)。main 程式未新增寫死 port/網址/token/逾時(EventDispatcher 的 LATE_THRESHOLD 24h 為既有常數)。application-test.yml 的值是測試假值,屬測試設定。

open_questions 的偏離是否違反驗收條件

  • lockDue(StateStore/EventStore)無排除清單、改取 failed+1 列略過:驗收條件的 lockDue 是指 OutboxStore,已移除;「本輪失敗者略過」行為達成,不違反。介面是否加排除參數由 review 判斷(副作用:失敗列在該小交易內被鎖住)。
  • LoopProperties 綁 CDP_LOOP_MAX_DRAIN:變數存在且生效,不違反;歸屬由 review 判斷。
  • 保留 rabbit.invoke(同一 channel 送出並等 confirm,仍在 DB 交易外):驗收條件寫「交易外整批 convertAndSend+CorrelationData,一起等 confirm」,invoke 不影響此行為,且 DeliveryResilience 全綠;不違反條件,與設計文字不同,由 review 判斷。
  • 迴圈數常數 3:驗收「啟動驗證 ≥ 迴圈數」達成;v0.3.0 加 JourneyStopSweeper 須改為 4(風險記錄,不是本單 FAIL)。

備註

  • 編譯警告:OutboxRelay 使用 CorrelationData.Confirm.isAck()/getReason(),已標 deprecated for removal(非失敗)。

FAIL 項目

無(0)。

@Marcus778899

Copy link
Copy Markdown
Owner Author

Code review(architect):APPROVE(commit 921c51d)

APPROVE 921c51d

Review PR #99(relay-loop-rewrite)

檢查了什麼

項目 結果
改動範圍(gh pr diff 99) main 程式全在負責檔案內(delivery/、AdvanceLoop、EventDispatcher、JdbcOutboxStore、common/fault/、兩個 yml);acceptance/ 的變更只有 qa 的 111f981、f6a908a(仲裁後 lock);無 migration、web/、docs/、.env.example
spec 07 §12「relay」 搶列:單一 UPDATE … WHERE key IN (SELECT … FOR UPDATE SKIP LOCKED) RETURNING,available_at = lease_until(截到微秒,fencing 比對正確);發送在 DB 交易外;回寫一個小交易 batchUpdate,條件 status='PENDING' AND available_at = lease;失敗 available_at = now + RETRY_DELAY;影響 0 列丟棄並記 log;取消 PENDING → CANCELLED;consumer 讀到 outbox CANCELLED 寫 delivery_task.CANCELLED
DESIGN-03「relay 與迴圈」「設定」 OutboxStore 介面、RELAY_BEFORE_WRITEBACK、每筆/每事件一個交易、三迴圈滿批續做、pool size、connection/shutdown timeout、lease > confirm 啟動驗證都有;六個變數全為 ${VAR},無寫死值;舊的 CONFIRM_TIMEOUT_MILLIS = 5000 常數已移除
交易邊界 DefaultEnrollmentAdvancer.advance、EngineJourneyRuntime.start 仍為 NESTED,在每筆的 TransactionTemplate 內失敗只回滾 savepoint、外層正常提交,不會 UnexpectedRollbackException
失敗與中斷路徑 RELAY_BEFORE_PUBLISH/RELAY_BEFORE_WRITEBACK 中斷:不回寫、維持 lease,到期重搶;convertAndSend 例外後同批其餘列直接列失敗(不逐列等逾時);invoke 本身例外:已送出的列保留 lease;clean=false 即停止續做,broker 異常時不空轉
測試 CI Check pass;qa 證據 204/204(含 acceptance/relay-loop-rewrite/ 13 個、acceptance/delivery/ 17 個);驗收情境對應派工單驗收條件 4

worker 偏離判定(都可接受,不阻擋)

# 偏離 判定 理由 後續
1 保留 rabbit.invoke,整批送出與等 confirm 包在一個 invoke 內 接受 設計要的是「不在 DB 交易內、整批送出一起等 confirm」,這兩點都做到;invoke 只決定共用同一 channel,不是 DB 交易。每則各取 channel 會在 broker 暫停時卡新 channel 開啟,DeliveryResilienceAcceptanceTest 全綠佐證 改設計文件(下方 A)
2 StateStore.lockDue/EventStore.lockUndispatched 無排除清單,改取 failed+1 列略過失敗者 接受 正確性沒問題(排序固定,F+1 列至少含一筆未失敗者)。EventDispatcher 失敗時 scheduleRetry 把 next_dispatch_at 往後移,失敗列下一筆就查不到,實際不會累積。AdvanceLoop 失敗列 next_wake_at 不變、排在最前面:同一 tick 有 F 筆失敗時每個交易要讀並鎖 F+1 列,大量系統性失敗時會變成平方成本(受 CDP_LOOP_MAX_DRAIN 限制,比改版前「整批失敗卡住」好)。原因是派工單沒把 StateStore/JdbcStateStore 列進負責檔案 補單 B1;改設計文件(A)
3 CDP_LOOP_MAX_DRAIN 綁在新的 com.cdp.delivery.LoopProperties(cdp.loop.max-drain) 接受 三個迴圈共用的設定獨立成一個 record,比塞進 EngineProperties(engine 專用)合理;變數名稱不變。缺點:engine.runtime、ingest 因此依賴 com.cdp.delivery 套件,套件位置不理想(ArchitectureTest 有過) 補單 B2(移到 com.cdp.common,可併入下一張碰到這些檔案的單);改設計文件(A)
4 LoopStartupValidator.LOOP_COUNT = 3 接受 本版確實三個迴圈;.env.example 預設 4 本來就留了第四條 JourneyStopSweeper 那張單須把常數改 4 並補驗收(B3);改設計文件(A)
5 CorrelationData.Confirm.isAck()/getReason() deprecated for removal 接受 目前不影響行為 補單 B4:改用替代 API(ack()/reason(),以所用 Spring AMQP 版本的 Javadoc 為準)

非阻擋觀察

  • OutboxRelay.sendAndConfirm:confirm 截止時間在全部送出「之後」才起算,總耗時 = 送出時間 + confirmTimeout;broker 觸發 connection.blocked 時 convertAndSend 可能長時間阻塞,回寫會因 lease 過期而 0 列、由下一輪重發。fencing 與 consumer 冪等保住正確性,「relay 卡住時計時器照常推進」也由獨立執行緒池保住;記入 QA-06 壓測觀察項即可,不需改。
  • writeBack 把 Statement.SUCCESS_NO_INFO 當成功只影響回傳筆數與 log,不影響 DB 狀態。
  • LoopStartupValidator、DeliveryListenerConfig 沒有 worker 自己的單元測試,靠驗收測試覆蓋;CI 覆蓋率閘門已過。

A. 需要改的設計文件(manager 派 architect 改)

檔案 位置 改成
docs/design/DESIGN-03.md 「relay 與迴圈」OutboxRelay.runOnce 列 「拿掉 rabbit.invoke」改為「整批送出與等 confirm 包在一個 rabbit.invoke(同一 channel,仍在 DB 交易外;每則各取 channel 在 broker 暫停時會卡新 channel 開啟)」
docs/design/DESIGN-03.md 「執行緒與小交易」列 寫明 v0.2.0 現況:lockDue(now, 失敗數+1) 略過本輪失敗者;排除參數版本見補單 B1;CDP_LOOP_MAX_DRAIN 綁 cdp.loop.max-drain(LoopProperties)
docs/design/DESIGN-03.md 做法 #7(JourneyStopSweeper,第四個迴圈) 加「LoopStartupValidator.LOOP_COUNT 改 4」
docs/design/DESIGN-03.md 派工表第 2 列驗收目錄 delivery/ 改 relay-loop-rewrite/(仲裁已提)

B. 建議補單(非阻擋)

# 內容 負責檔案
B1 StateStore.lockDue(now, limit, Set<String> exclude)(enrollment_id <> ALL(:exclude)),AdvanceLoop 改用;EventStore 可不動(已有退避) engine/StateStore.java、persistence/JdbcStateStore.java、AdvanceLoop.java
B2 LoopProperties 移到 com.cdp.common(或 com.cdp.common.loop) delivery/LoopProperties.java 與三個迴圈、LoopStartupValidator
B3 併入 JourneyStopSweeper 單:LOOP_COUNT 改 4、啟動驗證驗收補 pool=3 拒絕啟動 delivery/LoopStartupValidator.java
B4 換掉 Confirm.isAck()/getReason() deprecated API delivery/relay/OutboxRelay.java

@Marcus778899
Marcus778899 merged commit e02cd84 into develop Oct 22, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant