Domain#

安定性: 0 - 非推奨

ソースコード: lib/domain.js

このモジュールは非推奨化が予定されています。代替 API が確定次第、このモジュールは完全に非推奨となります。ほとんどの開発者はこのモジュールを使用する理由はないはずです。ドメインが提供する機能をどうしても必要とするユーザは、当面はそれに依存することができますが、将来的には別のソリューションに移行する必要があることを想定すべきです。

ドメインは、複数の異なる I/O 操作を単一のグループとして処理する方法を提供します。ドメインに登録されたイベントエミッタやコールバックのいずれかが 'error' イベントを発生させるか、エラーをスローした場合、ドメインオブジェクトに通知されます。これにより、process.on('uncaughtException') ハンドラでエラーのコンテキストが失われたり、エラーコードでプログラムが即座に終了したりすることを防ぎます。

警告: エラーを無視しないでください!#

ドメインのエラーハンドラは、エラーが発生したときにプロセスをシャットダウンすることの代替にはなりません。

JavaScript で throw が機能するまさにその性質上、参照のリークや、その他の未定義の不安定な状態を作り出すことなく、安全に「中断したところから再開する」方法はほとんどありません。

スローされたエラーに対応する最も安全な方法は、プロセスをシャットダウンすることです。もちろん、通常の Web サーバでは、多くの接続が開いている可能性があり、誰かによって引き起こされたエラーのためにそれらを突然シャットダウンすることは合理的ではありません。

より良いアプローチは、エラーを引き起こしたリクエストにエラーレスポンスを送信し、他のリクエストは通常の時間内に終了させ、そのワーカーで新しいリクエストのリッスンを停止することです。

このように、domain の使用はクラスタモジュールと密接に関連しています。なぜなら、プライマリプロセスはワーカーがエラーに遭遇したときに新しいワーカーをフォークできるからです。複数のマシンにスケールする Node.js プログラムの場合、終端プロキシやサービスレジストリが障害を検知し、それに応じて対応できます。

例えば、これは良いアイデアではありません。

// XXX WARNING! BAD IDEA!

const d = require('node:domain').create();
d.on('error', (er) => {
  // The error won't crash the process, but what it does is worse!
  // Though we've prevented abrupt process restarting, we are leaking
  // a lot of resources if this ever happens.
  // This is no better than process.on('uncaughtException')!
  console.log(`error, but oh well ${er.message}`);
});
d.run(() => {
  require('node:http').createServer((req, res) => {
    handleRequest(req, res);
  }).listen(PORT);
}); 

ドメインのコンテキストと、プログラムを複数のワーカープロセスに分離する回復力を利用することで、より適切に対応し、はるかに高い安全性でエラーを処理できます。

// Much better!

const cluster = require('node:cluster');
const PORT = +process.env.PORT || 1337;

if (cluster.isPrimary) {
  // A more realistic scenario would have more than 2 workers,
  // and perhaps not put the primary and worker in the same file.
  //
  // It is also possible to get a bit fancier about logging, and
  // implement whatever custom logic is needed to prevent DoS
  // attacks and other bad behavior.
  //
  // See the options in the cluster documentation.
  //
  // The important thing is that the primary does very little,
  // increasing our resilience to unexpected errors.

  cluster.fork();
  cluster.fork();

  cluster.on('disconnect', (worker) => {
    console.error('disconnect!');
    cluster.fork();
  });

} else {
  // the worker
  //
  // This is where we put our bugs!

  const domain = require('node:domain');

  // See the cluster documentation for more details about using
  // worker processes to serve requests. How it works, caveats, etc.

  const server = require('node:http').createServer((req, res) => {
    const d = domain.create();
    d.on('error', (er) => {
      console.error(`error ${er.stack}`);

      // We're in dangerous territory!
      // By definition, something unexpected occurred,
      // which we probably didn't want.
      // Anything can happen now! Be very careful!

      try {
        // Make sure we close down within 30 seconds
        const killtimer = setTimeout(() => {
          process.exit(1);
        }, 30000);
        // But don't keep the process open just for that!
        killtimer.unref();

        // Stop taking new requests.
        server.close();

        // Let the primary know we're dead. This will trigger a
        // 'disconnect' in the cluster primary, and then it will fork
        // a new worker.
        cluster.worker.disconnect();

        // Try to send an error to the request that triggered the problem
        res.statusCode = 500;
        res.setHeader('content-type', 'text/plain');
        res.end('Oops, there was a problem!\n');
      } catch (er2) {
        // Oh well, not much we can do at this point.
        console.error(`Error sending 500! ${er2.stack}`);
      }
    });

    // Because req and res were created before this domain existed,
    // we need to explicitly add them.
    // See the explanation of implicit vs explicit binding below.
    d.add(req);
    d.add(res);

    // Now run the handler function in the domain.
    d.run(() => {
      handleRequest(req, res);
    });
  });
  server.listen(PORT);
}

// This part is not important. Just an example routing thing.
// Put fancy application logic here.
function handleRequest(req, res) {
  switch (req.url) {
    case '/error':
      // We do some async stuff, and then...
      setTimeout(() => {
        // Whoops!
        flerb.bark();
      }, timeout);
      break;
    default:
      res.end('ok');
  }
} 

Error オブジェクトへの追加#

Error オブジェクトがドメインを介してルーティングされるたびに、いくつかの追加フィールドがそれに付加されます。

  • error.domain 最初にエラーを処理したドメイン。
  • error.domainEmitter エラーオブジェクトとともに 'error' イベントを発生させたイベントエミッタ。
  • error.domainBound ドメインにバインドされ、最初の引数としてエラーが渡されたコールバック関数。
  • error.domainThrown エラーがスローされたか、発生したか、バインドされたコールバック関数に渡されたかを示すブール値。

暗黙的なバインディング#

ドメインが使用されている場合、すべての新しい EventEmitter オブジェクト(Stream オブジェクト、リクエスト、レスポンスなどを含む)は、作成時のアクティブなドメインに暗黙的にバインドされます。

さらに、低レベルのイベントループリクエスト(fs.open() やその他のコールバックを受け取るメソッドなど)に渡されるコールバックは、自動的にアクティブなドメインにバインドされます。それらがスローした場合、ドメインがエラーをキャッチします。

過度のメモリ使用を防ぐために、Domain オブジェクト自体はアクティブなドメインの子として暗黙的に追加されません。もしそうなっていたら、リクエストオブジェクトとレスポンスオブジェクトが適切にガベージコレクションされるのを妨げることがあまりにも簡単になってしまいます。

Domain オブジェクトを親 Domain の子としてネストするには、明示的に追加する必要があります。

暗黙的なバインディングは、スローされたエラーと 'error' イベントを Domain'error' イベントにルーティングしますが、EventEmitterDomain に登録はしません。暗黙的なバインディングは、スローされたエラーと 'error' イベントのみを処理します。

明示的なバインディング#

時々、使用中のドメインが特定のイベントエミッタに使用されるべきものではないことがあります。あるいは、イベントエミッタがあるドメインのコンテキストで作成されたが、代わりに他のドメインにバインドされるべきである場合もあります。

例えば、HTTP サーバ用に 1 つのドメインが使用されているが、リクエストごとに別のドメインを使用したい場合などです。

それは明示的なバインディングによって可能です。

// Create a top-level domain for the server
const domain = require('node:domain');
const http = require('node:http');
const serverDomain = domain.create();

serverDomain.run(() => {
  // Server is created in the scope of serverDomain
  http.createServer((req, res) => {
    // Req and res are also created in the scope of serverDomain
    // however, we'd prefer to have a separate domain for each request.
    // create it first thing, and add req and res to it.
    const reqd = domain.create();
    reqd.add(req);
    reqd.add(res);
    reqd.on('error', (er) => {
      console.error('Error', er, req.url);
      try {
        res.writeHead(500);
        res.end('Error occurred, sorry.');
      } catch (er2) {
        console.error('Error sending 500', er2, req.url);
      }
    });
  }).listen(1337);
}); 

domain.create()#

クラス: Domain#

Domain クラスは、エラーとキャッチされなかった例外をアクティブな Domain オブジェクトにルーティングする機能をカプセル化します。

キャッチしたエラーを処理するには、その 'error' イベントをリッスンします。

domain.members#

ドメインに明示的に追加されたタイマーとイベントエミッタの配列。

domain.add(emitter)#

エミッタをドメインに明示的に追加します。エミッタによって呼び出されたイベントハンドラがエラーをスローするか、エミッタが 'error' イベントを発生させた場合、暗黙的なバインディングと同様に、ドメインの 'error' イベントにルーティングされます。

これは setInterval()setTimeout() から返されるタイマーでも機能します。それらのコールバック関数がスローすると、ドメインの 'error' ハンドラによってキャッチされます。

タイマーまたは EventEmitter が既にドメインにバインドされていた場合、それは前のドメインから削除され、代わりにこのドメインにバインドされます。

domain.bind(callback)#

返される関数は、提供されたコールバック関数のラッパーになります。返された関数が呼び出されると、スローされたエラーはドメインの 'error' イベントにルーティングされます。

const d = domain.create();

function readSomeFile(filename, cb) {
  fs.readFile(filename, 'utf8', d.bind((er, data) => {
    // If this throws, it will also be passed to the domain.
    return cb(er, data ? JSON.parse(data) : null);
  }));
}

d.on('error', (er) => {
  // An error occurred somewhere. If we throw it now, it will crash the program
  // with the normal line number and stack message.
}); 

domain.enter()#

enter() メソッドは、アクティブなドメインを設定するために run()bind()intercept() メソッドによって使用される内部処理です。これは domain.activeprocess.domain をドメインに設定し、ドメインモジュールによって管理されるドメインスタックにドメインを暗黙的にプッシュします(ドメインスタックの詳細については domain.exit() を参照)。enter() の呼び出しは、ドメインにバインドされた非同期呼び出しと I/O 操作の連鎖の始まりを区切ります。

enter() を呼び出すとアクティブなドメインのみが変更され、ドメイン自体は変更されません。enter()exit() は、単一のドメインで任意の回数呼び出すことができます。

domain.exit()#

exit() メソッドは現在のドメインを終了し、ドメインスタックからポップします。実行が別の非同期呼び出しの連鎖のコンテキストに切り替わるときはいつでも、現在のドメインが終了されることを確認することが重要です。exit() の呼び出しは、ドメインにバインドされた非同期呼び出しと I/O 操作の連鎖の終了または中断を区切ります。

現在の実行コンテキストに複数のネストされたドメインがバインドされている場合、exit() はこのドメイン内にネストされているすべてのドメインを終了します。

exit() を呼び出すとアクティブなドメインのみが変更され、ドメイン自体は変更されません。enter()exit() は、単一のドメインで任意の回数呼び出すことができます。

domain.intercept(callback)#

  • callback <Function> コールバック関数
  • 戻り値: <Function> インターセプトされた関数

このメソッドは domain.bind(callback) とほぼ同じです。ただし、スローされたエラーをキャッチすることに加えて、関数の最初の引数として送信された Error オブジェクトもインターセプトします。

このようにして、一般的な if (err) return callback(err); パターンを、単一の場所にある単一のエラーハンドラに置き換えることができます。

const d = domain.create();

function readSomeFile(filename, cb) {
  fs.readFile(filename, 'utf8', d.intercept((data) => {
    // Note, the first argument is never passed to the
    // callback since it is assumed to be the 'Error' argument
    // and thus intercepted by the domain.

    // If this throws, it will also be passed to the domain
    // so the error-handling logic can be moved to the 'error'
    // event on the domain instead of being repeated throughout
    // the program.
    return cb(null, JSON.parse(data));
  }));
}

d.on('error', (er) => {
  // An error occurred somewhere. If we throw it now, it will crash the program
  // with the normal line number and stack message.
}); 

domain.remove(emitter)#

domain.add(emitter) の逆です。指定されたエミッタからドメインのハンドリングを削除します。

domain.run(fn[, ...args])#

提供された関数をドメインのコンテキストで実行し、そのコンテキストで作成されるすべてのイベントエミッタ、タイマー、低レベルリクエストを暗黙的にバインドします。オプションで、関数に引数を渡すことができます。

これはドメインを使用する最も基本的な方法です。

const domain = require('node:domain');
const fs = require('node:fs');
const d = domain.create();
d.on('error', (er) => {
  console.error('Caught error!', er);
});
d.run(() => {
  process.nextTick(() => {
    setTimeout(() => { // Simulating some various async stuff
      fs.open('non-existent file', 'r', (er, fd) => {
        if (er) throw er;
        // proceed...
      });
    }, 100);
  });
}); 

この例では、プログラムをクラッシュさせる代わりに、d.on('error') ハンドラがトリガーされます。

ドメインと Promise#

Node.js 8.0.0 以降、Promise のハンドラは .then() または .catch() の呼び出し自体が行われたドメイン内で実行されます。

const d1 = domain.create();
const d2 = domain.create();

let p;
d1.run(() => {
  p = Promise.resolve(42);
});

d2.run(() => {
  p.then((v) => {
    // running in d2
  });
}); 

コールバックは domain.bind(callback) を使用して特定のドメインにバインドすることができます。

const d1 = domain.create();
const d2 = domain.create();

let p;
d1.run(() => {
  p = Promise.resolve(42);
});

d2.run(() => {
  p.then(p.domain.bind((v) => {
    // running in d1
  }));
}); 

ドメインは Promise のエラーハンドリングメカニズムに干渉しません。つまり、未処理の Promise のリジェクトに対して 'error' イベントは発生しません。