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

[Stable: 1 - Experimental]

Stable: 1 Stability: 1 - Experimental

Отключает экземпляр 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

[Stable: 1 - Experimental]

Stable: 1 Stability: 1 - Experimental

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

Пример:

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();

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

// Вернуть ID триггера для экземпляра 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();

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

// Вернуть ID триггера для экземпляра 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 before, вызовет функцию, вызовет обратные вызовы AsyncHooks after, а затем восстановит исходный контекст выполнения.

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 из списка и запустите новый 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 из списка и запустите новый 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);