Azure Functions V4 が Public Preview になりました🎉
で、このエントリーは V4 とは全く関係ないですが、Azure Functions での関数実装について、最近気になったことをいくつかのところで目にしたので書いてみました。
context
オブジェクト
Node.js を使った Azure Functions では、各関数の引数に context
というオブジェクトが渡ってきます。
これは、context.bindings
を通したバインディングや、context.log
を通した Azure 基盤側へのログ記録などが行えるオブジェクトです。
その中で、context.done
というメソッドがあります。
context.done
メソッド
これは、Azure Functions が V1 だった頃は、関数の処理が終わったことを Function Worker 側に伝える必須のメソッドです。
バインディングのログの情報を一通り詰めて、context.done
を呼ぶと、Function Worker から Function Host 側に context オブジェクトが転送され、バインディングの実行やログの記録などが実行される仕組みです。
Azure Functions での Promise/async 関数と context.done
一方で、関数が Promise
を返す場合、または V2 以降でサポートされた Javascript の async 関数を使った場合 (Promise
を返す) は、関数コード内に明示的に context.done
を書かなくてもよくなりました。
関数の戻り値が Promise (厳密には Thenable
) の場合は、Node.js Worker のこの辺り で、context.done
が自動的に実行されます。
ですが、V1 から Azure Functions を使っている開発者の中には、context.done
オブジェクトを呼ぶことが習慣になっている方がいるようです。
もちろん、Promise を返す関数や async 関数内で context.done
を実行すること自体は問題ありません。
context.done
は、2 回目の実行時に警告だけログに記録し、その他の処理をスキップするようになっています。
ただ気を付けてほしい点としては、context.done
を実行した時点で関数の処理が終了するわけではない、ということです。
context.done
を実行した時点で context
オブジェクトにかかわる情報を Function Host に転送しますが、以降の処理はそのまま実行されます。
一方で、バインディングやログなど context
オブジェクトにかかわる情報は、context.done
の時点で Function Worker から Function Host に転送されて、バインディング処理/ログ記録等々が行われるので、context.done
以降に関連する処理を書いても反映されません。
これが、一見すると、関数処理が終わっているように誤認させる*1一因ですが、以降の関数コードもそのまま実行されるので、気を付けましょう。
例えば、context.done
以降に自分で SDK などを使って外部を呼び出ししている、などは、関数自体が終わっていないので、その呼び出しは実行されてしまい、書き込み用の呼び出しであれば書き込み自体も行われます。
どうするといいの?
async 関数内では、可能な限り context.done
の利用はやめましょう。
既存の関数で既に context.done
を書いていているのであれば、context.done
とセットで return;
を使いましょう。
(9/25 追記)
return
してもダメなケースもあるので、ちゃんと書きます。
例1: エラー処理の中で「関数終了」を意図して context.done
を使っている場合
こんな感じ。
module.exports = async (context) => { // ︙ if (error) { // 何かの処理のエラー判定 // エラー時の処理 // ︙ context.done(); // 「ここで終るよね?」 -> いえ、終わりません。 } // 正常時の処理。context.done 以降も実行。 // ︙ }
context.done
のタイミングで関数を終了させたければ、単純に return
してしまいましょう。
module.exports = async (context) => { // ︙ if (error) { // 何かの処理のエラー判定 // エラー時の処理 // ︙ return; // 「ここで終るよね?」-> はい、終わります } // 正常時の処理 // ︙ }
例2: コールバック関数の中のエラー処理後に context.done
を使っている場合
何かの処理をする非同期関数 someFunc
のコールバック関数の中で、エラー時の関数終了として context.done
を使っていたとします。
module.exports = async (context) => { someFunc((err) => { if (error) { // `someFunc` のエラー判定 // エラー時の処理 // ︙ context.done(); // 「ここで終るよね?」 -> いえ、終わりません。 } // 処理 A context.done 以降も実行。 // ︙ }); // 処理 B someFunc 実行前に実行 // ︙ }
この場合、処理 A も B も実行されます。
処理 A は context.done
実行後に、処理 B は someFunc
のコールバック関数実行前に、それぞれ実行されます。
もしエラー時には処理 B も実行させたくなければ、下記のように Promise でくるんで try - catch すると見通しもよくなります。
module.exports = async (context) => { try { await new Promise ((resolve, reject) => { someFunc((err) => { if (error) { // `someFunc` のエラー判定 // エラー時の処理 // ︙ reject(error); // 「ここで終るよね?」 -> 終らないけど、return した時に catch ブロックに飛ばします。 return; } // 処理 A // ︙ resolve(); }) }); // 処理 B // ︙ } catch { // エラー時の処理 // ︙ } }
(9/25 追記ここまで)
取り急ぎ。
*1: context.done 実行により Function Host へのログ送信を打ち切っているので以降のログが出ません。