ネストされたドメインの奇妙な振る舞い
はじめに
ちょっと前の nodejs のメーリングリストで 「Odd behavior of nested Domaons」
https://groups.google.com/d/topic/nodejs/i8NjWjVvk2I/discussion
という質問が投げかけられていました。
私も内容が気になり、動作結果が非常に奇妙に思えたので調べてみました。(ML宛には解答を返事してますのでネタバレ注意です)
問題の奇妙な振る舞い
質問で提示されたコードは下記のものでした。
var domain = require('domain'); var topDomain = domain.create(); //親ドメインの作成 var subDomain = domain.create(); //子ドメインの作成 topDomain.add(subDomain); //親ドメインに子ドメインを追加 topDomain.on("error", function(err) { console.log("Top saw an error: " + err.message); }); subDomain.on("error", function(err) { console.log("Sub saw an error: " + err.message); throw new Error("bar"); // 親ドメイン宛に bar 例外スロー }); topDomain.run( function() { subDomain.run(function() { throw new Error("foo"); // 子ドメインで foo 例外スロー }); });
このコードは、
- 初めに topDomain と subDomain の2つを作成し、subDomain を topDomain に .add() しておきます。(ドメインの親子関係を明示的にバインドしている)
- subDomain 内で "foo" エラーのスローを実行すると、subDomain のエラーハンドラで捕獲してコンソールに出力します。
- ここで "bar" エラーをスローすると親ドメインの topDomain のエラーハンドラで受け、コンソールに出力するだろう。
というのがそもそもの狙いです。
で、これを実際に実行してみると以下の通り出力されます。
> node test10.js Sub saw an error: foo /home/ohtsu/tmp/domain_samples/test10.js:10 throw new Error("bar"); ^ Error: bar at Domain.<anonymous> (/home/ohtsu/tmp/domain_samples/test10.js:10:9) at Domain.EventEmitter.emit (events.js:88:17) at process.uncaughtHandler (domain.js:61:20) at process.EventEmitter.emit (events.js:115:20)
あらっ、 subDomain の方はちゃんとエラーを受けるのに、親ドメインの topDomain の方はエラー捕獲しません!
この理由が直感的にはわからず、ソースを調べてみてもしばらく悩みました。
解答編
Node.js では try/catch で捕獲していない例外スローは、 process オブジェクトの uncaughtException イベントで捕獲することができます。Node.js の Domain 実装は、実はこの uncaughtException イベントを使ってドメイン宛の例外スローを捕獲しているんです。ソースでは https://github.com/joyent/node/blob/master/lib/domain.js#L51-68 の部分になります。
process.on('uncaughtException', uncaughtHandler); function uncaughtHandler(er) { // アクティブなドメインが存在して、disposed されていない場合 if (exports.active && !exports.active._disposed) { util._extend(er, { // エラーオブジェクトに付加情報をつける domain: exports.active, domain_thrown: true }); exports.active.emit('error', er); // アクティブなドメインでエラーイベントを発生 exports.active.exit(); } else if (process.listeners('uncaughtException').length === 1) { throw er; } }
ここでは例外がスローされた時、 uncaughException イベントをひっかけてアクティブドメイン export.active(=process.domain) がどうか判断を行い、ドメインへエラーイベントを emit します。そのため、 subDomain のエラーハンドラ内で再度例外をスローすることは、 uncaughtException で受けたエラーハンドラで再度エラーをスローすることと一緒になるわけです。
例えばこんな感じです。
process.on('uncaughtException', function(e) { console.log(e.message); throw new Error('bar'); // uncaughtException 内で例外スロー }); throw new Error('foo');
これを実行すると、
> node test08.js foo /home/ohtsu/tmp/domain_samples/test08.js:3 throw new Error('bar'); ^ Error: bar at process.<anonymous> (/home/ohtsu/tmp/domain_samples/test08.js:3:9) at process.EventEmitter.emit (events.js:115:20)
先程の出力と全く同じです。
この理由は、 uncaughtException 内で uncaught な例外を再度受けるとループしてしまうことです。だからこれはそのまま終わらせるしかないです*1。ということで納得です。
「ドメインで受けたエラーイベントハンドラは uncaughtException のハンドラと同じなので uncaught な例外スローは他のドメインで捕獲することはできない。」
ワークアラウンド
じゃどうしたらいいのかと、もともとの質問のコードに返り回避策を考えたんですが、emit の方は問題なく動作するのでアクティブドメイン(this.domain)に明示的にエラーイベントを発生すればいいんじゃないかと考えました。
こんな感じになります。
var domain = require('domain'); var topDomain = domain.create(); var subDomain = domain.create(); topDomain.add(subDomain); topDomain.on("error", function(err) { console.log("Top saw an error: " + err.message); }); subDomain.on("error", function(err) { console.log("Sub saw an error: " + err.message); // バインドされているドメインが存在すれば、エラーイベントを発生 if (this.domain) this.domain.emit('error',new Error("bar")); }); topDomain.run( function() { subDomain.run(function() { throw new Error("foo"); }); });
出力も見事に期待通りです。
> node test11.js
Sub saw an error: foo
Top saw an error: bar
なんかもやもやしてたのがなくなり、ホントすっきりしました。
*1:ソース的には https://github.com/joyent/node/blob/master/src/node.cc#L1821-1826 です。 event_try_catch を受けたのでソース行付きのエラーレポートを出力してプロセス終了しています