From 97d95012540468b395f33b39dd34cdfd96b50849 Mon Sep 17 00:00:00 2001 From: James M Snell Date: Thu, 8 Dec 2022 07:32:43 -0800 Subject: [PATCH] Implementation of async context tracking --- samples/async-context/config.capnp | 35 +++ samples/async-context/worker.js | 23 ++ src/workerd/api/global-scope.c++ | 107 ++++++--- src/workerd/api/global-scope.h | 30 +-- src/workerd/api/node/async-hooks.c++ | 120 ++++++++++ src/workerd/api/node/async-hooks.h | 226 +++++++++++++++++++ src/workerd/api/node/node.h | 18 ++ src/workerd/jsg/README.md | 70 ++++++ src/workerd/jsg/async-context.c++ | 324 +++++++++++++++++++++++++++ src/workerd/jsg/async-context.h | 193 ++++++++++++++++ src/workerd/jsg/jsg.c++ | 4 + src/workerd/jsg/jsg.h | 27 +++ src/workerd/jsg/setup.c++ | 30 +++ src/workerd/jsg/setup.h | 35 +++ src/workerd/server/workerd-api.c++ | 6 + src/workerd/tools/api-encoder.c++ | 4 +- 16 files changed, 1197 insertions(+), 55 deletions(-) create mode 100644 samples/async-context/config.capnp create mode 100644 samples/async-context/worker.js create mode 100644 src/workerd/api/node/async-hooks.c++ create mode 100644 src/workerd/api/node/async-hooks.h create mode 100644 src/workerd/api/node/node.h create mode 100644 src/workerd/jsg/async-context.c++ create mode 100644 src/workerd/jsg/async-context.h diff --git a/samples/async-context/config.capnp b/samples/async-context/config.capnp new file mode 100644 index 00000000000..a3593950e7a --- /dev/null +++ b/samples/async-context/config.capnp @@ -0,0 +1,35 @@ +# Imports the base schema for workerd configuration files. + +# Refer to the comments in /src/workerd/server/workerd.capnp for more details. + +using Workerd = import "/workerd/workerd.capnp"; + +# A constant of type Workerd.Config defines the top-level configuration for an +# instance of the workerd runtime. A single config file can contain multiple +# Workerd.Config definitions and must have at least one. +const helloWorldExample :Workerd.Config = ( + + # Every workerd instance consists of a set of named services. A worker, for instance, + # is a type of service. Other types of services can include external servers, the + # ability to talk to a network, or accessing a disk directory. Here we create a single + # worker service. The configuration details for the worker are defined below. + services = [ (name = "main", worker = .helloWorld) ], + + # Every configuration defines the one or more sockets on which the server will listene. + # Here, we create a single socket that will listen on localhost port 8080, and will + # dispatch to the "main" service that we defined above. + sockets = [ ( name = "http", address = "*:8080", http = (), service = "main" ) ] +); + +# The definition of the actual helloWorld worker exposed using the "main" service. +# In this example the worker is implemented as a single simple script (see worker.js). +# The compatibilityDate is required. For more details on compatibility dates see: +# https://developers.cloudflare.com/workers/platform/compatibility-dates/ + +const helloWorld :Workerd.Worker = ( + modules = [ + (name = "worker", esModule = embed "worker.js") + ], + compatibilityDate = "2022-11-08", + compatibilityFlags = ["nodejs_18_compat_experimental"] +); diff --git a/samples/async-context/worker.js b/samples/async-context/worker.js new file mode 100644 index 00000000000..1cf577fcd7d --- /dev/null +++ b/samples/async-context/worker.js @@ -0,0 +1,23 @@ +import { default as async_hooks } from 'node:async_hooks'; +const { AsyncLocalStorage, AsyncResource } = async_hooks; + +const als = new AsyncLocalStorage(); + +export default { + async fetch(request) { + const differentScope = als.run(123, () => AsyncResource.bind(() => { + console.log(als.getStore()); + })); + + return als.run("Hello World", async () => { + + // differentScope is attached to a different async context, so + // it will see a different value for als.getStore() (123) + setTimeout(differentScope, 5); + + // Some simulated async delay. + await scheduler.wait(10); + return new Response(als.getStore()); // "Hello World" + }); + } +}; diff --git a/src/workerd/api/global-scope.c++ b/src/workerd/api/global-scope.c++ index 0733b35621d..6cb462dd4f5 100644 --- a/src/workerd/api/global-scope.c++ +++ b/src/workerd/api/global-scope.c++ @@ -7,6 +7,7 @@ #include #include +#include #include #include #include @@ -96,6 +97,24 @@ void ExecutionContext::passThroughOnException() { IoContext::current().setFailOpen(); } +ServiceWorkerGlobalScope::ServiceWorkerGlobalScope(v8::Isolate* isolate) + : unhandledRejections( + [this](jsg::Lock& js, + v8::PromiseRejectEvent event, + jsg::V8Ref promise, + jsg::Value value) { + // If async context tracking is enabled, then we need to ensure that we enter the frame + // associated with the promise before we invoke the unhandled rejection callback handling. + kj::Maybe maybeScope; + KJ_IF_MAYBE(context, jsg::AsyncContextFrame::tryGetContext(js, promise)) { + if (!context->isRoot(js)) { + maybeScope.emplace(js, jsg::AsyncContextFrame::tryGetContext(js, promise)); + } + } + auto ev = jsg::alloc(event, kj::mv(promise), kj::mv(value)); + dispatchEventImpl(js, kj::mv(ev)); + }) {} + void ServiceWorkerGlobalScope::clear() { removeAllHandlers(); unhandledRejections.clear(); @@ -478,8 +497,10 @@ v8::Local ServiceWorkerGlobalScope::atob(kj::String data, v8::Isolat return jsg::v8StrFromLatin1(isolate, decoded.asBytes()); } -void ServiceWorkerGlobalScope::queueMicrotask(v8::Local task, v8::Isolate* isolate) { - isolate->EnqueueMicrotask(task); +void ServiceWorkerGlobalScope::queueMicrotask( + jsg::Lock& js, + v8::Local task) { + js.v8Isolate->EnqueueMicrotask(jsg::AsyncContextFrame::wrap(js, task, nullptr, nullptr)); } v8::Local ServiceWorkerGlobalScope::structuredClone( @@ -508,27 +529,36 @@ TimeoutId::NumberType ServiceWorkerGlobalScope::setTimeoutInternal( } TimeoutId::NumberType ServiceWorkerGlobalScope::setTimeout( + jsg::Lock& js, jsg::V8Ref function, jsg::Optional msDelay, - jsg::Varargs args, - v8::Isolate* isolate) { + jsg::Varargs args) { + auto& context = jsg::AsyncContextFrame::current(js); + if (!context.isRoot(js)) { + // If the AsyncContextFrame is the root frame, we do not have to wrap it at all. + // This is because setInterval/setTimeout callbacks are always invoked by the + // system. If there is no AsyncContextFrame::Scope on the stack, it will always + // use the root frame. + function = js.v8Ref(jsg::AsyncContextFrame::current(js).wrap( + js, function.getHandle(js), nullptr, nullptr)); + } auto argv = kj::heapArrayFromIterable(kj::mv(args)); auto timeoutId = IoContext::current().setTimeoutImpl( - timeoutIdGenerator, - /* repeats = */ false, - [function = function.addRef(isolate), argv = kj::mv(argv)](jsg::Lock& js) mutable { - auto isolate = js.v8Isolate; - auto context = isolate->GetCurrentContext(); - auto localFunction = function.getHandle(isolate); - auto localArgs = KJ_MAP(arg, argv) { - return arg.getHandle(isolate); - }; - auto argc = localArgs.size(); + timeoutIdGenerator, + /* repeats = */ false, + [function = function.addRef(js), + argv = kj::mv(argv)] + (jsg::Lock& js) mutable { + auto context = js.v8Isolate->GetCurrentContext(); + auto localFunction = function.getHandle(js); + auto localArgs = KJ_MAP(arg, argv) { + return arg.getHandle(js); + }; + auto argc = localArgs.size(); - // Cast to void to discard the result value. - (void)jsg::check(localFunction->Call(context, context->Global(), argc, &localArgs.front())); - }, - msDelay.orDefault(0)); + // Cast to void to discard the result value. + (void)jsg::check(localFunction->Call(context, context->Global(), argc, &localArgs.front())); + }, msDelay.orDefault(0)); return timeoutId.toNumber(); } @@ -539,27 +569,36 @@ void ServiceWorkerGlobalScope::clearTimeout(kj::Maybe tim } TimeoutId::NumberType ServiceWorkerGlobalScope::setInterval( + jsg::Lock& js, jsg::V8Ref function, jsg::Optional msDelay, - jsg::Varargs args, - v8::Isolate* isolate) { + jsg::Varargs args) { + auto& context = jsg::AsyncContextFrame::current(js); + if (!context.isRoot(js)) { + // If the AsyncContextFrame is the root frame, we do not have to wrap it at all. + // This is because setInterval/setTimeout callbacks are always invoked by the + // system. If there is no AsyncContextFrame::Scope on the stack, it will always + // use the root frame. + function = js.v8Ref(jsg::AsyncContextFrame::current(js).wrap( + js, function.getHandle(js), nullptr, nullptr)); + } auto argv = kj::heapArrayFromIterable(kj::mv(args)); auto timeoutId = IoContext::current().setTimeoutImpl( - timeoutIdGenerator, - /* repeats = */ true, - [function = function.addRef(isolate), argv = kj::mv(argv)](jsg::Lock& js) mutable { - auto isolate = js.v8Isolate; - auto context = isolate->GetCurrentContext(); - auto localFunction = function.getHandle(isolate); - auto localArgs = KJ_MAP(arg, argv) { - return arg.getHandle(isolate); - }; - auto argc = localArgs.size(); + timeoutIdGenerator, + /* repeats = */ true, + [function = function.addRef(js), + argv = kj::mv(argv)] + (jsg::Lock& js) mutable { + auto context = js.v8Isolate->GetCurrentContext(); + auto localFunction = function.getHandle(js); + auto localArgs = KJ_MAP(arg, argv) { + return arg.getHandle(js); + }; + auto argc = localArgs.size(); - // Cast to void to discard the result value. - (void)jsg::check(localFunction->Call(context, context->Global(), argc, &localArgs.front())); - }, - msDelay.orDefault(0)); + // Cast to void to discard the result value. + (void)jsg::check(localFunction->Call(context, context->Global(), argc, &localArgs.front())); + }, msDelay.orDefault(0)); return timeoutId.toNumber(); } diff --git a/src/workerd/api/global-scope.h b/src/workerd/api/global-scope.h index c3fd91b315f..64576689f40 100644 --- a/src/workerd/api/global-scope.h +++ b/src/workerd/api/global-scope.h @@ -179,15 +179,7 @@ class ServiceWorkerGlobalScope: public WorkerGlobalScope { // Global object API exposed to JavaScript. public: - ServiceWorkerGlobalScope(v8::Isolate* isolate) - : unhandledRejections( - [this](jsg::Lock& js, - v8::PromiseRejectEvent event, - jsg::V8Ref promise, - jsg::Value value) { - auto ev = jsg::alloc(event, kj::mv(promise), kj::mv(value)); - dispatchEventImpl(js, kj::mv(ev)); - }) {} + ServiceWorkerGlobalScope(v8::Isolate* isolate); void clear(); // Drop all references to JavaScript objects so that the context can be garbage-collected. Call @@ -237,7 +229,7 @@ class ServiceWorkerGlobalScope: public WorkerGlobalScope { kj::String btoa(v8::Local data, v8::Isolate* isolate); v8::Local atob(kj::String data, v8::Isolate* isolate); - void queueMicrotask(v8::Local task, v8::Isolate* isolate); + void queueMicrotask(jsg::Lock& js, v8::Local task); struct StructuredCloneOptions { jsg::Optional> transfer; @@ -250,22 +242,20 @@ class ServiceWorkerGlobalScope: public WorkerGlobalScope { jsg::Optional options, v8::Isolate* isolate); - TimeoutId::NumberType setTimeout( - jsg::V8Ref function, - jsg::Optional msDelay, - jsg::Varargs args, - v8::Isolate* isolate); + TimeoutId::NumberType setTimeout(jsg::Lock& js, + jsg::V8Ref function, + jsg::Optional msDelay, + jsg::Varargs args); void clearTimeout(kj::Maybe timeoutId); TimeoutId::NumberType setTimeoutInternal( jsg::Function function, double msDelay); - TimeoutId::NumberType setInterval( - jsg::V8Ref function, - jsg::Optional msDelay, - jsg::Varargs args, - v8::Isolate* isolate); + TimeoutId::NumberType setInterval(jsg::Lock& js, + jsg::V8Ref function, + jsg::Optional msDelay, + jsg::Varargs args); void clearInterval(kj::Maybe timeoutId) { clearTimeout(timeoutId); } jsg::Promise> fetch( diff --git a/src/workerd/api/node/async-hooks.c++ b/src/workerd/api/node/async-hooks.c++ new file mode 100644 index 00000000000..0d046be64b5 --- /dev/null +++ b/src/workerd/api/node/async-hooks.c++ @@ -0,0 +1,120 @@ +// Copyright (c) 2017-2022 Cloudflare, Inc. +// Licensed under the Apache 2.0 license found in the LICENSE file or at: +// https://opensource.org/licenses/Apache-2.0 +#include "async-hooks.h" +#include + +namespace workerd::api::node { + +jsg::Ref AsyncLocalStorage::constructor(jsg::Lock& js) { + return jsg::alloc(); +} + +v8::Local AsyncLocalStorage::run( + jsg::Lock& js, + v8::Local store, + v8::Local callback, + jsg::Varargs args) { + kj::Vector> argv(args.size()); + for (auto arg : args) { + argv.add(arg.getHandle(js)); + } + + auto context = js.v8Isolate->GetCurrentContext(); + + jsg::AsyncContextFrame::StorageScope scope(js, *key, js.v8Ref(store)); + + return jsg::check(callback->Call( + context, + context->Global(), + argv.size(), + argv.begin())); +} + +v8::Local AsyncLocalStorage::exit( + jsg::Lock& js, + v8::Local callback, + jsg::Varargs args) { + // Node.js defines exit as running "a function synchronously outside of a context". + // It goes on to say that the store is not accessible within the callback or the + // asynchronous operations created within the callback. Any getStore() call done + // within the callbackfunction will always return undefined... except if run() is + // called which implicitly enables the context again within that scope. + // + // We do not have to emulate Node.js enable/disable behavior since we are not + // implementing the enterWith/disable methods. We can emulate the correct + // behavior simply by calling run with the store value set to undefined, which + // will propagate correctly. + return run(js, v8::Undefined(js.v8Isolate), callback, kj::mv(args)); +} + +v8::Local AsyncLocalStorage::getStore(jsg::Lock& js) { + KJ_IF_MAYBE(value, jsg::AsyncContextFrame::current(js).get(*key)) { + return value->getHandle(js); + } + return v8::Undefined(js.v8Isolate); +} + +AsyncResource::AsyncResource(jsg::Lock& js) + : frame(kj::addRef(jsg::AsyncContextFrame::current(js))) {} + +jsg::Ref AsyncResource::constructor( + jsg::Lock& js, + jsg::Optional type, + jsg::Optional options) { + // The type and options are required as part of the Node.js API compatibility + // but our implementation does not currently make use of them at all. It is ok + // for us to silently ignore both here. + return jsg::alloc(js); +} + +v8::Local AsyncResource::staticBind( + jsg::Lock& js, + v8::Local fn, + jsg::Optional type, + jsg::Optional> thisArg, + const jsg::TypeHandler>& handler) { + return AsyncResource::constructor(js, kj::mv(type) + .orDefault([] { return kj::str("AsyncResource"); })) + ->bind(js, fn, thisArg, handler); +} + +v8::Local AsyncResource::bind( + jsg::Lock& js, + v8::Local fn, + jsg::Optional> thisArg, + const jsg::TypeHandler>& handler) { + auto& frame = jsg::AsyncContextFrame::current(js); + v8::Local bound = jsg::AsyncContextFrame::wrap(js, fn, frame, thisArg); + + // Per Node.js documentation (https://nodejs.org/dist/latest-v19.x/docs/api/async_context.html#asyncresourcebindfn-thisarg), the returned function "will have an + // asyncResource property referencing the AsyncResource to which the function + // is bound". + jsg::check(bound->Set(js.v8Isolate->GetCurrentContext(), + jsg::v8StrIntern(js.v8Isolate, "asyncResource"_kj), + handler.wrap(js, JSG_THIS))); + return bound; +} + +v8::Local AsyncResource::runInAsyncScope( + jsg::Lock& js, + v8::Local fn, + jsg::Optional> thisArg, + jsg::Varargs args) { + kj::Vector> argv(args.size()); + for (auto arg : args) { + argv.add(arg.getHandle(js)); + } + + auto context = js.v8Isolate->GetCurrentContext(); + + jsg::AsyncContextFrame::Scope scope(js, *frame); + + return jsg::check(fn->Call( + context, + thisArg.orDefault(context->Global()), + argv.size(), + argv.begin())); +} + +} // namespace workerd::api::node diff --git a/src/workerd/api/node/async-hooks.h b/src/workerd/api/node/async-hooks.h new file mode 100644 index 00000000000..808766d0313 --- /dev/null +++ b/src/workerd/api/node/async-hooks.h @@ -0,0 +1,226 @@ +// Copyright (c) 2017-2022 Cloudflare, Inc. +// Licensed under the Apache 2.0 license found in the LICENSE file or at: +// https://opensource.org/licenses/Apache-2.0 +#pragma once + +#include +#include +#include +#include + +namespace workerd::api::node { + +class AsyncLocalStorage final: public jsg::Object { + // Implements a subset of the Node.js AsyncLocalStorage API. + // + // Example: + // + // import * as async_hooks from 'node:async_hooks'; + // const als = new async_hooks.AsyncLocalStorage(); + // + // async function doSomethingAsync() { + // await scheduler.wait(100); + // console.log(als.getStore()); // 1 + // } + // + // als.run(1, async () => { + // console.log(als.getStore()); // 1 + // await doSomethingAsync(); + // console.log(als.getStore()); // 1 + // }); + // console.log(als.getStore()); // undefined +public: + AsyncLocalStorage() : key(kj::refcounted()) {} + ~AsyncLocalStorage() noexcept(false) { key->reset(); } + + static jsg::Ref constructor(jsg::Lock& js); + + v8::Local run(jsg::Lock& js, + v8::Local store, + v8::Local callback, + jsg::Varargs args); + + v8::Local exit(jsg::Lock& js, + v8::Local callback, + jsg::Varargs args); + + v8::Local getStore(jsg::Lock& js); + + inline void enterWith(jsg::Lock&, v8::Local) { + KJ_UNIMPLEMENTED("asyncLocalStorage.enterWith() is not implemented"); + } + + inline void disable(jsg::Lock&) { + KJ_UNIMPLEMENTED("asyncLocalStorage.disable() is not implemented"); + } + + JSG_RESOURCE_TYPE(AsyncLocalStorage, CompatibilityFlags::Reader flags) { + JSG_METHOD(run); + JSG_METHOD(exit); + JSG_METHOD(getStore); + JSG_METHOD(enterWith); + JSG_METHOD(disable); + + if (flags.getNodeJsCompat()) { + JSG_TS_OVERRIDE(AsyncLocalStorage { + getStore(): T | undefined; + run(store: T, callback: (...args: TArgs) => R, ...args: TArgs): R; + exit(callback: (...args: TArgs) => R, ...args: TArgs): R; + disable(): void; + enterWith(store: T): void; + }); + } else { + JSG_TS_OVERRIDE(type AsyncLocalStorage = never); + } + } + +private: + kj::Own key; +}; + + +class AsyncResource final: public jsg::Object { + // The AsyncResource class is an object that user code can use to define its own + // async resources for the purpose of storage context propagation. For instance, + // let's imagine that we have an EventTarget and we want to register two event listeners + // on it that will share the same AsyncLocalStorage context. We can use AsyncResource + // to easily define the context and bind multiple event handler functions to it: + // + // const als = new AsyncLocalStorage(); + // const context = als.run(123, () => new AsyncResource('foo')); + // const target = new EventTarget(); + // target.addEventListener('abc', context.bind(() => console.log(als.getStore()))); + // target.addEventListener('xyz', context.bind(() => console.log(als.getStore()))); + // target.addEventListener('bar', () => console.log(als.getStore())); + // + // When the 'abc' and 'xyz' events are emitted, their event handlers will print 123 + // to the console. When the 'bar' event is emitted, undefined will be printed. + // + // Alternatively, we can use EventTarget's object event handler: + // + // const als = new AsyncLocalStorage(); + // + // class MyHandler extends AsyncResource { + // constructor() { super('foo'); } + // void handleEvent() { + // this.runInAsyncScope(() => console.log(als.getStore())); + // } + // } + // + // const handler = als.run(123, () => new MyHandler()); + // const target = new EventTarget(); + // target.addEventListener('abc', handler); + // target.addEventListener('xyz', handler); +public: + struct Options { + jsg::WontImplement triggerAsyncId; + // Node.js' API allows user code to create AsyncResource instances within an + // explicitly specified parent execution context (what we call an "Async Context + // Frame") that is specified by a numeric ID. We do not track our context frames + // by ID and always create new AsyncResource instances within the current Async + // Context Frame. To prevent subtle bugs, we'll throw explicitly if user code + // tries to set the triggerAsyncId option. + + // Node.js also has an additional `requireManualDestroy` boolean option + // that we do not implement. We can simply omit it here. There's no risk of + // bugs or unexpected behavior by doing so. + + JSG_STRUCT_TS_OVERRIDE(type AsyncResourceOptions = never); + JSG_STRUCT(triggerAsyncId); + }; + + AsyncResource(jsg::Lock& js); + + static jsg::Ref constructor(jsg::Lock& js, jsg::Optional type, + jsg::Optional options = nullptr); + // While Node.js' API expects the first argument passed to the `new AsyncResource(...)` + // constructor to be a string specifying the resource type, we do not actually use it + // for anything. We'll just ignore the value and not store it, but we at least need to + // accept the argument and validate that it is a string. + + inline jsg::Unimplemented asyncId() { return {}; } + inline jsg::Unimplemented triggerAsyncId() { return {}; } + // The Node.js API uses numeric identifiers for all async resources. We do not + // implement that part of their API. To prevent subtle bugs, we'll throw explicitly. + + static v8::Local staticBind( + jsg::Lock& js, + v8::Local fn, + jsg::Optional type, + jsg::Optional> thisArg, + const jsg::TypeHandler>& handler); + + v8::Local bind( + jsg::Lock& js, + v8::Local fn, + jsg::Optional> thisArg, + const jsg::TypeHandler>& handler); + // Binds the given function to this async context. + + v8::Local runInAsyncScope( + jsg::Lock& js, + v8::Local fn, + jsg::Optional> thisArg, + jsg::Varargs args); + // Calls the given function within this async context. + + JSG_RESOURCE_TYPE(AsyncResource, CompatibilityFlags::Reader flags) { + JSG_STATIC_METHOD_NAMED(bind, staticBind); + JSG_METHOD(asyncId); + JSG_METHOD(triggerAsyncId); + JSG_METHOD(bind); + JSG_METHOD(runInAsyncScope); + + if (flags.getNodeJsCompat()) { + JSG_TS_OVERRIDE(interface AsyncResourceOptions { + triggerAsyncId?: number; + }); + + JSG_TS_OVERRIDE(AsyncResource { + constructor(type: string, options?: AsyncResourceOptions); + static bind any, ThisArg>( + fn: Func, + type?: string, + thisArg?: ThisArg): Func & { asyncResource: AsyncResource; }; + bind any>( + fn: Func ): Func & { asyncResource: AsyncResource; }; + runInAsyncScope(fn: (this: This, ...args: any[]) => Result, thisArg?: This, + ...args: any[]): Result; + asyncId(): number; + triggerAsyncId(): number; + }); + } else { + JSG_TS_OVERRIDE(type AsyncResource = never); + } + } + +private: + kj::Own frame; +}; + +class AsyncHooksModule final: public jsg::Object { + // We have no intention of fully-implementing the Node.js async_hooks module. + // We provide this because AsyncLocalStorage is exposed via async_hooks in + // Node.js. +public: + + JSG_RESOURCE_TYPE(AsyncHooksModule, CompatibilityFlags::Reader flags) { + JSG_NESTED_TYPE(AsyncLocalStorage); + JSG_NESTED_TYPE(AsyncResource); + + if (flags.getNodeJsCompat()) { + JSG_TS_ROOT(); + JSG_TS_OVERRIDE(AsyncHooksModule {}); + } else { + JSG_TS_OVERRIDE(type AsyncHooksModule = never); + } + } +}; + +#define EW_NODE_ASYNCHOOKS_ISOLATE_TYPES \ + api::node::AsyncHooksModule, \ + api::node::AsyncResource, \ + api::node::AsyncResource::Options, \ + api::node::AsyncLocalStorage + +} // namespace workerd::api::node diff --git a/src/workerd/api/node/node.h b/src/workerd/api/node/node.h new file mode 100644 index 00000000000..c0f1ba36cb2 --- /dev/null +++ b/src/workerd/api/node/node.h @@ -0,0 +1,18 @@ +#pragma once + +#include "async-hooks.h" +#include + +namespace workerd::api::node { + +template +void registerNodeJsCompatModules( + workerd::jsg::ModuleRegistryImpl& registry, auto featureFlags) { + registry.template addBuiltinModule("node:async_hooks", + workerd::jsg::ModuleRegistry::Type::BUILTIN); +} + +#define EW_NODE_ISOLATE_TYPES \ + EW_NODE_ASYNCHOOKS_ISOLATE_TYPES + +} // namespace workerd::api::node diff --git a/src/workerd/jsg/README.md b/src/workerd/jsg/README.md index 454d2e28e57..f39a6b49887 100644 --- a/src/workerd/jsg/README.md +++ b/src/workerd/jsg/README.md @@ -1477,3 +1477,73 @@ type. This macro accepts a single define parameter containing one or more TypeSc can contain `,` outside of balanced brackets. This macro can only be used once per `JSG_RESOURCE_TYPE` block/`JSG_STRUCT` definition. The `declare` modifier will be added to any `class`, `enum`, `const`, `var` or `function` definitions if it's not already present. + +## Async Context Tracking + +JSG provides a mechanism for rudimentary async context tracking to support the implementation +of async local storage. This is provided by the `workerd::jsg::AsyncContextFrame` class defined in +`src/workerd/jsg/async-context.h`. + +`AsyncContextFrame`s form a stack. For every `v8::Isolate` there is always a root frame. +"Entering" a frame means pushing it to the top of the stack, making it "current". Exiting +a frame means popping it back off the top of the stack. + +Every `AsyncContextFrame` has a storage context. The storage context is a map of multiple +individual storage cells, each tied to an opaque key. + +When a new `AsyncContextFrame` is created, the storage context of the current is propagated to the +new resource's storage context. + +All JavaScript promises, timers, and microtasks propagate the async context. In JavaScript, +we have implemented the `AsyncLocalStorage` and `AsyncResource` APIs for this purpose. +For example: + +```js +import { default as async_hooks } from 'node:async_hooks'; +const { AsyncLocalStorage } = async_hooks; + +const als = new AsyncLocalStorage(); + +als.run(123, () => scheduler.wait(10)).then(() => { + console.log(als.getStore()); // 123 +}); +console.log(als.getStore()); // undefined +``` + +Any type can act as an async resource by acquiring a reference to the current Async +Context Frame. + +```cpp +jsg::Lock& js = ...; +kj::Own frame = kj::addRef(jsg::AsyncResource::current(js)); + +// enter the async resource scope: +{ + jsg::AsyncContextFrame::Scope asyncScope(js, *frame); + // run some code synchronously... +} +// The async scope will exit automatically... +``` + +The `jsg::AsyncResource::StorageScope` can be used to temporarily set the stored value +associated with a given storage key in the current AsyncResource's storage context: + +```js +class MyStorageKey: public jsg::StorageKey { + // ... +} + +jsg::Lock& js = ... +jsg::Value value = ... + +// The storage key must be kj::Refcounted. References to the key will be retained +// by any AsyncResources that are created within the storage scope. +kj::Own key = kj::refcounted(); + +{ + jsg::AsyncResource::StorageScope(js, *key, value); + // run some code synchronously +} +// The storage cell will be automatically reset to the previous value +// with the scope exits. +``` diff --git a/src/workerd/jsg/async-context.c++ b/src/workerd/jsg/async-context.c++ new file mode 100644 index 00000000000..9bb073f9fbb --- /dev/null +++ b/src/workerd/jsg/async-context.c++ @@ -0,0 +1,324 @@ +// Copyright (c) 2017-2022 Cloudflare, Inc. +// Licensed under the Apache 2.0 license found in the LICENSE file or at: +// https://opensource.org/licenses/Apache-2.0 +#include "async-context.h" +#include "jsg.h" +#include "setup.h" +#include + +namespace workerd::jsg { + +namespace { +kj::Maybe tryGetContextFrame( + v8::Isolate* isolate, + v8::Local handle) { + // Gets the held AsyncContextFrame from the opaque wrappable but does not consume it. + KJ_IF_MAYBE(wrappable, Wrappable::tryUnwrapOpaque(isolate, handle)) { + AsyncContextFrame* frame = dynamic_cast(wrappable); + KJ_ASSERT(frame != nullptr); + return *frame; + } + return nullptr; +} + +} // namespace + +AsyncContextFrame::AsyncContextFrame(IsolateBase& isolate) + : isolate(isolate) {} + +AsyncContextFrame::AsyncContextFrame( + Lock& js, + kj::Maybe maybeParent, + kj::Maybe maybeStorageEntry) + : AsyncContextFrame(IsolateBase::from(js.v8Isolate)) { + // Lazily enables the hooks for async context tracking. + isolate.setAsyncContextTrackingEnabled(); + + // Propagate the storage context of the parent frame to this newly created frame. + const auto propagate = [&](AsyncContextFrame& parent) { + parent.storage.eraseAll([](const auto& entry) { return entry.key->isDead(); }); + for (auto& entry : parent.storage) { + storage.insert(entry.clone(js)); + } + + KJ_IF_MAYBE(entry, maybeStorageEntry) { + storage.upsert(kj::mv(*entry), [](StorageEntry& existing, StorageEntry&& row) mutable { + existing.value = kj::mv(row.value); + }); + } + }; + + KJ_IF_MAYBE(parent, maybeParent) { + propagate(*parent); + } else { + propagate(current(js)); + } +} + +kj::Maybe AsyncContextFrame::tryGetContext( + Lock& js, + v8::Local promise) { + auto handle = js.getPrivateSymbolFor(Lock::PrivateSymbols::ASYNC_CONTEXT); + // We do not use the normal unwrapOpaque here since that would consume the wrapped + // value, and we need to be able to unwrap multiple times. + return tryGetContextFrame(js.v8Isolate, + check(promise->GetPrivate(js.v8Isolate->GetCurrentContext(), handle))); +} + +kj::Maybe AsyncContextFrame::tryGetContext( + Lock& js, + V8Ref& promise) { + return tryGetContext(js, promise.getHandle(js)); +} + +AsyncContextFrame& AsyncContextFrame::current(Lock& js) { + auto& isolateBase = IsolateBase::from(js.v8Isolate); + if (isolateBase.asyncFrameStack.size() == 0) { + return *isolateBase.getRootAsyncContext(); + } + return *isolateBase.asyncFrameStack.back(); +} + +Ref AsyncContextFrame::create( + Lock& js, + kj::Maybe maybeParent, + kj::Maybe maybeStorageEntry) { + return alloc(js, maybeParent, kj::mv(maybeStorageEntry)); +} + +v8::Local AsyncContextFrame::wrap( + Lock& js, + v8::Local fn, + kj::Maybe maybeFrame, + kj::Maybe> thisArg) { + auto isolate = js.v8Isolate; + auto context = isolate->GetCurrentContext(); + return js.wrapReturningFunction(context, JSG_VISITABLE_LAMBDA( + ( + frame = Ref(kj::addRef(AsyncContextFrame::current(js))), + thisArg = js.v8Ref(thisArg.orDefault(context->Global())), + fn = js.v8Ref(fn) + ), + (frame, thisArg, fn), + (Lock& js, const v8::FunctionCallbackInfo& args) { + auto function = fn.getHandle(js); + auto context = js.v8Isolate->GetCurrentContext(); + + kj::Vector> argv(args.Length()); + for (int n = 0; n < args.Length(); n++) { + argv.add(args[n]); + } + + AsyncContextFrame::Scope scope(js, *frame); + v8::Local result; + return check(function->Call(context, thisArg.getHandle(js), args.Length(), argv.begin())); + })); +} + +void AsyncContextFrame::attachContext( + Lock& js, + v8::Local promise, + kj::Maybe maybeFrame) { + auto handle = js.getPrivateSymbolFor(Lock::PrivateSymbols::ASYNC_CONTEXT); + auto context = js.v8Isolate->GetCurrentContext(); + + KJ_DASSERT(!check(promise->HasPrivate(context, handle))); + + // Otherwise, we have to create an opaque wrapper holding a ref to the current frame + // because we do not have the option of using an internal field with promises. + auto frame = kj::addRef(AsyncContextFrame::current(js)); + KJ_ASSERT(check(promise->SetPrivate(context, handle, frame->getJSWrapper(js)))); +} + +kj::Maybe AsyncContextFrame::get(StorageKey& key) { + KJ_ASSERT(!key.isDead()); + storage.eraseAll([](const auto& entry) { return entry.key->isDead(); }); + return storage.find(key).map([](auto& entry) -> Value& { return entry.value; }); +} + +AsyncContextFrame::Scope::Scope(Lock& js, kj::Maybe resource) + : Scope(js.v8Isolate, resource) {} + +AsyncContextFrame::Scope::Scope(v8::Isolate* isolate, kj::Maybe frame) + : isolate(IsolateBase::from(isolate)) { + KJ_IF_MAYBE(f, frame) { + this->isolate.pushAsyncFrame(*f); + } else { + this->isolate.pushAsyncFrame(*this->isolate.getRootAsyncContext()); + } +} + +AsyncContextFrame::Scope::~Scope() noexcept(false) { + isolate.popAsyncFrame(); +} + +AsyncContextFrame::StorageScope::StorageScope( + Lock& js, + StorageKey& key, + Value store) + : frame(AsyncContextFrame::create(js, nullptr, StorageEntry { + .key = kj::addRef(key), + .value = kj::mv(store) + })), + scope(js, *frame) {} + +bool AsyncContextFrame::isRoot(Lock& js) const { + return IsolateBase::from(js.v8Isolate).getRootAsyncContext() == this; +} + +v8::Local AsyncContextFrame::getJSWrapper(Lock& js) { + KJ_IF_MAYBE(handle, tryGetHandle(js.v8Isolate)) { + return *handle; + } + return attachOpaqueWrapper(js.v8Isolate->GetCurrentContext(), true); +} + +void AsyncContextFrame::jsgVisitForGc(GcVisitor& visitor) { + for (auto& entry : storage) { + visitor.visit(entry.value); + } +} + +void IsolateBase::pushAsyncFrame(AsyncContextFrame& next) { + asyncFrameStack.add(&next); +} + +void IsolateBase::popAsyncFrame() { + KJ_DASSERT(asyncFrameStack.size() > 0, "the async context frame stack was corrupted"); + asyncFrameStack.removeLast(); +} + +void IsolateBase::setAsyncContextTrackingEnabled() { + // Enabling async context tracking installs a relatively expensive callback on the v8 isolate + // that attaches additional metadata to every promise created. The additional metadata is used + // to implement support for the Node.js AsyncLocalStorage API. Since that is the only current + // use for it, we only install the promise hook when that api is used. + if (asyncContextTrackingEnabled) return; + asyncContextTrackingEnabled = true; + ptr->SetPromiseHook(&promiseHook); +} + +AsyncContextFrame* IsolateBase::getRootAsyncContext() { + KJ_IF_MAYBE(frame, rootAsyncFrame) { + return frame->get(); + } + // We initialize this lazily instead of in the IsolateBase constructor + // because AsyncContextFrame is a Wrappable and rootAsyncFrame is a Ref. + // Calling alloc during IsolateBase construction is not allowed because + // Ref's constructor requires the Isolate lock to be held already. + KJ_ASSERT(asyncFrameStack.size() == 0); + rootAsyncFrame = alloc(*this); + return KJ_ASSERT_NONNULL(rootAsyncFrame).get(); +} + +void IsolateBase::promiseHook(v8::PromiseHookType type, + v8::Local promise, + v8::Local parent) { + auto isolate = promise->GetIsolate(); + + // V8 will call the promise hook even while execution is terminating. In that + // case we don't want to do anything here. + if (isolate->IsExecutionTerminating() || isolate->IsDead()) { + return; + } + + // This is a fairly expensive method. It is invoked at least once, and a most + // four times for every JavaScript promise that is created within an isolate. + // Accordingly, the hook is only installed when the AsyncLocalStorage API is + // used. + + auto& js = Lock::from(isolate); + auto& isolateBase = IsolateBase::from(isolate); + auto& currentFrame = AsyncContextFrame::current(js); + + const auto isRejected = [&] { return promise->State() == v8::Promise::PromiseState::kRejected; }; + + // TODO(later): The try/catch block here echoes the semantics of LiftKj. + // We don't use LiftKj here because that currently requires a FunctionCallbackInfo, + // which we don't have (or want here). If we end up needing this pattern elsewhere, + // we can implement a variant of LiftKj that does so and switch this over to use it. + try { + switch (type) { + case v8::PromiseHookType::kInit: { + // The kInit event is triggered by v8 when a deferred Promise is created. This + // includes all calls to `new Promise(...)`, `then()`, `catch()`, `finally()`, + // uses of `await ...`, `Promise.all()`, etc. + // Whenever a Promise is created, we associate it with the current AsyncContextFrame. + // As a performance optimization, we only attach the context if the current is not + // the root. + if (!currentFrame.isRoot(js)) { + KJ_DASSERT(AsyncContextFrame::tryGetContext(js, promise) == nullptr); + AsyncContextFrame::attachContext(js, promise); + } + break; + } + case v8::PromiseHookType::kBefore: { + // The kBefore event is triggered immediately before a Promise continuation. + // We use it here to enter the AsyncContextFrame that was associated with the + // promise when it was created. + KJ_IF_MAYBE(frame, AsyncContextFrame::tryGetContext(js, promise)) { + isolateBase.pushAsyncFrame(*frame); + } else { + // If the promise does not have a frame attached, we assume the root + // frame is used. Just to keep bookkeeping easier, we still go ahead + // and push the frame onto the stack again so we can just unconditionally + // pop it off in the kAfter without performing additional checks. + isolateBase.pushAsyncFrame(*isolateBase.getRootAsyncContext()); + } + // We do not use AsyncContextFrame::Scope here because we do not exit the frame + // until the kAfter event fires. + break; + } + case v8::PromiseHookType::kAfter: { + #ifdef KJ_DEBUG + KJ_IF_MAYBE(frame, AsyncContextFrame::tryGetContext(js, promise)) { + // The frame associated with the promise must be the current frame. + KJ_ASSERT(frame == ¤tFrame); + } else { + KJ_ASSERT(currentFrame.isRoot(js)); + } + #endif + isolateBase.popAsyncFrame(); + + // If the promise has been rejected here, we have to maintain the association of the + // async context to the promise so that the context can be propagated to the unhandled + // rejection handler. However, if the promise has been fulfilled, we do not expect + // the context to be used any longer so we can break the context association here and + // allow the opaque wrapper to be garbage collected. + if (!isRejected()) { + auto handle = js.getPrivateSymbolFor(Lock::PrivateSymbols::ASYNC_CONTEXT); + check(promise->DeletePrivate(js.v8Isolate->GetCurrentContext(), handle)); + } + + break; + } + case v8::PromiseHookType::kResolve: { + // This case is a bit different. As an optimization, it appears that v8 will skip + // the kInit, kBefore, and kAfter events for Promises that are immediately resolved (e.g. + // Promise.resolve, and Promise.reject) and instead will emit the kResolve event first. + // When this event occurs, and the promise is rejected, we need to check to see if the + // promise is already wrapped, and if it is not, do so. + if (!currentFrame.isRoot(js) && isRejected() && + AsyncContextFrame::tryGetContext(js, promise) == nullptr) { + AsyncContextFrame::attachContext(js, promise); + } + break; + } + } + } catch (JsExceptionThrown&) { + // Catching JsExceptionThrown implies that an exception is already scheduled on the isolate + // so we don't need to throw it again, just allow it to bubble up and out. + } catch (std::exception& ex) { + // This case is purely defensive and is included really just to align with the + // semantics in LiftKj. We'd be using LiftKj here already if that didn't require + // use of a FunctionCallbackInfo. + throwInternalError(isolate, ex.what()); + } catch (kj::Exception& ex) { + throwInternalError(isolate, kj::mv(ex)); + } catch (...) { + throwInternalError(isolate, kj::str("caught unknown exception of type: ", + kj::getCaughtExceptionType())); + } +} + +} // namespace workerd::jsg diff --git a/src/workerd/jsg/async-context.h b/src/workerd/jsg/async-context.h new file mode 100644 index 00000000000..d8afdffe025 --- /dev/null +++ b/src/workerd/jsg/async-context.h @@ -0,0 +1,193 @@ +// Copyright (c) 2017-2022 Cloudflare, Inc. +// Licensed under the Apache 2.0 license found in the LICENSE file or at: +// https://opensource.org/licenses/Apache-2.0 +#pragma once + +#include "jsg.h" +#include + +namespace workerd::jsg { + +class AsyncContextFrame: public Wrappable { + // Provides for basic internal async context tracking. Eventually, it is expected that + // this will be provided by V8 assuming that the AsyncContext proposal advances through + // TC-39. For now, however, we implement a model that is similar but not quite identical + // to that implemented by Node.js. + // + // At any point in time when JavaScript is running, there is a current "Async Context Frame", + // within which any number of "async resources" can be created. The term "resource" here + // comes from Node.js (which really doesn't take the time to define it properly). Conceptually, + // an "async resource" is some Thing that generates asynchronous activity over time (either + // once or repeatedly). For instance, a timer is an async resource that invokes a callback + // after a certain period of time elapses; a promise is an async resource that may trigger + // scheduling of a microtask at some point in the future, and so forth. Whether or not + // "resource" is the best term to use to describe these, it's what we have because our + // intent here is to stay aligned with Node.js' model as closely as possible. + // + // Every async resource maintains a reference to the Async Context Frame that was current + // at the moment the resource is created. + // + // Frames form a stack. The default frame is the Root. We "enter" a frame by pushing it + // onto to top of the stack (making it "current"), then perform some action within that + // frame, then "exit" by popping it back off the stack. The Root is associated with the + // Isolate itself such that every isolate always has at least one frame on the stack at + // all times. In Node.js terms, the "Async Context Frame" would be most closely aligned + // with the concept of an "execution context" or "execution scope". + // + // Every Frame has a storage context. The current frame determines the currently active + // storage context. So, for instance, when we start executing, the Root Frame's storage + // context is active. When a timeout elapses and a timer is going to fire, we enter the + // timer's Frame which makes that frame's storage context active. Once the timer + // callback has completed, we return back to the Root frame and storage context. + // + // All frames (except for the Root) are created within the scope of a parent, which by + // default is whichever frame is current when the new frame is created. When the new frame + // is created, it inherits a copy storage context of the parent. + // + // AsyncContextFrame's are Wrappables because for certain kinds of async resources + // like promises or JS functions, we have to be able to store a reference to the frame + // using an opaque private property rather than an internal field or lambda capture. + // In such cases, we attach an opaque JS wrapper to the frame and use that opaque + // wrapper to hold the frame reference. +public: + class StorageKey: public kj::Refcounted { + // An opaque key that identifies an async-local storage cell within the frame. + public: + StorageKey() : hash(kj::hashCode(this)) {} + + void reset() { dead = true; } + // The owner of the key should reset it when it goes away. + // The StorageKey is typically owned by an instance of AsyncLocalstorage (see + // the api/node/async-hooks.h). When the ALS instance is garbage collected, it + // must call reset to signal that this StorageKey is "dead" and can never be + // looked up again. Subsequent accesses to a frame will remove dead keys from + // the frame lazily. The lazy cleanup does mean that values may persist in + // memory a bit longer so if it proves to be problematic we can make the cleanup + // a bit more proactive. + // + // TODO(later): We should also evaluate the relatively unlikely case where an + // ALS is capturing a reference to itself and therefore can never be cleaned up. + + bool isDead() const { return dead; } + inline uint hashCode() const { return hash; } + inline bool operator==(const StorageKey& other) const { + return hash == other.hash; + } + + private: + uint hash; + bool dead = false; + }; + + struct StorageEntry { + kj::Own key; + Value value; + + inline StorageEntry clone(Lock& js) { + return { + .key = addRef(*key), + .value = value.addRef(js) + }; + } + }; + + AsyncContextFrame(IsolateBase& isolate); + AsyncContextFrame( + Lock& js, + kj::Maybe maybeParent = nullptr, + kj::Maybe maybeStorageEntry = nullptr); + + static AsyncContextFrame& current(Lock& js); + // Returns the reference to the AsyncContextFrame currently at the top of the stack. + + static Ref create( + Lock& js, + kj::Maybe maybeParent = nullptr, + kj::Maybe maybeStorageEntry = nullptr); + // Create a new AsyncContextFrame. If maybeParent is not specified, uses the current(). + // If maybeStorageEntry is non-null, the associated storage cell in the new frame is + // set to the given value. + + static v8::Local wrap( + Lock& js, v8::Local fn, + kj::Maybe maybeFrame, + kj::Maybe> thisArg); + // Associates the given JavaScript function with the given AsyncContextFrame, returning + // a wrapper function that will ensure appropriate propagation of the async context + // when the wrapper function is called. If maybeFrame is not specified, the current() + // frame is used. + + static void attachContext(Lock& js, v8::Local promise, + kj::Maybe maybeFrame = nullptr); + // Associates the given JavaScript promise with the given AsyncContextFrame, returning + // the same promise back. If maybeFrame is not specified, the current() frame is used. + + static kj::Maybe tryGetContext(Lock& js, V8Ref& promise); + static kj::Maybe tryGetContext(Lock& js, v8::Local promise); + // Returns a reference to the AsyncContextFrame that was current when the JS Promise + // was created. When async context tracking is enabled, this should always return a + // non-null value. + + struct Scope { + // AsyncContextFrame::Scope makes the given AsyncContextFrame the current in the + // stack until the scope is destroyed. + IsolateBase& isolate; + Scope(Lock& js, kj::Maybe frame = nullptr); + Scope(v8::Isolate* isolate, kj::Maybe frame = nullptr); + // If frame is nullptr, the root frame is assumed. + ~Scope() noexcept(false); + KJ_DISALLOW_COPY(Scope); + }; + + kj::Maybe get(StorageKey& key); + // Retrieves the value that is associated with the given key. + + bool isRoot(Lock& js) const; + // True only if this AsyncContextFrame is the root frame for the given isolate. + + v8::Local getJSWrapper(Lock& js); + // Gets an opaque JavaScript Object wrapper object for this frame. If a wrapper + // does not currently exist, one is created. This wrapper is only used to set a + // private reference to the frame on JS objects like promises and functions. + // See the attachContext and wrap functions for details. + + struct StorageScope { + // Creates a new AsyncContextFrame with a new value for the given + // StorageKey and sets that frame as current for as long as the StorageScope + // is alive. + Ref frame; + Scope scope; + // Note that the scope here holds a bare ref to the AsyncContextFrame so it + // is important that these member fields stay in the correct cleanup order. + + StorageScope(Lock& js, StorageKey& key, Value store); + KJ_DISALLOW_COPY(StorageScope); + }; + +private: + struct StorageEntryCallbacks { + StorageKey& keyForRow(StorageEntry& entry) const { + return *entry.key; + } + + bool matches(const StorageEntry& entry, StorageKey& key) const { + return entry.key->hashCode() == key.hashCode(); + } + + uint hashCode(StorageKey& key) const { + return key.hashCode(); + } + }; + + using Storage = kj::Table>; + Storage storage; + + IsolateBase& isolate; + + void jsgVisitForGc(GcVisitor& visitor) override; + + friend struct StorageScope; + friend class IsolateBase; +}; + +} // namespace workerd::jsg diff --git a/src/workerd/jsg/jsg.c++ b/src/workerd/jsg/jsg.c++ index 5aa341b150b..67dca9e19e4 100644 --- a/src/workerd/jsg/jsg.c++ +++ b/src/workerd/jsg/jsg.c++ @@ -173,6 +173,10 @@ void Lock::requestGcForTesting() const { v8::Isolate::GarbageCollectionType::kFullGarbageCollection); } +v8::Local Lock::getPrivateSymbolFor(Lock::PrivateSymbols symbol) { + return IsolateBase::from(v8Isolate).getPrivateSymbolFor(symbol); +} + Name Lock::newSymbol(kj::StringPtr symbol) { return Name(*this, v8::Symbol::New(v8Isolate, v8StrIntern(v8Isolate, symbol))); } diff --git a/src/workerd/jsg/jsg.h b/src/workerd/jsg/jsg.h index bca15691196..263fa622942 100644 --- a/src/workerd/jsg/jsg.h +++ b/src/workerd/jsg/jsg.h @@ -1787,6 +1787,12 @@ template class Isolate; // Defined in setup.h -- most code doesn't need to use these directly. +class AsyncContextFrame; + +#define JSG_PRIVATE_SYMBOLS(V) \ + V(ASYNC_CONTEXT, "asyncContext") +// Defines the enum values for Lock::PrivateSymbols. + class Lock { // Represents an isolate lock, which allows the current thread to execute JavaScript code within // an isolate. A thread must lock an isolate -- obtaining an instance of `Lock` -- before it can @@ -1975,6 +1981,15 @@ class Lock { virtual v8::Local wrapBytes(kj::Array data) = 0; virtual v8::Local wrapSimpleFunction(v8::Local context, jsg::Function& info)> simpleFunction) = 0; + virtual v8::Local wrapReturningFunction(v8::Local context, + jsg::Function(const v8::FunctionCallbackInfo& info)> returningFunction) = 0; + // A variation on wrapSimpleFunction that allows for a return value. While the wrapSimpleFunction + // implementation passes the FunctionCallbackInfo into the called function, any call to + // GetReturnValue().Set(...) to specify a return value will be ignored by the FunctorCallback + // wrapper. The wrapReturningFunction variation forces the wrapper to use the version that + // pays attention to the return value. + // TODO(later): See if we can easily combine wrapSimpleFunction and wrapReturningFunction + // into one. bool toBool(v8::Local value); virtual kj::String toString(v8::Local value) = 0; @@ -2002,6 +2017,18 @@ class Lock { // it will throw. If a need for a minor GC is needed look at the call in jsg.c++ and the // implementation in setup.c++. Use responsibly. +#define V(name, _) name, + enum PrivateSymbols { + JSG_PRIVATE_SYMBOLS(V) + SYMBOL_COUNT, + // The SYMBOL_COUNT is a special token used to size the array for storing the + // symbol instances. It must always be the last item in the enum. To add private + // symbols, add values to the JSG_PRIVATE_SYMBOLS define. + }; +#undef V + + v8::Local getPrivateSymbolFor(PrivateSymbols symbol); + private: friend class IsolateBase; template diff --git a/src/workerd/jsg/setup.c++ b/src/workerd/jsg/setup.c++ index cab5739929c..099a2535a15 100644 --- a/src/workerd/jsg/setup.c++ +++ b/src/workerd/jsg/setup.c++ @@ -8,6 +8,7 @@ #endif #include "setup.h" +#include "async-context.h" #include #include "libplatform/libplatform.h" #include @@ -352,6 +353,11 @@ void IsolateBase::dropWrappers(kj::Own typeWrapperInstance) { // Destroy all wrappers. heapTracer.clearWrappers(); + + // Clear any cached private symbols here while we are in the isolate lock. + for (auto& maybeSymbol : privateSymbols) { + maybeSymbol = nullptr; + } } void IsolateBase::fatalError(const char* location, const char* message) { @@ -502,6 +508,30 @@ void IsolateBase::jitCodeEvent(const v8::JitCodeEvent* event) noexcept { } } +v8::Local IsolateBase::getPrivateSymbolFor(Lock::PrivateSymbols symbol) { + KJ_ASSERT(symbol != Lock::PrivateSymbols::SYMBOL_COUNT); + int pos = static_cast(symbol); + // If the private symbol has already been retrieved before, it will be memoized in + // the privateSymbols array. Just grab the reference and return it. + KJ_IF_MAYBE(i, privateSymbols[pos]) { + return i->getHandle(ptr); + } + // Otherwise, we have to ask v8 for the symbol. The list of symbols available is + // defined by the JSG_PRIVATE_SYMBOLS define in jsg.h. + v8::Local handle; + switch (symbol) { +#define V(name, val) \ + case Lock::PrivateSymbols::name: \ + handle = v8::Private::ForApi(ptr, v8StrIntern(ptr, #val)); break; + JSG_PRIVATE_SYMBOLS(V) +#undef V + default: + KJ_UNREACHABLE; + } + privateSymbols[pos] = V8Ref(ptr, handle); + return handle; +} + kj::Maybe getJsStackTrace(void* ucontext, kj::ArrayPtr scratch) { if (!v8Initialized) { return nullptr; diff --git a/src/workerd/jsg/setup.h b/src/workerd/jsg/setup.h index 4713b64cc65..dd016866e23 100644 --- a/src/workerd/jsg/setup.h +++ b/src/workerd/jsg/setup.h @@ -6,10 +6,12 @@ // Public API for setting up JavaScript context. Only high-level code needs to include this file. #include "jsg.h" +#include "async-context.h" #include "type-wrapper.h" #include #include #include +#include namespace workerd::jsg { @@ -99,6 +101,11 @@ class IsolateBase { KJ_IF_MAYBE(logger, maybeLogger) { (*logger)(js, message); } } + void setAsyncContextTrackingEnabled(); + AsyncContextFrame* getRootAsyncContext(); + + v8::Local getPrivateSymbolFor(Lock::PrivateSymbols symbol); + private: template friend class Isolate; @@ -137,6 +144,7 @@ class IsolateBase { // and there are a number of async APIs that currently throw. When the captureThrowsAsRejections // flag is set, that old behavior is changed to be correct. bool exportCommonJsDefault = false; + bool asyncContextTrackingEnabled = false; kj::Maybe> maybeLogger; @@ -144,6 +152,9 @@ class IsolateBase { // FunctionTemplate used by Wrappable::attachOpaqueWrapper(). Just a constructor for an empty // object with 2 internal fields. + // Keep the size here in sync w + kj::Maybe> privateSymbols[Lock::PrivateSymbols::SYMBOL_COUNT]; + static constexpr auto DESTRUCTION_QUEUE_INITIAL_SIZE = 8; // We expect queues to remain relatively small -- 8 is the largest size I have observed from local // testing. @@ -224,6 +235,25 @@ class IsolateBase { // use `->InstanceTemplate()->NewInstance()` to construct an object, and you can pass this to // `FindInstanceInPrototypeChain()` on an existing object to check whether it was created using // this template. + + static void promiseHook(v8::PromiseHookType type, + v8::Local promise, + v8::Local parent); + void pushAsyncFrame(AsyncContextFrame& next); + // Pushes the frame onto the stack making it current. Importantly, the stack + // does not maintain a refcounted reference to the frame so it is important + // for the caller to ensure that the frame is kept alive. + void popAsyncFrame(); + + kj::Vector asyncFrameStack; + kj::Maybe> rootAsyncFrame; + // The rootAsyncFrame is a maybe because it is lazily initialized. + // We cannot create Ref's within the IsolateBase constructor and + // we don't want this to be a bare kj::Own because it might have + // a JS wrapper associated with it. Calling getRootAsyncContext + // for the first time will lazily create the root frame. + + friend class AsyncContextFrame; }; kj::Maybe getJsStackTrace(void* ucontext, kj::ArrayPtr scratch); @@ -346,6 +376,11 @@ class Isolate: public IsolateBase { simpleFunction) override { return jsgIsolate.wrapper->wrap(context, nullptr, kj::mv(simpleFunction)); } + v8::Local wrapReturningFunction(v8::Local context, + jsg::Function(const v8::FunctionCallbackInfo& info)> + returningFunction) override { + return jsgIsolate.wrapper->wrap(context, nullptr, kj::mv(returningFunction)); + } kj::String toString(v8::Local value) override { return jsgIsolate.wrapper->template unwrap( v8Isolate->GetCurrentContext(), value, jsg::TypeErrorContext::other()); diff --git a/src/workerd/server/workerd-api.c++ b/src/workerd/server/workerd-api.c++ index 7fff6770871..98bc08ac95c 100644 --- a/src/workerd/server/workerd-api.c++ +++ b/src/workerd/server/workerd-api.c++ @@ -18,6 +18,7 @@ #include #include #include +#include #include #include #include @@ -63,6 +64,7 @@ JSG_DECLARE_ISOLATE_TYPE(JsgWorkerdIsolate, EW_URL_STANDARD_ISOLATE_TYPES, EW_URLPATTERN_ISOLATE_TYPES, EW_WEBSOCKET_ISOLATE_TYPES, + EW_NODE_ISOLATE_TYPES, jsg::TypeWrapperExtension, jsg::InjectConfiguration, @@ -312,6 +314,10 @@ kj::Own WorkerdApiIsolate::compileModules( } } + if (getFeatureFlags().getNodeJsCompat()) { + api::node::registerNodeJsCompatModules(*modules, getFeatureFlags()); + } + jsg::setModulesForResolveCallback(lock, modules); return modules; diff --git a/src/workerd/tools/api-encoder.c++ b/src/workerd/tools/api-encoder.c++ index 19d648006e4..e29ec307293 100644 --- a/src/workerd/tools/api-encoder.c++ +++ b/src/workerd/tools/api-encoder.c++ @@ -15,6 +15,7 @@ #include #include #include +#include #include #include @@ -42,7 +43,8 @@ F("url-standard", EW_URL_STANDARD_ISOLATE_TYPES) \ F("url-pattern", EW_URLPATTERN_ISOLATE_TYPES) \ F("websocket", EW_WEBSOCKET_ISOLATE_TYPES) \ - F("sockets", EW_SOCKETS_ISOLATE_TYPES) + F("sockets", EW_SOCKETS_ISOLATE_TYPES) \ + F("node", EW_NODE_ISOLATE_TYPES) namespace workerd::api { namespace {