migrating-world-v4-to-v5 について
migrating-world-v4-to-v5 は、`@workflow/world` の `World` を実装するパッケージ向けの移行スキルで、アプリケーションコード用ではありません。イベント ID の割り当て、互換性確認、適合性テストを中心に v5 仕様への変更を案内します。
主な機能
- 01既存の World 実装を監査
- 02イベント ID を番号付きスロットへ移行
- 03仕様とランタイムの互換性確認
- 04ストレージの原子性とバイト処理を確認
- 05適合性テストを追加または実行
活用シーン
- 01カスタム World バックエンドの更新
- 02移行後のリプレイ障害を診断
- 03ランタイムとの互換性を確認
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ vercel/ workflow --skill migrating-world-v4-to-v5インストール後、Claude Code で/migrating-world-v4-to-v5を入力して読み込まれたか確認します。
詳細
翻訳後に原文が更新されています。最新の内容は原文をご確認ください。
World を v4 仕様から v5 に移行する
このスキルは、ストレージ、キュー、ストリームのバックエンドであり、Workflow ランタイムが通信する @workflow/world の World を実装するパッケージ向けです。アプリケーションコード向けではありません。アプリの workflow 依存関係を更新する場合は、代わりに migrating-workflow-v4-to-v5 スキルを使用してください。アプリが Workflow を使用し、独自の World も提供している場合は、先にそのスキルを実行し、次にこのスキルを実行してください。
Vercel、Local、Postgres World を使用しているアプリには、このスキルで対応する作業はありません。これらは SDK に同梱されており、すでに v5 仕様に対応しています。
作業の大半を占める変更が 1 つあります。イベント ID の割り当ては必須ですが、型シグネチャからはわかりません。これを省略した World は型チェックに通り、実行を開始しますが、最初のリプレイで失敗します。 まずこの対応を行い、その後で機械的な書き換えを進めてください。型エラーから始めないでください。型エラーは作業の半分にも満たず、それを解消しても、移行できたように見えるだけで実際には移行できていない World ができあがります。
事前確認
編集を始める前に、次の項目をそれぞれ確認して報告してください。
- World の場所。
implements World、: World、World>、from '@workflow/world'を検索します。エクスポートされているファクトリーを読みます。 - 現在のイベント ID の生成方法。
eventId、ulid、uuid、nanoid、nextval、AUTO_INCREMENT、IDENTITYを検索します。ストレージに書き込まれる ID を生成する正確な行を見つけます。 - 書き込み競合をどう解決しているか。
events.createの実装を読みます。ID または順序がプロセス内(読み取り後に書き込み、インメモリーカウンター、読み込んだイベントに対するMath.max)で決まるのか、ストア内(一意制約、条件付き書き込み、INSERT ... ON CONFLICT、トランザクション)で決まるのかを確認します。 - 宣言されている
specVersion。specVersionを検索します。リテラルか、インポートされた定数かを確認します。 - 存在するオプションメンバー。
capabilities、analytics、getRuntimeDeadline、getEnvironment、createRunId、describeRun、getEncryptionKeyForRun、resolveLatestDeploymentId、cancelMany、experimentalSetAttributesを検索します。 - ステップトピックをプロビジョニングしているか。
'step'、__wkf_step、stepQueueを検索します。 - 古い書き込みを拒否しているか。
PreconditionFailedError、preconditionGuard、stateUpdatedAt、stateEventCount、stateCursor、412を検索します。 - プロセス全体で共有される状態をどこに保持しているか。 World のモジュールで、プール、クライアント、ソケット、レジストリ、キャッシュ、ULID ファクトリー、または一度だけログを出すためのブール値を保持するトップレベルの
const/letを検索します。該当するものをすべて記録してください。これらはrequire()されるパッケージでは正しく機能しますが、バンドルされたパッケージでは問題になります。 - テスト方法。
@workflow/world-testingとcreateTestSuiteを検索します。適合性テストスイートが組み込まれていない World は、この移行で組み込みます。 - 実行がどこに配置されるか。 単一のデプロイメントがすべての実行を処理するのか、実行が作成元のデプロイメントに固定されるのかを確認またはデプロイメントモデルから判断します。これは手順 7 のロールアウト方法を決定しますが、コードからは判断できません。
該当しない項目がある場合は、黙って省略せず、その旨を報告してください。
ステップ 1 — イベント ID の割り当て
v4 では、イベント ID は World が好きなように発行する ULID でした。v5 では、イベント ID はイベントの実行ログ内での 位置 です。evnt_ の後に 1 始まりのスロット番号を続け、26 文字になるようゼロ埋めします。そのため、実行の最初のイベントは evnt_00000000000000000000000001 になります。
import { slotToEventId, eventIdToSlot, FIRST_EVENT_SLOT } from '@workflow/world';
slotToEventId(1); // 'evnt_00000000000000000000000001'
eventIdToSlot('evnt_00000000000000000000000042'); // 42
eventIdToSlot('evnt_01JQ...'); // null
ID のフォーマットには slotToEventId() を使ってください。ゼロ埋めを独自に実装してはいけません。固定幅にすることで、辞書順が位置順と一致します。World が異なる幅でゼロ埋めすると、イベント数が 10 を超えたときに、その World のログの順序が誤ります。
宣言する機能も、フォールバック経路もありません。ランタイムは読み込む ID に対して requireEventSlot() を呼び出します。これは Event id is not slot-numbered: <id>. This World allocates event positions the runtime cannot read. というエラーをスローします。
実装には 4 つのルールが適用されます。intake の項目 2 と 3 で見つかったコードで、それぞれを確認してください。
- 一意性。 2 つの並行する追記が同じスロットを取得してはいけません。ストアが整合性を保証する仕組みで解決してください。
(runId, eventId)に対する一意制約、条件付き書き込み、またはシリアライズ可能なトランザクションを使います。プロセス内で最大スロットを読み取り、1 を加える方法は、このルールが防ぐべき失敗パターンです。並行実行時にしか問題が起きないため、軽いテストでは見逃されがちです。 - 連続性。 スロットは 1 から始まり、欠番があってはいけません。競合に負けた書き込み側は、ストアからスロットを再計算し、次の空きスロットを取得します。競合に負けた後でローカルの番号を増やすと、欠番が恒久的に残ります。ランタイムは欠番を越えてリプレイせず、
CORRUPTED_EVENT_LOGで実行を失敗させます。 - 繰り上げと報告。
events.create()のパラメーターにはeventCountが含まれます。これは、書き込み側がリプレイ元のログに保持していたイベント数であり、期待するスロットはeventCount + 1です。そのスロットがすでに使われていても、書き込みを拒否してはいけません。 次の空きスロットに書き込み、成功レスポンスのeventsに、飛ばしたスロットを占めているイベントを返します。対応するcursorとhasMoreも返してください。並行ファンアウトでは古いカウントが普通に発生します。これを拒否すると、ランタイムが意図的に並行して発行する書き込みが直列化されてしまいます。eventCountなしで届いた create は、ログを読み込んでいない呼び出し元(キューに入れられたステップの本文や、範囲外の書き込み側など)からのものなので、常に受け付けます。 - コミット時に割り当てる。 スロットはイベントを追記するのと同じ操作の中で取得し、それより前に取得してはいけません。これにより、読み取り側のログは欠番を含む接頭辞ではなく、実行ログの 接頭辞 になります。読み取り側がすでに通過したスロットの後ろにイベントが書き込まれることはありません。リクエストハンドラー内でスロットを割り当て、後からコミットする World は、あらゆるリプレイが依存するこの性質を破ります。
(run_id, event_id) に一意キーを持つ SQL ストアで、4 つすべてのルールを満たす方法は、ID を insert の 内部で 計算し、一意制約に競合を裁定させることです。
INSERT INTO events (run_id, event_id, event_type, data)
SELECT $1,
'evnt_' || lpad((coalesce(
(SELECT cast(substring(prev.event_id from 6) AS bigint)
FROM events prev WHERE prev.run_id = $1
ORDER BY prev.event_id DESC LIMIT 1), 0) + 1)::text, 26, '0'),
$2, $3
ON CONFLICT (run_id, event_id) DO NOTHING
RETURNING event_id;
行が返されなければ、別の書き込み側がそのスロットを取得しています。同じステートメントを再試行してください。すでに進んでいるストアから最大値を再読み込みすることで、スロットが繰り上がります。@workflow/world-postgres は、まさにこの方法を採用しています。再試行回数には上限があり、最初の数回の即時再試行の後はジッター付きのバックオフを行います。また、これらの insert はエラーによってトランザクションが使用不能になるトランザクション内で実行されるため、エラーを発生させるのではなく DO NOTHING で競合を吸収します。
正確なステートメントよりも重要なのは、この性質です。スロットの計算と行の挿入を、制約が保護する行に対する 1 つのアトミックな操作で行います。そのため、競合に負けた書き込み側は、記憶していた番号ではなくストアを基準に再試行します。条件付き書き込みができないストアでは、プロセス内のロックではなく、シリアライズ可能なトランザクションを使ってください。プロセス内のロックで順序付けできるのは、1 つのプロセス内の書き込み側だけです。
最後に、コミットされたスロットが eventCount + 1 を超えた場合は、飛ばした範囲を返します。eventCount を実際より小さく申告するのは安全ですが、大きく申告するのは安全ではありません。書き込み側の実際の位置より小さいカウントでは、報告される範囲が広がるだけで、書き込み側はログにすでにあるイベントを除外できます。一方、実際より大きいカウントでは、World が書き込み側に不足しているイベント数より少なく報告するため、書き込み側が知ることのない欠番が生じます。
ステップ 2 — 仕様バージョンを宣言する
specVersion は World が実装するプロトコルバージョンであり、World が作成するすべての実行に付与される番号です。定数をインポートするのではなく、ヘルパーを呼び出してください。
import { mintedSpecVersion } from '@workflow/world';
export function createWorld(): World {
return {
specVersion: mintedSpecVersion(),
// ...
};
}
ランタイムは、何かを作成または再生する前に、宣言されたバージョンがサポート範囲内かどうかを確認します。範囲外の World は拒否し、範囲と World が宣言したバージョンの両方を示します。下限は、スロット番号付き ID が導入されたバージョンです。それより古い World では、ランタイムが位置を読み取れない ID が割り当てられるためです。上限は、ランタイムが読み取れる最も新しいバージョンです。
mintedSpecVersion() は関数です。World が付与するバージョンはデプロイ時の選択であり、デフォルトではシールドログバージョンを返し、WORKFLOW_SEALED_LOG=0 によって新しい実行をオプトアウトした場合は、その一つ前のバージョンを返すためです。どちらも許容範囲内です。プロセスで両方のモードの World を作成できるよう、モジュールの読み込み時に値をキャッシュするのではなく、createWorld() の内部で呼び出してください。
次の項目は放置せず、指摘事項として報告してください。
- ハードコードされた数値。次のバージョン更新時に World が一つ前のバージョンのままになり、同時に出荷されるランタイムから拒否されます。
- 宣言に
SPEC_VERSION_CURRENTまたはSPEC_VERSION_SUPPORTS_SLOT_IDENTITYを使用している箇所。どちらもシールドログ設定に応じて変わらないため、ここでは別名のリテラルにすぎません。 - すべての実行が常に現在の World の宣言に一致するという前提のコード。実行のバージョンは作成時に永続化され、その実行が存続する限り保持されるため、実行から読み取ってください。
シールドログと noop
シールドログバージョンでは、コミット時(ステップ 1)に位置を割り当てる方法に代えて、コミット前に実行ごとのカウンターから位置を割り当てられます。これにより、並行する書き込みが同じ位置を取り合うことがありません。そのうえで、放棄されたことが確実な位置に noop イベントを書き込み、読み取り時に連続した位置を保ちます。noop は位置を占有しますが、意味はありません。再生時には通知されず、決定論的なクロックも進みません。
ほとんどの移行では何も変更する必要がありません。その場合は省略せず、その旨を明記してください。コミット時に位置を割り当てる World はすでに準拠しており、noop を出力することはありません。書き込みによって位置が空いたままになることがないためです。位置を事前割り当てする World に限り、位置を封印する仕組みを実装してください。その場合も、途中に穴のあるページを返してはいけません。穴より前の連続した範囲を返し、位置が解決された後に次のページを取得してください。
常に適用されるのは読み取り側の処理です。noop はユーザーが作成できず、events.create() に送信されることもありません。noop を書き込めるのは、World 独自の読み取りパスだけです。読み取り時にイベントタイプを検証する World では、noop を解析できるようにしてください。
ステップ 3 — 機械的な書き換えを適用する
ここではシグネチャとモジュール構成を変更します。該当するパターンがある箇所にのみ適用してください。
ストリームは streams 名前空間に移動し、runId が先頭になります
| v4 | v5 |
|---|---|
writeToStream(name, runId, chunk) | streams.write(runId, name, chunk) |
writeToStreamMulti(name, runId, chunks) | streams.writeMulti(runId, name, chunks) |
closeStream(name, runId) | streams.close(runId, name) |
readFromStream(name, startIndex?) | streams.get(runId, name, startIndex?) |
getStreamChunks(name, runId, options?) | streams.getChunks(runId, name, options?) |
listStreamsByRunId(runId) | streams.list(runId) |
引数の順序が入れ替わったため、引数を入れ替えずにメソッドだけ移動すると、実行 ID が必要な箇所にストリーム名が渡されます。どちらも string の場合は型チェックを通過してしまいます。readFromStream には runId がありませんでしたが、streams.get では必須なので、所有者である実行を渡してください。
streams.streamFlushIntervalMs はフラッシュの間隔を設定します。v5 のデフォルトは 0 なので、最初のチャンクはすぐにフラッシュされます。書き込みを意図的にまとめる場合に限り、値を設定してください。
steps.get() には runId が必要です
最初のパラメーターは string | undefined でしたが、現在は string です。ID だけを使ってステップを検索していた World では、キーまたはインデックスに実行を含める必要があります。
events.listByCorrelationId() には runId が必要です
相関 ID は、実行をまたいでではなく、実行内のステップ、フック、または待機を識別します。検索対象を1つの実行に絞ってください。イベント ID でページネーションする World は、同じ相関 ID を持つ実行が複数存在するようになるため、カーソルの比較にも実行を含める必要があります。
analytics.events.listByCorrelationId() も同じ runId を受け取るようになり、そのうえで非推奨になりました。これは analytics.events.list({ runId, correlationId }) の特別なケースであり、次のメジャーバージョンで削除されます。ひとまず移行し、新たにこれを使うものは作らないでください。上記のストレージの events.listByCorrelationId() は非推奨ではなく、独自のエンドポイントを維持します。
createWorld() ファクトリーをエクスポートする
ファーストパーティパッケージから createLocalWorld() と createVercelWorld() が削除され、代わりに createWorld() がエクスポートされるようになりました。同じ形に合わせてください。引数に変更はなく、名前の変更です。
World はビルド時に注入されます
World の選択は静的で、実行時に動的に検索されるのではなく、ビルドによってホストのバンドルに解決されます。アップグレード後も World が解決され、バンドル後もモジュールグラフが維持されることを確認してください。環境変数から計算したパスを実行時の require で読み込んでいた World は見つかりません。
Step 4 — プロセス全体で共有する状態を globalThis に移す
この問題は表面化しません。型チェックに通り、単独のテストにも合格しますが、World をバンドルするホストでのみ失敗します。
モジュールのトップレベルにある const または let は、プロセスごとではなくモジュールインスタンスごとに1つ存在します。Next.js はサーバー出力を独立したモジュールグラフにコンパイルし、バンドルされたモジュールはそれぞれのグラフ内に独自のモジュールスコープのバインディングを持つ形でコンパイルされます。ランタイムは World オブジェクトをプロセス全体でキャッシュしますが、World がクロージャーとして保持するモジュール状態は各レイヤー内にとどまります。そのため、World がリクエスト時に参照するものはすべて、プロセス全体で共有される必要があります。
intake item 8 で見つかったものをすべて1つのオブジェクトにまとめてください。
import { globalSingleton } from '@workflow/utils';
const state = globalSingleton('@my-org/world-foo//connections', 1, () => ({
pool: undefined as Pool | undefined,
warnedOnce: false,
}));
globalSingleton は globalThis 上の Symbol.for を使ってオブジェクトのキーを定めるため、モジュールのどのコピーからも同じオブジェクトが使われます。第2引数は形状のバージョンです。オブジェクトの形状に互換性のない変更を加えるときは、これをインクリメントしてください。そうすれば、プロセス内で共有されている古いパッケージのコピーは、あなたの状態を誤って読み取ることなく、独自の状態を保持できます。let は参照を共有できないため、ログを一度だけ出すためのフラグは変数のままではなく、フィールドにする必要があります。
変更が不要だった場合も含め、これについて報告し、防止できる障害の内容を明記してください。@workflow/world-vercel では、WebSocket イベントのトランスポートが影響を受けていました。キューのコンシューマーがあるレイヤーのレジストリーにチャネルを登録し、書き込みパスは別のレイヤーにある空のレジストリーを参照していたため、プロセスの稼働中、すべてのイベントが HTTP にフォールバックしていました。ログは何も出ず、テストも失敗しませんでした。
ステップ 5 — 契約の変更
これらの変更でシグネチャは変わりません。型だけを移植した World はコンパイルできますが、その後、誤った動作をします。
- サスペンションのディスパッチはバッチ処理されます。 非対称な
{ timeoutSeconds }の待機戻り値契約はなくなりました。待機はdelaySecondsを含む通常のキュー継続処理であり、サスペンションは待機とステップをまとめて並列バッチとしてディスパッチします。サスペンションごとにメッセージが 1 件だけ届くと想定していたキューは、バッチを処理できるようにする必要があります。 - ステップキューのトピックは廃止されました。
'step'キュー種別はなくなりました。キューに入ったステップは、ペイロードにstepIdとstepNameを含めてワークフロートピックを通り、統合フローハンドラーで実行されます。__wkf_step_*トピックのプロビジョニングはすべて削除してください。 preconditionGuard機能はなくなり、それが必要だった理由もなくなりました。 ログより古いスナップショットを使ったイベント作成を拒否していた World は、そのコードとstateUpdatedAt/stateEventCount/stateCursorの処理を削除できます。Bump-and-report に置き換わったため、古いリプレイは拒否ではなくマージのコストがかかります。PreconditionFailedErrorは引き続き存在しますが、コミットとは別にスロットを割り当てる World が、報告するより拒否することを選ぶ場合のためのものです。ステップ 1 に従う World は該当しません。- 機能はフェイルクローズで動作します。 機能を宣言しない場合、正しさではなくパフォーマンスが犠牲になります。そのため、不完全な World も対応が済むまでは正しく動作します。逆は成り立ちません。実装されていない機能を宣言すると、ランタイムが依存していたガードがなくなります。動作を実装してからフラグを設定してください。
- イベント作成は差分を返すことがあります。
events.create()は、作成したイベントに加えて、events/cursor/hasMoreを含むイベントを返すことがあります。ステップ 1 の bump-and-report の場合に加え、ランタイムはこれを使って、run_started、sinceCursorを含むステップ終了時の書き込み、preloadEventsを含むhook_receivedの書き込み後に行うevents.listを省略します。いずれも任意の最適化です。作成したイベントだけを返しても正しく動作しますが、ラウンドトリップが 1 回増えます。 - 終了済みの実行は
step_startedを拒否します。 実行が有効かどうかは World で検証します。実行行とクレームを同じ処理で確認できるのは World だけだからです。実行がすでにcompleted、failed、またはcancelledである場合は、ステップ行がまだrunningでも、その実行に対するstep_startedを拒否してください。この組み合わせは、以前の配信ですでにクレームされた開始メッセージの再配信です。受け入れると、結果が誰にも読み取られることのないステップ本体を実行してしまいます。終了済みの実行に対するstep_completedとstep_failedは引き続き受け入れます。すでに実行中のステップが結果を記録できるようにするためです。@workflow/world-localと@workflow/world-postgresは、どちらもこの場合にRunExpiredErrorをスローします。これは単なる防御策ではなく、動作の要となる変更です。ステップ実行メッセージには実行の不変な識別情報(WorkflowInvokePayloadのrunContext)が含まれるようになったため、コンシューマーはブロッキングを伴うruns.getを省略でき、実行のステータスは意図的に含まれていません。残された有効性チェックはクレームだけです。キューでフィールド許可リストを使ってペイロードを再シリアライズする World は、runContextを変更せずに渡してください。そうしないと、すべてのステップディスパッチで追加のラウンドトリップが暗黙に発生します。 runs.listはステータスの配列を受け付けます。ListWorkflowRunsParams.statusはWorkflowRunStatus | WorkflowRunStatus[]です。配列を指定した場合、ステータスが列挙された値のいずれかであれば実行が一致します。status: []は SQL のIN ()と同様に何も一致せず、フィールドを省略した場合とは異なります。パラメーターを単なる文字列として型付けしている World は、以前の形式を前提とするコードに対してコンパイルできても、status = '[object Array]'でフィルタリングしてしまうか、ストアによっては例外をスローします。配列形式を実装するか、誤ったページを黙って返すのではなく、明示的なエラーで拒否してください。@workflow/world-vercelは現在、後者の方法を採用しており、バックエンドに複数ステータスのフィルターがないため、INVALID_ARGUMENTを含むWorkflowWorldErrorをスローします。
ステップ 6 — 採用を検討する価値のある任意の機能
これらはどれも必須ではなく、ランタイムは各機能がなくても動作します。何も言われずにすべてを実装するのではなく、World に不足しているものを報告してください。
capabilities(hookRetention.active、hookResumeDedup、deploymentAffinity、maxConcurrency)、analytics、analytics.events.getMany()、runs.experimentalSetAttributes、runs.cancelMany、runs.waitForTerminalStatus()、events.createBatch()、streams.createWriteSession()、getRuntimeDeadline()、getEnvironment()、createRunId()、describeRun()、getEncryptionKeyForRun()、resolveLatestDeploymentId()、close()。
なくても報告されるだけでなく、その影響を実際に感じるため、こちらから提案する価値のあるものが 4 つあります。
getRuntimeDeadline()がない場合、インライン再生の予算は一律 2 分になります。そのため、関数のタイムアウト時間が長いホストでも、1 回の呼び出しで可能な処理量が少なくなります。close()がない場合、CLI コマンドや短時間で終了するプロセスは、process.exit()を使わずに正常終了できません。events.createBatch()がない場合、中断時のstep_createdとwait_createdの書き込みには、それぞれ個別のラウンドトリップが必要です。このメソッドを実装すること自体が宣言となるため、フラグはありません。そのため、試行ごとにアトミックに処理し、競合に負けた場合は何も残さないようにするか、実装自体を見送る必要があります。run_created、run_started、run_cancelled、hook_created、hook_disposed、attr_setは表現できず、World はこれらのいずれかが含まれるバッチ全体を拒否します。runs.waitForTerminalStatus()がない場合、await run.returnValueはロングポーリングではなく、一定間隔のポーリングにフォールバックします。streams.createWriteSession()がない場合、ランタイムはステートレスなwrite/writeMulti/closeメソッドで書き込みます。これは正しい動作ですが、書き込みごとにトランスポートを確立するコストがかかります。たとえば接続を開いたままにするなど、1 つのライターのチャンク間で状態を保持することで利点が得られる場合に実装してください。ランタイムはメモリ内のWritableStreamごとに最大 1 つのセッションを作成し、writerIdを渡します。そのため、セッションのwrite(chunkSeq, chunks)が受け取るシーケンス番号は、ストリーム全体ではなくライターごとの番号になります。これにより、World は同じストリームへの並行するライターの順序を決められます。close()は、それ以前のすべての書き込みが永続化されるまで解決してはなりません。dispose()は任意で、ストリームを終了せずにトランスポートリソースを解放します。
analytics の実装後は、2 つの上限が適用されます。ランタイムはリクエストが呼び出し元を離れる前に、プロセス内で両方を検証し、違反時には RangeError をスローします。World に届いたリクエストはすでにこの範囲内なので、再検証せず、契約として扱ってください。pagination.limit はデフォルトが 40 で、実行単位の一覧(steps.list、events.list、waits.list)では最大 1000、実行をまたぐ一覧(runs.list、attributes.list、hooks.list)では最大 100 です。analytics.events.getMany() は 1 回の実行内のイベント ID を 1~100 件受け取り、ページネーションには対応しません。また、analytics がまだ取り込んでいない ID はエラーにせず省略します。
ステップ 7 — ロールアウト
デプロイ前に、移行レポートでユーザーに警告してください。
ストア内にすでにある実行は、新しいコードでは再生できません。 ULID で番号付けされた実行は位置として読み取れないため、ランタイムは推測せずに拒否します。混在方式のモードも、実行ごとのフォールバックもありません。
その後の対応は、インテーク項目 10 によって決まります。実行が、それを作成したデプロイ上で処理される場合は、これらの実行は開始したビルド上で完了し、新しいコードに遭遇しないため、問題は自然に解消します。すべての実行を単一のデプロイで処理する場合は、v5 World をデプロイする前に、実行中のものを 4.x ビルド上で完了させる必要があります。そうしないと失敗します。
検証
まず適合性スイートを接続してください。これがステップ 1 の作業に誤りがないかを確認する最も手軽な方法です。
import { createTestSuite } from '@workflow/world-testing';
createTestSuite('@my-org/my-world'); // or a path to the built entrypoint
このスイートは、WORKFLOW_TARGET_WORLD にその値を設定してサーバーを起動し、実際のワークフローを実行します。numbers events by position テストは、ID をスロットにデコードできない World、実行が 1 から始まる連続した番号になっていない World、または ID が正規形ではない World で失敗します。これにより、初回のリプレイで初めて発生するはずの失敗を、テスト出力の 1 行で確認できます。
次の順番で実行し、実際の出力を報告してください。
- World パッケージをビルドし、型チェックします。
- 適合性スイートを実行します。
- World 独自のテストスイートを実行します。
- 実際のアプリに対してエンドツーエンドで実行し、次のケースを網羅します。ステップで中断する実行と、待機で中断する実行。並列ファンアウトにより、同時実行される
events.create()の呼び出しが同じスロットを取り合うケース。実行が進行した後に再開されるフック。書き込み後に読み戻すストリーム。リーダーが接続する前に閉じられたストリームも含めます。
同時実行性は、簡単なテストでは見落としやすい部分です。World のテストで同じ実行に対する 2 つの events.create() 呼び出しを同時に発行していない場合は、移行完了とする前に、そのケースを追加してください。
次のいずれかに該当する場合は、移行を失敗としてください。
- イベント ID が
slotToEventId()以外の方法で生成されている - イベントをコミットする操作の外で、最大値またはカウンターを読み取ってスロットを選択している
- コミット前にスロットを払い出している
-
events.create()が、eventCount + 1のスロットが使用済みだった場合に、次の空きスロットへ進まず、書き込みを拒否、スロー、または再試行している - スロットを繰り下げた書き込みが、スキップしたイベントを
events/cursor/hasMoreに含めずに戻っている -
eventCountを持たない create が拒否されている -
specVersionがmintedSpecVersion()ではなく、リテラル、SPEC_VERSION_CURRENT、またはSPEC_VERSION_SUPPORTS_SLOT_IDENTITYになっている -
streams.*呼び出しで、v4 の引数順(runId より前に name)が使われている -
steps.getまたはlistByCorrelationIdが run ID なしで呼び出せる - 終端状態の実行で
step_startedが受け入れられる -
ListWorkflowRunsParams.statusが単一のステータスとして型指定または処理され、配列形式が実装も明示的な拒否もされていない - 動作が実装されていない機能がアドバタイズされている
- プール、クライアント、ソケット、レジストリ、キャッシュ、ID ファクトリー、またはログを一度だけ出力するためのラッチが、モジュールスコープに保持されたままになっている
-
@workflow/world-testingが接続されていない、または結果が報告されていない - ステップ 7 のロールアウト警告が提示されていない
- ビルド、型チェック、またはテストの結果が実際に実行され、報告されていない
必須の出力形式
## Summary
## Event ID Allocation
## Interface Changes
## Process-Wide State
## Contract Changes
## Optional Surface Not Implemented
## Rollout
## Verification
## Open Questions
## Event ID Allocationでは、スロットを現在どこで計算するか、競合を何によって解決するか、繰り下げた書き込みがスキップした範囲をどう報告するかを記載します。コードを引用してください。## Process-Wide Stateでは、見つかったすべてのモジュールスコープのシングルトンと、それらの移動先を列挙するか、該当するものがなかったことを記載します。## Optional Surface Not Implementedでは、実装しなかったものと、それぞれがないことで生じる影響を列挙し、ユーザーが判断できるようにします。## Rolloutには、ステップ 7 の警告と、このデプロイに当てはまる 2 つのケースのうちどちらかを記載します。
参考資料
- World を v5 にアップグレードする:https://workflow.dev/worlds/upgrading-to-v5
- World のビルド:https://workflow.dev/worlds/building-a-world
- イベント ID:https://workflow.dev/docs/how-it-works/event-sourcing#event-ids
- v5 の新機能(アプリケーション向け):https://workflow.dev/docs/whats-new
- リファレンス実装:
packages/world-localおよびpackages/world-postgres(https://github.com/vercel/workflow)