iT邦幫忙

2025 iThome 鐵人賽

DAY 21
0
Software Development

Temporal 開發指南:掌握 Workflow as Code 打造穩定可靠的分散式流程系列 第 21

Day21 - Temporal Cancel:設計能被中途喊停的流程

  • 分享至 

  • xImage
  •  

1. 為什麼需要 Cancel 機制?

在長生命週期的流程中,有可能退單、有可能取消審核、外部系統可能逾時或條件失效,所以「取消」不是例外,是日常需求。

使用 Cancel 可以讓 Workflow 觸發補償邏輯、終止執行並留下完整的事件紀錄,確保流程在中途停止時,系統狀態仍保持一致與可追蹤。

2. 如何發動 Cancel?

  • 從外部 Client 對 Workflow 發出取消請求
  • Workflow 內部在等待 Activity、Timer、Child Workflow 時會收到取消訊號
  • 你可以在 Workflow 端用 CancellationScope 捕捉取消,做善後清理
  • 在 Activity 端,也要透過 ActivityExecutionContext 或 Heartbeat 感知取消

3. 程式碼範例

3.1 設計取消範圍

在 Workflow 內,要把「可能被取消」的區塊包在 CancellationScope,並在捕捉到取消時進行清理。
清理動作則放在「不受取消影響」的區塊,用 Workflow.newDetachedCancellationScope(...)

import io.temporal.failure.CanceledFailure;
import io.temporal.workflow.CancellationScope;
import io.temporal.workflow.Workflow;

public class MyWorkflowImpl implements MyWorkflow {
  private final MyActivities activities = Workflow.newActivityStub(
      MyActivities.class,
      ActivityOptions.newBuilder()
          .setStartToCloseTimeout(Duration.ofMinutes(5))
          .build());

  @Override
  public void run(String input) {
    // 先將要執行的操作放至可取消的範圍 CancellationScope
    CancellationScope scope = Workflow.newCancellationScope(() -> {
      activities.runningStep(input);
    });

    // ... 其他計算或流程
    
    try {
      // 開始執行,外部觸發 cancel 時,則拋出 CanceledFailure
      scope.run();
    } catch (CanceledFailure e) {
      // 用 detached scope 做清理,避免清理也被取消打斷
      Workflow.newDetachedCancellationScope(() -> {
        activities.cleanup(input);
      }).run();
      // 視需求:可以選擇往上拋出,讓整個 Workflow 以 Canceled 結束
      throw e;
    }
  }
}

3.2 發動取消

從 Client 取消一個正在跑的 Workflow

public class ClientCancelExample {
  public static void main(String[] args) {
    WorkflowServiceStubs service = WorkflowServiceStubs.newLocalServiceStubs();
    WorkflowClient client = WorkflowClient.newInstance(service);

    // 建立 typed stub
    MyWorkflow myWorkflow = client.newWorkflowStub(
        MyWorkflow.class,
        WorkflowOptions.newBuilder()
            .setTaskQueue("demo-queue")
            .setWorkflowId("wf-cancel-demo")
            .build());

    // 啟動流程
    WorkflowClient.start(myWorkflow::run, "payload");

    // 發出取消請求
    WorkflowStub workflowStub = WorkflowStub.fromTyped(myWorkflow);
    workflowStub.cancel();
  }
}

4. 程式碼重點

4.1 可被取消的區塊

CancellationScope:把「可被取消」的一段邏輯包起來;當 client.cancel() 時,這段會收到取消訊號並拋出 CanceledFailure。

Workflow.newCancellationScope(() -> {
    pay.charge(orderId);
    Workflow.sleep(Duration.ofMinutes(30));
}).run();

4.2 不想被取消的關鍵區塊

如有「不想被取消打斷」的邏輯,可用 Workflow.newDetachedCancellationScope(() -> { ... }) 把它包起來,讓它不被上層取消波及(例如關鍵的補償步驟)。

Workflow.newDetachedCancellationScope(() -> {
    refund(orderId); // 即使外部 cancel,這段也會完整執行
}).run();

4.3 發動取消

WorkflowStub workflowStub = WorkflowStub.fromTyped(workflow);
workflowStub.cancel(); // 發出取消請求,Workflow 收到後觸發上述 catch

4.4 實務建議

  • 外部呼叫務必設定 timeout:避免 Activity 卡死;也要讓取消能「很快」回來
  • 清理/補償需冪等:取消很可能在重試後再次發生;清理多次不應出事
  • 活動取消策略:對 Activity 可依情境選擇 ActivityCancellationType.TRY_CANCELWAIT_CANCELLATION_COMPLETEDABANDON
  • 避免阻塞不可中斷的區塊:長時間 I/O、無限等待、外部 SDK 無 timeout 都會讓取消卡住
  • 定期 Heartbeat:長任務 Activity 務必 heartbeat(例如每 5–15 秒),否則取消不會即時生效

結語

Cancel 是「優雅收尾」的機制:Client 下令、Workflow 接招、Activity/Child 配合,中間要靠 CancellationScope、Heartbeat 與冪等清理來把風險控好。把「等待」設計成可取消,把「清理」設計成不會被取消打斷,你的流程就能進退自如。


上一篇
Day20 - Asynchronous Activity Completion - 非同步完成,讓資源不再卡住
系列文
Temporal 開發指南:掌握 Workflow as Code 打造穩定可靠的分散式流程21
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言