在 Temporal 裡,我們會把流程封裝成一個 Workflow。
但當流程越來越複雜時,我們會發現:
如果全都只靠 Activity,Workflow 會很快膨脹,並且難以維護;
這時候,Child Workflow 就可以派上用場,讓複雜流程可以結構化的拆分,保持簡單。
使用時機 | 具體情境 | 為什麼用 Child Workflow | 不用會怎樣 | 設計思維 |
---|---|---|---|---|
分離服務 / Worker 池 | 支付與訂單分流到不同 Task Queue/團隊 | 子流程可掛在不同 Task Queue / Worker,不跟父流程搶 CPU、IO 資源 | 所有任務擠在同一個 Worker,造成延遲或卡死 | 父流程只協調,子流程各自跑,分散資源消耗 |
拆大量工作 | 1 萬筆報表/批次同步需要 fan-out/fan-in | 父流程 Event History 有上限,長期任務會導致歷史膨脹 | 父流程歷史過大 → 重播慢、記憶體暴增 | 把子任務拆成多個子流程,降低單一流程的歷史負擔 |
資源唯一對應 | 以商品 ID/host 為單位序列化更新 | 一個子流程守護一個資源(如商品 ID、host),確保操作序列化 | 多個操作同時打到父流程,容易出現亂序或 race condition | 以 resourceId 當子流程 ID,天然序列化 |
週期性邏輯 | 每日對帳/外部狀態輪詢/定時同步 | 子流程可用 Continue-As-New 做週期性任務,不讓父流程變肥 | 父流程歷史不斷累積,最後變成「巨無霸」 | 把週期性工作獨立成子流程,靠續跑保持輕量 |
workflowId
可避免重複啟動相同子流程。。Task Queue
。child.result()
(同步等待完成)child.started
(確保已啟動),之後並行處理其他工作ParentClosePolicy
決定當父流程關閉時對子流程的處置。操作 | 情境 | 最佳實踐 | 注意事項 |
---|---|---|---|
REQUEST_CANCEL | 父流程關閉時對子流程送出取消請求,給子流程時間進行補償或清理 | 預設優先;實作取消/補償路徑;設定監控避免長尾取消中流程 | 子流程需實作取消處理;未妥善處理可能佔用資源或留下不一致 |
ABANDON | 父流程關閉不影響子流程,子流程持續執行;適合可獨立完成的分析/延遲任務 | 僅用於可獨立完成任務;加上 Search Attributes;定期掃描並收斂孤兒 | 容易產生孤兒工作;務必搭配觀測(Search Attributes、警報)與收斂機制 |
TERMINATE | 立即終止子流程;用於必須立刻停止或確定不需收尾的情境 | 僅在確定無需收尾或外部自動回滾時使用;文件化副作用;定期演練 | 不給清理時間,可能留下外部副作用或半完成狀態 |
以下展示使用 Java SDK 啟動、等待子流程的常見寫法。
import io.temporal.workflow.SignalMethod;
import io.temporal.workflow.WorkflowInterface;
import io.temporal.workflow.WorkflowMethod;
@WorkflowInterface
public interface ChildWorkflow {
@WorkflowMethod
ChildResult execute(ChildArgs args);
@SignalMethod
void expedite(String reason);
}
@WorkflowInterface
public interface ParentWorkflow {
@WorkflowMethod
void execute(String orderId);
}
import io.temporal.api.common.v1.WorkflowExecution;
import io.temporal.api.enums.v1.ParentClosePolicy;
import io.temporal.common.RetryOptions;
import io.temporal.workflow.Async;
import io.temporal.workflow.ChildWorkflowOptions;
import io.temporal.workflow.Promise;
import io.temporal.workflow.Workflow;
import java.time.Duration;
public class ParentWorkflowImpl implements ParentWorkflow {
@Override
public void execute(String orderId) {
ChildWorkflowOptions options = ChildWorkflowOptions.newBuilder()
.setWorkflowId("order:" + orderId + ":payment") // 指定子流程 WorkflowId,利於冪等與追蹤
.setTaskQueue("payments") // 子流程由哪個 Task Queue 的 Workers 執行
.setParentClosePolicy(ParentClosePolicy.PARENT_CLOSE_POLICY_REQUEST_CANCEL) // 父流程關閉時對子流程送出取消請求
.setRetryOptions(RetryOptions.newBuilder()
.setInitialInterval(Duration.ofSeconds(10)) // 重試初始間隔
.setBackoffCoefficient(2.0) // 指數退避係數
.setMaximumAttempts(10) // 最大嘗試次數
.build()) // 建立子流程重試設定
.build(); // 建立子流程選項
ChildWorkflow child = Workflow.newChildWorkflowStub(ChildWorkflow.class, options); // 建立子流程型別化 stub(尚未啟動)
// 啟動子流程並取得「已啟動」的保證
Promise<WorkflowExecution> started = Workflow.getWorkflowExecution(child);
Promise<ChildResult> resultPromise = Async.function(child::execute, new ChildArgs(orderId));
// 僅等待子流程已啟動,之後可並行做其他事
started.get();
// 對子流程發送訊號
child.expedite("VIP customer");
// 視需求可取消:取消 scope 或透過 stub 取消
// WorkflowStub.fromTyped(child).cancel();
// 等待子流程結果(fan-in 可等待多個 Promise)
ChildResult result = resultPromise.get();
// 使用 result 繼續後續步驟
}
}
import io.temporal.workflow.Workflow;
import java.time.Duration;
public class ChildWorkflowImpl implements ChildWorkflow {
private boolean expedited;
@Override
public void expedite(String reason) {
this.expedited = true;
}
@Override
public ChildResult execute(ChildArgs args) {
// 模擬長流程
Workflow.sleep(Duration.ofHours(1));
// 根據 expedited 調整流程邏輯或參數
return new ChildResult(true, expedited);
}
}
public record ChildArgs(String orderId) {}
public record ChildResult(boolean ok, boolean expedited) {}
Workflow.newChildWorkflowStub
+ Async.function
啟動子流程;用 Workflow.getWorkflowExecution(child)
等待「已啟動」。@SignalMethod
直接呼叫 child.expedite(...)
發送訊號。WorkflowStub.fromTyped(child).cancel()
(依實作需求選擇)。Task Queue
:尖峰資源爭奪,父流程決策飢餓。workflowId
冪等與重用未管理:重複啟動同一子流程。透過 Child Workflow,把大型流程切割成可治理、可伸縮、可觀測的子單元,讓整體系統更穩健、可維運且易於演進。