Обработчики асинхронных событий
Исходный код: lib/async_hooks.js
Модуль async_hooks предоставляет API для отслеживания асинхронных ресурсов. К нему можно получить доступ с помощью:
const async_hooks = require('async_hooks'); Терминология
Асинхронный ресурс представляет собой объект с ассоциированным обратным вызовом. Этот обратный вызов может быть вызван несколько раз, например, событие 'connection' в net.createServer(), или только один раз, как в fs.open(). Ресурс также может быть закрыт до вызова обратного вызова. AsyncHook не явно различает эти случаи, но представляет их как абстрактное понятие — ресурс.
Если используются Worker, каждый поток имеет независимый интерфейс async_hooks, и каждый поток будет использовать новый набор идентификаторов асинхронных событий.
Обзор
Ниже представлен простой обзор публичного 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 (such as TCPWrap), and will be called exactly 1
// time for requests (such as 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)
-
callbacks<Объект> Обратные вызовы хука для регистрации - Возвращает: <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()); Поскольку промисы являются асинхронными ресурсами, жизненный цикл которых отслеживается с помощью механизма асинхронных обработчиков, обратные вызовы init(), before(), after(), и destroy() не должны быть асинхронными функциями, возвращающими промисы.
Обработка ошибок
Если какой-либо обратный вызов 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 Embedder пользователи могут предоставлять и документировать свои собственные объекты ресурсов. Например, такой объект ресурса может содержать выполняемый SQL-запрос.
В некоторых случаях объект ресурса повторно используется для повышения производительности, поэтому его нельзя использовать в качестве ключа в 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 embedder emitDestroy().
Некоторые ресурсы зависят от сборки мусора для очистки, поэтому если ссылка на объект resource, переданный в init, сохраняется, возможно, destroy никогда не будет вызван, что приведёт к утечке памяти в приложении. Если ресурс не зависит от сборки мусора, это не проблема.
promiseResolve(asyncId)
-
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()
- Возвращает: <Объект> Ресурс, представляющий текущее выполнение. Полезно для хранения данных в ресурсе.
Объекты ресурсов, возвращаемые 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()
- Возвращает: <число>
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 (process.nextTick()) because all
// callbacks passed to .listen() are wrapped in a nextTick().
async_hooks.executionAsyncId();
}); Контексты Promise могут не получать точные executionAsyncIds по умолчанию. Смотрите раздел о отслеживании выполнения Promise.
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();
}); Контексты Promise могут не получать допустимые triggerAsyncId по умолчанию. Смотрите раздел о отслеживании выполнения Promise.
Отслеживание выполнения Promise
По умолчанию выполнения Promise не назначаются asyncId из-за относительно высокой стоимости API интроспекции Promise, предоставляемого V8. Это означает, что программы, использующие Promise или async/await , не получат правильных идентификаторов выполнения и триггеров для контекстов обратного вызова Promise по умолчанию.
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 включает отслеживание выполнения Promise:
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. В примере выше есть два Promise: Promise, созданный Promise.resolve(), и Promise, возвращённый вызовом then(). В примере выше первый Promise получил asyncId 6, а последний — asyncId 7. Во время выполнения обратного вызова then(), мы работаем в контексте Promise с asyncId 7. Этот Promise был вызван асинхронным ресурсом 6.
Ещё одна тонкость с Promise заключается в том, что обратные вызовы before и after выполняются только для связанных Promise. Это означает, что Promise, не созданные через then()/catch(), не будут иметь обратные вызовы before и after.
Для получения более подробной информации см. детали API V8 PromiseHooks.
JavaScript API для внедрения
Разработчики библиотек, которые обрабатывают свои собственные асинхронные ресурсы, выполняющие задачи, такие как ввод-вывод, пул подключений или управление очередями обратных вызовов, могут использовать AsyncResource JavaScript API, чтобы все необходимые обратные вызовы вызывались.
Класс: 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извлекается, и чувствительные APIemitDestroyвызываются с ним. При установке в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])
-
fn<функция> Функция, которая должна быть привязана к текущему контексту выполнения. -
type<строка> Необязательное имя, которое необходимо связать с базовымAsyncResource.
Привязывает заданную функцию к текущему контексту выполнения.
У возвращаемой функции будет свойство asyncResource, ссылающееся на AsyncResource, к которому привязана функция.
asyncResource.bind(fn)
-
fn<функция> Функция, которая должна быть привязана к текущемуAsyncResource.
Привязывает заданную функцию к области действия этого AsyncResource.
У возвращаемой функции будет свойство asyncResource, ссылающееся на AsyncResource, к которому привязана функция.
asyncResource.runInAsyncScope(fn[, thisArg, ...args])
-
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 = [];
this.tasks = [];
for (let i = 0; i < numThreads; i++)
this.addNewWorker();
// Any time the kWorkerFreedEvent is emitted, dispatch
// the next task pending in the queue, if any.
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) => {
// 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.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 не связано с созданием задач и не даёт информации о том, когда задачи были запланированы.
Этот пул можно использовать следующим образом:
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
Этот класс используется для создания асинхронного состояния внутри обратных вызовов и цепочек промисов. Он позволяет хранить данные на протяжении всего жизненного цикла веб-запроса или любого другого асинхронного периода. Он похож на хранилище ссылок на потоки в других языках.
Хотя вы можете создать свою собственную реализацию поверх модуля async_hooks, рекомендуется использовать AsyncLocalStorage, так как это производительная и безопасная с точки зрения памяти реализация, включающая значительные оптимизации, которые не очевидны при самостоятельной реализации.
В следующем примере используется 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()
Создаёт новый экземпляр AsyncLocalStorage. Хранилище доступно только внутри вызова run() или после вызова enterWith().
asyncLocalStorage.disable()
Отключает экземпляр AsyncLocalStorage. Все последующие вызовы asyncLocalStorage.getStore() вернут undefined до тех пор, пока не будет снова вызван asyncLocalStorage.run() или asyncLocalStorage.enterWith().
При вызове asyncLocalStorage.disable(), все текущие контексты, связанные с экземпляром, будут завершены.
Вызов asyncLocalStorage.disable() необходим, прежде чем asyncLocalStorage сможет быть удален сборщиком мусора. Это не относится к хранилищам, предоставляемым asyncLocalStorage, так как эти объекты удаляются сборщиком мусора вместе с соответствующими асинхронными ресурсами.
Используйте этот метод, когда asyncLocalStorage больше не используется в текущем процессе.
asyncLocalStorage.getStore()
- Возвращает: <любой тип>
Возвращает текущее хранилище. Если вызов осуществляется вне асинхронного контекста, инициализированного вызовом asyncLocalStorage.run() или asyncLocalStorage.enterWith(), он возвращает undefined.
asyncLocalStorage.enterWith(store)
-
store<любой тип>
Переходит в контекст на оставшуюся часть текущего синхронного выполнения, а затем сохраняет хранилище через любые последующие асинхронные вызовы.
Пример:
const store = { id: 1 };
// Replaces previous store with the given store object
asyncLocalStorage.enterWith(store);
asyncLocalStorage.getStore(); // Returns the store object
someAsyncOperation(() => {
asyncLocalStorage.getStore(); // Returns the same object
}); Это переключение продлится в течение всего синхронного выполнения. Это означает, что если, например, контекст введён в обработчик события, последующие обработчики событий также будут выполняться в этом контексте, пока явно не привязаны к другому контексту с помощью AsyncResource. Именно поэтому run() предпочтительнее enterWith(), если нет веских причин использовать последний метод.
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])
-
store<любой тип> -
callback<Функция> -
...args<любой тип>
Выполняет функцию синхронно в контексте и возвращает её результат. Хранилище недоступно за пределами функции обратного вызова. Хранилище доступно для любых асинхронных операций, созданных внутри обратного вызова.
Необязательные args передаются в функцию обратного вызова.
Если функция обратного вызова вызывает ошибку, ошибка вызывается и run() тоже. Стек-трейс не затрагивается этим вызовом, и контекст завершается.
Пример:
const store = { id: 2 };
try {
asyncLocalStorage.run(store, () => {
asyncLocalStorage.getStore(); // Returns the store object
setTimeout(() => {
asyncLocalStorage.getStore(); // Returns the store object
}, 200);
throw new Error();
});
} catch (e) {
asyncLocalStorage.getStore(); // Returns undefined
// The error will be caught here
} asyncLocalStorage.exit(callback[, ...args])
-
callback<Функция> -
...args<любой тип>
Выполняет функцию синхронно вне контекста и возвращает её результат. Хранилище недоступно внутри функции обратного вызова или асинхронных операций, созданных внутри обратного вызова. Любой вызов getStore() внутри функции обратного вызова всегда вернёт undefined.
Необязательные 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-v14.x/docs/api/async_hooks.html