Skip to content

Domain

[履歴]

バージョン変更点
v8.8.0VM コンテキストで作成された Promise.domain プロパティを持たなくなりました。ただし、それらのハンドラーは適切なドメインで実行され、メインコンテキストで作成された Promise は依然として .domain プロパティを持っています。
v8.0.0Promise のハンドラーは、チェーンの最初の Promise が作成されたドメインで呼び出されるようになりました。
v1.4.2非推奨: v1.4.2 以降

[安定: 0 - 非推奨]

安定: 0 安定性: 0 - 非推奨

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

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

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

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

ドメインエラーハンドラーは、エラーが発生したときにプロセスを終了させるための代替手段ではありません。

JavaScript での throw の動作の性質上、参照をリークしたり、他の種類の未定義の脆い状態を作成したりすることなく、「中断した場所から安全に再開する」方法はおそらくほとんどありません。

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

より良いアプローチは、エラーをトリガーしたリクエストにエラー応答を送信し、他のリクエストは通常の時間内に完了させ、そのワーカーでの新しいリクエストのリスニングを停止することです。

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

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

js
// XXX 警告!悪い考え!

const d = require('node:domain').create()
d.on('error', er => {
  // エラーはプロセスをクラッシュさせませんが、それよりも悪いことをしています!
  // 急なプロセスの再起動を防いだとしても、これが発生した場合、多くのリソースをリークしています。
  // これは process.on('uncaughtException') と何ら変わりません!
  console.log(`error, but oh well ${er.message}`)
})
d.run(() => {
  require('node:http')
    .createServer((req, res) => {
      handleRequest(req, res)
    })
    .listen(PORT)
})

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

js
// はるかに良い!

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

if (cluster.isPrimary) {
  // より現実的なシナリオでは、2 つ以上のワーカーがあり、おそらくプライマリとワーカーを同じファイルに配置しないでしょう。
  //
  // また、ログ記録について少し凝ったり、DoS攻撃やその他の悪質な動作を防ぐために必要なカスタムロジックを実装したりすることも可能です。
  //
  // クラスタードキュメントのオプションを参照してください。
  //
  // 重要なことは、プライマリが実行する処理が非常に少ないことです。これにより、予期しないエラーに対する回復力が向上します。

  cluster.fork()
  cluster.fork()

  cluster.on('disconnect', worker => {
    console.error('disconnect!')
    cluster.fork()
  })
} else {
  // ワーカー
  //
  // ここにバグを配置します!

  const domain = require('node:domain')

  // リクエストを処理するためのワーカープロセスの使用方法、注意事項などについては、クラスタードキュメントを参照してください。

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

      // 危険な領域にいます!
      // 定義上、予期しないことが発生しました。
      // これは、私たちが望んでいない可能性が高いです。
      // 今後何が起こるかはわかりません!十分に注意してください!

      try {
        // 30秒以内に必ずクローズするようにします
        const killtimer = setTimeout(() => {
          process.exit(1)
        }, 30000)
        // ただし、そのためにプロセスをオープンしたままにしないでください!
        killtimer.unref()

        // 新しいリクエストの受付を停止します。
        server.close()

        // プライマリに終了したことを知らせます。これにより、クラスタープライマリで 'disconnect' がトリガーされ、新しいワーカーがフォークされます。
        cluster.worker.disconnect()

        // 問題をトリガーしたリクエストにエラーを送信しようとします
        res.statusCode = 500
        res.setHeader('content-type', 'text/plain')
        res.end('大変申し訳ありません。問題が発生しました。\n')
      } catch (er2) {
        // まあ、この時点ではできることはあまりありません。
        console.error(`500の送信中にエラーが発生しました! ${er2.stack}`)
      }
    })

    // req と res はこのドメインが存在する前に作成されたため、明示的に追加する必要があります。
    // 暗黙的なバインディングと明示的なバインディングの説明については以下を参照してください。
    d.add(req)
    d.add(res)

    // 次に、ドメイン内でハンドラー関数を実行します。
    d.run(() => {
      handleRequest(req, res)
    })
  })
  server.listen(PORT)
}

// この部分は重要ではありません。ルーティングの単なる例です。
// ここに高度なアプリケーションロジックを配置します。
function handleRequest(req, res) {
  switch (req.url) {
    case '/error':
      // いくつかの非同期処理を実行した後...
      setTimeout(() => {
        // おっと!
        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' イベントのみを処理します。

明示的バインディング

場合によっては、使用中のドメインが特定のイベントエミッターに使用すべきドメインではない場合があります。または、イベントエミッターが 1 つのドメインのコンテキストで作成されたが、代わりに別のドメインにバインドされるべきである可能性があります。

たとえば、HTTP サーバーに使用中のドメインが 1 つある可能性があり、リクエストごとに別のドメインを使用したい場合があります。

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

js
// サーバーのトップレベルドメインを作成
const domain = require('node:domain')
const http = require('node:http')
const serverDomain = domain.create()

serverDomain.run(() => {
  // サーバーは serverDomain のスコープ内で作成されます
  http
    .createServer((req, res) => {
      // Req と res も serverDomain のスコープ内で作成されます
      // ただし、リクエストごとに別のドメインを持つことを推奨します。
      // 最初に追加して、req と res を追加します。
      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()

Class: Domain

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

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

domain.members

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

domain.add(emitter)

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

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

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

domain.bind(callback)

返された関数は、指定されたコールバック関数をラップします。返された関数が呼び出されたとき、スローされたエラーはドメインの'error'イベントにルーティングされます。

js
const d = domain.create()

function readSomeFile(filename, cb) {
  fs.readFile(
    filename,
    'utf8',
    d.bind((er, data) => {
      // これがスローした場合、それもドメインに渡されます。
      return cb(er, data ? JSON.parse(data) : null)
    })
  )
}

d.on('error', er => {
  // どこかでエラーが発生しました。今すぐスローすると、通常の行番号とスタックメッセージでプログラムがクラッシュします
  //
})

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); パターンを、単一の場所にある単一のエラーハンドラーに置き換えることができます。

js
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])

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

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

js
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(() => {
      // 様々な非同期処理をシミュレート
      fs.open('non-existent file', 'r', (er, fd) => {
        if (er) throw er
        // 処理を続行...
      })
    }, 100)
  })
})

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

ドメインと Promise

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

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

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

d2.run(() => {
  p.then(v => {
    // d2 で実行
  })
})

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

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

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

d2.run(() => {
  p.then(
    p.domain.bind(v => {
      // d1 で実行
    })
  )
})

ドメインは、Promise のエラー処理メカニズムを妨害しません。言い換えれば、未処理の Promise の拒否に対して 'error' イベントは発行されません。