File
Blob: src/workerd/jsg/async-context.h
| 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 | |
| 10 | namespace 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. |
| 68 | class 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 |