Skip to content
File

Blob: src/workerd/jsg/async-context.h

cpp242 lines
1// Copyright (c) 2017-2022 Cloudflare, Inc.
2// Licensed under the Apache 2.0 license found in the LICENSE file or at:
3// https://opensource.org/licenses/Apache-2.0
4#pragma once
5 
6#include "jsg.h"
7 
8#include <v8.h>
9 
10namespace workerd::jsg {
11 
12#ifndef V8_ENABLE_CONTINUATION_PRESERVED_EMBEDDER_DATA
13#error "V8_ENABLE_CONTINUATION_PRESERVED_EMBEDDER_DATA must be defined"
14#endif
15 
16// Provides for basic internal async context tracking. Eventually, it is expected that
17// this will be provided by V8 assuming that the AsyncContext proposal advances through
18// TC-39. For now, however, we implement a model that is similar but not quite identical
19// to that implemented by Node.js.
20//
21// At any point in time when JavaScript is running, there is a current "Async Context Frame",
22// within which any number of "async resources" can be created. The term "resource" here
23// comes from Node.js (which really doesn't take the time to define it properly). Conceptually,
24// an "async resource" is some Thing that generates asynchronous activity over time (either
25// once or repeatedly). For instance, a timer is an async resource that invokes a callback
26// after a certain period of time elapses; a promise is an async resource that may trigger
27// scheduling of a microtask at some point in the future, and so forth. Whether or not
28// "resource" is the best term to use to describe these, it's what we have because our
29// intent here is to stay aligned with Node.js' model as closely as possible.
30//
31// Every async resource maintains a reference to the Async Context Frame that was current
32// at the moment the resource is created.
33//
34// Frames form a logical stack. The default frame is the Root. We "enter" a frame by pushing
35// it onto to top of the stack (making it "current"), then perform some action within that
36// frame, then "exit" by popping it back off the stack. The Root is associated with the
37// Isolate itself such that every isolate always has at least one frame logically on the stack
38// at all times. In Node.js terms, the "Async Context Frame" would be most closely aligned
39// with the concept of an "execution context" or "execution scope".
40//
41// Every Frame has a storage context. The current frame determines the currently active
42// storage context. So, for instance, when we start executing, the Root Frame's storage
43// context is active. When a timeout elapses and a timer is going to fire, we enter the
44// timer's Frame which makes that frame's storage context active. Once the timer
45// callback has completed, we return back to the Root frame and storage context.
46//
47// All frames (except for the Root) are created within the scope of a parent, which by
48// default is whichever frame is current when the new frame is created. When the new frame
49// is created, it inherits a copy storage context of the parent.
50//
51// To implement all of this, however, we depend largely on an obscure v8 API on the
52// v8::Context object called SetContinuationPreservedEmbedderData and
53// GetContinuationPreservedEmbedderData. An AsyncContextFrame is a Wrappable because
54// because instances of AsyncContextFrame are set as the continuation-preserved embedder
55// data and that API requires a JS value.
56//
57// AsyncContextFrame::current() returns the current frame or nullptr. Returning nullptr
58// implies that we are in the "root" frame.
59//
60// AsyncContextFrame::StorageScope is created on stack to create a new frame and set
61// a stored value in the storage context before entering it.
62//
63// AsyncContextFrame::Scope is created on the stack to temporarily enter an existing
64// frame.
65//
66// AsyncContextFrame::StorageKey is used to define a storage cell within the storage
67// context.
68class AsyncContextFrame final: public Wrappable {
69 public:
70 // An opaque key that identifies an async-local storage cell within the frame.
71 class StorageKey: public kj::Refcounted {
72 public:
73 StorageKey(): hash(kj::hashCode(this)) {}
74 KJ_DISALLOW_COPY_AND_MOVE(StorageKey);
75 
76 // The owner of the key should reset it when it goes away.
77 // The StorageKey is typically owned by an instance of AsyncLocalStorage (see
78 // the api/node/async-hooks.h). When the ALS instance is garbage collected, it
79 // must call reset to signal that this StorageKey is "dead" and can never be
80 // looked up again. Subsequent accesses to a frame will remove dead keys from
81 // the frame lazily. The lazy cleanup does mean that values may persist in
82 // memory a bit longer so if it proves to be problematic we can make the cleanup
83 // a bit more proactive.
84 void reset() {
85 dead = true;
86 }
87 // TODO(later): We should also evaluate the relatively unlikely case where an
88 // ALS is capturing a reference to itself and therefore can never be cleaned up.
89 
90 bool isDead() const {
91 return dead;
92 }
93 inline uint hashCode() const {
94 return hash;
95 }
96 inline bool operator==(const StorageKey& other) const {
97 return this == &other;
98 }
99 
100 JSG_MEMORY_INFO(StorageKey) {}
101 
102 private:
103 uint hash;
104 bool dead = false;
105 };
106 
107 struct StorageEntry {
108 kj::Own<StorageKey> key;
109 Value value;
110 StorageEntry(kj::Own<StorageKey> key, Value value);
111 StorageEntry clone(Lock& js);
112 
113 JSG_MEMORY_INFO(StorageEntry) {
114 tracker.trackField("key", key);
115 tracker.trackField("value", value);
116 }
117 };
118 
119 AsyncContextFrame(Lock& js, StorageEntry storageEntry);
120 
121 inline Ref<AsyncContextFrame> addRef() {
122 return JSG_THIS;
123 }
124 
125 // Returns the reference to the AsyncContextFrame currently at the top of the stack, if any.
126 static kj::Maybe<AsyncContextFrame&> current(Lock& js);
127 
128 // Returns the reference to the AsyncContextFrame currently at the top of the stack, if any.
129 static kj::Maybe<AsyncContextFrame&> current(v8::Isolate* isolate);
130 
131 // Convenience variation on current() that returns the result wrapped in a Ref for when we
132 // need to make sure the frame stays alive.
133 static kj::Maybe<Ref<AsyncContextFrame>> currentRef(Lock& js);
134 
135 // Create a new AsyncContextFrame. The new frame inherits the storage context of the current
136 // frame (if any) and the given StorageEntry is added.
137 static Ref<AsyncContextFrame> create(Lock& js, StorageEntry storageEntry);
138 
139 // Wraps the given JavaScript function such that whenever the wrapper function is called,
140 // the root AsyncContextFrame will be entered.
141 static v8::Local<v8::Function> wrapRoot(
142 Lock& js, v8::Local<v8::Function> fn, kj::Maybe<v8::Local<v8::Value>> thisArg = kj::none);
143 
144 // Returns a function that captures the current frame and calls the function passed
145 // in as an argument within that captured context. Equivalent to wrapping a function
146 // with the signature (cb, ...args) => cb(...args).
147 // The validate function is called to ensure that the current frame is still valid.
148 // If the validate function throws, the wrapper will throw.
149 static v8::Local<v8::Function> wrapSnapshot(Lock& js, jsg::Function<void()> validate);
150 
151 // Associates the given JavaScript function with this AsyncContextFrame, returning
152 // a wrapper function that will ensure appropriate propagation of the async context
153 // when the wrapper function is called.
154 v8::Local<v8::Function> wrap(Lock& js,
155 V8Ref<v8::Function>& fn,
156 jsg::Function<void()> validate,
157 kj::Maybe<v8::Local<v8::Value>> thisArg = kj::none);
158 
159 // Associates the given JavaScript function with this AsyncContextFrame, returning
160 // a wrapper function that will ensure appropriate propagation of the async context
161 // when the wrapper function is called.
162 v8::Local<v8::Function> wrap(Lock& js,
163 v8::Local<v8::Function> fn,
164 jsg::Function<void()> validate,
165 kj::Maybe<v8::Local<v8::Value>> thisArg = kj::none);
166 
167 // AsyncContextFrame::Scope makes the given AsyncContextFrame the current in the
168 // stack until the scope is destroyed.
169 struct Scope {
170 v8::Isolate* isolate;
171 kj::Maybe<AsyncContextFrame&> prior;
172 // If frame is nullptr, the root frame is assumed.
173 Scope(Lock& js, kj::Maybe<AsyncContextFrame&> frame = kj::none);
174 // If frame is nullptr, the root frame is assumed.
175 Scope(v8::Isolate* isolate, kj::Maybe<AsyncContextFrame&> frame = kj::none);
176 // If frame is nullptr, the root frame is assumed.
177 Scope(Lock& js, kj::Maybe<Ref<AsyncContextFrame>>& frame);
178 ~Scope() noexcept(false);
179 KJ_DISALLOW_COPY(Scope);
180 };
181 
182 // Retrieves the value that is associated with the given key.
183 kj::Maybe<Value&> get(StorageKey& key);
184 
185 // Gets an opaque JavaScript Object wrapper object for this frame. If a wrapper
186 // does not currently exist, one is created.
187 v8::Local<v8::Object> getJSWrapper(v8::Isolate* isolate);
188 
189 // Gets an opaque JavaScript Object wrapper object for this frame. If a wrapper
190 // does not currently exist, one is created.
191 v8::Local<v8::Object> getJSWrapper(Lock& js);
192 
193 // Creates a new AsyncContextFrame with a new value for the given
194 // StorageKey and sets that frame as current for as long as the StorageScope
195 // is alive.
196 struct StorageScope {
197 Ref<AsyncContextFrame> frame;
198 // Note that the scope here holds a bare ref to the AsyncContextFrame so it
199 // is important that these member fields stay in the correct cleanup order.
200 Scope scope;
201 
202 StorageScope(Lock& js, StorageKey& key, Value store);
203 KJ_DISALLOW_COPY(StorageScope);
204 };
205 
206 kj::StringPtr jsgGetMemoryName() const override {
207 return "AsyncContextFrame"_kjc;
208 }
209 size_t jsgGetMemorySelfSize() const override {
210 return sizeof(AsyncContextFrame);
211 }
212 void jsgGetMemoryInfo(MemoryTracker& tracker) const override {
213 Wrappable::jsgGetMemoryInfo(tracker);
214 tracker.trackField("storage", storage);
215 }
216 
217 private:
218 struct StorageEntryCallbacks {
219 StorageKey& keyForRow(StorageEntry& entry) const {
220 return *entry.key;
221 }
222 
223 bool matches(const StorageEntry& entry, StorageKey& key) const {
224 return entry.key.get() == &key;
225 }
226 
227 uint hashCode(StorageKey& key) const {
228 return key.hashCode();
229 }
230 };
231 
232 using Storage = kj::Table<StorageEntry, kj::HashIndex<StorageEntryCallbacks>>;
233 Storage storage;
234 
235 void jsgVisitForGc(GcVisitor& visitor) override;
236 
237 friend struct StorageScope;
238 friend class IsolateBase;
239};
240 
241} // namespace workerd::jsg