Durableコードの記述
ワークフローは プレーンなPHPクラス です。ベースクラス、インターフェース、フレームワークタイプは必要ありません。シグネチャにそれらは含まれません。ワークフローをDurableにするのは、エンジンがジャーナルからワークフローをリプレイするという点です。したがって、唯一の制約は決定論です。
3つの形状
#[AsWorkflow('checkout')] // クラスターが認識する名前
final class CheckoutWorkflow
{
public function __construct(
private readonly WorkflowEnvironment $environment,
) {}
#[AsWorkflowMethod] // ワークフロークラスにつき1つだけ
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) |
アクティビティは非決定論が属する場所です。アクティビティは試行ごとに1回実行され、その結果はジャーナルに書き込まれ、リプレイ時にはコードの代わりにジャーナルが応答します。
⚠ アクティビティはリトライされます。2回目の試行が安全になるように記述してください。 エンジンは結果が一度だけ記録されることを保証しますが、副作用が一度だけ実行されることを保証するわけではありません。カードを二重に請求することは、フレームワークのバグではなくビジネスインシデントです。
待機
スケジュール可能なすべてのものは Awaitable を返します。awaitするまでは何も起こりません — これが並列性を実現する方法でもあります。
$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、リトライポリシー、4つのタイムアウト、再試行不可の例外、ハートビート、キャンセル。references/nexus.md— Nexus契約、ワークフローがオペレーションを完了するときに契約が2つに分割される理由、そして静かに失敗するペイロードの落とし穴。references/migrations.md— 既存のコードに触れる前にこれを読んでください。 Temporal PHP SDKからの移行、またはDurableのあるバージョンから次のバージョンへの移行:gplanchat/durable-rectorが書き換えを行い、リファレンスは主にそのルールが推測を拒否する5つのことについて説明しています。そのうち2つは、すでに実行中のサーバー上で静かに失敗します。
コードを返却する前に
- ワークフロークラスごとに1つの
#[AsWorkflowMethod]があり、そのクラスはホストに宣言されています(Symfony/Syliusは自動設定、Magentoはdi.xmlにリスト)。 - すべてのアクティビティ呼び出しは契約から構築されたスタブを経由します。文字列名は使用しません。
- ワークフロー本体に非決定論的なものはありません。
- すべてのタイマーにサマリーがあります。サマリーは監視タイムラインの行に名前を付け、
TimerScheduledは午前3時にオペレーターに何も伝えません。 - ワークフローは補償できないものをキャッチしません。未処理の失敗は実行を終了し、可視化されます。飲み込まれた失敗は注文が中途半端に処理されたまま静かに放置されます。