when、branch、foreach、暗黙の parallel のちょうど4つです。そしてそれらはすべて、コンパイル済みの FlowSpec では、手順ごとの2つのフィールド、つまり when 述語と foreach ソースだけに lowering されます。実行時に制御フローインタープリターは存在しません。コンパイラーがすべての選択を静的に解決するため、Run は DAG の純粋な走査になります。
4つの構文
それぞれの構文には YAML 形式と SDK 形式があり、それぞれがグラフ内の厳密な意味に対応します。| 構文 | YAML | 意味 |
|---|---|---|
| condition | 手順上の when: ${{ … }} | 述語が truthy の場合にのみ手順を実行する |
| branch | yes: / no: の手順リストを持つ branch: ブロック | 実行する手順のサブリストを選ぶ |
| fan-out | 手順上の foreach: ${{ … }} | 配列要素ごとに手順を実行し、出力を集約する |
| parallel | (暗黙) | 間にデータエッジがない手順は並行に実行される |
| nesting | use: flow:checkout@2 | 別の Flow を1つの手順(composite tool)として呼び出す |
when と foreach の2つは FlowSpec に直接保存されます。残りは、それらへ lowering されるオーサリング用の糖衣構文です(Lowering を参照)。
並列性は暗黙です
並列性を宣言することはありません。コンパイラーはデータ参照から依存グラフを導出します。つまり、手順のwith、when、foreach 式に含まれるすべての steps.<id> 参照を集め、明示的な needs と合わせて、referenced_id → this_id というエッジを追加します。間にエッジがない手順同士は独立しており、並行実行の対象になります。エッジはデータ参照から生まれるため、グラフは常に正直です。先に実行されたことを保証するエッジを作らずに、ある手順の出力を読むことはできません。FlowSpec §6 を参照してください。
branch の2つの書き方
daily-price-watch Flow は、取得した価格がしきい値を下回った場合にのみ、シークレット webhook に通知します。以下は、その条件付き通知を両方の surface で branch として書いたものです。
branch: ブロックはなく、when: を持つ notify 手順があるだけです。SDK の .branch() がコンパイルされる先も、まさにこの形です。
foreach とネスト
foreach は、配列式の各要素へ単一の手順を fan-out し、要素ごとの出力を集約します。branch と異なり、これは .step() の上に重ねられた糖衣構文ではありません。foreach(id, over, def) という独自のビルダーメソッドであり、手順ID、配列式、要素ごとに実行する StepDef を受け取ります。その手順のコンテキスト内では、c.item が現在の要素、c.index がその位置です。
use: が flow:checkout@2 である手順は、他の tool と同じようにバージョンで固定され、別の Flow を単一の composite tool として呼び出します。式サブ言語には、設計上ループもユーザー定義関数もありません。反復は手順レベルの foreach であり、式の内部では決して行いません。Expression language を参照してください。
Lowering
branch、when、foreach が唯一の制御構文であり、IR が保存するのはそのうち2つだけです。手順ごとの when 述語と、手順ごとの foreach です。オーサリング用の糖衣構文は機械的に lowering されるため、コンパイル済み FlowSpec には、スケジューラーが解釈すべき特別な制御ノードはありません。FlowSpec §6a を参照してください。
branch は手順ごとの when に lowering される
branch(pred, \{ yes, no \}) は、それに含まれる各手順上の when に lowering されます。yes: アーム内のすべての手順には when: pred が付き(既存の when がある場合は AND されます)、no: アーム内のすべての手順には when: !(pred) が付きます。FlowSpec には branch ノードは存在しません。あるのは手順だけであり、それぞれが自分自身の述語でゲートされます。不要な noop は取り除かれる
noop(上の no: noop("unchanged") アームなど)は、lowering 後のグラフでは実在する手順です。効果がなく、かつ downstream から参照されていない場合、canonicalization によって取り除かれます。これは dead-noop elimination であり、そのため digest には決して到達しません。何かが参照している場合は保持されます。lowering 後の daily-price-watch に unchanged アームの痕跡がないのはこのためです。skip propagation
手順は、そのwhen が false と評価されると skipped になります。その後、skip はデータエッジに沿って 1つのルールで伝播します。このルールは静的に決まり、競合によって決まることはありません。
Xが skipped だったときにsteps.X.outputを参照する手順は、それ自体も skipped になります。ただし、そのような参照がすべて fallback、つまりdefault(steps.X.output, …)またはsteps.X.output ?? …によって guard されている場合を除きます。すべての参照が guard されている場合、その参照はnullを返し、手順は実行されます。- Flow の
outputも同じ方法で評価されます。条件によって skipped になりうる手順を fallback なしで読む projection はコンパイル時に拒否されるため、Flow の出力が暗黙にnullになることはありません。
succeeded、skipped、failed のいずれか1つに正確に到達します。
skip の伝播はいつ止まりますか?
skip の伝播はいつ止まりますか?
skipped になった出力へのすべての read が guard されている最初の手順で止まります。その guard された手順は、欠けている値の代わりに
null を使い、succeeded まで実行されます(または、それ自身の理由で failed になります)。その 手順より downstream の手順から見ると、実在する完了済み producer があるため、伝播は guard を越えて続きません。なぜ run time ではなく静的に skip を決めるのですか?
なぜ run time ではなく静的に skip を決めるのですか?
決定論(tenet T3)のためです。downstream 手順の運命が、複数の並行手順のうちどれが先にコミットしたかに依存すると、同じ FlowSpec が実行ごとに異なる結果を生む可能性があります。コンパイル済みグラフから skip propagation を決めることで、結果は FlowSpec digest の性質になり、どこで実行しても同一になります。これこそが、一度コンパイルしてどこでも実行できるようにする目的です。
skipped の手順は failed の手順と同じですか?
skipped の手順は failed の手順と同じですか?
いいえ。
skipped、succeeded、failed は3つの異なる終端状態です。skipped の手順は、その when が false だったため(または skip を継承したため)実行されませんでした。failed の手順は実行され、エラーになりました。totality により、すべての手順はこの3つのうち正確に1つに到達することが保証されます。関連
FlowSpec と IR
lowering、canonicalization、digest が、糖衣構文を安定した artifact に変える方法。
式言語
${{ … }} の内部にある純粋なサブ言語。default(…) と ?? も含みます。Diagnostics
unguarded_skip を含む、すべてのコンパイル時エラー。