Flow
Flowは、依存関係を持つ複数のジョブを有向非巡回グラフ(DAG)で定義する仕組みです。
Runは、Flowを1回実行した単位です。
定義
createFlowは、performersを受け取り、Flowを定義する関数を戻り値とします。
binding名とpayloadの型は、単独のジョブと同様に型検査されます。
import { createFlow, defineTsumugi } from 'tsumugi';
const performers = { ListNames, Greet, Report };
const flow = createFlow(performers);
const flows = {
GREETINGS: flow<{ prefix: string }>((f) => {
const list = f.node('list', 'ListNames', {
input: (i) => ({ prefix: i.prefix }),
});
const each = f.fanOut('greet', 'Greet', {
after: { list },
over: (_i, d) => d.list.names,
input: (name) => ({ name }),
});
f.node('report', 'Report', {
after: { each },
input: (_i, d) => ({ total: d.each.total, failed: d.each.failed }),
});
}),
};
const tsumugi = defineTsumugi({ performers, flows, auth: /* ... */ });
f.nodeの第1引数にはノードIDを、第2引数にはbinding名を指定します。
binding名は、exportした名前と一致させてください。
ノードIDは、英数字、ハイフン、アンダースコアで構成します。
inputには、Flowの入力と前段の戻り値からpayloadを生成する関数を指定します。
第2引数は、afterに渡したオブジェクトのキーをプロパティ名とするオブジェクトです。
ノードの定義には、ジョブの投入時と同じオプション(maxAttempts, backoff, timeoutMs, priority, concurrencyKeyなど)を任意で指定します。
開始
const runId = await tsumugi.start(env, 'GREETINGS', { prefix: 'hello' });
inputの型は、Flowの型引数から導出されます。startの第4引数に{ id }を渡すと、runIdは<flow>:<id>に固定されます。
同じIDで2回目に開始した場合は、新しいRunが作成されず、既存のrunIdが戻り値です。
リトライされるHTTPハンドラから呼んでも、Runは重複しません。
第4引数の{ deadlineMs }が、Run全体の期限です。
fan-out
配列の要素数に応じた並列処理は、f.fanOutで定義します。overの戻り値の要素ごとに、子ノードが1つずつ作成されます。
子ノードのIDは、要素の添字です。たとえばgreet:0, greet:1となります。keyを渡した場合は、keyの戻り値が添字の代わりに使われます。
使える文字は、ノードIDと同様です。
後続のノードが受け取るのは、次の形式の集計値です。
{ total: 3, succeeded: 2, failed: 1 }
子ノードごとの戻り値は、後続のノードに渡されません。
個別の結果が必要な場合は、performerがR2やD1などに書き込み、その参照を戻り値にしてください。
子ノードが失敗しても、fan-outのノード自体は失敗しません。
失敗の有無は、後続のノードが受け取るfailedの値で判断します。
perform内からのノード追加
ctx.spawnを呼ぶと、実行中のノードに子ノードが追加されます。
class Crawl extends Performer<{ url: string }, void, {}, Env> {
async perform(payload: { url: string }, ctx: JobContext): Promise<void> {
for (const found of await this.discover(payload.url)) {
ctx.spawn(found.id, 'CRAWL', { url: found.url });
}
}
}
第1引数のIDは必須です。
子ノードのIDは<親のノードID>:<指定したID>で構成され、使用できる文字はノードIDと同一です。
同一のIDで複数回呼び出しても、子ノードは重複して作成されません。performが失敗した試行で追加された子ノードは、作成されません。
再実行時のperformで、改めて追加してください。
spawnには、型検査が適用されません。
binding名とpayloadは、実行時の値として渡します。
別Workerのperformerから呼ぶ場合は、awaitが必要です。
subflow
f.subflowは、別のFlowをRunとして開始します。
ノードは、そのRunが終了した時点で完了します。
const REPORTING = flow<{ ids: string[] }>((f) => {
// ...
});
const PIPELINE = flow<{ prefix: string }>((f) => {
const list = f.node('list', 'LIST', { input: (i) => ({ prefix: i.prefix }) });
const reported = f.subflow('report', REPORTING, { input: (_i, d) => ({ ids: d.list.ids }) });
f.node('notify', 'NOTIFY', { after: { reported }, input: () => ({}) });
});
第2引数には、Flowの定義そのものを渡します。inputの型は、渡したFlowの型引数から導出されます。
開始するFlowは、flowsに登録してください。
未登録の場合は、開始時にエラーが発生します。
子のRunのrunIdは、<子のFlow名>:<親のrunIdのローカル部>-<ノードID>です。
子のRunの状態が、そのままノードの状態として反映されます。
状態は、COMPLETED, FAILED, CANCELLEDのいずれかです。
親のRunを取り消すと、子のRunも取り消されます。
親のノードは、子のRunの戻り値を受け取りません。
結果が必要なら、performerがR2やD1などに書き込みます。
入れ子の上限は、制約を参照してください。
待ち合わせ
ノードは、自身と子孫のノードがすべて終了した時点で完了します。afterで指定したノードに依存する後続のノードは、fan-outの子ノードとspawnで追加された子孫のノードが終了した後に実行されます。
実行条件
既定では、依存するノードがすべて成功したときに限り、ノードが実行されます。triggerは、依存するノードが失敗した場合の挙動を指定します。
| 値 | 実行される条件 |
|---|---|
'success' | 依存するノードがすべて成功。既定値 |
'failure' | 依存するノードのうち1つ以上がFAILED, STALLED, CANCELLEDのいずれか |
'always' | 依存するノードがすべて終了。成否は不問 |
const flows = {
BACKUP: flow<{ target: string }>((f) => {
const dump = f.node('dump', 'DUMP', { input: (i) => ({ target: i.target }) });
// 失敗時のみ実行
f.node('alert', 'ALERT', {
after: { dump },
trigger: 'failure',
input: (i) => ({ message: `backup failed: ${i.target}` }),
});
// 成否によらず実行
f.node('unlock', 'UNLOCK', { after: { dump }, trigger: 'always', input: (i) => ({ target: i.target }) });
}),
};
afterのないノードにtriggerを指定すると、エラーが発生します。'failure'の条件に数えるのは、実際に失敗したノードに限られます。triggerやwhenの条件を満たさずSKIPPEDになったノードは、失敗に数えません。
依存するノードがすべてSKIPPEDの場合、'failure'のノードは実行されません。
'failure'と'always'では、失敗したノードの戻り値がありません。inputの第2引数の型はundefinedを含むため、値がない場合の処理を記述してください。
f.node('alert', 'ALERT', {
after: { dump },
trigger: 'always',
// d.dumpはundefinedの可能性あり
input: (_i, d) => ({ size: d.dump?.size ?? 0 }),
});
条件分岐
whenは、Flowの入力や前段の結果に基づき、ノードの実行可否を決定します。whenがfalseを返した場合、そのノードはSKIPPEDに遷移し、依存する後続のノードも実行されません。when内で例外が発生したときは、ノードはFAILEDに遷移します。
f.node('detail', 'DETAIL', {
after: { list },
when: (i, d) => i.verbose && d.list.items.length > 0,
input: (_i, d) => ({ items: d.list.items }),
});
whenとinputの評価では、引数の値のみを使用し、外部への問い合わせは行わないでください。
失敗時の動作
ノードが失敗した場合、'success'で依存する後続のノードはSKIPPEDへ遷移します。triggerが'failure'または'always'の後続のノードは、実行されます。
失敗したノードに依存しないノードは、最後まで実行されます。
すべてのノードが終了すると、RunはFAILEDへ遷移します。
RunがFAILEDになるのは、失敗したノードがある場合に限られます。triggerやwhenの条件で実行されなかったノードは、失敗に数えません。trigger: 'failure'のノードが成功しても、前段のノードが失敗していれば、RunはFAILEDで終了します。SKIPPEDになった理由は、ノードのerrorに記録され、ダッシュボードのRunの詳細に表示されます。
FAILEDのRunは、ダッシュボードまたはREST APIで再開します。
成功したノードの結果はそのまま使われ、それ以外のノードは未実行として改めて実行されます。
取り消し
Runを取り消すと、未実行のノードは開始されません。
実行中のノードには、取り消しの要求が送信されます。
実行中のジョブは停止されません。
performerが要求を確認して終了するか、要求からreaperGraceMs(既定30秒)が過ぎても結果が報告されないと、RunはCANCELLEDへ遷移します。
期限
Run全体の期限は、Flowの定義の第2引数で指定します。
const GREETINGS = flow<{ prefix: string }>(
(f) => {
// ...
},
{ deadlineMs: 10 * 60 * 1000 },
);
deadlineMsは、正の整数(ミリ秒)で指定します。startの第4引数に{ deadlineMs }を渡した場合は、その値が優先されます。
期限を過ぎたRunは、取り消しと同じ手順で中断されます。
未実行のノードは、run deadline exceededのエラーによりFAILEDへ遷移します。
実行中のジョブは停止されず、そのジョブが終了した時点でRunはFAILEDへ遷移します。
実行中の子のRunは、取り消されます。
期限までに終了しなかったRunは、残りのノードがすべて成功してもFAILEDへ遷移します。
このRunは、通常の失敗と同様に再開の対象です。
再開した場合、期限は再開した時点から改めて計測されます。
保持期間
終了したRunは、runs.sweepAfterMs(既定5分)の経過後に削除されます。FAILEDのRunは、runs.failedRetentionMs(既定7日)の経過後に削除されます。
詳細はRunSettingsを参照してください。
デプロイと実行中のRun
実行中のRunは、開始時点のノード構成で継続されます。inputなどの関数を変更しても、反映は次に開始されるノードからです。
定義からノードを削除すると、そのノードを含む実行中のRunは、そのノードで失敗します。
ノードを削除する場合は、実行中のすべてのRunが終了してからデプロイします。
テスト
simulateFlowは、指定された入力に対するノードの実行順序と、各ノードに渡されるpayloadを検証します。
performerの実際の処理は実行されません。
import { simulateFlow } from 'tsumugi/testing';
const result = simulateFlow(flows.GREETINGS, { prefix: 'hello' }, { results: { list: { names: ['a', 'b'] } } });
expect(result.nodes.map((node) => node.id)).toEqual(['list', 'greet:0', 'greet:1', 'greet', 'report']);
expect(result.nodes[1].payload).toEqual({ name: 'a' });
各ノードの戻り値は、resultsにノードIDをキーとして渡します。
値は、戻り値そのものと、戻り値を作る関数のどちらも受け付けます。
渡さなかったノードの戻り値は、undefinedです。failsに渡したノードは失敗し、後続のノードはSKIPPEDへ遷移します。
RunはFAILEDへ遷移します。
fan-outの子ノードはoverの戻り値から作成されますが、ctx.spawnで追加される子ノードは含まれません。
制約
ノードではuniqueKeyを使用できません。uniqueKeyを必須にしたperformerをノードに指定すると、型エラーが発生します。
ノードの戻り値が8,192文字を超える場合、または直列化できない場合、その試行は失敗として扱われます。
失敗後は、maxAttemptsまで再試行が行われます。
大きな結果はR2などに書き込み、その参照を戻り値にしてください。
1つのRunに含められるノード数は、既定で10,000件です。
subflowの入れ子は、既定で3段です。
上限は、defineTsumugiのruns.maxNodesとruns.maxDepthで変更します。
設定
flowsを指定する場合は、wranglerの設定に次の2箇所を追記してください。
指定しない場合は、これらの設定は不要です。
{
"durable_objects": {
"bindings": [
{ "name": "JOB_SHARD", "class_name": "TsumugiJobShard" },
{ "name": "RUN", "class_name": "TsumugiRun" },
],
},
"migrations": [
{ "tag": "v1", "new_sqlite_classes": ["TsumugiJobShard"] },
{ "tag": "v2", "new_sqlite_classes": ["TsumugiRun"] },
],
}
TsumugiRunは、defineTsumugiの戻り値に含まれるrunClassを継承してexportしてください。
D1のマイグレーションは、再度適用が必要です。
export { TsumugiJobShard } from 'tsumugi';
export class TsumugiRun extends tsumugi.runClass {}
pnpm wrangler d1 migrations apply my-jobs --remote
公開日: 最終更新: