使用 async_hooks 跟踪上下文

Tracking context with async_hooks

我正在尝试使用节点 async_hooks 通过异步堆栈跟踪上下文。它适用于大多数情况,但是我发现这个用例我想不出如何解决:

service.js:

const asyncHooks = require('async_hooks');

class Service {
  constructor() {
    this.store = {};
    this.hooks = asyncHooks.createHook({
      init: (asyncId, type, triggerAsyncId) => {
        if (this.store[triggerAsyncId]) {
          this.store[asyncId] = this.store[triggerAsyncId];
        }
      },
      destroy: (asyncId) => {
        delete this.store[asyncId];
      },
    });
    this.enable();
  }

  async run(fn) {
    this.store[asyncHooks.executionAsyncId()] = {};
    await fn();
  }

  set(key, value) {
    this.store[asyncHooks.executionAsyncId()][key] = value;
  }

  get(key) {
    const state = this.store[asyncHooks.executionAsyncId()];
    if (state) {
      return state[key];
    } else {
      return null;
    }
  }

  enable() {
    this.hooks.enable();
  }

  disable() {
    this.hooks.disable();
  }
}

module.exports = Service;

service.spec.js

const assert = require('assert');
const Service = require('./service');

describe('Service', () => {
  let service;

  afterEach(() => {
    service.disable();
  });

  it('can handle promises created out of the execution stack', async () => {
    service = new Service();

    const p = Promise.resolve();

    await service.run(async () => {
      service.set('foo');

      await p.then(() => {
        assert.strictEqual('foo', service.get());
      });
    });
  });
});

此测试用例将失败,因为调用 next 时创建的承诺的 triggerAsyncId 是 Promise.resolve() 调用的 executionAsyncId。它是在当前异步堆栈之外创建的,并且是一个单独的上下文。我看不出有什么方法可以将 next 函数异步上下文与其创建时的上下文结合起来。

https://github.com/domarmstrong/async_hook_then_example

我写了一个非常相似的包node-request-context with a blog post来解释它。

您没有为 foo 定义任何值,并且在没有任何键的情况下调用 service.get() 时您没有要求任何值。但我想那是你写问题时的一个小错误。

您指出的主要问题是 Promise.resolve 的位置。我同意,没有办法让它发挥作用。这正是您创建 run 函数的原因,因此您将捕获 executionAsyncId 并使用它跟踪您的代码。否则,您无法跟踪任何上下文。

你的代码只是为了测试,但如果你真的需要,你可以使用箭头函数作弊:

it('can handle promises created out of the execution stack', async () => {
  service = new Service();

  const p = () => Promise.resolve();

  await service.run(async () => {


    service.set('foo', 'bar');

    await p().then(() => {
      assert.strictEqual('bar', service.get('foo'));
    });
  });
});

我找到了一个解决方案,虽然不完美,但确实有效。用 Promise.all 包装原始承诺将解析为正确的 executionAsyncId。但它确实依赖于调用代码了解承诺上下文。

const assert = require('assert');
const Service = require('./service');

describe('Service', () => {
  let service;

  afterEach(() => {
    service.disable();
  });

  it('can handle promises created out of the execution stack', async () => {
    service = new Service();

    const p = Promise.resolve();

    await service.run(async () => {
      service.set('foo');

      await Promise.all([p]).then(() => {
        assert.strictEqual('foo', service.get());
      });
    });
  });
});