背景工作
了解 Reaction 非同步命令、非同步記錄與流量統計三條佇列的職責、重試方式及排查入口。
Saavo 使用 Cloudflare Queues,處理不適合阻塞目前請求的工作。專案內建三條用途明確的佇列,分別處理業務事件產生的非同步 Command、非同步記錄,以及第一方流量統計事件。
Reaction 不是一條「萬用非同步佇列」。Event 代表已發生的業務事實,Command 代表系統接下來要執行的動作。每個 Command 自行決定採用同步還是非同步執行。
警告
不要將自訂事件放入這些預先定義的佇列,它們各有自己的功能與處理邏輯。如果業務需要非同步執行,可以使用 Reaction 的 Event / Command 機制,或另建一條非同步業務處理佇列。
內建佇列
| 佇列 | Binding | 預設批次與並行數 | 用途 |
|---|---|---|---|
async-policy-task | ASYNC_POLICY_TASK_QUEUE | 每批 1 筆,並行數 1 | 執行 Reaction 非同步 Command |
async-logger | ASYNC_LOGGER_QUEUE | 每批最多 50 筆,並行數 1 | 將記錄寫入 KV |
analytics-events | ANALYTICS_QUEUE | 每批最多 8 筆,並行數 1 | 將第一方統計事件寫入 ANALYTICS_DB |
三個取用者的 max_retries 目前都是 3,平台設定允許首次投遞後,最多再投遞 3 次,也就是最多嘗試取用 4 次。但取用者可以提前確認訊息並停止重試。Reaction 取用者依最多 4 次投遞處理,統計與記錄取用者則會在第 3 次處理失敗時,主動確認訊息並記錄錯誤或警示。
wrangler.jsonc 同時設定了佇列名稱、Binding 與 *_QUEUE_NAME。入口會依實際佇列名稱精確選擇取用者。名稱不一致時,訊息不會自動改由其他佇列處理。
Reaction 命令如何執行
業務程式碼透過 reaction.processor.emit() 發出 Event。處理器會先將 Event 與 Command 執行紀錄寫入儲存體,再依序執行:
mode: 'sync':在目前請求、Webhook 或排程入口中,立即嘗試執行。mode: 'async':將 Event 執行 ID 傳送至async-policy-task,由取用者繼續執行。
目前同步 Command 主要用於授予或撤回角色與使用權益。非同步 Command 則包括寄送郵件、傳送通知、建立站內通知、同步付款客戶的電子郵件地址、處理到期權益週期,以及清理過期資料。
同步只表示「目前呼叫流程會執行它」,不代表失敗一定會變成例外。Command 回傳的一般失敗會寫入執行紀錄,emit() 仍可能回傳 accepted。如果呼叫端必須確認某項 Command 已成功,除了檢查 Event 是否已接收,也要檢查回傳的 Command 狀態,或讀取後台執行紀錄。
管理後台提供兩個排查入口:
/dashboard/reaction/events
/dashboard/reaction/commands重試、去除重複與失敗處理
Event 使用穩定的 idempotencyKey 去除重複。再次發出相同 Event 類型與等冪鍵時,處理器會回傳 duplicate,不會建立第二套執行紀錄。
每個 Command 也有自己的 maxAttempts,預設通常為 3。它控制 Command 處理函式的業務重試,Cloudflare 的 max_retries 則控制佇列訊息重新投遞。這是不同層級的重試,排查時不要混淆。
佇列採用至少一次投遞,訊息可能重複送達。Reaction 透過已儲存的狀態、穩定的執行 ID 與租用機制,減少重複執行。但若涉及寄信、呼叫第三方 API 等外部副作用,Command 仍應使用穩定的業務鍵實作等冪性。
目前 async-policy-task 沒有設定死信佇列。訊息在最後一次投遞仍失敗時,取用者會確認訊息,並發出高優先順序警示,之後需要人工檢查執行紀錄。如果外部副作用已完成,結果狀態卻無法寫入儲存體,取用者也會停止重新投遞並發出警示,避免重複執行該副作用。
成功加入佇列,不代表工作已完成
emit() 回傳 accepted,表示 Event 與 Command 已被接受並開始分派,非同步 Command 此時可能仍在排隊。回傳 abandoned 則表示多次嘗試發出 Event 後,仍未可靠地完成接收,呼叫端不能將它視為成功。
在業務程式碼中觸發事件
業務功能應使用 Reaction,不要直接向 ASYNC_POLICY_TASK_QUEUE 傳送自訂 JSON:
import { reaction } from '@/core/reaction';
const result = await reaction.processor.emit(
workerCtx,
SomeEvent.create({
idempotencyKey: 'some-event:stable-business-id',
payload: { /* 業務資料 */ },
}),
);emit() 可能回傳下列結果:
| 結果 | 意義 |
|---|---|
accepted | 新 Event 已寫入儲存體,並開始執行或加入佇列 |
duplicate | 相同的等冪事件已存在,回傳原本的執行紀錄 |
ignored | Event 沒有產生任何 Command |
abandoned | 發出流程最終失敗,已寫入記錄與警示 |
呼叫端是否需要檢查這些結果,取決於業務以什麼條件判定完成。註冊授權、付款權益等重要流程,不能只確認函式已回傳,也需要確認對應同步 Command 的狀態。單純通知類的非同步工作,則可以在可靠地加入佇列後,結束目前請求。
HTTP 路由使用 resolveFetchWorkerCtx(c) 取得執行脈絡。Cron 與佇列入口,則使用各自執行環境已建立的 WorkerCtx。應用程式碼不要直接呼叫 createWorkerCtx。
選擇適合的處理方式
- 業務事實與後續動作:定義 Reaction Event / Command。
- 頁面造訪與產品事件追蹤:使用流量統計 API,由
analytics-events處理。 - 應用程式記錄:使用既有 logger,由
async-logger處理。 - 與既有業務無關的長時間運算或通用工作:個別評估執行平台,不要直接放入
async-policy-task。
config/deploy.ts 中的 logger.enableAsyncLoggerQueue 預設啟用。關閉後,記錄不再進入非同步記錄佇列,取用者收到遺留訊息時,會確認並捨棄。完全停用流量統計後,統計取用者也會確認並捨棄已排隊的事件。切換這些開關前,應先評估是否允許捨棄累積的訊息。
沒有量測資料時,不要任意提高批次大小或並行數。async-policy-task 維持每批一筆、並行數一,是為了依執行紀錄順序與租用機制,安全地推進業務 Command。
上線檢查
- 三條佇列的名稱、Binding 與
*_QUEUE_NAME完全一致。 - 產生者與取用者已部署至同一套環境設定。
- 註冊、測試購買與訂閱結束後,Reaction 執行紀錄符合預期。
- 新 Event 使用穩定的業務等冪鍵。
- 外部副作用可以安全重試,不依賴「佇列只會投遞一次」的假設。
- 重要呼叫端會辨識
abandoned,並依業務要求檢查同步 Command 狀態。 - 關閉記錄或統計開關前,已確認可以捨棄佇列中的遺留訊息。
- 記錄與警示管道可以收到最終取用失敗、結果儲存失敗的通知。
常見問題
接下來
- 設定排程觸發:排程工作
- 了解第一方統計寫入:流量統計
- 查看付款與權益流程:付款與方案、權限與使用權益
- 查閱已註冊的 Event 與 Command:Reaction Event 與 Command