Search code examples
node.jspromiseasync-awaitasync-hooks

Tracking context with async_hooks


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


Solution

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