Tracking context with async_hooks

2.8k Views Asked by At

I'm trying to track context through the async stack using node async_hooks. It works for most cases, however I have found this use case that I can't think how to resolve:

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());
      });
    });
  });
});

This test case will fail because the triggerAsyncId of the promise created when calling next is the executionAsyncId of the Promise.resolve() call. Which was created outside the current async stack and is a separate context. I can't see any way to marry the next functions async context with the context it was created in.

https://github.com/domarmstrong/async_hook_then_example

2

There are 2 best solutions below

0
On BEST ANSWER

I found a solution, which is not perfect, but does work. Wrapping the original promise with Promise.all will resolve to the correct executionAsyncId. But it does rely on the calling code being aware of the promises context.

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());
      });
    });
  });
});
3
On

I wrote a very similar package called node-request-context with a blog post to explain it.

You haven't define any value for foo and you are not asking for any value when calling service.get() without any key. But I guess that was a minor mistake when you wrote the question.

The main issue you named was the location of Promise.resolve. I agree, there is no way to make it work. This is exactly the reason you've create the run function, so you will catch the executionAsyncId and track your code using it. Otherwise, you couldn't track any context.

Your code was just for testing but if you really need, you can cheat by using arrow function:

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'));
    });
  });
});