Async Wars

Vladimir de Turckheim

Lead Node.js engineer @ Sqreen​ (ex. Steamulo, Secway)

PSA

Join Node.js Security WG

  • Do not need to be a security expert
  • Awesome opportunity to learn
  • Mostly review and communication work
  • Ping me!

Outline

  • Holliday special
  • The phantom problem
  • Attack of the event loop
  • Revenge of the Hackers
  • A new Hope
  • Hackers strike back
  • Return of the domain
  • The power awakens
  • The last code

The async_hooks module provides an API to register callbacks tracking the lifetime of asynchronous resources created inside a Node.js application.

Holliday special

Let me start with an example

The phantom problem

app.get(/stuff, (req, res) => {
    throw new Error('hello');
});
MacBook-Air-de-Vladimir:tt vladimir$ node index.js 
Error: hello
    at app.get (/Users/vladimir/tmp/tt/index.js:5:11)
    at Layer.handle [as handle_request] (/Users/vladimir/tmp/tt/node_modules/express/lib/router/layer.js:95:5)
    at next (/Users/vladimir/tmp/tt/node_modules/express/lib/router/route.js:137:13)
    at Route.dispatch (/Users/vladimir/tmp/tt/node_modules/express/lib/router/route.js:112:3)
    at Layer.handle [as handle_request] (/Users/vladimir/tmp/tt/node_modules/express/lib/router/layer.js:95:5)
    at /Users/vladimir/tmp/tt/node_modules/express/lib/router/index.js:281:22
    at Function.process_params (/Users/vladimir/tmp/tt/node_modules/express/lib/router/index.js:335:12)
    at next (/Users/vladimir/tmp/tt/node_modules/express/lib/router/index.js:275:10)
    at expressInit (/Users/vladimir/tmp/tt/node_modules/express/lib/middleware/init.js:40:5)
    at Layer.handle [as handle_request] (/Users/vladimir/tmp/tt/node_modules/express/lib/router/layer.js:95:5)
app.get('/other', (req, res) => {
    setTimeout(() => {
        throw new Error('hello')
    });
});
MacBook-Air-de-Vladimir:tt vladimir$ node index.js 
/Users/vladimir/tmp/tt/index.js:10
        throw new Error('hello')
        ^

Error: hello
    at Timeout.setTimeout [as _onTimeout] (/Users/vladimir/tmp/tt/index.js:10:15)
    at ontimeout (timers.js:469:11)
    at tryOnTimeout (timers.js:304:5)
    at Timer.listOnTimeout (timers.js:264:5)

Process crashed

Stacktraces in Node.js are short

Stacktraces in Node.js are useless

Meet the event loop

Attack of the event loop

app.get('/stuff', (req, res) => {
  fs.readFile('./myFile', (err, result) => {
    // context loss
    db.getById(555, async (err, doc) => {
      // context loss
      const api_result = await API.call('thing');
      // context loss
      res.json({ result, doc, api_result });
    });
  });
});
app.get('/other', (req, res) => {
    setTimeout(() => {
        res.end('ok');
    });
});

Debugging is impossible

Historical solutions

Revenge of the Hackers

Domains

const d = require('domain').create();
d.on('error', (er) => {
  console.log(`error, but oh well ${er.message}`);
});
d.run(() => {
  require('http').createServer((req, res) => {
    handleRequest(req, res);
  }).listen(PORT);
});

Userland patching

var fs = require('fs');
massWrap(
  fs,
  [
    'watch',
    'rename',
    'truncate',
    'chown',
    'fchown',
    'chmod',
    'fchmod',
    'stat',
    'lstat',
    'fstat',
    'link',
    'symlink',
    'readlink',
    'realpath',
    'unlink',
    'rmdir',
    'mkdir',
    'readdir',
    'close',
    'open',
    'utimes',
    'futimes',
    'fsync',
    'write',
    'read',
    'readFile',
    'writeFile',
    'appendFile',
    'watchFile',
    'unwatchFile',
    "exists",
  ],
  activator
);

Does not work with async/await

Enter Async Hooks

A new Hope

async_hooks.createHook({
  init(asyncId, type, triggerAsyncId) {
    // I run when an async resource is created
  },
  before(asyncId) {
    // I run before the callback
  },
  after(asyncId) {
    // I run after the callback
  },
  destroy(asyncId) {
    // I run when the resource is destroyed
  },
}).enable();

Action

Hackers strike back

app.get('/other', (req, res) => {
    console.log(new Error());
    setTimeout(() => {
        console.log(new Error());
        res.end('ok');
    });
});
Error
    at app.get (/Users/vladimir/tmp/tt/index.js:27:17)
    at Layer.handle [as handle_request] (/Users/vladimir/tmp/tt/node_modules/express/lib/router/layer.js:95:5)
    at next (/Users/vladimir/tmp/tt/node_modules/express/lib/router/route.js:137:13)
    at Route.dispatch (/Users/vladimir/tmp/tt/node_modules/express/lib/router/route.js:112:3)
    at Layer.handle [as handle_request] (/Users/vladimir/tmp/tt/node_modules/express/lib/router/layer.js:95:5)
    at /Users/vladimir/tmp/tt/node_modules/express/lib/router/index.js:281:22
    at Function.process_params (/Users/vladimir/tmp/tt/node_modules/express/lib/router/index.js:335:12)
    at next (/Users/vladimir/tmp/tt/node_modules/express/lib/router/index.js:275:10)
    at expressInit (/Users/vladimir/tmp/tt/node_modules/express/lib/middleware/init.js:40:5)
    at Layer.handle [as handle_request] (/Users/vladimir/tmp/tt/node_modules/express/lib/router/layer.js:95:5)
Error
    at Timeout.setTimeout [as _onTimeout] (/Users/vladimir/tmp/tt/index.js:29:21)
    at ontimeout (timers.js:469:11)
    at tryOnTimeout (timers.js:304:5)
    at Timer.listOnTimeout (timers.js:264:5)

Let's do it?

const AsyncHook = require('async_hooks');

const map = new Map();

AsyncHook.createHook({
    init(asyncId, type, triggerAsyncId) {

        const stack = map.get(triggerAsyncId) || '' ;
        const stackPart = (new Error()).stack.split('\n').slice(1).join('\n');
        map.set(asyncId, stackPart + stack);
    }
})
    .enable();

const getError = function (...args) {

    const err = new Error(...args);
    err.stack += map.get(AsyncHook.executionAsyncId());
    return err;
};
app.get('/other', (req, res) => {
    setTimeout(() => {
        console.log(getError());
        res.end('ok');
    });
});
Error
    at getError (/Users/vladimir/tmp/tt/index.js:17:17)
    at Timeout.setTimeout [as _onTimeout] (/Users/vladimir/tmp/tt/index.js:28:21)
    at ontimeout (timers.js:469:11)
    at tryOnTimeout (timers.js:304:5)
    at Timer.listOnTimeout (timers.js:264:5)    at AsyncHook.init (/Users/vladimir/tmp/tt/index.js:9:28)
    at emitInitNative (async_hooks.js:458:43)
    at emitInitScript (async_hooks.js:361:3)
    at new Timeout (timers.js:573:5)
    at createSingleTimeout (timers.js:453:15)
    at setTimeout (timers.js:437:10)
    at app.get (/Users/vladimir/tmp/tt/index.js:27:5)
    at Layer.handle [as handle_request] (/Users/vladimir/tmp/tt/node_modules/express/lib/router/layer.js:95:5)
    at next (/Users/vladimir/tmp/tt/node_modules/express/lib/router/route.js:137:13)
    at Route.dispatch (/Users/vladimir/tmp/tt/node_modules/express/lib/router/route.js:112:3)    at AsyncHook.init (/Users/vladimir/tmp/tt/index.js:9:28)
    at HTTPParser.emitInitNative (async_hooks.js:458:43)
    at Server.connectionListener (_http_server.js:307:10)
    at emitOne (events.js:115:13)
    at Server.emit (events.js:210:7)
    at TCP.onconnection (net.js:1560:8)    at AsyncHook.init (/Users/vladimir/tmp/tt/index.js:9:28)
    at TCP.emitInitNative (async_hooks.js:458:43)
    at createServerHandle (net.js:1267:14)
    at Server.setupListenHandle [as _listen2] (net.js:1310:14)
    at listenInCluster (net.js:1391:12)
    at Server.listen (net.js:1474:7)
    at Function.listen (/Users/vladimir/tmp/tt/node_modules/express/lib/application.js:618:24)
    at Object.<anonymous> (/Users/vladimir/tmp/tt/index.js:33:5)
    at Module._compile (module.js:624:30)
    at Object.Module._extensions..js (module.js:635:10)

More?

Return of the domain

Domains are

dead

Long

live domains

What else?

The power awakens

We can

  • Have asynchronous flamecharts (WIP)
  • Have a request context in servers
  • Implement Zones
  • ...

Last news!

Wrap up

The last code

Async Hooks solve historical Node.js problems

Async Hooks allow people to build tools that make Node.js more entreprise friendly

Async Hooks is not dark magic

Node.js Async hooks

By Vladimir de Turckheim

Node.js Async hooks

  • 1,826