编写 Durable 代码
工作流是一个普通的 PHP 类。其签名中不需要基类、接口或框架类型。使其具备 durable(持久)特性的关键在于引擎会根据日志重放执行,因此唯一必须遵守的规则就是确定性。
三种形状
#[AsWorkflow('checkout')] // 集群识别的名称
final class CheckoutWorkflow
{
public function __construct(
private readonly WorkflowEnvironment $environment,
) {}
#[AsWorkflowMethod] // 每个工作流类仅有一个
public function run(string $orderId): string
{
$orders = $this->environment->activityStub(OrderActivities::class);
$receipt = $this->environment->await($orders->charge($orderId));
$this->environment->sleep(Duration::seconds(30), '冷却中');
return $this->environment->await($orders->notify($receipt));
}
}
interface OrderActivities // 契约携带名称
{
#[AsActivityMethod(name: 'shop.order.charge')]
public function charge(string $orderId): string;
}
final class DoctrineOrderActivities implements OrderActivities // 处理器执行实际工作
{
public function charge(string $orderId): string { /* 实际的副作用代码 */ }
}
#[AsNexusService('billing')] // 另一个团队,另一个部署
interface BillingContract
{
#[AsNexusOperation('check')]
public function check(string $order, int $amountInCents, string $currency): array;
}
永远不要在调用点将活动名称写成字符串。 存根会读取契约的属性,因此名称只存在于一个地方。这是破坏现有工作流最常见的方式:重命名方法后,保留属性,正在运行的执行仍能找到其活动。
必须遵守的规则:工作流体会被重放
#[AsWorkflowMethod] 中的所有代码都会从顶部重新执行,每次引擎恢复执行时都会发生——可能在另一个进程中,也可能在几天后。因此,工作流体在被重放时必须给出相同的结果:
| 工作流体中禁止使用的操作 | 代替方案 |
|---|---|
rand()、uniqid()、time()、new \DateTime() | $environment->sideEffect(fn() => …) —— 结果只记录一次 |
sleep()、usleep() | $environment->sleep($duration, '等待原因') |
| 数据库查询、HTTP 调用、文件写入 | 活动 |
使用 if (getenv(…)) 根据部署环境分支 | $environment->version($changeId, $min, $max) |
活动 是非确定性代码的归属地。每次尝试它只会运行一次,其结果被写入日志;重放时,日志返回结果而不是代码本身。
⚠ 活动可能会被重试;请确保第二次尝试是安全的。 引擎保证 结果 被记录一次,但不保证你的副作用只执行一次。重复扣款是业务事故,而不是框架缺陷。
等待
所有可调度操作都返回一个 Awaitable。在你等待它之前什么也不会发生——这也是实现并行的方法:
$a = $stub->reserve($order); // 已调度,未等待
$b = $stub->quote($order);
[$reserved, $quoted] = $this->environment->await($this->environment->all($a, $b));
$first = $this->environment->await($this->environment->any($a, $b));
$twoOfThree = $this->environment->await($this->environment->some(2, $a, $b, $c));
await() 可以接受一个可选的截止时间:await($awaitable, Duration::minutes(5))。
参考文件
按需阅读,不必全部读完:
references/workflows.md—— 信号、更新、子工作流、定时器、continueAsNew、版本控制以及如何启动一个执行(不要在 Web 请求中内联启动)。references/activities.md——ActivityOptions、重试策略、四种超时、不可重试异常、心跳、取消。references/nexus.md—— Nexus 契约、为什么当工作流履行操作时契约会分裂为两部分、以及会导致静默失败的 payload 陷阱。references/migrations.md—— 在修改现有代码前必须先阅读此文件。 从 Temporal PHP SDK 迁移,或者从一个 Durable 版本升级到下一个:gplanchat/durable-rector会自动完成重写,该参考主要描述了其规则无法猜测的五种情况——其中两种会在已经运行的服务器上静默失败。
提交代码前请检查
- 每个工作流类只有一个
#[AsWorkflowMethod],并且该类已向宿主注册(Symfony/Sylius 自动配置;Magento 在di.xml中列出)。 - 每个活动调用都通过基于契约构建的存根进行——没有字符串名称。
- 工作流体中没有非确定性代码。
- 每个定时器都有摘要说明:它是在观测时间线中标识该行的名称,
TimerScheduled在凌晨 3 点向操作员传递的信息非常有限。 - 工作流不会捕获它无法补偿的异常。未处理的失败会结束执行并可见;被吞掉的异常会让订单处于半处理状态且悄无声息。