Dynamic Workflows:多租户动态代码如何跨休眠恢复

This article is extracted from the chat log with AI. Please identify it with caution.

说明:本文由 Codex 根据作者提供的主题、对话素材与 Cloudflare 官方文档辅助生成,属于 AI 生成/整理内容,非作者原创。请读者自行甄别并交叉验证。

Dynamic Workers 解决“现在加载哪段代码”;Workflows 解决“这次流程在 sleep、重试或等待事件后如何继续”。Dynamic Workflows 解决二者结合后最棘手的问题:一个数天前创建的实例恢复时,怎样找回当时那一版租户代码,而不是不小心执行当前最新版本。

这是多租户自动化平台的进阶能力,不是普通固定业务流程的默认选择。先理解 Dynamic Workers:Worker Loader、受控 Binding 与安全执行Workflows:实例、步骤、事件与恢复 API 实战 再使用本页。

1. 恢复路径:metadata 是路由信息,不是秘密仓库#

flowchart LR
  A["请求:tenantId + codeVersion"] --> H["可信宿主 Worker"]
  H --> L["Worker Loader:加载不可变代码版本"]
  L --> D["Dynamic Worker:TenantWorkflow"]
  D --> W["wrapWorkflowBinding(metadata)"]
  W --> I["Workflows instance"]
  I --> S["sleep / wait / retry / 进程回收"]
  S --> E["DynamicWorkflow entrypoint"]
  E --> H
  H --> L
  L --> D

metadata 必须至少包含租户 ID、代码版本或内容 hash 等可公开的、可序列化的路由信息。它会随实例状态持久化,不能放 API key、数据库密码、OAuth token 或任何 secret。恢复流程只能依据 metadata 加载正确的 immutable bundle。

2. 最小配置:一个 Loader,一个普通 Workflow binding#

{
  "$schema": "./node_modules/wrangler/config-schema.json",
  "name": "tenant-automation",
  "main": "src/index.ts",
  "compatibility_date": "2026-08-07",
  "worker_loaders": [
    { "binding": "LOADER" }
  ],
  "workflows": [
    {
      "name": "dynamic-workflow",
      "binding": "WORKFLOWS",
      "class_name": "DynamicWorkflow"
    }
  ]
}

这里的 WORKFLOWS 是真正由 Workflows Engine 管理的 binding;Dynamic Worker 不能直接把它原样跨 RPC 边界使用,而要由 wrapWorkflowBinding() 包装。

3. 宿主:加载版本并提供恢复入口#

import {
  createDynamicWorkflowEntrypoint,
  DynamicWorkflowBinding,
  wrapWorkflowBinding,
  type WorkflowRunner,
} from "@cloudflare/dynamic-workflows";

export { DynamicWorkflowBinding };

type RouteMetadata = {
  tenantId: string;
  codeVersion: string;
};

function workerId(route: RouteMetadata) {
  return "tenant:" + route.tenantId + ":workflow:" + route.codeVersion;
}

function loadTenantWorkflow(env, route: RouteMetadata) {
  return env.LOADER.get(workerId(route), async () => ({
    compatibilityDate: "2026-08-07",
    mainModule: "index.js",
    modules: {
      "index.js": await readImmutableBundle(
        route.tenantId,
        route.codeVersion,
      ),
    },
    globalOutbound: null,
    env: {
      WORKFLOWS: wrapWorkflowBinding(route),
      CRM: makeTenantScopedCrmCapability(route.tenantId),
    },
    limits: { cpuMs: 50, subRequests: 20 },
  }));
}

export const DynamicWorkflow = createDynamicWorkflowEntrypoint(
  async ({ env, metadata }) => {
    const route = metadata as RouteMetadata;

    return loadTenantWorkflow(env, route).getEntrypoint("TenantWorkflow")
      as unknown as WorkflowRunner;
  },
);

这段宿主代码有三项职责:

  1. 强制版本不可变。 readImmutableBundle() 必须按 tenant + version/hash 返回同一份内容。代码、依赖、权限或 compatibility 配置改变时,版本号随之改变。
  2. 重新建立 capability。 恢复时重新创建给该租户的 CRM、存储或出站网关 binding;不把原始资源或 secret 塞进动态代码。
  3. 提供 Engine 可调用的入口。 createDynamicWorkflowEntrypoint() 在每次恢复时读 metadata,重新加载正确 entrypoint。

4. 租户代码:看起来像普通 Workflow#

租户 bundle 内的 Workflow 写法接近普通 Workflows,只是 env.WORKFLOWS 已被包装,创建实例时会自动附带 metadata:

import { WorkflowEntrypoint } from "cloudflare:workers";

export class TenantWorkflow extends WorkflowEntrypoint {
  async run(event, step) {
    const updated = await step.do("update tenant CRM", async () => {
      return this.env.CRM.upsert(event.payload);
    });

    await step.sleep("wait for human review", "1 day");

    const review = await step.waitForEvent(
      "wait for approval",
      { type: "approval", timeout: "7 days" },
    );

    return { updated, review };
  }
}

export default {
  async fetch(request, env) {
    const params = await request.json();
    const instance = await env.WORKFLOWS.create({ params });

    // 这里跨 Dynamic Worker 的 RPC 边界,id 是 RpcPromise。
    return Response.json({ id: await instance.id });
  },
};

Dynamic Workflow 的 status()、等待、重试、暂停、恢复和终止语义来自普通 Workflows;库添加的是“实例状态携带路由 metadata,恢复时自动回到该版本动态代码”。

5. 接口地图与最常见故障#

组件接口 / 约定必须保证的事情
宿主入口env.LOADER.get(id, callback)ID 与 callback 必须对应同一份不可变代码和配置。
动态代码wrapWorkflowBinding(metadata)每个 create()/createBatch() 都携带可恢复路由信息。
Engine 入口createDynamicWorkflowEntrypoint(loadRunner)运行/恢复时由 metadata 找回对应 runner。
必须 exportDynamicWorkflowBindingruntime 依靠它创建给动态代码的包装 binding。
instance IDawait instance.idDynamic Worker 侧通过 RPC 获得 ID,不要漏掉 await。
metadatatenant ID、version/hash、非敏感路由键JSON 可序列化;绝不存 secret。

四个高频问题:

  1. 只保存 tenantId,不保存版本。 旧实例恢复时执行了“最新代码”,可能改变审批逻辑、权限或副作用。
  2. 忘记 re-export DynamicWorkflowBinding 动态代码没有正确的 Workflow wrapper,创建实例会在运行期出错。
  3. 把原始 Workflow binding 直接传给动态代码。 必须用 wrapper 让 metadata 跟随实例;否则恢复路径缺失。
  4. 把 metadata 当配置仓库。 metadata 只存路由,敏感值应保留在宿主 Worker 的 secrets 或受控 Service Binding 中。

6. 什么时候值得承担这层复杂度#

Dynamic Workflows 很适合客户自定义 onboarding、审批链、webhook 自动化,或 AI Agent 为不同租户生成长期任务计划。它不适合流程定义固定、只有一两个同步步骤、没有用户代码的常规后端业务;这类场景直接选择 Workflows 更容易审计、测试和升级。

上线前至少测试:sleep 后恢复、事件提前到达、同一租户新旧版本并存、bundle 缺失、权限变更、外部副作用重试、metadata 错误和强制终止时的补偿路径。

参考资料#

本文共 1763 字,创建于 Aug 7, 2026

相关标签: Cloud, DevOps, TypeScript, ByAI