読者です 読者をやめる 読者になる 読者になる

ぶれすとつーる

だいたいjavascript

timer#unrefで効率よくプロセスkillする

Node javascript

作ったアプリケーションが突然のエラーに見舞われることなんてざらにありますね。

で、エラーが起きたときはすみやかに、おだやかにそのワーカーをヌッコロしたいわけですがその終了作業中になんらかの処理がビジーな感じになって強制killしなきゃいけないこともあると思います。

インフラエンジニアな方々の貴重な休日を搾取しないためにもおだやかですみやかなプロセスkillは必須です。

これらを前提として適当にワーカーを殺す処理を書くとこんな感じになると思います。

var cluster = require('cluster')
  , domain = require('domain')
  ;

if (cluster.isMaster) {
  cluster.fork();
  cluster.fork();
} else {
  cluster.on('disconnect', function () {
    console.log('disconnect');
    cluster.fork();
  });

  doTask();
}

function doTask() {
  var d = domain.create()
    ;

  d.on('error', function () {
    var killtimer = setTimeout(function () {
        // 終了処理がなんらかの事情にビジー状態になってしまった時のために
        // 一律、30秒間待ってからプロセスを強制終了する
        process.exit(1);
    }, 30 * 1000);

    // --- 終了処理 ---

    // close some connections
    hoge.close();
    fuga.close();
    foo.close();

    // disconnect cluster
    cluster.worker.disconnect();

    // ----------------
  });

  d.run(function () {
    // メインななんらかの処理
    someTaskRun();
  });
}

これでserverやらなんらかの終了処理がビジーになったとしても30秒後には確実(終了処理で例外が発生しない限り)にworkerを殺すことができます。

ただ、無駄が多いですよね。

ビジー処理に見舞われずに正常に終了できた場合でも保険の30秒間はワーカーを維持することになります。

これはもったいないですね。

できることならば正常に処理が終了した後は30秒をまたずにworkerをヌッコロしたいです。


そんな時のために(?)、NodeのsetTimeoutはクライアントサイドのsetTimeoutと違い、timerオブジェクトを返すようになっており、timer#unrefメソッドを持っています。

unref()#

setTimeout() あるいは setInterval() が返す不透明な値は、 アクティブであるにもかかわらず、それがイベントループの最後の一つになっても プログラムの実行を継続しないタイマを作ることを可能にする、 timer.unref() メソッドを持っています。 既に unref されたタイマで再び unref が呼び出されても影響はありません。

setTimeout() が unref された場合、イベントループを起こすために独立した タイマが作成されるため、それらがあまりに多く作成されるとイベントループの パフォーマンスに悪影響を与えます -- 賢明に使ってください。

少し言葉が難しいですが、要するにtimer#unrefを実行すると、timerを返したsetTimeoutがイベントループの最後の処理になった時に、設定した時間をまたずにコールバックを実行して終わってくれるってことです。

こんな便利なものがあるのでさっきの処理はもっと効率よく書くことができる。

var cluster = require('cluster')
  , domain = require('domain')
  ;

if (cluster.isMaster) {
  cluster.fork();
  cluster.fork();
} else {
  cluster.on('disconnect', function () {
    console.log('disconnect');
    cluster.fork();
  });

  doTask();
}

function doTask() {
  var d = domain.create()
    ;

  d.on('error', function () {
    var killtimer = setTimeout(function () {
        // 終了処理がなんらかの事情にビジー状態になってしまった時のために
        // 一律、30秒間待ってからプロセスを強制終了する
        process.exit(1);
    }, 30 * 1000);

    // もし終了処理がうまくいってあとはタイマーを待つだけの状態に
    // なったらタイマーをまたずにsetTimeoutのコールバックを実行する。
    killtimer.unref();

    // --- 終了処理 ---

    // close some connections
    hoge.close();
    fuga.close();
    foo.close();

    // disconnect cluster
    cluster.worker.disconnect();

    // ----------------
  });

  d.run(function () {
    // メインななんらかの処理
    someTaskRun();
  });
}


無駄になることが多いであろうタイマー処理が無駄にならなくなって効率のよいコードになりました。

おしまい。


補足
ここでいうビジーっていうのはなんらか(closeとか)の非同期処理の向こう側で起きてるビジー状態なことをいってます。
これがもしNode側で無限ループとか発生してたらスレッドがブロックされるのでsetTimeoutのコールバックはいつまでたっても発火しないので検証の際はご注意ください




timer#unrefの挙動がよくわかんなかった人はこれを実行してみればなんとなくわかると思います。

var start = +new Date;

function randomElapsed() {
  var elapsed = (Math.random() * 10 + 1) << 0
    , timer
    ;

  console.log('timer is ', elapsed);

  timer = setTimeout(function () {
    console.log('done!', +new Date - start);
  }, elapsed * 1000);

  timer.unref();
  setTimeout(function lastTask() {
    console.log('last task is done', +new Date - start);
  }, 5000);
}

randomElapsed();