为什么我使用异步挂钩 API 会导致 Node.js 异常终止?
Why does my use of the async hooks API cause an abnormal termination of Node.js?
我坏了Node.js!!
我正在使用 async hooks API,我的代码使 Node.js 异常终止。
我的问题是:导致 Node.js 以这种方式终止的代码是什么,我可以在解决问题的代码中更改什么吗?
我的应用程序 Data-Forge Notebook 需要能够跨 JavaScript 笔记本的评估跟踪异步操作,以了解笔记本的评估何时完成。
所以我创建了一个名为 AsyncTracker 的 JavaScript class,它包装了异步挂钩 API,这样我就可以为一段代码启用异步跟踪。在代码部分的末尾,我可以禁用跟踪并等待当前异步操作完成。
为了初始化跟踪,我这样做了:
this.asyncHook = async_hooks.createHook({
init: (asyncId, type, triggerAsyncId, resource) => {
this.addAsyncOperation(asyncId, type);
},
after: asyncId => {
this.removeAsyncOperation(asyncId);
},
destroy: asyncId => {
this.removeAsyncOperation(asyncId);
},
promiseResolve: asyncId => {
this.removeAsyncOperation(asyncId);
},
});
this.asyncHook.enable();
异步操作记录在 JS 映射中,但它们仅在通过将 trackAsyncOperations
设置为 true
启用跟踪时添加。这是允许在代码部分的开头启用跟踪的变量:
addAsyncOperation(asyncId, type) {
if (this.trackAsyncOperations) {
this.asyncOperations.add(asyncId);
this.openAsyncOperations.set(asyncId, type);
}
}
各种异步挂钩导致从映射中删除异步操作:
removeAsyncOperation(asyncId) {
if (this.asyncOperations.has(asyncId)) {
this.asyncOperations.delete(asyncId);
this.openAsyncOperations.delete(asyncId);
if (this.asyncOperationsAwaitResolver &&
this.asyncOperations.size <= 0) {
this.asyncOperationsAwaitResolver();
this.asyncOperationsAwaitResolver = undefined;
}
}
}
注意代码行 this.asyncOperationsAwaitResolver()
,这是触发我们在代码部分末尾等待的承诺的解决,以等待挂起的异步操作完成。
禁用跟踪然后等待完成挂起的异步操作的函数如下所示:
awaitCurrentAsyncOperations() {
// At this point we stop tracking new async operations.
// We don't care about any async op started after this point.
this.trackAsyncOperations = false;
let promise;
if (this.asyncOperations.size > 0) {
promise = new Promise(resolve => {
// Extract the resolve function so we can call it when all current async operations have completed.
this.asyncOperationsAwaitResolver = resolve;
});
}
else {
this.asyncOperationsAwaitResolver = undefined;
promise = Promise.resolve();
}
return promise;
}
总而言之,这是一个使用跟踪器的最小示例,它使 Node.js 在没有警告的情况下中止:
const asyncTracker = new AsyncTracker();
asyncTracker.init();
asyncTracker.enableTracking(); // Enable async operation tracking.
// ---- Async operations created from here on are tracked.
// The simplest async operation that causes this problem.
// If you comment out this code the program completes normally.
await Promise.resolve();
// --- Now we disable tracking of async operations,
// then wait for all current operations to complete before continuing.
// Disable async tracking and wait.
await asyncTracker.awaitCurrentAsyncOperations();
请注意,此代码并未全面损坏。当与基于回调或基于承诺的异步操作一起使用时,它似乎工作正常(Node.js 正常终止)。只有当我将 await
关键字添加到组合中时,它才会失败。因此,例如,如果我将 await Promise.resolve()
替换为对 setTimeout
的调用,它会按预期工作。
GitHub:
上有一个这样的工作示例
https://github.com/ashleydavis/nodejs-async-tracking-example
运行 使 Node.js 爆炸的代码。要重现克隆 repo,运行 npm install
,然后是 npm start
.
此代码已在 Windows 10 和 Node.js 版本 8.9.4、10.15.2 和 12.6.0 上进行了测试。
此代码现已在 MacOS v8.11.3、10.15.0 和 12.6.0 上进行了测试。
它在所有测试的版本上都有相同的行为。
代码审查
在 GitHub 和 运行 上使用 Node v10.16.0 查看 Windows 10 上的完整代码后,看起来 AsyncTracker.awaitCurrentAsyncOperations
中返回的承诺是从未解决,这会阻止 main()
中的代码超出 await asyncTracker.awaitCurrentAsyncOperations();
部分。
这就解释了为什么 ** 33 **
部分永远不会输出以及为什么 main()
的 then(...)
回调永远不会打印出 Done
。上述 promise 的 resolve 方法被分配给 this.asyncOperationsAwaitResolver
,但是(根据当前的实现)它只会在 this.asyncOperation
集中没有更多的 promise 时被调用。如下面第一个控制台输出所示,情况并非如此。
问题重现
我稍微修改了代码以处理 index.js
脚本末尾的 process.on
事件
process.on('exit', () => { console.log('Process exited') });
process.on('uncaughtException', (err) => { console.error(err && err.stack || err) });
process.on('unhandledRejection', (reason, promise) => { console.error('Unhandled Rejection at: ', promise, ', reason: ', reason) });
process.on('multipleResolves', (type, promise, reason) => { console.error(type, promise, reason) });
并且退出回调是唯一被调用的回调。 ** 22 ** 标记后的控制台输出为:
** 22 **
>>>>>>> Cell has ended, async operation tracking has been disabled, currently have 3 async ops in progress.
Waiting for operations to complete, creating a promise.
!! Have 3 remaining async operations:
#9 - PROMISE.
#10 - PROMISE.
#11 - PROMISE.
%% removed async operation #9
!! Have 2 remaining async operations:
#10 - PROMISE.
#11 - PROMISE.
Process exited
解决方案
问题是由removeAsyncOperation(asyncId)
方法中的错字引起的。
而不是:
removeAsyncOperation(asyncId) {
// ...
if (this.asyncOperationsAwaitResolver && this.asyncOperations.size <= 0) {
//...
}
}
}
如果队列中有 promise,这会阻止 promise 被解析,你需要做:
removeAsyncOperation(asyncId) {
// ...
if (this.asyncOperationsAwaitResolver && this.asyncOperations.size >= 0) {
//...
}
}
}
只要队列中有承诺,承诺就会得到解决。
对 async-tracker.js
进行上述更改后,应用程序按预期运行,生成输出:
** 22 **
>>>>>>> Cell has ended, async operation tracking has been disabled, currently have 3 async ops in progress.
Waiting for operations to complete, creating a promise.
!! Have 3 remaining async operations:
#9 - PROMISE.
#10 - PROMISE.
#11 - PROMISE.
%% removed async operation #9
!! Have 2 remaining async operations:
#10 - PROMISE.
#11 - PROMISE.
%% resolving the async op promise!
** 33 **
Done
%% removed async operation #10
!! Have 1 remaining async operations:
#11 - PROMISE.
%% removed async operation #11
!! Have 0 remaining async operations:
Process exited
好的,我现在有答案了。我会继续更新这个,因为我更好地理解了我造成的这个问题。
我的回答还没有完全解释 Node.js 的运作方式,但它是某种解决方案。
我的代码试图等待一段代码中发生的任意异步操作的完成。在我的代码示例中,异步操作的跟踪发生在 main
函数内,而 then
和 catch
处理程序发生在 main
函数之外。我的理论是,正在跟踪的异步操作 'kept alive' 通过永远不会执行的代码:导致异步操作完成的代码永远不会执行。
我通过删除使我的程序正常终止的 then
和 catch
回调发现了这一点。
所以我的工作理论是我造成了某种 Node.js 承诺死锁,Node.js 无法处理(为什么会这样?)所以它就退出了。
我意识到这是 Node.js 的一个完全病态的用例,没有人会使用它。但我已经构建了一个开发人员工具,并且正在探索这个工具,因为我希望能够跟踪和监控用户发起的任意异步操作。
我坏了Node.js!!
我正在使用 async hooks API,我的代码使 Node.js 异常终止。
我的问题是:导致 Node.js 以这种方式终止的代码是什么,我可以在解决问题的代码中更改什么吗?
我的应用程序 Data-Forge Notebook 需要能够跨 JavaScript 笔记本的评估跟踪异步操作,以了解笔记本的评估何时完成。
所以我创建了一个名为 AsyncTracker 的 JavaScript class,它包装了异步挂钩 API,这样我就可以为一段代码启用异步跟踪。在代码部分的末尾,我可以禁用跟踪并等待当前异步操作完成。
为了初始化跟踪,我这样做了:
this.asyncHook = async_hooks.createHook({
init: (asyncId, type, triggerAsyncId, resource) => {
this.addAsyncOperation(asyncId, type);
},
after: asyncId => {
this.removeAsyncOperation(asyncId);
},
destroy: asyncId => {
this.removeAsyncOperation(asyncId);
},
promiseResolve: asyncId => {
this.removeAsyncOperation(asyncId);
},
});
this.asyncHook.enable();
异步操作记录在 JS 映射中,但它们仅在通过将 trackAsyncOperations
设置为 true
启用跟踪时添加。这是允许在代码部分的开头启用跟踪的变量:
addAsyncOperation(asyncId, type) {
if (this.trackAsyncOperations) {
this.asyncOperations.add(asyncId);
this.openAsyncOperations.set(asyncId, type);
}
}
各种异步挂钩导致从映射中删除异步操作:
removeAsyncOperation(asyncId) {
if (this.asyncOperations.has(asyncId)) {
this.asyncOperations.delete(asyncId);
this.openAsyncOperations.delete(asyncId);
if (this.asyncOperationsAwaitResolver &&
this.asyncOperations.size <= 0) {
this.asyncOperationsAwaitResolver();
this.asyncOperationsAwaitResolver = undefined;
}
}
}
注意代码行 this.asyncOperationsAwaitResolver()
,这是触发我们在代码部分末尾等待的承诺的解决,以等待挂起的异步操作完成。
禁用跟踪然后等待完成挂起的异步操作的函数如下所示:
awaitCurrentAsyncOperations() {
// At this point we stop tracking new async operations.
// We don't care about any async op started after this point.
this.trackAsyncOperations = false;
let promise;
if (this.asyncOperations.size > 0) {
promise = new Promise(resolve => {
// Extract the resolve function so we can call it when all current async operations have completed.
this.asyncOperationsAwaitResolver = resolve;
});
}
else {
this.asyncOperationsAwaitResolver = undefined;
promise = Promise.resolve();
}
return promise;
}
总而言之,这是一个使用跟踪器的最小示例,它使 Node.js 在没有警告的情况下中止:
const asyncTracker = new AsyncTracker();
asyncTracker.init();
asyncTracker.enableTracking(); // Enable async operation tracking.
// ---- Async operations created from here on are tracked.
// The simplest async operation that causes this problem.
// If you comment out this code the program completes normally.
await Promise.resolve();
// --- Now we disable tracking of async operations,
// then wait for all current operations to complete before continuing.
// Disable async tracking and wait.
await asyncTracker.awaitCurrentAsyncOperations();
请注意,此代码并未全面损坏。当与基于回调或基于承诺的异步操作一起使用时,它似乎工作正常(Node.js 正常终止)。只有当我将 await
关键字添加到组合中时,它才会失败。因此,例如,如果我将 await Promise.resolve()
替换为对 setTimeout
的调用,它会按预期工作。
GitHub:
上有一个这样的工作示例https://github.com/ashleydavis/nodejs-async-tracking-example
运行 使 Node.js 爆炸的代码。要重现克隆 repo,运行 npm install
,然后是 npm start
.
此代码已在 Windows 10 和 Node.js 版本 8.9.4、10.15.2 和 12.6.0 上进行了测试。
此代码现已在 MacOS v8.11.3、10.15.0 和 12.6.0 上进行了测试。
它在所有测试的版本上都有相同的行为。
代码审查
在 GitHub 和 运行 上使用 Node v10.16.0 查看 Windows 10 上的完整代码后,看起来 AsyncTracker.awaitCurrentAsyncOperations
中返回的承诺是从未解决,这会阻止 main()
中的代码超出 await asyncTracker.awaitCurrentAsyncOperations();
部分。
这就解释了为什么 ** 33 **
部分永远不会输出以及为什么 main()
的 then(...)
回调永远不会打印出 Done
。上述 promise 的 resolve 方法被分配给 this.asyncOperationsAwaitResolver
,但是(根据当前的实现)它只会在 this.asyncOperation
集中没有更多的 promise 时被调用。如下面第一个控制台输出所示,情况并非如此。
问题重现
我稍微修改了代码以处理 index.js
脚本末尾的 process.on
事件
process.on('exit', () => { console.log('Process exited') });
process.on('uncaughtException', (err) => { console.error(err && err.stack || err) });
process.on('unhandledRejection', (reason, promise) => { console.error('Unhandled Rejection at: ', promise, ', reason: ', reason) });
process.on('multipleResolves', (type, promise, reason) => { console.error(type, promise, reason) });
并且退出回调是唯一被调用的回调。 ** 22 ** 标记后的控制台输出为:
** 22 **
>>>>>>> Cell has ended, async operation tracking has been disabled, currently have 3 async ops in progress.
Waiting for operations to complete, creating a promise.
!! Have 3 remaining async operations:
#9 - PROMISE.
#10 - PROMISE.
#11 - PROMISE.
%% removed async operation #9
!! Have 2 remaining async operations:
#10 - PROMISE.
#11 - PROMISE.
Process exited
解决方案
问题是由removeAsyncOperation(asyncId)
方法中的错字引起的。
而不是:
removeAsyncOperation(asyncId) {
// ...
if (this.asyncOperationsAwaitResolver && this.asyncOperations.size <= 0) {
//...
}
}
}
如果队列中有 promise,这会阻止 promise 被解析,你需要做:
removeAsyncOperation(asyncId) {
// ...
if (this.asyncOperationsAwaitResolver && this.asyncOperations.size >= 0) {
//...
}
}
}
只要队列中有承诺,承诺就会得到解决。
对 async-tracker.js
进行上述更改后,应用程序按预期运行,生成输出:
** 22 **
>>>>>>> Cell has ended, async operation tracking has been disabled, currently have 3 async ops in progress.
Waiting for operations to complete, creating a promise.
!! Have 3 remaining async operations:
#9 - PROMISE.
#10 - PROMISE.
#11 - PROMISE.
%% removed async operation #9
!! Have 2 remaining async operations:
#10 - PROMISE.
#11 - PROMISE.
%% resolving the async op promise!
** 33 **
Done
%% removed async operation #10
!! Have 1 remaining async operations:
#11 - PROMISE.
%% removed async operation #11
!! Have 0 remaining async operations:
Process exited
好的,我现在有答案了。我会继续更新这个,因为我更好地理解了我造成的这个问题。
我的回答还没有完全解释 Node.js 的运作方式,但它是某种解决方案。
我的代码试图等待一段代码中发生的任意异步操作的完成。在我的代码示例中,异步操作的跟踪发生在 main
函数内,而 then
和 catch
处理程序发生在 main
函数之外。我的理论是,正在跟踪的异步操作 'kept alive' 通过永远不会执行的代码:导致异步操作完成的代码永远不会执行。
我通过删除使我的程序正常终止的 then
和 catch
回调发现了这一点。
所以我的工作理论是我造成了某种 Node.js 承诺死锁,Node.js 无法处理(为什么会这样?)所以它就退出了。
我意识到这是 Node.js 的一个完全病态的用例,没有人会使用它。但我已经构建了一个开发人员工具,并且正在探索这个工具,因为我希望能够跟踪和监控用户发起的任意异步操作。