AWS Lambdaでcontext.succeed()とcallback()の挙動の違いを理解する

こんにちは、せーのです。今日は業務で私がハマってしまったところを備忘録として残しておきます。

正常に終わってるはずなのにタイムアウト

まずはこんな感じのLambdaがあるとします。

exports.handler = (event, context, callback) => {
    
        callback(null, "Hello Lambda");
       
};

これは、こう書き換えてもおなじです。

exports.handler = (event, context, callback) => {
    
        context.succeed("Hello Lambda");
       
};

Lambdaの対象Nodeバージョンが4.3も選べるようになり、4.3の場合プログラミングモデルがCallbackモデルになりました。処理を戻しているだけなのでどちらでも良い気がします。

めんどくさいのはこれからです。この処理の上にこんなのをつけてみます。

setInterval(() => console.log("interval"),2000);

exports.handler = (event, context, callback) => {
    
        context.succeed("Hello Lambda");
       
};

これも正常終了します。しかし

setInterval(() => console.log("interval"),2000);

exports.handler = (event, context, callback) => {
    
        callback(null, "Hello Lambda");
       
};

これはタイムアウトになってしまいます。湧き上がる無力感。どうしてほぼコードをほぼ変更していないのにタイムアウトになるのでしょう。

イベントループが全て終わるまで続く

実はLambdaのcallbackは仕様として「イベントループのすべてのイベントが処理されるまで待機」となっています。つまり、Lambdaのどこかで何かの処理が走っていた場合、Callbackが呼ばれても待たされます。一方contextは呼ばれた瞬間に全てのプロセスが終了します。上の例ではsetIntervalで動いている処理があるためCallbackが無視されて、結果タイムアウトになったのですね。

「promiseとかasync/awaitとかで処理を同期化すればいいじゃないか」なんて簡単に考えていた時期が私にもありました。でもこの「どこかで何かの処理が走る」がライブラリ依存だと手も足も出ない、ということがわかりました。例えば

var mysql = require('mysql');

var pool = mysql.createPool({
    host     : process.env.host,
    user     : process.env.user,
    password : process.env.password,
    port     : process.env.port,
    database : process.env.database
});

exports.handler = (event, context, callback) => {
    
        funcDB((ret1,ret2) => callback(null, "ret1: " + ret1 + ", ret2: " + ret2));

};

funcDB(callback) {
  pool.getConnection(function(err, conn) {
  var ret1 = "DB";
    var ret2 = "OK";
    
    conn.release();
    callback(ret1, ret2);
           
  });        
}

これはmySqlのプールコネクションを使って接続を確立しているのですが、このpool.getConnection()が悪さをするためタイムアウトになってしまいます。この場合処理を抜ける部分のcallback(null, "ret1: " + ret1 + ", ret2: " + ret2)context.succeed("ret1: " + ret1 + ", ret2: " + ret2)だとうまくいきます。

設定で変更

でもコード内容によってcontextとcallbackをかき分けるのは何か気持ち悪いです。そういう場合はcontext.callbackWaitsForEmptyEventLoopという設定を事前にしておくことでcallbackでもプロセスを切ることができます(まあ、これもcontextなんですが。。。)。

var mysql = require('mysql');

var pool = mysql.createPool({
    host     : process.env.host,
    user     : process.env.user,
    password : process.env.password,
    port     : process.env.port,
    database : process.env.database
});

exports.handler = (event, context, callback) => {
    context.callbackWaitsForEmptyEventLoop = false;
  funcDB((ret1,ret2) => callback(null, "ret1: " + ret1 + ", ret2: " + ret2));

};

funcDB(callback) {
  pool.getConnection(function(err, conn) {

    conn.release();
    callback(ret1, ret2);
           
  });        
}

この設定は名前の通り「イベントループが空になるまでcallbackはwaitするかどうか」という設定です。デフォルトはtrueになっているので、こんな感じでfalseにしてあげればOKです。もちろん逆に「すべての処理を終わらせてからcallbackしたい」という場合はtrueにしておけば(デフォルト)OKですね。

まとめ

今回は業務でハマったポイントについて書いておきました。よくよく調べるとNodeが4.3になった時にいろんな資料で注記されていたんですね。公式はちゃんと読もう。

参考リンク