Spec-Zone.ru › Node.js 12 LTS

Async hooks

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

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

Модуль async_hooks предоставляет API для отслеживания асинхронных ресурсов. К нему можно получить доступ с помощью:

const async_hooks = require('async_hooks');

Терминология

Асинхронный ресурс представляет собой объект с ассоциированным обратным вызовом. Этот обратный вызов может вызываться несколько раз, например, событие 'connection' в net.createServer(), или только один раз, как в fs.open(). Ресурс также может быть закрыт до вызова обратного вызова. AsyncHook не делает явного различия между этими различными случаями, но будет представлять их как абстрактное понятие – ресурс.

Если используются Worker, каждый поток имеет независимый интерфейс async_hooks, и каждый поток будет использовать новый набор асинхронных идентификаторов.

Общедоступный API

Обзор

Ниже приведён краткий обзор общедоступного API.

const async_hooks = require('async_hooks');

// Return the ID of the current execution context.
const eid = async_hooks.executionAsyncId();

// Return the ID of the handle responsible for triggering the callback of the
// current execution scope to call.
const tid = async_hooks.triggerAsyncId();

// Create a new AsyncHook instance. All of these callbacks are optional.
const asyncHook =
    async_hooks.createHook({ init, before, after, destroy, promiseResolve });

// Allow callbacks of this AsyncHook instance to call. This is not an implicit
// action after running the constructor, and must be explicitly run to begin
// executing callbacks.
asyncHook.enable();

// Disable listening for new asynchronous events.
asyncHook.disable();

//
// The following are the callbacks that can be passed to createHook().
//

// init is called during object construction. The resource may not have
// completed construction when this callback runs, therefore all fields of the
// resource referenced by "asyncId" may not have been populated.
function init(asyncId, type, triggerAsyncId, resource) { }

// Before is called just before the resource's callback is called. It can be
// called 0-N times for handles (e.g. TCPWrap), and will be called exactly 1
// time for requests (e.g. FSReqCallback).
function before(asyncId) { }

// After is called just after the resource's callback has finished.
function after(asyncId) { }

// Destroy is called when the resource is destroyed.
function destroy(asyncId) { }

// promiseResolve is called only for promise resources, when the
// `resolve` function passed to the `Promise` constructor is invoked
// (either directly or through other means of resolving a promise).
function promiseResolve(asyncId) { }

async_hooks.createHook(callbacks)

Добавлен в: v8.1.0
  • callbacks <Объект> Набор Обратных вызовов хука для регистрации
    • init <Функция> Обратный вызов init.
    • before <Функция> Обратный вызов before.
    • after <Функция> Обратный вызов after.
    • destroy <Функция> Обратный вызов destroy.
    • promiseResolve <Функция> Обратный вызов promiseResolve.
  • Возвращает: <AsyncHook> Экземпляр для отключения и включения хуков

Регистрирует функции, которые будут вызываться для различных событий жизненного цикла каждой асинхронной операции.

Обратные вызовы init()/before()/after()/destroy() вызываются для соответствующего асинхронного события в течение жизненного цикла ресурса.

Все обратные вызовы необязательны. Например, если необходимо отслеживать только очистку ресурсов, то необходимо передать только обратный вызов destroy. Подробности всех функций, которые можно передать в callbacks, указаны в разделе Обратные вызовы хуков.

const async_hooks = require('async_hooks');

const asyncHook = async_hooks.createHook({
  init(asyncId, type, triggerAsyncId, resource) { },
  destroy(asyncId) { }
});

Обратные вызовы будут унаследованы через цепочку прототипов:

class MyAsyncCallbacks {
  init(asyncId, type, triggerAsyncId, resource) { }
  destroy(asyncId) {}
}

class MyAddedCallbacks extends MyAsyncCallbacks {
  before(asyncId) { }
  after(asyncId) { }
}

const asyncHook = async_hooks.createHook(new MyAddedCallbacks());
Обработка ошибок

Если любой из обратных вызовов AsyncHook вызывает ошибку, приложение выведет трассировку стека и завершится. Путь завершения аналогичен пути завершения при неуловленной ошибке, но все слушатели 'uncaughtException' удаляются, тем самым принудительно завершая процесс. Обратные вызовы 'exit' по-прежнему будут вызваны, если только приложение не запущено с --abort-on-uncaught-exception, в этом случае будет напечатана трассировка стека, и приложение завершится, сохранив файл core.

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

Вывод в обратных вызовах AsyncHooks

Поскольку вывод в консоль является асинхронной операцией, console.log() вызовет обратные вызовы AsyncHooks. Использование console.log() или аналогичных асинхронных операций внутри функции обратного вызова AsyncHooks приведет к бесконечной рекурсии. Простым решением этой проблемы при отладке является использование синхронной операции логирования, такой как fs.writeFileSync(file, msg, flag). Это выведет данные в файл и не вызовет рекурсию AsyncHooks, поскольку она является синхронной.

const fs = require('fs');
const util = require('util');

function debug(...args) {
  // Use a function like this one when debugging inside an AsyncHooks callback
  fs.writeFileSync('log.out', `${util.format(...args)}\n`, { flag: 'a' });
}

Если для логирования требуется асинхронная операция, можно отслеживать, что вызвало асинхронную операцию, используя информацию, предоставленную самим AsyncHooks. Тогда логирование должно быть пропущено, когда это само логирование вызвало вызов обратного вызова AsyncHooks. Таким образом, бесконечная рекурсия прерывается.

Класс: AsyncHook

Класс AsyncHook предоставляет интерфейс для отслеживания событий жизненного цикла асинхронных операций.

asyncHook.enable()

  • Возвращает: <AsyncHook> Ссылка на asyncHook.

Включает обратные вызовы для данного экземпляра AsyncHook. Если обратные вызовы не предоставлены, включение является пустой операцией.

Экземпляр AsyncHook по умолчанию отключён. Если экземпляр AsyncHook должен быть включён сразу после создания, можно использовать следующий шаблон.

const async_hooks = require('async_hooks');

const hook = async_hooks.createHook(callbacks).enable();

asyncHook.disable()

  • Возвращает: <AsyncHook> Ссылка на asyncHook.

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

Для обеспечения согласованности API disable() также возвращает экземпляр AsyncHook.

Обратные вызовы хуков

Ключевые события в жизненном цикле асинхронных событий были классифицированы по четырём категориям: инициализация, до/после вызова обратного вызова и при уничтожении экземпляра.

init(asyncId, type, triggerAsyncId, resource)
  • asyncId <число> Уникальный идентификатор асинхронного ресурса.
  • type <строка> Тип асинхронного ресурса.
  • triggerAsyncId <число> Уникальный идентификатор асинхронного ресурса, в контексте выполнения которого был создан данный асинхронный ресурс.
  • resource <объект> Ссылка на ресурс, представляющий асинхронную операцию, который необходимо освободить во время destroy.

Вызывается при создании класса, который может генерировать асинхронное событие. Это не означает, что экземпляр должен вызвать before/after перед вызовом destroy, а только то, что такая возможность существует.

Это поведение можно наблюдать, открыв ресурс, а затем закрыв его, прежде чем ресурс сможет быть использован. Следующий фрагмент демонстрирует это.

require('net').createServer().listen(function() { this.close(); });
// OR
clearTimeout(setTimeout(() => {}, 10));

Каждый новый ресурс получает уникальный идентификатор в пределах текущего экземпляра Node.js.

type

type – строка, идентифицирующая тип ресурса, который вызвал вызов init. Как правило, она соответствует имени конструктора ресурса.

FSEVENTWRAP, FSREQCALLBACK, GETADDRINFOREQWRAP, GETNAMEINFOREQWRAP, HTTPINCOMINGMESSAGE,
HTTPCLIENTREQUEST, JSSTREAM, PIPECONNECTWRAP, PIPEWRAP, PROCESSWRAP, QUERYWRAP,
SHUTDOWNWRAP, SIGNALWRAP, STATWATCHER, TCPCONNECTWRAP, TCPSERVERWRAP, TCPWRAP,
TTYWRAP, UDPSENDWRAP, UDPWRAP, WRITEWRAP, ZLIB, SSLCONNECTION, PBKDF2REQUEST,
RANDOMBYTESREQUEST, TLSWRAP, Microtask, Timeout, Immediate, TickObject

Также существует тип ресурса PROMISE, который используется для отслеживания экземпляров Promise и асинхронной работы, запланированной ими.

Пользователи могут определять собственные type при использовании общедоступного API встраивания.

Возможны коллизии имён типов. Встраивающим рекомендуется использовать уникальные префиксы, такие как имя пакета npm, чтобы предотвратить коллизии при прослушивании хуков.

triggerAsyncId

triggerAsyncId – asyncId ресурса, который вызвал (или «спровоцировал») инициализацию нового ресурса и вызвало init. Это отличается от async_hooks.executionAsyncId(), которая показывает когда был создан ресурс, в то время как triggerAsyncId показывает почему ресурс был создан.

Ниже приведён простой пример triggerAsyncId:

async_hooks.createHook({
  init(asyncId, type, triggerAsyncId) {
    const eid = async_hooks.executionAsyncId();
    fs.writeSync(
      process.stdout.fd,
      `${type}(${asyncId}): trigger: ${triggerAsyncId} execution: ${eid}\n`);
  }
}).enable();

require('net').createServer((conn) => {}).listen(8080);

Вывод при обращении к серверу с nc localhost 8080:

TCPSERVERWRAP(5): trigger: 1 execution: 1
TCPWRAP(7): trigger: 5 execution: 0

TCPSERVERWRAP – это сервер, который принимает подключения.

TCPWRAP – новое подключение от клиента. При создании нового подключения экземпляр TCPWrap сразу же создаётся. Это происходит вне стека JavaScript. (executionAsyncId() 0 означает, что он выполняется из C++ без стека JavaScript над ним.) С этой единственной информацией было бы невозможно связать ресурсы в том, что их создало, поэтому triggerAsyncId отвечает за распространение информации о том, какой ресурс отвечает за существование нового ресурса.

resource

resource – это объект, представляющий фактический асинхронный ресурс, который был инициализирован. Он может содержать полезную информацию, которая может меняться в зависимости от значения type. Например, для типа ресурса GETADDRINFOREQWRAP resource предоставляет имя хоста, используемое при поиске IP-адреса для хоста в net.Server.listen(). API для доступа к этой информации не поддерживается, но с помощью API встраивания пользователи могут предоставлять и документировать собственные объекты ресурсов. Например, такой объект ресурса может содержать SQL-запрос, который выполняется.

В случае с Promise объект resource будет содержать свойство isChainedPromise, установленное в true, если у Promise есть родительский Promise, и в false в противном случае. Например, в случае b = a.then(handler), a считается родительским Promise для b. Здесь b считается цепочкой Promise.

В некоторых случаях объект ресурса повторно используется для повышения производительности, поэтому небезопасно использовать его в качестве ключа в WeakMap или добавлять к нему свойства.

Пример асинхронного контекста

Ниже приведен пример с дополнительной информацией о вызовах init между вызовами before и after, а именно, о том, как будет выглядеть обратный вызов listen(). Форматирование вывода немного более подробное, чтобы проще было увидеть контекст вызова.

let indent = 0;
async_hooks.createHook({
  init(asyncId, type, triggerAsyncId) {
    const eid = async_hooks.executionAsyncId();
    const indentStr = ' '.repeat(indent);
    fs.writeSync(
      process.stdout.fd,
      `${indentStr}${type}(${asyncId}):` +
      ` trigger: ${triggerAsyncId} execution: ${eid}\n`);
  },
  before(asyncId) {
    const indentStr = ' '.repeat(indent);
    fs.writeSync(process.stdout.fd, `${indentStr}before:  ${asyncId}\n`);
    indent += 2;
  },
  after(asyncId) {
    indent -= 2;
    const indentStr = ' '.repeat(indent);
    fs.writeSync(process.stdout.fd, `${indentStr}after:  ${asyncId}\n`);
  },
  destroy(asyncId) {
    const indentStr = ' '.repeat(indent);
    fs.writeSync(process.stdout.fd, `${indentStr}destroy:  ${asyncId}\n`);
  },
}).enable();

require('net').createServer(() => {}).listen(8080, () => {
  // Let's wait 10ms before logging the server started.
  setTimeout(() => {
    console.log('>>>', async_hooks.executionAsyncId());
  }, 10);
});

Вывод только при запуске сервера:

TCPSERVERWRAP(5): trigger: 1 execution: 1
TickObject(6): trigger: 5 execution: 1
before:  6
  Timeout(7): trigger: 6 execution: 6
after:   6
destroy: 6
before:  7
>>> 7
  TickObject(8): trigger: 7 execution: 7
after:   7
before:  8
after:   8

Как показано в примере, executionAsyncId() и execution указывают значение текущего контекста выполнения; это определяется вызовами before и after.

Использование только execution для построения графика распределения ресурсов приводит к следующему:

  root(1)
     ^
     |
TickObject(6)
     ^
     |
 Timeout(7)

TCPSERVERWRAP не является частью этого графика, хотя он был причиной вызова console.log(). Это связано с тем, что привязка к порту без имени хоста является синхронной операцией, но для поддержания полностью асинхронного API обратный вызов пользователя помещается в process.nextTick(). Вот почему TickObject присутствует в выводе и является «родительским» элементом для обратного вызова .listen().

График показывает только когда ресурс был создан, а не почему, поэтому для отслеживания почему используйте triggerAsyncId. Это можно представить в виде следующего графика:

 bootstrap(1)
     |
     ˅
TCPSERVERWRAP(5)
     |
     ˅
 TickObject(6)
     |
     ˅
  Timeout(7)
before(asyncId)
  • asyncId <число>

Когда инициируется асинхронная операция (например, прием нового подключения TCP-сервером) или она завершается (например, запись данных на диск), вызывается обратный вызов для уведомления пользователя. Обратный вызов before вызывается незадолго до выполнения указанного обратного вызова. asyncId — это уникальный идентификатор, назначенный ресурсу, который собирается выполнить обратный вызов.

Обратный вызов before будет вызываться от 0 до N раз. Обратный вызов before обычно вызывается 0 раз, если асинхронная операция была отменена или, например, если TCP-сервер не получает никаких подключений. Персистентные асинхронные ресурсы, такие как TCP-сервер, обычно вызывают обратный вызов before несколько раз, в то время как другие операции, например fs.open() , вызывают его только один раз.

after(asyncId)
  • asyncId <число>

Вызывается сразу после завершения обратного вызова, указанного в before.

Если во время выполнения обратного вызова произойдет непредвиденная ошибка, тогда after выполнится после того, как будет вызвано событие 'uncaughtException' или обработчик domain.

destroy(asyncId)
  • asyncId <число>

Вызывается после уничтожения ресурса, соответствующего asyncId. Он также вызывается асинхронно из API встраиваемого модуля emitDestroy().

Некоторые ресурсы зависят от сбора мусора для очистки, поэтому если ссылка на объект resource , переданный в init , сохраняется, то возможно, что destroy никогда не будет вызван, что приведёт к утечке памяти в приложении. Если ресурс не зависит от сбора мусора, то это не будет проблемой.

promiseResolve(asyncId)
Добавлен в: v8.6.0
  • asyncId <число>

Вызывается, когда функция resolve , переданная конструктору Promise, вызывается (прямо или через другие способы разрешения обещания).

resolve() не выполняет никаких наблюдаемых синхронных операций.

Promise не обязательно выполнен или отклонен на данном этапе, если Promise был разрешен, предполагая состояние другого Promise.

new Promise((resolve) => resolve(true)).then((a) => {});

вызывает следующие обратные вызовы:

init for PROMISE with id 5, trigger id: 1
  promise resolve 5      # corresponds to resolve(true)
init for PROMISE with id 6, trigger id: 5  # the Promise returned by then()
  before 6               # the then() callback is entered
  promise resolve 6      # the then() callback resolves the promise by returning
  after 6

async_hooks.executionAsyncResource()

Добавлен в: v12.17.0
  • Возвращает: <Объект> Ресурс, представляющий текущее выполнение. Полезно для хранения данных внутри ресурса.

Объекты ресурсов, возвращаемые executionAsyncResource(), чаще всего являются внутренними объектами обработчика Node.js с недокументированными API. Использование каких-либо функций или свойств объекта, скорее всего, приведёт к аварийному завершению приложения, и от этого следует воздержаться.

Использование executionAsyncResource() в контексте верхнего уровня вернёт пустой объект, так как нет объекта-обработчика или запроса, который можно использовать, но наличие объекта, представляющего верхний уровень, может быть полезным.

const { open } = require('fs');
const { executionAsyncId, executionAsyncResource } = require('async_hooks');

console.log(executionAsyncId(), executionAsyncResource());  // 1 {}
open(__filename, 'r', (err, fd) => {
  console.log(executionAsyncId(), executionAsyncResource());  // 7 FSReqWrap
});

Это можно использовать для реализации локального хранения продолжения без использования отслеживающего Map для хранения метаданных:

const { createServer } = require('http');
const {
  executionAsyncId,
  executionAsyncResource,
  createHook
} = require('async_hooks');
const sym = Symbol('state'); // Private symbol to avoid pollution

createHook({
  init(asyncId, type, triggerAsyncId, resource) {
    const cr = executionAsyncResource();
    if (cr) {
      resource[sym] = cr[sym];
    }
  }
}).enable();

const server = createServer((req, res) => {
  executionAsyncResource()[sym] = { state: req.url };
  setTimeout(function() {
    res.end(JSON.stringify(executionAsyncResource()[sym]));
  }, 100);
}).listen(3000);

async_hooks.executionAsyncId()

История
Версия Изменения
v8.2.0

Переименовано из currentId

v8.1.0

Добавлен в: v8.1.0

  • Возвращает: <число> asyncId текущего контекста выполнения. Полезно для отслеживания времени вызова.
const async_hooks = require('async_hooks');

console.log(async_hooks.executionAsyncId());  // 1 - bootstrap
fs.open(path, 'r', (err, fd) => {
  console.log(async_hooks.executionAsyncId());  // 6 - open()
});

Идентификатор, возвращаемый executionAsyncId(), относится к времени выполнения, а не к причинно-следственной связи (что описывается triggerAsyncId()):

const server = net.createServer((conn) => {
  // Returns the ID of the server, not of the new connection, because the
  // callback runs in the execution scope of the server's MakeCallback().
  async_hooks.executionAsyncId();

}).listen(port, () => {
  // Returns the ID of a TickObject (i.e. process.nextTick()) because all
  // callbacks passed to .listen() are wrapped in a nextTick().
  async_hooks.executionAsyncId();
});

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

async_hooks.triggerAsyncId()

  • Возвращает: <число> Идентификатор ресурса, ответственного за вызов обратного вызова, который в настоящее время выполняется.
const server = net.createServer((conn) => {
  // The resource that caused (or triggered) this callback to be called
  // was that of the new connection. Thus the return value of triggerAsyncId()
  // is the asyncId of "conn".
  async_hooks.triggerAsyncId();

}).listen(port, () => {
  // Even though all callbacks passed to .listen() are wrapped in a nextTick()
  // the callback itself exists because the call to the server's .listen()
  // was made. So the return value would be the ID of the server.
  async_hooks.triggerAsyncId();
});

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

Отслеживание выполнения обещаний

По умолчанию выполнения обещаний не назначаются asyncId из-за относительно высокой стоимости API для интроспекции обещаний, предоставленного V8. Это означает, что программы, использующие обещания или async/await , по умолчанию не получат правильных идентификаторов выполнения и триггеров для контекстов обратных вызовов обещаний.

const ah = require('async_hooks');
Promise.resolve(1729).then(() => {
  console.log(`eid ${ah.executionAsyncId()} tid ${ah.triggerAsyncId()}`);
});
// produces:
// eid 1 tid 0

Обратите внимание, что обратный вызов then() утверждает, что был выполнен в контексте внешнего пространства имён, хотя участвовало асинхронное переключение. Кроме того, значение triggerAsyncId равно 0, что означает, что отсутствует контекст о ресурсе, который вызвал (сработал триггером) обратный вызов then().

Установка асинхронных хуков через async_hooks.createHook включает отслеживание выполнения обещаний:

const ah = require('async_hooks');
ah.createHook({ init() {} }).enable(); // forces PromiseHooks to be enabled.
Promise.resolve(1729).then(() => {
  console.log(`eid ${ah.executionAsyncId()} tid ${ah.triggerAsyncId()}`);
});
// produces:
// eid 7 tid 6

В этом примере добавление любой реальной функции хука позволило отслеживать обещания. В приведённом примере есть два обещания: обещание, созданное Promise.resolve(), и обещание, возвращаемое по вызову then(). В примере первое обещание получило asyncId 6, а второе — asyncId 7. Во время выполнения обратного вызова then(), мы выполняемся в контексте обещания с asyncId 7. Это обещание было сработано асинхронным ресурсом 6.

Ещё одна тонкость с обещаниями заключается в том, что обратные вызовы before и after выполняются только для связанных обещаний. Это означает, что для обещаний, не созданных then()/catch(), не будут вызваны обратные вызовы before и after. Для получения дополнительной информации см. подробности API V8 PromiseHooks.

API встраиваемого модуля JavaScript

Разработчики библиотек, которые обрабатывают свои собственные асинхронные ресурсы, выполняющие задачи, такие как ввод-вывод, пул соединений или управление очередями обратных вызовов, могут использовать API AsyncResource JavaScript, чтобы все соответствующие обратные вызовы вызывались.

Класс: AsyncResource

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

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

Ниже приведён обзор API AsyncResource.

const { AsyncResource, executionAsyncId } = require('async_hooks');

// AsyncResource() is meant to be extended. Instantiating a
// new AsyncResource() also triggers init. If triggerAsyncId is omitted then
// async_hook.executionAsyncId() is used.
const asyncResource = new AsyncResource(
  type, { triggerAsyncId: executionAsyncId(), requireManualDestroy: false }
);

// Run a function in the execution context of the resource. This will
// * establish the context of the resource
// * trigger the AsyncHooks before callbacks
// * call the provided function `fn` with the supplied arguments
// * trigger the AsyncHooks after callbacks
// * restore the original execution context
asyncResource.runInAsyncScope(fn, thisArg, ...args);

// Call AsyncHooks destroy callbacks.
asyncResource.emitDestroy();

// Return the unique ID assigned to the AsyncResource instance.
asyncResource.asyncId();

// Return the trigger ID for the AsyncResource instance.
asyncResource.triggerAsyncId();

new AsyncResource(type[, options])

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

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

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

Добавлен в: v12.19.0
  • fn <Функция> Функция, которая будет привязана к текущему контексту выполнения.
  • type <строка> Необязательное имя, которое будет ассоциировано с базовым AsyncResource.

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

Возвращаемая функция будет иметь свойство asyncResource , ссылающееся на AsyncResource , к которому привязана функция.

asyncResource.bind(fn)

Добавлен в: v12.19.0
  • fn <Функция> Функция, которую нужно привязать к текущему AsyncResource.

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

Возвращаемая функция будет иметь свойство asyncResource, ссылающееся на AsyncResource, к которому привязана функция.

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

Добавлен в: v9.6.0
  • fn <Функция> Функция, которую нужно вызвать в контексте выполнения этого асинхронного ресурса.
  • thisArg <любой> Приёмник, который будет использован для вызова функции.
  • ...args <любой> Необязательные аргументы, которые нужно передать функции.

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

asyncResource.emitDestroy()

  • Возвращает: <AsyncResource> Ссылка на asyncResource.

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

asyncResource.asyncId()

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

asyncResource.triggerAsyncId()

  • Возвращает: <число> То же самое triggerAsyncId, что передаётся конструктору AsyncResource.

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

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

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

const { parentPort } = require('worker_threads');
parentPort.on('message', (task) => {
  parentPort.postMessage(task.a + task.b);
});

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

const { AsyncResource } = require('async_hooks');
const { EventEmitter } = require('events');
const path = require('path');
const { Worker } = require('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`s are used only once.
  }
}

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

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

  addNewWorker() {
    const worker = new Worker(path.resolve(__dirname, 'task_processor.js'));
    worker.on('message', (result) => {
      // In case of success: Call the callback that was passed to `runTask`,
      // remove the `TaskInfo` associated with the Worker, and mark it as free
      // again.
      worker[kTaskInfo].done(null, result);
      worker[kTaskInfo] = null;
      this.freeWorkers.push(worker);
      this.emit(kWorkerFreedEvent);
    });
    worker.on('error', (err) => {
      // In case of an uncaught exception: Call the callback that was passed to
      // `runTask` with the error.
      if (worker[kTaskInfo])
        worker[kTaskInfo].done(err, null);
      else
        this.emit('error', err);
      // Remove the worker from the list and start a new Worker to replace the
      // current one.
      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) {
      // No free threads, wait until a worker thread becomes free.
      this.once(kWorkerFreedEvent, () => this.runTask(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 не связано с созданием задач и не предоставляет информацию о том, когда задачи были запланированы.

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

const WorkerPool = require('./worker_pool.js');
const os = require('os');

const pool = new WorkerPool(os.cpus().length);

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 или аналогичному классу с событийной обработкой.

const { createServer } = require('http');
const { AsyncResource, executionAsyncId } = require('async_hooks');

const server = createServer((req, res) => {
  req.on('close', AsyncResource.bind(() => {
    // Execution context is bound to the current outer scope.
  }));
  req.on('close', () => {
    // Execution context is bound to the scope that caused 'close' to emit.
  });
  res.end();
}).listen(3000);

Класс: AsyncLocalStorage

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

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

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

const http = require('http');
const { AsyncLocalStorage } = require('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');
    // Imagine any chain of async operations here
    setImmediate(() => {
      logWithId('finish');
      res.end();
    });
  });
}).listen(8080);

http.get('http://localhost:8080');
http.get('http://localhost:8080');
// Prints:
//   0: start
//   1: start
//   0: finish
//   1: finish

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

new AsyncLocalStorage()

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

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

asyncLocalStorage.disable()

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

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

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

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

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

asyncLocalStorage.getStore()

Добавлен в: v12.17.0
  • Возвращает: <любой>

Этот метод возвращает текущее хранилище. Если этот метод вызывается вне асинхронного контекста, инициализированного вызовом asyncLocalStorage.run, он вернёт undefined.

asyncLocalStorage.enterWith(store)

Добавлен в: v12.17.0
  • store <любой>

Вызов asyncLocalStorage.enterWith(store) перейдёт в контекст на оставшуюся часть текущего синхронного выполнения и сохранится во всех последующих асинхронных вызовах.

Пример:

const store = { id: 1 };
asyncLocalStorage.enterWith(store);
asyncLocalStorage.getStore(); // Returns the store object
someAsyncOperation(() => {
  asyncLocalStorage.getStore(); // Returns the same object
});

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

const store = { id: 1 };

emitter.on('my-event', () => {
  asyncLocalStorage.enterWith(store);
});
emitter.on('my-event', () => {
  asyncLocalStorage.getStore(); // Returns the same object
});

asyncLocalStorage.getStore(); // Returns undefined
emitter.emit('my-event');
asyncLocalStorage.getStore(); // Returns the same object

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

Добавлен в: v12.17.0
  • store <любой>
  • callback <Функция>
  • ...args <любой>

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

Необязательно, но можно передавать аргументы в функцию. Они будут переданы функции-обработчику.

Если функция-обработчик выбросит ошибку, она также будет выброшена run. Эта запись в стеке вызовов не повлияет на контекст, который будет завершен.

Пример:

const store = { id: 2 };
try {
  asyncLocalStorage.run(store, () => {
    asyncLocalStorage.getStore(); // Returns the store object
    throw new Error();
  });
} catch (e) {
  asyncLocalStorage.getStore(); // Returns undefined
  // The error will be caught here
}

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

Добавлен в: v12.17.0
  • callback <Функция>
  • ...args <любой>

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

Необязательно, но можно передавать аргументы в функцию. Они будут переданы функции-обработчику.

Если функция-обработчик выбросит ошибку, она также будет выброшена exit. Эта запись в стеке вызовов не повлияет на контекст, который будет возвращён обратно.

Пример:

// Within a call to run
try {
  asyncLocalStorage.getStore(); // Returns the store object or value
  asyncLocalStorage.exit(() => {
    asyncLocalStorage.getStore(); // Returns undefined
    throw new Error();
  });
} catch (e) {
  asyncLocalStorage.getStore(); // Returns the same object or value
  // The error will be caught here
}

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

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

async function fn() {
  await asyncLocalStorage.run(new Map(), () => {
    asyncLocalStorage.getStore().set('key', value);
    return foo(); // The return value of foo will be awaited
  });
}

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

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

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

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

Если вам нужно продолжать использовать API на основе обратных вызовов или ваш код предполагает собственную реализацию thenable, используйте класс AsyncResource для связывания асинхронной операции с правильным контекстом выполнения.

© Joyent, Inc. and other Node contributors
Licensed under the MIT License.
Node.js is a trademark of Joyent, Inc. and is used with its permission.
We are not endorsed by or affiliated with Joyent.
https://nodejs.org/dist/latest-v12.x/docs/api/async_hooks.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API