はじめに
タイムアウトに負けずに長時間動き続ける Lambda Function を作る試みです。Step Functions とか使えという話ではあるのですが、できれば外部の設定を持ち込まずにコードだけで完結させたいところです。
Azure の Lambda 相当のサービスである Azure Functions には Durable Functions というものがあり、C# や JavaScript だけで長時間実行し続ける特殊な関数を記述することができます。この Durable Functions からアイデアを拝借し、AWS 上で簡易的な Durable Functions もどきを作れないか検証しました。
コード
概念とコードの説明
超簡単に用語の説明だけしておきます。
元ネタの Durable Functions について詳しくは MSのドキュメント を参照してください。
オーケストレータ
ジョブ全体の実行フローの制御をする関数です。コード中の orchestrator
関数がこれに相当します。
この関数は必ず決定的である必要があります。つまり、入力が同じなら常に同じ実行の流れで同じ結果を返さなくてはなりません。また、I/O処理も一切行ってはいけません。
アクティビティ
オーケストレータから呼び出され、実際の処理を行う関数です。コード中の print
関数がこれに相当します。オーケストレータとは異なり、決定的である必要はなく、I/O処理も可能です。
実行方法
- Dynamo DB のテーブルを作成します。名前は
ZombieFunctionsHistory
としてください。Primary partition key にjobId
(String型) を指定しておきます。 - Lambda 関数のパッケージをビルドします。コマンドは
npm install && npm run package
です。 - 適当な Lambda 関数を作成し、2. で作成したパッケージをアップロードします。ロールは Dynamo DB の読み書きに加え、自身に対する
lambda:Invoke
を許可しておく必要があります。 - あとは普通に Lambda 関数を実行します。
仕組み
ポイントは callActivity
です。callActivity
の呼び出しは次に示すような動作をします。
- 既に関数が実行されていて結果が Dynamo DB テーブルに記録されている場合は、実際には関数を実行せずに記録済みの結果を返す。
- 関数が未実行の場合は関数を実行し、結果をテーブルに記録する。その時点で現在の Lambda 関数の呼出を打ち切り、再度同じ Lambda 関数を最初から実行する。
これにより、同じ Lambda 関数が再度最初から実行されたときには、実行済みの処理はスキップして未実行箇所まで一気に進み、未実行処理のところまで到達したらその処理を実行して一旦 Lambda 関数の実行を打ち切る、という流れになります。結果として、各処理が都度、新しい Lambda 関数呼び出しとして実行され、タイムアウトしない長時間の実行が理屈上可能にまります。
オリジナルの Durable Functions と比較した課題
- オーケストレータ関数とアクティビティ関数が同じコンテキストで動いているため、オーケストレータ関数からみてアクティビティ関数を非同期実行できません。したがって、一つのジョブ内で複数のアクティビティ関数を並列実行するようなことはできません。
- アクティビティ関数が失敗した場合のリトライの仕組みがありません。
- Lambda 関数の実行は "at least once" であり、一度の呼び出しによって同時に複数起動することがあります。その場合、不整合が生じて正しく動作しません。(たぶん)
- オーケストレータ関数の実行を一時停止させることができません。Durable Functions では、処理を一時停止させて一定時間経過後に再開させることが可能で、一時停止中は課金されません。外部処理の完了をポーリングしながらのんびり待つようなケースでは重要な機能です。
- 外部イベントの発生を待機する仕組みがありません。
- その他、挙げるとキリがありません
まとめ
非常に簡易的なものではありますが、長時間動き続ける Function の基礎が実現できました。完全な実装になるまでにはまだまだ検証すべきことが多そうです。気が向いたらやります。