Anthony Giniers

@antogyn

@aginiers

Asynchronous context tracking in Node.js

https://slides.com/antogyn/als

Asynchronous context

Context that is kept across asynchronous boundaries

  • During the async operation
  • After the async operation
async function start() {
  const context = { ... };
  await callApi(..., context);
  console.log(context); // updated
}
async function callApi(..., context) {
  ...
  context.apiCallIsDone = true;
}

Using the stack

πŸ˜“

// we need context in all our code!
console.log(..., { traceId: context.traceId });

Thread Local

Common solution in other languages: Thread-scoped context (Thread Local)

But Node.js is single-threaded πŸ˜“

let requestId;

async function start() {
  requestId = randomUUID();
  await callApi();
}

async function callApi() {
  ...
  console.log("Call API ok", { requestId });
}

start();

Thread Local

Common solution in other languages: Thread-scoped context (Thread Local)

But Node.js is single-threaded πŸ˜“

let requestId;

async function start() {
  requestId = randomUUID();
  await callApi();
}

async function callApi() {
  ...
  console.log("Call API ok", { requestId });
}

start();

Thread Local

Common solution in other languages: Thread-scoped context (Thread Local)

But Node.js is single-threaded πŸ˜“

let requestId;

async function start() {
  requestId = randomUUID();
  await callApi();
}

async function callApi() {
  ...
  console.log("Call API ok", { requestId });
}

start();

Thread Local

Common solution in other languages: Thread-scoped context (Thread Local)

But Node.js is single-threaded πŸ˜“

let requestId;

async function start() {
  requestId = randomUUID();
  await callApi();
}

async function callApi() {
  ...
  console.log("Call API ok", { requestId });
}

start();

πŸ’₯ Breaks with multiple concurrent requests

Continuation Local Storage

Continuation Local Storage

import cls from "continuation-local-storage";

const namespace = cls.createNamespace("request");

async function start() {
  namespace.run(async () => {
    ...
    ...
  });
}


async function callApi() {
  ...
  console.log("Call API ok", { requestId: namespace.get("requestId") });
}

Continuation Local Storage

import cls from "continuation-local-storage";

const namespace = cls.createNamespace("request");

async function start() {
  namespace.run(async () => {
    ...
    ...
  });
}


async function callApi() {
  ...
  console.log("Call API ok", { requestId: namespace.get("requestId") });
}

Continuation Local Storage

import cls from "continuation-local-storage";

const namespace = cls.createNamespace("request");

async function start() {
  namespace.run(async () => {
    namespace.set("requestId", randomUUID());
    await callApi();
  });
}


async function callApi() {
  ...
  console.log("Call API ok", { requestId: namespace.get("requestId") });
}

Continuation Local Storage

import cls from "continuation-local-storage";

const namespace = cls.createNamespace("request");

async function start() {
  namespace.run(async () => {
    namespace.set("requestId", randomUUID());
    await callApi();
  });
}


async function callApi() {
  ...
  console.log("Call API ok", { requestId: namespace.get("requestId") });
}

Continuation Local Storage

import cls from "continuation-local-storage";

const namespace = cls.createNamespace("request");

async function start() {
  namespace.run(async () => {
    namespace.set("requestId", randomUUID());
    await callApi();
  });
}


async function callApi() {
  ...
  console.log("Call API ok", { requestId: namespace.get("requestId") });
}

πŸ‘

Async Local Storage

Async Local Storage

import { AsyncLocalStorage } from "node:async_hooks";

const requestALS = new AsyncLocalStorage<{ requestId: string, userId?: string }>();

async function start() {
  requestALS.run({ requestId: randomUUID() }, async () => {
    ...
    ...
    ...
  });
}

async function callApi() {
  ...
  const { requestId, userId } = requestALS.getStore();
  console.log("Call API ok", { requestId, userId });
}

Async Local Storage

import { AsyncLocalStorage } from "node:async_hooks";

const requestALS = new AsyncLocalStorage<{ requestId: string, userId?: string }>();

async function start() {
  requestALS.run({ requestId: randomUUID() }, async () => {
    ...
    ...
    ...
  });
}

async function callApi() {
  ...
  const { requestId, userId } = requestALS.getStore();
  console.log("Call API ok", { requestId, userId });
}

Async Local Storage

import { AsyncLocalStorage } from "node:async_hooks";

const requestALS = new AsyncLocalStorage<{ requestId: string, userId?: string }>();

async function start() {
  requestALS.run({ requestId: randomUUID() }, async () => {
    const requestStore = requestALS.getStore();
    ...
    ...
  });
}

async function callApi() {
  ...
  const { requestId, userId } = requestALS.getStore();
  console.log("Call API ok", { requestId, userId });
}

Async Local Storage

import { AsyncLocalStorage } from "node:async_hooks";

const requestALS = new AsyncLocalStorage<{ requestId: string, userId?: string }>();

async function start() {
  requestALS.run({ requestId: randomUUID() }, async () => {
    const requestStore = requestALS.getStore();
    requestStore.userId = getUserId();
    await callApi();
  });
}

async function callApi() {
  ...
  const { requestId, userId } = requestALS.getStore();
  console.log("Call API ok", { requestId, userId });
}

Async Local Storage

import { AsyncLocalStorage } from "node:async_hooks";

const requestALS = new AsyncLocalStorage<{ requestId: string, userId?: string }>();

async function start() {
  requestALS.run({ requestId: randomUUID() }, async () => {
    const requestStore = requestALS.getStore();
    requestStore.userId = getUserId();
    await callApi();
  });
}

async function callApi() {
  ...
  const { requestId, userId } = requestALS.getStore();
  console.log("Call API ok", { requestId, userId });
}

ALS at Swan

import { initRequestContextValueMiddleware } from "@swan-io/utils";

function initNestjsMiddlewares(app) {
  ...
  app.use(initRequestContextValueMiddleware());
}

ALS at Swan

import { setRequestContextValue, getRequestContextValue } from "@swan-io/utils";

async function start() {
  setRequestContextValue<string>("requestId", randomUUID());
  await callApi();
}

async function callApi() {
  const requestId = getRequestContextValue<string>("requestId");
  console.log("Call API ok", { requestId });
}

ALS at Swan

import { setRequestContextValue, getRequestContextValue } from "@swan-io/utils";

async function start() {
  setRequestContextValue<string>("requestId", randomUUID());
  await callApi();
}

async function callApi() {
  const requestId = getRequestContextValue<string>("requestId");
  console.log("Call API ok", { requestId });
}

ALS at Swan

import { setRequestContextValue, getRequestContextValue } from "@swan-io/utils";

async function start() {
  setRequestContextValue<string>("requestId", randomUUID());
  await callApi();
}

async function callApi() {
  const requestId = getRequestContextValue<string>("requestId");
  console.log("Call API ok", { requestId });
}

ALS at Swan

import { setRequestContextValue, getRequestContextValue } from "@swan-io/utils";

async function start() {
  setRequestContextValue<string>("requestId", randomUUID());
  await callApi();
}

async function callApi() {
  const requestId = getRequestContextValue<string>("requestId");
  console.log("Call API ok", { requestId });
}
  • Not type safe
  • requestIdΒ is repeated

ALS at Swan

import { initRequestContextValueHandler } from "@swan-io/utils";

const requestIdContext = initRequestContextValueHandler<string>("requestId");

async function start() {
  requestIdContext.set(randomUUID());
  await callApi();
}

async function callApi() {
  const requestId = requestIdContext.get();
  console.log("Call API ok", { requestId });
}

ALS at Swan

import { initRequestContextValueHandler } from "@swan-io/utils";

const requestIdContext = initRequestContextValueHandler<string>("requestId");

async function start() {
  requestIdContext.set(randomUUID());
  await callApi();
}

async function callApi() {
  const requestId = requestIdContext.get();
  console.log("Call API ok", { requestId });
}

ALS at Swan

import { initRequestContextValueHandler } from "@swan-io/utils";

const requestIdContext = initRequestContextValueHandler<string>("requestId");

async function start() {
  requestIdContext.set(randomUUID());
  await callApi();
}

async function callApi() {
  const requestId = requestIdContext.get();
  console.log("Call API ok", { requestId });
}

ALS at Swan

import { initRequestContextValueHandler } from "@swan-io/utils";

const requestIdContext = initRequestContextValueHandler<string>("requestId");

async function start() {
  requestIdContext.set(randomUUID());
  await callApi();
}

async function callApi() {
  const requestId = requestIdContext.get();
  console.log("Call API ok", { requestId });
}

βœ…πŸ’―

Thank you!

Questions ?