Skip to content

AsyncLocalStorage without Async Hooks #46265

Open
@jasnell

Description

@jasnell

I've recently gone through and implemented a subset of the AsyncLocalStorage API for Cloudflare's workerd runtime, and I've done so without implementing any part of the async_hooks API under the covers, proving that it's not only possible but results in a significant performance improvement and greatly simplified implementation. I propose that we could greatly improve our implementation by eliminating dependency on async_hooks and adopting a model similar to what we've implemented in workerd.

First, an overview of how things currently work in Node.js...

  • At any given time, there is a "current execution context". This context is strongly associated with specific individual objects (timers, nextticks, promises, uv handles, etc).
  • For every AsyncLocalStorage instance, there is an associated hidden property on the resource associated with the current execution context. Whenever als.run(...) is called, we temporarily mutate the value associated with that key and run a function. Any async resources that are created while that function is running ends up receiving a full copy of that map.

This design ends up being very inefficient as it means we are allocating and copying the context for every single object. For applications that are particularly promise heavy, this gets very expensive very quickly.

The way we implemented this in workerd is far more efficient:

At any given time, there is a current AsyncContextFrame. By default there is a logical "root" frame but we do not actually need to allocate anything to represent it.

Whenever we call als.run(...), we create a new AsyncContextFrame that inherits the context of the current, and set the new frame as current. Any async resource that is created simply acquires a reference to the current AsyncContextFrame.

The context is only copied when a new AsyncContextFrame is created, which happens far less frequently than creating a new async resource.

Further, we do not need to rely on V8's PromiseHooks API to propagate context through promises. There is a built-in v8 API v8::Context::SetContinuationPreservedEmbedderData() that can do the propagation for us much more efficiently. Whenever we enter an AsyncContextFrame, we set the ContinuationPreservedEmbedderData to an opaque object that wraps a reference to the frame. V8 takes care of properly associating that with all promise continuations.

There's a lot more to it that I'm glossing over here but the basic idea is that AsyncLocalStorage is absolutely possible to implement without async_hooks and I think there's a ton of value in doing so.

There are some caveats:

  • In the workerd implementation, we have chosen not to implement the als.enterWith(...) and als.disable() APIs. With the new model, these would need to be removed as they make things way more complicated than they are worth.
  • Handling around the unhandledRejection event would need to change (see AsyncLocalStorage and deferred promises #46262)
  • It's a massive change that touches lots of code. The way async context references are held on to by async resources would need to change dramatically, so it's not a small ask to change things.

The point of this issue is to see if there is interest in an approach to AsyncLocalStorage that is completely decoupled from async_hooks before @flakey5 and I decide whether or not to work on it.

/cc @mcollina @vdeturckheim @bmeck @bengl @nodejs/async_hooks

Metadata

Metadata

Assignees

No one assigned

    Labels

    async_hooksIssues and PRs related to the async hooks subsystem.async_local_storageAsyncLocalStorage

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions