Continuation-local Storage


Recently, I got to work with cls-hooked in a project I’m working on. For those who don’t know it, cls-hooked is a library implementation of a concept called Continuation-local Storage in Node.js. As a new comer to the world of asynchronous programming, I decided to dig deeper into the concept and write about it in this post and try to relate it to the world of multi-threaded programming!

To put it simply, continuation-local storage is a way to preserve data between asynchronous function calls within the context of the same call stack. Instead of passing repeated parameters between function calls, cls acts like a set of global variables easily accessible only between async function calls. Here’s a small example to demonstrate how to use the library:

const { createNamespace } = require("cls-hooked");

const cls = createNamespace("MyNamespace");

const parentFunc = async () => {
  cls.set("uuid", 123);

  await _subFunc();

  return cls.get("uuid");
};
const _subFunc = async () => {
  cls.set("uuid", 456);
};

const secondParentFunc = async () => {
  cls.set("uuid", 123);

  await _secondSubFunc();

  return cls.get("uuid");
};
const _secondSubFunc = async () => {
  // Do not change the value...
};

cls.run(async () => {
  let result = await parentFunc();
  console.log(result);  // 456

  let secondResult = await secondParentFunc();
  console.log(secondResult); // 123 (unchanged and unaffected by `parentFunc()` scope)
});

In the example above, parentFunc() has a different call stack from secondParentFunc() and each call stack has its own local storage so the property uuid is not shared between the two different stacks but each continuation-local variable is, in fact, shared between the functions in a single call stack.

Continuation-local Storage is analogous to Thread-local storage in thread programming. It resembles cls in that it acts as a global variable but inside the context of each thread, thus preventing the very possible issue of data race. Let’s see a little primitive example:

#include <iostream>
#include <string>
#include <thread>

// every thread will have access to a separate variable `n` with value = 2
thread_local int n = 2;

void thread_func(int td) {
  ++n;
  std::cout << "Thread " << td << " has n = " << n << std::endl << std::flush;
}

int main() {
  std::thread it1(thread_func, 1);
  std::thread it2(thread_func, 2);
  std::thread it3(thread_func, 3);
  it1.join();
  it2.join();
  it3.join();

  // Thread 2 has n = 3
  // Thread 1 has n = 3
  // Thread 3 has n = 3
}

In the above C++ code, the variable n is a thread-local variable that is, a version of the variable n is available to each thread separately and each thread is writing and reading from it’s own version thus; no data racing here.

A practical example for where this might come useful in the context of asynchronous web programming is authenticating a user and keeping the user session data in a continuation-local variable. Since the continuation-local variable is preserved between the async function call stack, it can be used during the session or before sending out the response in order to log the actions of the user and relate them to the user using the user session data (with user_id for example).

That was a quick intro into Continuation-local storage and how it can be useful in web programming. Thanks for reading!