3

我已经破坏了 Node.js!

我正在使用异步钩子 API,我的代码使 Node.js 异常终止。

我的问题是:这段代码是什么让 Node.js 以这种方式终止,我可以在代码中更改什么来解决问题吗?

我的应用程序Data-Forge Notebook需要能够跨 JavaScript 笔记本的评估跟踪异步操作,以了解笔记本的评估何时完成。

因此,我创建了一个名为AsyncTracker的 JavaScript 类,它包装了异步钩子 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 上进行了测试。

它在所有测试版本上具有相同的行为。

4

2 回答 2

4

代码审查

在查看了 GitHub 上的完整代码并在 Windows 10 上使用 Node v10.16.0 运行它之后,看起来返回的承诺AsyncTracker.awaitCurrentAsyncOperations永远不会解决,这会阻止代码main()超出await asyncTracker.awaitCurrentAsyncOperations();部分。

这就解释了为什么该** 33 **部分从不输出以及为什么从不打印的then(...)回调。上述 promise 的 resolve 方法被分配给,但是(根据当前的实现)只有当集合中没有更多的 promise 时才会调用它。如下面的第一个控制台输出所示,情况并非如此。main()Donethis.asyncOperationsAwaitResolverthis.asyncOperation

问题再现

我稍微修改了代码以在脚本process.on末尾处理事件index.js

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) {
        //...
      }
    }
  } 

如果队列中有承诺,这会阻止承诺被解决,你需要这样做:

  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
于 2019-07-30T12:14:36.647 回答
0

好的,我现在有答案了。我会继续更新这个,因为我更好地理解了我造成的这个问题。

我的回答还没有完全解释 Node.js 的运作方式,但它是某种解决方案。

我的代码试图等待在一段代码中发生的任意异步操作的完成。在我的代码示例中,异步操作的跟踪发生在main函数内部,thencatch处理程序发生在main函数外部。我的理论是,被跟踪的异步操作被永远不会被执行的代码“保持活跃”:导致异步操作完成的代码永远不会被执行。

我通过删除thencatch回调发现了这一点,这使我的程序正常终止。

所以我的工作理论是我正在导致某种 Node.js 承诺死锁,Node.js 无法处理(为什么会这样?)所以它就退出了。

我意识到这对于 Node.js 来说是一个完全病态的用例,没有一个头脑正常的人会使用它。但是我已经构建了一个开发工具,并且我正在探索这个工具,因为我希望能够跟踪和监控我的用户发起的任意异步操作。

于 2019-08-08T08:16:37.153 回答