Skip to content

Асинхронное отслеживание контекста

[Стабильно: 2 - Стабильно]

Стабильно: 2 Стабильность: 2 - Стабильно

Исходный код: lib/async_hooks.js

Введение

Эти классы используются для связывания состояния и его распространения через обратные вызовы и цепочки промисов. Они позволяют хранить данные на протяжении всего времени существования веб-запроса или любого другого асинхронного периода. Это похоже на локальное хранилище потока в других языках.

Классы AsyncLocalStorage и AsyncResource являются частью модуля node:async_hooks:

js
import { AsyncLocalStorage, AsyncResource } from 'node:async_hooks'
js
const { AsyncLocalStorage, AsyncResource } = require('node:async_hooks')

Класс: AsyncLocalStorage

[История]

ВерсияИзменения
v16.4.0AsyncLocalStorage теперь является Стабильным. Ранее он был Экспериментальным.
v13.10.0, v12.17.0Добавлено в: v13.10.0, v12.17.0

Этот класс создает хранилища, которые остаются согласованными во время асинхронных операций.

Хотя вы можете создать свою собственную реализацию поверх модуля node:async_hooks, следует предпочесть AsyncLocalStorage, поскольку это производительная и безопасная в плане памяти реализация, которая включает в себя значительные оптимизации, которые неочевидны для реализации.

В следующем примере используется AsyncLocalStorage для создания простого регистратора, который присваивает идентификаторы входящим HTTP-запросам и включает их в сообщения, регистрируемые в каждом запросе.

js
import http from 'node:http'
import { AsyncLocalStorage } from 'node:async_hooks'

const asyncLocalStorage = new AsyncLocalStorage()

function logWithId(msg) {
  const id = asyncLocalStorage.getStore()
  console.log(`${id !== undefined ? id : '-'}:`, msg)
}

let idSeq = 0
http
  .createServer((req, res) => {
    asyncLocalStorage.run(idSeq++, () => {
      logWithId('start')
      // Представьте любую цепочку асинхронных операций здесь
      setImmediate(() => {
        logWithId('finish')
        res.end()
      })
    })
  })
  .listen(8080)

http.get('http://localhost:8080')
http.get('http://localhost:8080')
// Выводит:
//   0: start
//   1: start
//   0: finish
//   1: finish
js
const http = require('node:http')
const { AsyncLocalStorage } = require('node:async_hooks')

const asyncLocalStorage = new AsyncLocalStorage()

function logWithId(msg) {
  const id = asyncLocalStorage.getStore()
  console.log(`${id !== undefined ? id : '-'}:`, msg)
}

let idSeq = 0
http
  .createServer((req, res) => {
    asyncLocalStorage.run(idSeq++, () => {
      logWithId('start')
      // Представьте любую цепочку асинхронных операций здесь
      setImmediate(() => {
        logWithId('finish')
        res.end()
      })
    })
  })
  .listen(8080)

http.get('http://localhost:8080')
http.get('http://localhost:8080')
// Выводит:
//   0: start
//   1: start
//   0: finish
//   1: finish

Каждый экземпляр AsyncLocalStorage поддерживает независимый контекст хранения. Несколько экземпляров могут безопасно существовать одновременно, не рискуя помешать данным друг друга.

new AsyncLocalStorage()

[История]

ВерсияИзменения
v19.7.0, v18.16.0Удалена экспериментальная опция onPropagate.
v19.2.0, v18.13.0Добавлена опция onPropagate.
v13.10.0, v12.17.0Добавлено в: v13.10.0, v12.17.0

Создает новый экземпляр AsyncLocalStorage. Хранилище предоставляется только внутри вызова run() или после вызова enterWith().

Статический метод: AsyncLocalStorage.bind(fn)

Добавлено в: v19.8.0, v18.16.0

[Стабильность: 1 - Экспериментальный]

Стабильность: 1 Стабильность: 1 - Экспериментальный

  • fn <Function> Функция, которую нужно привязать к текущему контексту выполнения.
  • Возвращает: <Function> Новую функцию, которая вызывает fn в захваченном контексте выполнения.

Привязывает заданную функцию к текущему контексту выполнения.

Статический метод: AsyncLocalStorage.snapshot()

Добавлено в: v19.8.0, v18.16.0

[Стабильность: 1 - Экспериментальный]

Стабильность: 1 Стабильность: 1 - Экспериментальный

  • Возвращает: <Function> Новую функцию с сигнатурой (fn: (...args) : R, ...args) : R.

Захватывает текущий контекст выполнения и возвращает функцию, которая принимает функцию в качестве аргумента. При каждом вызове возвращаемой функции, она вызывает переданную ей функцию в захваченном контексте.

js
const asyncLocalStorage = new AsyncLocalStorage()
const runInAsyncScope = asyncLocalStorage.run(123, () => AsyncLocalStorage.snapshot())
const result = asyncLocalStorage.run(321, () => runInAsyncScope(() => asyncLocalStorage.getStore()))
console.log(result) // возвращает 123

AsyncLocalStorage.snapshot() может заменить использование AsyncResource для простых целей отслеживания асинхронного контекста, например:

js
class Foo {
  #runInAsyncScope = AsyncLocalStorage.snapshot()

  get() {
    return this.#runInAsyncScope(() => asyncLocalStorage.getStore())
  }
}

const foo = asyncLocalStorage.run(123, () => new Foo())
console.log(asyncLocalStorage.run(321, () => foo.get())) // возвращает 123

asyncLocalStorage.disable()

Добавлено в: v13.10.0, v12.17.0

[Стабильность: 1 - Экспериментальная]

Стабильность: 1 Стабильность: 1 - Экспериментальная

Отключает экземпляр AsyncLocalStorage. Все последующие вызовы asyncLocalStorage.getStore() будут возвращать undefined, пока снова не будет вызван asyncLocalStorage.run() или asyncLocalStorage.enterWith().

При вызове asyncLocalStorage.disable() все текущие контексты, связанные с экземпляром, будут завершены.

Вызов asyncLocalStorage.disable() необходим перед тем, как сборщик мусора сможет собрать asyncLocalStorage. Это не относится к хранилищам, предоставляемым asyncLocalStorage, поскольку эти объекты собираются сборщиком мусора вместе с соответствующими асинхронными ресурсами.

Используйте этот метод, когда asyncLocalStorage больше не используется в текущем процессе.

asyncLocalStorage.getStore()

Добавлено в: v13.10.0, v12.17.0

  • Возвращает: <any>

Возвращает текущее хранилище. Если вызывается вне асинхронного контекста, инициализированного вызовом asyncLocalStorage.run() или asyncLocalStorage.enterWith(), он возвращает undefined.

asyncLocalStorage.enterWith(store)

Добавлено в: v13.11.0, v12.17.0

[Стабильность: 1 - Экспериментальная]

Стабильность: 1 Стабильность: 1 - Экспериментальная

Переходит в контекст на оставшуюся часть текущего синхронного выполнения, а затем сохраняет хранилище при любых последующих асинхронных вызовах.

Пример:

js
const store = { id: 1 }
// Заменяет предыдущее хранилище заданным объектом хранилища
asyncLocalStorage.enterWith(store)
asyncLocalStorage.getStore() // Возвращает объект хранилища
someAsyncOperation(() => {
  asyncLocalStorage.getStore() // Возвращает тот же объект
})

Этот переход будет продолжаться в течение всего синхронного выполнения. Это означает, что если, например, контекст введен в обработчике событий, последующие обработчики событий также будут выполняться в этом контексте, если он специально не привязан к другому контексту с помощью AsyncResource. Именно поэтому run() следует предпочитать enterWith(), если нет веских причин использовать последний метод.

js
const store = { id: 1 }

emitter.on('my-event', () => {
  asyncLocalStorage.enterWith(store)
})
emitter.on('my-event', () => {
  asyncLocalStorage.getStore() // Возвращает тот же объект
})

asyncLocalStorage.getStore() // Возвращает undefined
emitter.emit('my-event')
asyncLocalStorage.getStore() // Возвращает тот же объект

asyncLocalStorage.run(store, callback[, ...args])

Добавлено в: v13.10.0, v12.17.0

Синхронно выполняет функцию в контексте и возвращает ее возвращаемое значение. Хранилище недоступно за пределами функции обратного вызова. Хранилище доступно для любых асинхронных операций, созданных внутри обратного вызова.

Необязательные args передаются в функцию обратного вызова.

Если функция обратного вызова вызывает ошибку, то ошибка также выбрасывается run(). На стек вызовов не влияет этот вызов, и контекст завершается.

Пример:

js
const store = { id: 2 }
try {
  asyncLocalStorage.run(store, () => {
    asyncLocalStorage.getStore() // Возвращает объект хранилища
    setTimeout(() => {
      asyncLocalStorage.getStore() // Возвращает объект хранилища
    }, 200)
    throw new Error()
  })
} catch (e) {
  asyncLocalStorage.getStore() // Возвращает undefined
  // Ошибка будет перехвачена здесь
}

asyncLocalStorage.exit(callback[, ...args])

Добавлено в: v13.10.0, v12.17.0

[Stable: 1 - Experimental]

Stable: 1 Стабильность: 1 - Экспериментальная

Синхронно выполняет функцию вне контекста и возвращает ее возвращаемое значение. Хранилище недоступно внутри функции обратного вызова или асинхронных операций, созданных внутри обратного вызова. Любой вызов getStore(), выполненный внутри функции обратного вызова, всегда будет возвращать undefined.

Необязательные args передаются в функцию обратного вызова.

Если функция обратного вызова вызывает ошибку, то ошибка также выбрасывается exit(). На стек вызовов не влияет этот вызов, и контекст восстанавливается.

Пример:

js
// Внутри вызова run
try {
  asyncLocalStorage.getStore() // Возвращает объект или значение хранилища
  asyncLocalStorage.exit(() => {
    asyncLocalStorage.getStore() // Возвращает undefined
    throw new Error()
  })
} catch (e) {
  asyncLocalStorage.getStore() // Возвращает тот же объект или значение
  // Ошибка будет перехвачена здесь
}

Использование с async/await

Если внутри асинхронной функции только один вызов await должен выполняться в контексте, следует использовать следующий шаблон:

js
async function fn() {
  await asyncLocalStorage.run(new Map(), () => {
    asyncLocalStorage.getStore().set('key', value)
    return foo() // Возвращаемое значение foo будет ожидать
  })
}

В этом примере хранилище доступно только в функции обратного вызова и функциях, вызываемых foo. Вне run вызов getStore вернет undefined.

Устранение неполадок: Потеря контекста

В большинстве случаев AsyncLocalStorage работает без проблем. В редких ситуациях текущее хранилище теряется в одной из асинхронных операций.

Если ваш код основан на обратных вызовах, достаточно промисифицировать его с помощью util.promisify(), чтобы он начал работать с нативными промисами.

Если вам нужно использовать API на основе обратных вызовов или ваш код предполагает пользовательскую реализацию thenable, используйте класс AsyncResource, чтобы связать асинхронную операцию с правильным контекстом выполнения. Найдите вызов функции, ответственный за потерю контекста, путем логирования содержимого asyncLocalStorage.getStore() после вызовов, которые, как вы подозреваете, ответственны за потерю. Когда код регистрирует undefined, последний вызванный обратный вызов, вероятно, отвечает за потерю контекста.

Класс: AsyncResource

[История]

ВерсияИзменения
v16.4.0AsyncResource теперь стабилен. Ранее он был экспериментальным.

Класс AsyncResource предназначен для расширения асинхронными ресурсами встраивателя. Используя это, пользователи могут легко запускать события жизненного цикла своих собственных ресурсов.

Хук init будет срабатывать при создании экземпляра AsyncResource.

Ниже представлен обзор API AsyncResource.

js
import { AsyncResource, executionAsyncId } from 'node:async_hooks'

// AsyncResource() предназначен для расширения. Создание нового
// экземпляра AsyncResource() также запускает init. Если triggerAsyncId опущен,
// используется async_hook.executionAsyncId().
const asyncResource = new AsyncResource(type, { triggerAsyncId: executionAsyncId(), requireManualDestroy: false })

// Запустите функцию в контексте выполнения ресурса. Это
// * установит контекст ресурса
// * запустит обратные вызовы AsyncHooks before
// * вызовет предоставленную функцию `fn` с предоставленными аргументами
// * запустит обратные вызовы AsyncHooks after
// * восстановит исходный контекст выполнения
asyncResource.runInAsyncScope(fn, thisArg, ...args)

// Вызовите обратные вызовы AsyncHooks destroy.
asyncResource.emitDestroy()

// Возвращает уникальный идентификатор, назначенный экземпляру AsyncResource.
asyncResource.asyncId()

// Возвращает идентификатор триггера для экземпляра AsyncResource.
asyncResource.triggerAsyncId()
js
const { AsyncResource, executionAsyncId } = require('node:async_hooks')

// AsyncResource() предназначен для расширения. Создание нового
// экземпляра AsyncResource() также запускает init. Если triggerAsyncId опущен,
// используется async_hook.executionAsyncId().
const asyncResource = new AsyncResource(type, { triggerAsyncId: executionAsyncId(), requireManualDestroy: false })

// Запустите функцию в контексте выполнения ресурса. Это
// * установит контекст ресурса
// * запустит обратные вызовы AsyncHooks before
// * вызовет предоставленную функцию `fn` с предоставленными аргументами
// * запустит обратные вызовы AsyncHooks after
// * восстановит исходный контекст выполнения
asyncResource.runInAsyncScope(fn, thisArg, ...args)

// Вызовите обратные вызовы AsyncHooks destroy.
asyncResource.emitDestroy()

// Возвращает уникальный идентификатор, назначенный экземпляру AsyncResource.
asyncResource.asyncId()

// Возвращает идентификатор триггера для экземпляра AsyncResource.
asyncResource.triggerAsyncId()

new AsyncResource(type[, options])

  • type <string> Тип асинхронного события.
  • options <Object>
    • triggerAsyncId <number> Идентификатор контекста выполнения, который создал это асинхронное событие. По умолчанию: executionAsyncId().
    • requireManualDestroy <boolean> Если установлено значение true, отключает emitDestroy, когда объект собирается сборщиком мусора. Обычно это не нужно устанавливать (даже если emitDestroy вызывается вручную), если только asyncId ресурса не извлекается и чувствительный API emitDestroy не вызывается с ним. Если установлено значение false, вызов emitDestroy при сборке мусора будет происходить только в том случае, если есть хотя бы один активный хук destroy. По умолчанию: false.

Пример использования:

js
class DBQuery extends AsyncResource {
  constructor(db) {
    super('DBQuery')
    this.db = db
  }

  getInfo(query, callback) {
    this.db.get(query, (err, data) => {
      this.runInAsyncScope(callback, null, err, data)
    })
  }

  close() {
    this.db = null
    this.emitDestroy()
  }
}

Статический метод: AsyncResource.bind(fn[, type[, thisArg]])

[История]

ВерсияИзменения
v20.0.0Свойство asyncResource, добавленное к связанной функции, устарело и будет удалено в будущей версии.
v17.8.0, v16.15.0Изменено поведение по умолчанию, когда thisArg не определен, чтобы использовать this из вызывающего объекта.
v16.0.0Добавлен необязательный thisArg.
v14.8.0, v12.19.0Добавлено в: v14.8.0, v12.19.0
  • fn <Function> Функция для привязки к текущему контексту выполнения.
  • type <string> Необязательное имя для связывания с базовым AsyncResource.
  • thisArg <any>

Связывает заданную функцию с текущим контекстом выполнения.

asyncResource.bind(fn[, thisArg])

[История]

ВерсияИзменения
v20.0.0Свойство asyncResource, добавленное к связанной функции, объявлено устаревшим и будет удалено в будущей версии.
v17.8.0, v16.15.0Изменено значение по умолчанию, когда thisArg не определен, чтобы использовать this от вызывающего.
v16.0.0Добавлен необязательный thisArg.
v14.8.0, v12.19.0Добавлено в: v14.8.0, v12.19.0
  • fn <Function> Функция для привязки к текущему AsyncResource.
  • thisArg <any>

Привязывает данную функцию для выполнения в области действия этого AsyncResource.

asyncResource.runInAsyncScope(fn[, thisArg, ...args])

Добавлено в: v9.6.0

  • fn <Function> Функция для вызова в контексте выполнения этого асинхронного ресурса.
  • thisArg <any> Приемник, используемый для вызова функции.
  • ...args <any> Необязательные аргументы для передачи функции.

Вызывает предоставленную функцию с предоставленными аргументами в контексте выполнения асинхронного ресурса. Это установит контекст, запустит обратные вызовы AsyncHooks до, вызовет функцию, запустит обратные вызовы AsyncHooks после, а затем восстановит исходный контекст выполнения.

asyncResource.emitDestroy()

Вызывает все хуки destroy. Это должно быть вызвано только один раз. Ошибка будет выдана, если он будет вызван более одного раза. Это должно быть вызвано вручную. Если ресурс будет оставлен для сбора GC, то хуки destroy никогда не будут вызваны.

asyncResource.asyncId()

  • Возвращает: <number> Уникальный asyncId, присвоенный ресурсу.

asyncResource.triggerAsyncId()

  • Возвращает: <number> Тот же triggerAsyncId, который передается конструктору AsyncResource.

Использование AsyncResource для пула потоков Worker

Следующий пример показывает, как использовать класс AsyncResource для правильного обеспечения отслеживания асинхронности для пула Worker. Другие пулы ресурсов, такие как пулы подключений к базе данных, могут следовать аналогичной модели.

Предполагая, что задача состоит в сложении двух чисел, используя файл с именем task_processor.js со следующим содержимым:

js
import { parentPort } from 'node:worker_threads'
parentPort.on('message', task => {
  parentPort.postMessage(task.a + task.b)
})
js
const { parentPort } = require('node:worker_threads')
parentPort.on('message', task => {
  parentPort.postMessage(task.a + task.b)
})

Пул Worker вокруг него может использовать следующую структуру:

js
import { AsyncResource } from 'node:async_hooks'
import { EventEmitter } from 'node:events'
import { Worker } from 'node:worker_threads'

const kTaskInfo = Symbol('kTaskInfo')
const kWorkerFreedEvent = Symbol('kWorkerFreedEvent')

class WorkerPoolTaskInfo extends AsyncResource {
  constructor(callback) {
    super('WorkerPoolTaskInfo')
    this.callback = callback
  }

  done(err, result) {
    this.runInAsyncScope(this.callback, null, err, result)
    this.emitDestroy() // `TaskInfo` используются только один раз.
  }
}

export default class WorkerPool extends EventEmitter {
  constructor(numThreads) {
    super()
    this.numThreads = numThreads
    this.workers = []
    this.freeWorkers = []
    this.tasks = []

    for (let i = 0; i < numThreads; i++) this.addNewWorker()

    // Каждый раз, когда возникает событие kWorkerFreedEvent, отправляется
    // следующая задача, ожидающая в очереди, если она есть.
    this.on(kWorkerFreedEvent, () => {
      if (this.tasks.length > 0) {
        const { task, callback } = this.tasks.shift()
        this.runTask(task, callback)
      }
    })
  }

  addNewWorker() {
    const worker = new Worker(new URL('task_processor.js', import.meta.url))
    worker.on('message', result => {
      // В случае успеха: вызываем обратный вызов, который был передан в `runTask`,
      // удаляем `TaskInfo`, связанный с Worker, и снова помечаем его как свободный.
      worker[kTaskInfo].done(null, result)
      worker[kTaskInfo] = null
      this.freeWorkers.push(worker)
      this.emit(kWorkerFreedEvent)
    })
    worker.on('error', err => {
      // В случае необработанного исключения: вызываем обратный вызов, который был передан в
      // `runTask` с ошибкой.
      if (worker[kTaskInfo]) worker[kTaskInfo].done(err, null)
      else this.emit('error', err)
      // Удаляем рабочий из списка и запускаем новый Worker для замены
      // текущего.
      this.workers.splice(this.workers.indexOf(worker), 1)
      this.addNewWorker()
    })
    this.workers.push(worker)
    this.freeWorkers.push(worker)
    this.emit(kWorkerFreedEvent)
  }

  runTask(task, callback) {
    if (this.freeWorkers.length === 0) {
      // Нет свободных потоков, ждем, пока рабочий поток не освободится.
      this.tasks.push({ task, callback })
      return
    }

    const worker = this.freeWorkers.pop()
    worker[kTaskInfo] = new WorkerPoolTaskInfo(callback)
    worker.postMessage(task)
  }

  close() {
    for (const worker of this.workers) worker.terminate()
  }
}
js
const { AsyncResource } = require('node:async_hooks')
const { EventEmitter } = require('node:events')
const path = require('node:path')
const { Worker } = require('node:worker_threads')

const kTaskInfo = Symbol('kTaskInfo')
const kWorkerFreedEvent = Symbol('kWorkerFreedEvent')

class WorkerPoolTaskInfo extends AsyncResource {
  constructor(callback) {
    super('WorkerPoolTaskInfo')
    this.callback = callback
  }

  done(err, result) {
    this.runInAsyncScope(this.callback, null, err, result)
    this.emitDestroy() // `TaskInfo` используются только один раз.
  }
}

class WorkerPool extends EventEmitter {
  constructor(numThreads) {
    super()
    this.numThreads = numThreads
    this.workers = []
    this.freeWorkers = []
    this.tasks = []

    for (let i = 0; i < numThreads; i++) this.addNewWorker()

    // Каждый раз, когда возникает событие kWorkerFreedEvent, отправляется
    // следующая задача, ожидающая в очереди, если она есть.
    this.on(kWorkerFreedEvent, () => {
      if (this.tasks.length > 0) {
        const { task, callback } = this.tasks.shift()
        this.runTask(task, callback)
      }
    })
  }

  addNewWorker() {
    const worker = new Worker(path.resolve(__dirname, 'task_processor.js'))
    worker.on('message', result => {
      // В случае успеха: вызываем обратный вызов, который был передан в `runTask`,
      // удаляем `TaskInfo`, связанный с Worker, и снова помечаем его как свободный.
      worker[kTaskInfo].done(null, result)
      worker[kTaskInfo] = null
      this.freeWorkers.push(worker)
      this.emit(kWorkerFreedEvent)
    })
    worker.on('error', err => {
      // В случае необработанного исключения: вызываем обратный вызов, который был передан в
      // `runTask` с ошибкой.
      if (worker[kTaskInfo]) worker[kTaskInfo].done(err, null)
      else this.emit('error', err)
      // Удаляем рабочий из списка и запускаем новый Worker для замены
      // текущего.
      this.workers.splice(this.workers.indexOf(worker), 1)
      this.addNewWorker()
    })
    this.workers.push(worker)
    this.freeWorkers.push(worker)
    this.emit(kWorkerFreedEvent)
  }

  runTask(task, callback) {
    if (this.freeWorkers.length === 0) {
      // Нет свободных потоков, ждем, пока рабочий поток не освободится.
      this.tasks.push({ task, callback })
      return
    }

    const worker = this.freeWorkers.pop()
    worker[kTaskInfo] = new WorkerPoolTaskInfo(callback)
    worker.postMessage(task)
  }

  close() {
    for (const worker of this.workers) worker.terminate()
  }
}

module.exports = WorkerPool

Без явного отслеживания, добавленного объектами WorkerPoolTaskInfo, будет казаться, что обратные вызовы связаны с отдельными объектами Worker. Однако создание Worker не связано с созданием задач и не предоставляет информации о том, когда задачи были запланированы.

Этот пул можно использовать следующим образом:

js
import WorkerPool from './worker_pool.js'
import os from 'node:os'

const pool = new WorkerPool(os.availableParallelism())

let finished = 0
for (let i = 0; i < 10; i++) {
  pool.runTask({ a: 42, b: 100 }, (err, result) => {
    console.log(i, err, result)
    if (++finished === 10) pool.close()
  })
}
js
const WorkerPool = require('./worker_pool.js')
const os = require('node:os')

const pool = new WorkerPool(os.availableParallelism())

let finished = 0
for (let i = 0; i < 10; i++) {
  pool.runTask({ a: 42, b: 100 }, (err, result) => {
    console.log(i, err, result)
    if (++finished === 10) pool.close()
  })
}

Интеграция AsyncResource с EventEmitter

Обработчики событий, запускаемые EventEmitter, могут выполняться в контексте выполнения, отличном от того, который был активен при вызове eventEmitter.on().

В следующем примере показано, как использовать класс AsyncResource для правильной связи обработчика событий с правильным контекстом выполнения. Тот же подход можно применить к Stream или аналогичному классу, управляемому событиями.

js
import { createServer } from 'node:http'
import { AsyncResource, executionAsyncId } from 'node:async_hooks'

const server = createServer((req, res) => {
  req.on(
    'close',
    AsyncResource.bind(() => {
      // Контекст выполнения привязан к текущей внешней области видимости.
    })
  )
  req.on('close', () => {
    // Контекст выполнения привязан к области видимости, вызвавшей излучение 'close'.
  })
  res.end()
}).listen(3000)
js
const { createServer } = require('node:http')
const { AsyncResource, executionAsyncId } = require('node:async_hooks')

const server = createServer((req, res) => {
  req.on(
    'close',
    AsyncResource.bind(() => {
      // Контекст выполнения привязан к текущей внешней области видимости.
    })
  )
  req.on('close', () => {
    // Контекст выполнения привязан к области видимости, вызвавшей излучение 'close'.
  })
  res.end()
}).listen(3000)