@mcp-b/do-runtime 0.2.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/dist/chunks/io-context-Ci3Rf6U5.js +1459 -0
- package/dist/chunks/io-context-Ci3Rf6U5.js.map +1 -0
- package/dist/gate.js +105 -0
- package/dist/gate.js.map +1 -0
- package/dist/index.js +109 -1442
- package/dist/index.js.map +1 -1
- package/dist/src/api/http.d.ts +1 -0
- package/dist/src/fixtures/await-transform.actor.d.ts +1 -0
- package/dist/src/gate.d.ts +4 -0
- package/dist/src/io/io-context.d.ts +6 -0
- package/dist/src/vite.d.ts +7 -0
- package/dist/vite.js +55 -0
- package/dist/vite.js.map +1 -0
- package/package.json +19 -3
|
@@ -0,0 +1,1459 @@
|
|
|
1
|
+
//#region src/io/io-gate.ts
|
|
2
|
+
/**
|
|
3
|
+
* ← workerd `src/workerd/io/io-gate.{h,c++}`
|
|
4
|
+
*
|
|
5
|
+
* An I/O gate allows someone to "lock" a type of I/O so that other concurrent tasks trying to
|
|
6
|
+
* perform that type of I/O are blocked until the lock is released.
|
|
7
|
+
*
|
|
8
|
+
* I/O gates are used in actors to implement consistency guarantees, allowing in-memory state and
|
|
9
|
+
* storage to be synchronized.
|
|
10
|
+
*
|
|
11
|
+
* Each Actor has two main gates:
|
|
12
|
+
* - Input gate: While locked, blocks all incoming I/O events of any type from being delivered to
|
|
13
|
+
* the actor, other than the specific event or events that hold the lock. This includes
|
|
14
|
+
* blocking responses to subrequests, timer events, input streams, etc. Used when storage
|
|
15
|
+
* operations are outstanding, so that awaiting a storage operation does not risk allowing
|
|
16
|
+
* concurrent events that render the state inconsistent.
|
|
17
|
+
* - Output gate: While locked, blocks all outgoing messages from an actor that would allow the
|
|
18
|
+
* rest of the world to observe the actor's state. Held while writes that have been confirmed
|
|
19
|
+
* to the application are still being flushed to disk. If the flush fails, these messages will
|
|
20
|
+
* never be sent, so that the rest of the world cannot observe a prematurely-confirmed write.
|
|
21
|
+
*
|
|
22
|
+
* Three things kj gives for free and JS does not, resolved the same way at every site:
|
|
23
|
+
*
|
|
24
|
+
* 1. **Destructors.** `~Lock` releases and `~CriticalSection` diagnoses a dropped section as
|
|
25
|
+
* deadlock. Both become explicit: `Lock.release()` and `CriticalSection.drop()`. A `Lock`
|
|
26
|
+
* released twice throws rather than corrupting the refcount.
|
|
27
|
+
* 2. **Cancel-by-drop.** Dropping a `kj::Promise` unwinds its waiter. Every such site takes an
|
|
28
|
+
* `AbortSignal`, the convention `Timer.afterDelay` already set in `io-context.ts`. Aborting a
|
|
29
|
+
* `wait()` rejects it with `CanceledError`; a never-settling promise would be an invisible
|
|
30
|
+
* hang, which is what this repo's fail-closed tenet exists to prevent.
|
|
31
|
+
* 3. **`kj::ForkedPromise` holding an exception with no branches.** JS reports that as an
|
|
32
|
+
* unhandled rejection, so every promise this module stores keeps a no-op `catch` of its own
|
|
33
|
+
* and hands observers a separate view.
|
|
34
|
+
*
|
|
35
|
+
* Error strings are copied verbatim from upstream; users and upstream tests match on them.
|
|
36
|
+
*
|
|
37
|
+
* Not ported: `SpanParent`/`SpanBuilder` tracing, since there is no `trace.h` here and upstream's
|
|
38
|
+
* own tests pass `nullptr` at every call site; and the `~InputGate` assertion that no locks
|
|
39
|
+
* outlive the gate, which guards against dangling references GC makes impossible.
|
|
40
|
+
*
|
|
41
|
+
* Spec: §1.1, §1.2, §1.5, decisions 3, 5 and 13 in
|
|
42
|
+
* docs/decisions.md.
|
|
43
|
+
*/
|
|
44
|
+
/**
|
|
45
|
+
* Raised when a `wait()` is cancelled through its `AbortSignal`.
|
|
46
|
+
*
|
|
47
|
+
* kj has no equivalent, because a cancelled continuation simply never runs. It is deliberately
|
|
48
|
+
* NOT a gate failure: a cancelled waiter leaves the gate exactly as it found it, so
|
|
49
|
+
* `CriticalSection.wait()` rethrows this one without calling `setBroken()`.
|
|
50
|
+
*/
|
|
51
|
+
var CanceledError = class extends Error {
|
|
52
|
+
name = "CanceledError";
|
|
53
|
+
};
|
|
54
|
+
/** Mirrors `OutputGate::makeUnfulfilledException()`: one place that spells the exception. */
|
|
55
|
+
function makeCanceledError() {
|
|
56
|
+
return new CanceledError("input gate wait was canceled");
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* `addEventListener("abort", ...)` never fires for a signal that has already aborted, so a
|
|
60
|
+
* pre-aborted signal would silently hold its lock forever. Every cancellation site goes through
|
|
61
|
+
* here, and every one of them first rejects a pre-aborted wait before touching gate state, so
|
|
62
|
+
* "cancelled" always means "left the gate exactly as it found it".
|
|
63
|
+
*/
|
|
64
|
+
function onAbort(signal, run) {
|
|
65
|
+
if (signal === void 0) return;
|
|
66
|
+
if (signal.aborted) {
|
|
67
|
+
run();
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
signal.addEventListener("abort", run, { once: true });
|
|
71
|
+
}
|
|
72
|
+
/** ← `InputGate::Hooks::DEFAULT`. */
|
|
73
|
+
var DEFAULT_INPUT_GATE_HOOKS = {
|
|
74
|
+
inputGateLocked() {},
|
|
75
|
+
inputGateReleased() {},
|
|
76
|
+
inputGateWaiterAdded() {},
|
|
77
|
+
inputGateWaiterRemoved() {}
|
|
78
|
+
};
|
|
79
|
+
/** ← `InputGate::Waiter`: a `kj::List` node plus the adapted promise's fulfiller. */
|
|
80
|
+
var Waiter = class {
|
|
81
|
+
/** Rewritten by `CriticalSection.succeeded()` when a straggler is reparented. */
|
|
82
|
+
gate;
|
|
83
|
+
isChildWaiter;
|
|
84
|
+
/** ← `link.isLinked()`. */
|
|
85
|
+
linked = true;
|
|
86
|
+
#resolve;
|
|
87
|
+
#reject;
|
|
88
|
+
constructor(gate, isChildWaiter, resolve, reject) {
|
|
89
|
+
this.gate = gate;
|
|
90
|
+
this.isChildWaiter = isChildWaiter;
|
|
91
|
+
this.#resolve = resolve;
|
|
92
|
+
this.#reject = reject;
|
|
93
|
+
gate.hooks.inputGateWaiterAdded();
|
|
94
|
+
if (isChildWaiter) gate.waitingChildren.push(this);
|
|
95
|
+
else gate.waiters.push(this);
|
|
96
|
+
}
|
|
97
|
+
unlink() {
|
|
98
|
+
if (!this.linked) return;
|
|
99
|
+
this.linked = false;
|
|
100
|
+
const list = this.isChildWaiter ? this.gate.waitingChildren : this.gate.waiters;
|
|
101
|
+
const index = list.indexOf(this);
|
|
102
|
+
if (index < 0) throw new Error("InputGate::Waiter is linked but absent from its gate's list");
|
|
103
|
+
list.splice(index, 1);
|
|
104
|
+
}
|
|
105
|
+
fulfill(lock) {
|
|
106
|
+
this.unlink();
|
|
107
|
+
this.gate.hooks.inputGateWaiterRemoved();
|
|
108
|
+
this.#resolve(lock);
|
|
109
|
+
}
|
|
110
|
+
reject(exception) {
|
|
111
|
+
this.unlink();
|
|
112
|
+
this.gate.hooks.inputGateWaiterRemoved();
|
|
113
|
+
this.#reject(exception);
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
/**
|
|
117
|
+
* An InputGate blocks incoming events from being delivered to an actor while the lock is held.
|
|
118
|
+
*
|
|
119
|
+
* Upstream marks the state below `private` and befriends `Lock` and `CriticalSection`.
|
|
120
|
+
* TypeScript has no friendship, and `protected` would not let `CriticalSection` reach these
|
|
121
|
+
* members on its *parent* gate, which `succeeded()` does. The boundary that actually holds is
|
|
122
|
+
* the package facade in `src/index.ts`, which exports none of these types.
|
|
123
|
+
*/
|
|
124
|
+
var InputGate = class {
|
|
125
|
+
hooks;
|
|
126
|
+
/**
|
|
127
|
+
* How many instances of `Lock` currently exist? When this reaches zero, we'll release some
|
|
128
|
+
* waiters.
|
|
129
|
+
*
|
|
130
|
+
* Upstream also carries a `bool isCriticalSection`, because `CriticalSection` inherits
|
|
131
|
+
* `InputGate` privately and has to `static_cast` back. `instanceof` is the same test with no
|
|
132
|
+
* cast and no field that can disagree with the object it describes.
|
|
133
|
+
*/
|
|
134
|
+
lockCount = 0;
|
|
135
|
+
waiters = [];
|
|
136
|
+
/**
|
|
137
|
+
* Waiters representing CriticalSections that are ready to start. These take priority over other
|
|
138
|
+
* waiters.
|
|
139
|
+
*/
|
|
140
|
+
waitingChildren = [];
|
|
141
|
+
/** A fulfiller for onBroken(), or an exception if already broken. */
|
|
142
|
+
brokenState;
|
|
143
|
+
#brokenPromise;
|
|
144
|
+
constructor(hooks = DEFAULT_INPUT_GATE_HOOKS) {
|
|
145
|
+
this.hooks = hooks;
|
|
146
|
+
const { promise, reject } = Promise.withResolvers();
|
|
147
|
+
this.#brokenPromise = promise;
|
|
148
|
+
this.brokenState = {
|
|
149
|
+
kind: "fulfiller",
|
|
150
|
+
reject
|
|
151
|
+
};
|
|
152
|
+
promise.catch(() => {});
|
|
153
|
+
}
|
|
154
|
+
/** Wait until there are no `Lock`s, then create a new one and return it. */
|
|
155
|
+
wait(signal) {
|
|
156
|
+
if (signal?.aborted === true) return Promise.reject(makeCanceledError());
|
|
157
|
+
else if (this.brokenState.kind === "exception") return Promise.reject(this.brokenState.exception);
|
|
158
|
+
else if (this.lockCount === 0) return Promise.resolve(new Lock(this));
|
|
159
|
+
else return this.newWaiterPromise(false, signal);
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Rejects if and when calls to `wait()` become broken due to a failed critical section. The
|
|
163
|
+
* actor should be shut down in this case. This promise never resolves, only rejects.
|
|
164
|
+
*/
|
|
165
|
+
onBroken() {
|
|
166
|
+
if (this.brokenState.kind === "exception") return Promise.reject(this.brokenState.exception);
|
|
167
|
+
else return this.#brokenPromise;
|
|
168
|
+
}
|
|
169
|
+
/** ← `kj::newAdaptedPromise<Lock, Waiter>(gate, isChildWaiter, span)`. */
|
|
170
|
+
newWaiterPromise(isChildWaiter, signal) {
|
|
171
|
+
const { promise, resolve, reject } = Promise.withResolvers();
|
|
172
|
+
const waiter = new Waiter(this, isChildWaiter, resolve, reject);
|
|
173
|
+
onAbort(signal, () => {
|
|
174
|
+
if (!waiter.linked) return;
|
|
175
|
+
waiter.reject(makeCanceledError());
|
|
176
|
+
});
|
|
177
|
+
return promise;
|
|
178
|
+
}
|
|
179
|
+
releaseLock() {
|
|
180
|
+
if (this instanceof CriticalSection && this.state === "REPARENTED") {
|
|
181
|
+
if (this.waitingChildren.length !== 0 || this.waiters.length !== 0 || this.lockCount !== 0) throw new Error("releasing a lock on a reparented CriticalSection that still holds state");
|
|
182
|
+
this.parentAsInputGate().releaseLock();
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
if (this.lockCount === 0) throw new Error("InputGate::releaseLock() with no locks outstanding");
|
|
186
|
+
this.lockCount--;
|
|
187
|
+
if (this.lockCount === 0) {
|
|
188
|
+
this.hooks.inputGateReleased();
|
|
189
|
+
const child = this.waitingChildren[0];
|
|
190
|
+
if (child !== void 0) child.fulfill(new Lock(this));
|
|
191
|
+
else {
|
|
192
|
+
const waiter = this.waiters[0];
|
|
193
|
+
if (waiter !== void 0) waiter.fulfill(new Lock(this));
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
/** Called when a critical section fails. All future waiters will throw this exception. */
|
|
198
|
+
setBroken(exception) {
|
|
199
|
+
for (const waiter of [...this.waitingChildren]) waiter.reject(exception);
|
|
200
|
+
for (const waiter of [...this.waiters]) waiter.reject(exception);
|
|
201
|
+
if (this.brokenState.kind === "fulfiller") this.brokenState.reject(exception);
|
|
202
|
+
this.brokenState = {
|
|
203
|
+
kind: "exception",
|
|
204
|
+
exception
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
/** ← `InputGate::Lock`. A lock that blocks all new events from being delivered while it exists. */
|
|
209
|
+
var Lock = class Lock {
|
|
210
|
+
/** ← "Becomes null on move." Here it becomes undefined on `release()`. */
|
|
211
|
+
#gate;
|
|
212
|
+
constructor(gate) {
|
|
213
|
+
this.#gate = gate;
|
|
214
|
+
let gateToLock = gate;
|
|
215
|
+
if (gate instanceof CriticalSection && gate.state === "REPARENTED") gateToLock = gate.parentAsInputGate();
|
|
216
|
+
if (++gateToLock.lockCount === 1) gateToLock.hooks.inputGateLocked();
|
|
217
|
+
}
|
|
218
|
+
/** ← `~Lock`. */
|
|
219
|
+
release() {
|
|
220
|
+
const gate = this.#gate;
|
|
221
|
+
if (gate === void 0) throw new Error("InputGate::Lock was released twice");
|
|
222
|
+
this.#gate = void 0;
|
|
223
|
+
gate.releaseLock();
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Increments the lock's refcount, returning a duplicate `Lock`. All `Lock`s must be released
|
|
227
|
+
* before the gate is unlocked.
|
|
228
|
+
*/
|
|
229
|
+
addRef() {
|
|
230
|
+
return new Lock(this.#requireGate());
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Start a new critical section from this lock. After `wait()` has been called on the returned
|
|
234
|
+
* critical section for the first time, no further Locks will be handed out by
|
|
235
|
+
* InputGate::wait() until the CriticalSection has been dropped.
|
|
236
|
+
*
|
|
237
|
+
* CriticalSections can be nested. If this Lock is itself part of a CriticalSection, the new
|
|
238
|
+
* CriticalSection will be nested within it and the outer CriticalSection's wait() won't
|
|
239
|
+
* produce a Lock again until the inner CriticalSection is dropped.
|
|
240
|
+
*/
|
|
241
|
+
startCriticalSection() {
|
|
242
|
+
return new CriticalSection(this.#requireGate());
|
|
243
|
+
}
|
|
244
|
+
/** If this lock was taken in a CriticalSection, return it. */
|
|
245
|
+
getCriticalSection() {
|
|
246
|
+
const gate = this.#requireGate();
|
|
247
|
+
return gate instanceof CriticalSection ? gate : void 0;
|
|
248
|
+
}
|
|
249
|
+
isFor(otherGate) {
|
|
250
|
+
if (otherGate instanceof CriticalSection) throw new Error("InputGate::Lock::isFor() takes the root gate, not a CriticalSection");
|
|
251
|
+
let ptr = this.#requireGate();
|
|
252
|
+
while (ptr instanceof CriticalSection) ptr = ptr.parentAsInputGate();
|
|
253
|
+
return ptr === otherGate;
|
|
254
|
+
}
|
|
255
|
+
/** ← `operator==`. */
|
|
256
|
+
equals(other) {
|
|
257
|
+
return this.#requireGate() === other.#requireGate();
|
|
258
|
+
}
|
|
259
|
+
#requireGate() {
|
|
260
|
+
const gate = this.#gate;
|
|
261
|
+
if (gate === void 0) throw new Error("InputGate::Lock was used after release");
|
|
262
|
+
return gate;
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
/**
|
|
266
|
+
* A CriticalSection is a procedure that must not be interrupted by anything "external".
|
|
267
|
+
* While a CriticalSection is running, all events that were not initiated by the
|
|
268
|
+
* CriticalSection itself will be blocked from being delivered.
|
|
269
|
+
*
|
|
270
|
+
* The difference between a Lock and a CriticalSection is that a critical section may succeed
|
|
271
|
+
* or fail. A failed critical section permanently breaks the input gate. Locks, on the other
|
|
272
|
+
* hand, are simply released when dropped.
|
|
273
|
+
*
|
|
274
|
+
* A CriticalSection itself holds a Lock, which blocks the "parent scope" from continuing
|
|
275
|
+
* execution until the critical section is done. Meanwhile, the code running inside the critical
|
|
276
|
+
* section obtains nested Locks. These nested locks control concurrency of the operations
|
|
277
|
+
* initiated within the critical section in the same way that input locks normally do at the
|
|
278
|
+
* top-level scope. E.g., if a critical section initiates a storage read and a fetch() at the
|
|
279
|
+
* same time, the fetch() is prevented from returning until after the storage read has returned.
|
|
280
|
+
*/
|
|
281
|
+
var CriticalSection = class CriticalSection extends InputGate {
|
|
282
|
+
state = "NOT_STARTED";
|
|
283
|
+
/**
|
|
284
|
+
* Points to the parent scope, which may be another CriticalSection in the case of nesting.
|
|
285
|
+
* ← `kj::OneOf<InputGate*, kj::Own<CriticalSection>>`; the two arms are one `instanceof` apart.
|
|
286
|
+
*/
|
|
287
|
+
#parent;
|
|
288
|
+
/**
|
|
289
|
+
* A lock in the parent scope. `parentLock` becomes non-null after the first lock is obtained,
|
|
290
|
+
* and becomes null again when succeeded() is called.
|
|
291
|
+
*/
|
|
292
|
+
#parentLock;
|
|
293
|
+
constructor(parent) {
|
|
294
|
+
super();
|
|
295
|
+
this.#parent = parent;
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Wait for a nested lock in order to continue this CriticalSection.
|
|
299
|
+
*
|
|
300
|
+
* The first call to wait() begins the CriticalSection. After that wait completes, until the
|
|
301
|
+
* CriticalSection is done and dropped, no other locks will be allowed on this InputGate, except
|
|
302
|
+
* locks requested by calling wait() on this CriticalSection -- or one of its children.
|
|
303
|
+
*
|
|
304
|
+
* Everything before the first `await` runs in the caller's synchronous slice, which is what
|
|
305
|
+
* lets the NOT_STARTED path take its parent lock before any other event can queue behind it.
|
|
306
|
+
*/
|
|
307
|
+
async wait(signal) {
|
|
308
|
+
if (signal?.aborted === true) throw makeCanceledError();
|
|
309
|
+
for (;;) switch (this.state) {
|
|
310
|
+
case "NOT_STARTED": {
|
|
311
|
+
this.state = "INITIAL_WAIT";
|
|
312
|
+
const target = this.parentAsInputGate();
|
|
313
|
+
if (target.brokenState.kind === "exception") {
|
|
314
|
+
const exception = target.brokenState.exception;
|
|
315
|
+
this.setBroken(exception);
|
|
316
|
+
throw exception;
|
|
317
|
+
}
|
|
318
|
+
if (target.lockCount === 0) {
|
|
319
|
+
this.state = "RUNNING";
|
|
320
|
+
this.#parentLock = new Lock(target);
|
|
321
|
+
continue;
|
|
322
|
+
} else {
|
|
323
|
+
let lock;
|
|
324
|
+
try {
|
|
325
|
+
lock = await target.newWaiterPromise(true, signal);
|
|
326
|
+
} catch (exception) {
|
|
327
|
+
if (exception instanceof CanceledError) throw exception;
|
|
328
|
+
this.state = "RUNNING";
|
|
329
|
+
this.setBroken(exception);
|
|
330
|
+
throw exception;
|
|
331
|
+
}
|
|
332
|
+
this.state = "RUNNING";
|
|
333
|
+
this.#parentLock = lock;
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
case "INITIAL_WAIT": throw new Error("CriticalSection::wait() should be called once initially");
|
|
338
|
+
case "RUNNING": return await super.wait(signal);
|
|
339
|
+
case "REPARENTED": return await this.#parent.wait(signal);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Call when the critical section has completed successfully. If this is not called before the
|
|
344
|
+
* CriticalSection is dropped, then failed() is called implicitly.
|
|
345
|
+
*
|
|
346
|
+
* Returns the input lock that was held on the parent critical section. This can be used to
|
|
347
|
+
* continue execution in the parent before any other input arrives.
|
|
348
|
+
*/
|
|
349
|
+
succeeded() {
|
|
350
|
+
if (this.state !== "RUNNING") throw new Error("CriticalSection::succeeded() requires a running critical section");
|
|
351
|
+
const parentGate = this.parentAsInputGate();
|
|
352
|
+
for (const waiter of this.waitingChildren) waiter.gate = parentGate;
|
|
353
|
+
parentGate.waitingChildren.push(...this.waitingChildren);
|
|
354
|
+
this.waitingChildren.length = 0;
|
|
355
|
+
for (const waiter of this.waiters) waiter.gate = parentGate;
|
|
356
|
+
parentGate.waiters.push(...this.waiters);
|
|
357
|
+
this.waiters.length = 0;
|
|
358
|
+
parentGate.lockCount += this.lockCount;
|
|
359
|
+
this.lockCount = 0;
|
|
360
|
+
this.state = "REPARENTED";
|
|
361
|
+
const result = this.#parentLock;
|
|
362
|
+
if (result === void 0) throw new Error("CriticalSection::succeeded() with no parent lock");
|
|
363
|
+
this.#parentLock = void 0;
|
|
364
|
+
return result;
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* Call to indicate the CriticalSection has failed with the given exception. This immediately
|
|
368
|
+
* breaks the InputGate.
|
|
369
|
+
*/
|
|
370
|
+
failed(exception) {
|
|
371
|
+
if (this.brokenState.kind === "exception") return;
|
|
372
|
+
this.setBroken(exception);
|
|
373
|
+
if (this.#parent instanceof CriticalSection) this.#parent.failed(exception);
|
|
374
|
+
else this.#parent.setBroken(exception);
|
|
375
|
+
}
|
|
376
|
+
/** ← `~CriticalSection`. */
|
|
377
|
+
drop() {
|
|
378
|
+
switch (this.state) {
|
|
379
|
+
case "NOT_STARTED": break;
|
|
380
|
+
case "INITIAL_WAIT": break;
|
|
381
|
+
case "RUNNING": this.failed(/* @__PURE__ */ new Error("jsg.Error: A critical section within this Durable Object awaited a Promise that apparently will never complete. This could happen in particular if a critical section awaits a task that was initiated outside of the critical section. Since a critical section blocks all other tasks from completing, this leads to deadlock."));
|
|
382
|
+
}
|
|
383
|
+
const parentLock = this.#parentLock;
|
|
384
|
+
if (parentLock !== void 0) {
|
|
385
|
+
this.#parentLock = void 0;
|
|
386
|
+
parentLock.release();
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
/** Return a reference for the parent scope, skipping any reparented CriticalSections */
|
|
390
|
+
parentAsInputGate() {
|
|
391
|
+
let parent = this.#parent;
|
|
392
|
+
for (;;) {
|
|
393
|
+
if (!(parent instanceof CriticalSection)) return parent;
|
|
394
|
+
if (parent.state !== "REPARENTED") return parent;
|
|
395
|
+
parent = parent.#parent;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
};
|
|
399
|
+
/** ← `OutputGate::Hooks::DEFAULT`. */
|
|
400
|
+
var DEFAULT_OUTPUT_GATE_HOOKS = {
|
|
401
|
+
/** ← `kj::NEVER_DONE`. */
|
|
402
|
+
makeTimeoutPromise: () => new Promise(() => {}),
|
|
403
|
+
outputGateLocked() {},
|
|
404
|
+
outputGateReleased() {},
|
|
405
|
+
outputGateWaiterAdded() {},
|
|
406
|
+
outputGateWaiterRemoved() {}
|
|
407
|
+
};
|
|
408
|
+
/** ← `OutputGate::makeUnfulfilledException()`. */
|
|
409
|
+
function makeUnfulfilledException() {
|
|
410
|
+
return /* @__PURE__ */ new Error("output lock was canceled before completion");
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* An OutputGate blocks outgoing messages from an Actor until writes which they might depend on
|
|
414
|
+
* are confirmed.
|
|
415
|
+
*
|
|
416
|
+
* A promise chain, not a counter (§1.1): each `lockWhile` joins a new link onto the chain and
|
|
417
|
+
* re-forks it, so a `wait()` is bound to exactly the locks outstanding when it was taken and is
|
|
418
|
+
* unaffected by any later `lockWhile`.
|
|
419
|
+
*/
|
|
420
|
+
var OutputGate = class {
|
|
421
|
+
#hooks;
|
|
422
|
+
#pastLocksPromise = Promise.resolve();
|
|
423
|
+
/** A fulfiller for onBroken(), or an exception if already broken. */
|
|
424
|
+
#brokenState = { kind: "none" };
|
|
425
|
+
constructor(hooks = DEFAULT_OUTPUT_GATE_HOOKS) {
|
|
426
|
+
this.#hooks = hooks;
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Block all future `wait()` calls until `promise` completes. Returns a wrapper around
|
|
430
|
+
* `promise`. If `promise` rejects, the exception will propagate to all future `wait()`s. If the
|
|
431
|
+
* returned promise is canceled before completion, all future `wait()`s will also throw.
|
|
432
|
+
*/
|
|
433
|
+
lockWhile(promise, signal) {
|
|
434
|
+
const fulfiller = this.#lock();
|
|
435
|
+
const raced = Promise.race([promise, this.#hooks.makeTimeoutPromise()]);
|
|
436
|
+
this.#hooks.outputGateLocked();
|
|
437
|
+
return new Promise((resolve, reject) => {
|
|
438
|
+
onAbort(signal, () => {
|
|
439
|
+
if (!fulfiller.isWaiting()) return;
|
|
440
|
+
this.#hooks.outputGateReleased();
|
|
441
|
+
const exception = makeUnfulfilledException();
|
|
442
|
+
this.#setBroken(exception);
|
|
443
|
+
fulfiller.reject(exception);
|
|
444
|
+
reject(exception);
|
|
445
|
+
});
|
|
446
|
+
raced.then((value) => {
|
|
447
|
+
if (!fulfiller.isWaiting()) return;
|
|
448
|
+
fulfiller.fulfill();
|
|
449
|
+
this.#hooks.outputGateReleased();
|
|
450
|
+
resolve(value);
|
|
451
|
+
}, (exception) => {
|
|
452
|
+
if (!fulfiller.isWaiting()) return;
|
|
453
|
+
this.#setBroken(exception);
|
|
454
|
+
fulfiller.reject(exception);
|
|
455
|
+
this.#hooks.outputGateReleased();
|
|
456
|
+
reject(exception);
|
|
457
|
+
});
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* Wait until all preceding locks are released. The wait will not be affected by any future
|
|
462
|
+
* call to `lockWhile()`.
|
|
463
|
+
*/
|
|
464
|
+
wait() {
|
|
465
|
+
this.#hooks.outputGateWaiterAdded();
|
|
466
|
+
return this.#pastLocksPromise.then(() => {
|
|
467
|
+
this.#hooks.outputGateWaiterRemoved();
|
|
468
|
+
}, (exception) => {
|
|
469
|
+
this.#hooks.outputGateWaiterRemoved();
|
|
470
|
+
throw exception;
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
/**
|
|
474
|
+
* Rejects if and when calls to `wait()` become broken due to a failed lockWhile(). The actor
|
|
475
|
+
* should be shut down in this case. This promise never resolves, only rejects.
|
|
476
|
+
*
|
|
477
|
+
* This method can only be called once.
|
|
478
|
+
*/
|
|
479
|
+
onBroken() {
|
|
480
|
+
if (this.#brokenState.kind === "fulfiller") throw new Error("onBroken() can only be called once");
|
|
481
|
+
if (this.#brokenState.kind === "exception") return Promise.reject(this.#brokenState.exception);
|
|
482
|
+
else {
|
|
483
|
+
const { promise, reject } = Promise.withResolvers();
|
|
484
|
+
this.#brokenState = {
|
|
485
|
+
kind: "fulfiller",
|
|
486
|
+
reject
|
|
487
|
+
};
|
|
488
|
+
promise.catch(() => {});
|
|
489
|
+
return promise;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
isBroken() {
|
|
493
|
+
return this.#brokenState.kind === "exception";
|
|
494
|
+
}
|
|
495
|
+
#lock() {
|
|
496
|
+
const { promise, resolve, reject } = Promise.withResolvers();
|
|
497
|
+
this.#setPastLocks(Promise.allSettled([this.#pastLocksPromise, promise]).then((results) => {
|
|
498
|
+
for (const result of results) if (result.status === "rejected") throw result.reason;
|
|
499
|
+
}));
|
|
500
|
+
let waiting = true;
|
|
501
|
+
return {
|
|
502
|
+
isWaiting: () => waiting,
|
|
503
|
+
fulfill: () => {
|
|
504
|
+
waiting = false;
|
|
505
|
+
resolve();
|
|
506
|
+
},
|
|
507
|
+
reject: (exception) => {
|
|
508
|
+
waiting = false;
|
|
509
|
+
reject(exception);
|
|
510
|
+
}
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
#setPastLocks(promise) {
|
|
514
|
+
this.#pastLocksPromise = promise;
|
|
515
|
+
promise.catch(() => {});
|
|
516
|
+
}
|
|
517
|
+
#setBroken(exception) {
|
|
518
|
+
if (this.#brokenState.kind === "fulfiller") this.#brokenState.reject(exception);
|
|
519
|
+
this.#brokenState = {
|
|
520
|
+
kind: "exception",
|
|
521
|
+
exception
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
};
|
|
525
|
+
//#endregion
|
|
526
|
+
//#region src/io/io-context.ts
|
|
527
|
+
/**
|
|
528
|
+
* ← workerd `src/workerd/io/io-context.{h,c++}`
|
|
529
|
+
*
|
|
530
|
+
* IoContext: the one door, plus the two await forms.
|
|
531
|
+
*
|
|
532
|
+
* This is the file with no true upstream correspondence for its *enforcement*.
|
|
533
|
+
* Workerd acquires the input gate at isolate entry, so acquisition is
|
|
534
|
+
* structural. We have no isolate hook, so a lock is taken at our own dispatch
|
|
535
|
+
* boundary instead. See "The enforcement point is the one thing we cannot port"
|
|
536
|
+
* in the design record.
|
|
537
|
+
*
|
|
538
|
+
* What this file does, stated before anything about how it got here:
|
|
539
|
+
*
|
|
540
|
+
* 1. `#currentInputLocks` is a STACK of the locks held by the slices that are
|
|
541
|
+
* running. It is upstream's single `kj::Maybe<InputGate::Lock>
|
|
542
|
+
* currentInputLock` member (`io-context.h:993`) with the `Maybe` widened,
|
|
543
|
+
* and a "frame" is an entry in it. A frame carries nothing else: the only
|
|
544
|
+
* two things ever read from one are the lock (`getInputLock`) and the
|
|
545
|
+
* critical section that lock belongs to (`getCriticalSection`), and the
|
|
546
|
+
* lock answers both.
|
|
547
|
+
* 2. A lock is released, and leaves the stack, at the END OF THE MICROTASK
|
|
548
|
+
* CHECKPOINT of the slice holding it — not when that slice's synchronous
|
|
549
|
+
* body returns. That is upstream's own boundary, not a relaxation of it:
|
|
550
|
+
* `runImpl`'s inner `KJ_DEFER` calls `js.runMicrotasks()`
|
|
551
|
+
* (`io-context.c++:1262`) and `runInContextScope`'s outer one then clears
|
|
552
|
+
* `currentInputLock` (`:1214`), inner scope first, so the whole checkpoint
|
|
553
|
+
* drains under the lock. `currentInputLock` holds the `Lock` by value, so
|
|
554
|
+
* clearing it is the release. `atCheckpointEnd` below is that point.
|
|
555
|
+
* 3. NEITHER await form releases anything at the moment of the await.
|
|
556
|
+
* `awaitIo` reads `getCriticalSection()` and `awaitIoWithInputLock` takes
|
|
557
|
+
* an `addRef`; both then return a promise and let the slice end normally.
|
|
558
|
+
* So an invocation that calls `awaitIo` keeps the gate until its own
|
|
559
|
+
* checkpoint-end exit and the gate opens there — which is still before any
|
|
560
|
+
* real I/O can complete, so "a bare timer releases the input gate" holds.
|
|
561
|
+
* Upstream is the same: `getCriticalSection()` (`io-context.c++:362`) does
|
|
562
|
+
* not touch `currentInputLock`, and `:1214` is the only place that clears
|
|
563
|
+
* it. The difference between the two forms is entirely on the far side —
|
|
564
|
+
* `awaitIo` re-enters through `run(func, criticalSection)` and queues for a
|
|
565
|
+
* fresh lock, `awaitIoWithInputLock` re-enters holding the ref it took.
|
|
566
|
+
* 4. Removal from the stack is by identity, not by popping, because entries do
|
|
567
|
+
* overlap — three deep in the unit tests. One invocation can have several
|
|
568
|
+
* held awaits outstanding (`Promise.all` over two storage reads), and a
|
|
569
|
+
* section's last body slice overlaps the slice that resolves it.
|
|
570
|
+
*
|
|
571
|
+
* Two mechanics were validated against real workerd before this scaffolding
|
|
572
|
+
* landed, and both are easy to get wrong:
|
|
573
|
+
*
|
|
574
|
+
* 1. The ambient "which lock am I under" must be a STACK of invocation frames,
|
|
575
|
+
* not a single slot, so that `current` always names the running invocation.
|
|
576
|
+
* The prototype that found this stated it as "`awaitIo` splices its frame
|
|
577
|
+
* out and re-pushes the SAME frame on resume". That is not what happens
|
|
578
|
+
* here and the difference is worth knowing: nothing splices at the point of
|
|
579
|
+
* the await, because a lock already leaves the stack when its slice ends,
|
|
580
|
+
* which is the same event that releases it (`#exit` does both). A
|
|
581
|
+
* resumption then pushes a fresh lock. The prototype needed the splice
|
|
582
|
+
* because its door held one lock for a whole invocation; the identity that
|
|
583
|
+
* has to survive an await here is the critical section, and that is
|
|
584
|
+
* captured at the call rather than looked up later.
|
|
585
|
+
* 2. That ambient is safe here for a reason that does NOT generalise to the
|
|
586
|
+
* §2.3 ambient-field hazard: the gate guarantees pushes and pops nest
|
|
587
|
+
* properly in time, because only one holder chain is inside at once.
|
|
588
|
+
* `_cf_currentSubAgentBridge` has no such guarantee, which is why it is a
|
|
589
|
+
* live bug and this is not.
|
|
590
|
+
*
|
|
591
|
+
* Consequence: no async context is required. Do not add a dependency on
|
|
592
|
+
* decision 8 here without re-running the conformance gate suite first.
|
|
593
|
+
*
|
|
594
|
+
* The invariant a future simplifier has to re-check: a single slot would pass
|
|
595
|
+
* every test in `io-context.test.ts`, and that was established by trying it,
|
|
596
|
+
* not by argument. `current()` is only read from inside a slice, every slice
|
|
597
|
+
* pushes its own lock last, and no second slice can begin while an earlier
|
|
598
|
+
* frame is still waiting for its checkpoint-end exit — that frame is holding
|
|
599
|
+
* the gate. So the top of a stack and the last write to a slot always agree.
|
|
600
|
+
* The stack is still what is here, because a slot would be holding a stale
|
|
601
|
+
* value at every one of the overlaps in (4), and that is unobservable for
|
|
602
|
+
* exactly as long as the invariant holds. Tightening (2) back to the end of the
|
|
603
|
+
* synchronous body breaks it immediately, which is the regime the mechanic was
|
|
604
|
+
* found in.
|
|
605
|
+
*
|
|
606
|
+
* Spec: §1.2, §1.3, §1.5, §1.6, §1.7.1, §1.9, decisions 1, 2, 4 and 13 in
|
|
607
|
+
* docs/decisions.md.
|
|
608
|
+
*
|
|
609
|
+
* `TimeoutManager` is the only part of this file a consumer's own code reaches
|
|
610
|
+
* directly: every host-provided async primitive a
|
|
611
|
+
* Durable Object can await has to route through here, or the continuation after
|
|
612
|
+
* it resumes with an empty invocation stack. `TimeoutManager` below is the
|
|
613
|
+
* timer half; `api/global-scope.ts` and `api/web-socket.ts` are the rest.
|
|
614
|
+
*
|
|
615
|
+
* Not ported, because the substrate has no equivalent to port onto: isolate and
|
|
616
|
+
* async locks (`Worker::Lock`, `jsg::Lock`, `takeAsyncLock`) and everything
|
|
617
|
+
* that exists to enter or leave an isolate — which is precisely the thing this
|
|
618
|
+
* file substitutes for; the limit enforcer and `afterLimitTimeout` (the
|
|
619
|
+
* deadline takes the `Timer` port instead); trace spans, already a recorded
|
|
620
|
+
* divergence documented in §1.12; subrequest channels and HTTP, which are
|
|
621
|
+
* `api/http.ts`'s gating over the substrate's own `fetch`;
|
|
622
|
+
* `IoOwn`/`IoPtr`/`DeleteQueue`, which guard cross-context dereferences that GC
|
|
623
|
+
* makes impossible; hang detection and `registerPendingEvent`, which need the
|
|
624
|
+
* isolate's own idea of pending work; and the thread-local
|
|
625
|
+
* `IoContext::current()` static, whose lock-resolving half the invocation stack
|
|
626
|
+
* replaces — its *identity* half is `currentSlice` below, narrowed to the
|
|
627
|
+
* synchronous slice, with one consumer and no resolver. `EventOutcome` and
|
|
628
|
+
* `RequestObserver` are metrics types with no port, so `waitUntilStatus()`
|
|
629
|
+
* returns the first exception instead.
|
|
630
|
+
*/
|
|
631
|
+
/** ← `static constexpr int64_t max = 3153600000000; // Milliseconds in 100 years`. */
|
|
632
|
+
var MAX_TIMEOUT_MS = 31536e8;
|
|
633
|
+
/** ← `afterLimitTimeout(30 * kj::SECONDS)` in `IoContext::blockConcurrencyWhile`. */
|
|
634
|
+
var BLOCK_CONCURRENCY_WHILE_TIMEOUT_MS = 3e4;
|
|
635
|
+
/** Copied verbatim: users and upstream tests match on it. */
|
|
636
|
+
var BLOCK_CONCURRENCY_WHILE_TIMEOUT_MESSAGE = "A call to blockConcurrencyWhile() in a Durable Object waited for too long. The call was canceled and the Durable Object was reset.";
|
|
637
|
+
/** ← `jsg::annotateBroken(msg, "broken.inputGateBroken")`. */
|
|
638
|
+
var INPUT_GATE_BROKEN_PREFIX = "broken.inputGateBroken; ";
|
|
639
|
+
/**
|
|
640
|
+
* THE check every storage entry point in `api/` makes before touching the
|
|
641
|
+
* database, and the only place this package throws for a missing input lock.
|
|
642
|
+
*
|
|
643
|
+
* Upstream never faces the question. `IoContext::current()` is a thread-local
|
|
644
|
+
* read with a `KJ_REQUIRE` behind it, and it cannot fail for a storage call
|
|
645
|
+
* because isolate entry is the only way into an actor and it always took a
|
|
646
|
+
* lock. We have no isolate hook (see this file's header), so a continuation
|
|
647
|
+
* that resumed from a raw `setTimeout`, a raw `fetch`, or any other promise the
|
|
648
|
+
* runtime does not own comes back with an empty invocation stack, and its next
|
|
649
|
+
* storage call reaches `ActorSqlite` — which is synchronous, touches no gate,
|
|
650
|
+
* and would happily serve it outside any transaction boundary.
|
|
651
|
+
*
|
|
652
|
+
* So this throws, matching the assert. It is deliberately ONE function called
|
|
653
|
+
* from every entry point rather than a check written at each of them: the
|
|
654
|
+
* policy is a design decision that belongs to the design record, and when it
|
|
655
|
+
* changes it has to change in one place rather than across a surface the
|
|
656
|
+
* vendored consumer reaches from hundreds of call sites. There is no lenient
|
|
657
|
+
* mode, no flag, and no implicit acquire — a lost invocation is loud, and a
|
|
658
|
+
* lost transaction boundary is not.
|
|
659
|
+
*/
|
|
660
|
+
function requireInputLock(ctx, op) {
|
|
661
|
+
if (ctx.hasCurrent()) return;
|
|
662
|
+
throw new Error(`${op}: no input lock available in this context${ctx.describeLostLock()}`);
|
|
663
|
+
}
|
|
664
|
+
/**
|
|
665
|
+
* A stack for `noteGateUse`, captured where user frames are still on the stack.
|
|
666
|
+
*
|
|
667
|
+
* The invocation stack's own push and pop are scheduler moments — `#runImpl`
|
|
668
|
+
* runs from a gate resumption and `#exit` from a `MessageChannel` callback — so
|
|
669
|
+
* a trace taken there names only runtime internals. The moments that still see
|
|
670
|
+
* the caller are the synchronous entries into the gate machinery: an `awaitIo`
|
|
671
|
+
* call, an `entry` dispatch, a callback's registration. V8's zero-cost async
|
|
672
|
+
* traces extend those with the awaiting chain, which is usually the frame the
|
|
673
|
+
* reader actually wants.
|
|
674
|
+
*
|
|
675
|
+
* The first slice drops the `Error` header (absent on SpiderMonkey) and the two
|
|
676
|
+
* runtime frames: this helper and the gate entry point that called it.
|
|
677
|
+
*/
|
|
678
|
+
function captureGateStack() {
|
|
679
|
+
const stack = (/* @__PURE__ */ new Error()).stack;
|
|
680
|
+
if (stack === void 0) return void 0;
|
|
681
|
+
const frames = stack.split("\n");
|
|
682
|
+
const trimmed = frames.slice(frames[0]?.startsWith("Error") ? 3 : 2).join("\n");
|
|
683
|
+
return trimmed === "" ? void 0 : trimmed;
|
|
684
|
+
}
|
|
685
|
+
/**
|
|
686
|
+
* The end of the microtask checkpoint — the moment `runImpl`'s `KJ_DEFER` fires.
|
|
687
|
+
*
|
|
688
|
+
* Upstream drains the whole checkpoint INSIDE the isolate run: the defer calls
|
|
689
|
+
* `js.runMicrotasks()` and only the outer defer in `runInContextScope` then
|
|
690
|
+
* clears `currentInputLock`. So a continuation that awaits nothing but
|
|
691
|
+
* already-resolved promises stays under the same lock, and a continuation that
|
|
692
|
+
* waits on real I/O does not. In JS the only observable end of a microtask drain
|
|
693
|
+
* is the next macrotask, so that is where a lock leaves the invocation stack.
|
|
694
|
+
*
|
|
695
|
+
* Measured, because the obvious alternatives are wrong in ways nothing catches:
|
|
696
|
+
* releasing synchronously when the invoked function returns, or on the next
|
|
697
|
+
* microtask, both hand the lock back BEFORE the code awaiting the I/O resumes as
|
|
698
|
+
* soon as one promise sits between the two — one `async` wrapper, a `.then`, a
|
|
699
|
+
* `Promise.all` — and `actor-state.ts` is exactly such a wrapper. The gate would
|
|
700
|
+
* then open in the middle of a storage await with no test failing, which is the
|
|
701
|
+
* silent loss of atomicity §1.7.1 names.
|
|
702
|
+
*
|
|
703
|
+
* `MessageChannel` and not `setTimeout`, decided by benchmark rather than by
|
|
704
|
+
* argument, because the two are three orders of magnitude apart on the shapes
|
|
705
|
+
* that wait for a release. A chain of these schedules each hand-off from inside
|
|
706
|
+
* the previous one's callback, so a `setTimeout` chain's nesting level climbs
|
|
707
|
+
* past five and stays there, where browsers clamp it to 4ms. Median per hand-off
|
|
708
|
+
* over 50 chained hops:
|
|
709
|
+
*
|
|
710
|
+
* | | setTimeout | MessageChannel |
|
|
711
|
+
* | node | 1.273 ms | 0.018 ms |
|
|
712
|
+
* | chromium page | 4.96 ms | 0.024 ms |
|
|
713
|
+
* | chromium Worker | 5.542 ms | 0.022 ms |
|
|
714
|
+
*
|
|
715
|
+
* Only two shapes pay it: an `awaitIo` chain, which has to re-acquire the lock
|
|
716
|
+
* its slice gave up, and a queue of events waiting on the holder. A chain of
|
|
717
|
+
* held storage awaits does not — the lock passes from `addRef` to `addRef` and
|
|
718
|
+
* the release is off the critical path. So 50 sequential facet RPCs cost 277ms
|
|
719
|
+
* of pure clamp in a Worker under `setTimeout` and 1.1ms under this.
|
|
720
|
+
*
|
|
721
|
+
* One channel per BATCH, and a batch is an explicit array: `atCheckpointEnd`
|
|
722
|
+
* pushes onto `pendingCheckpointEnds` and only the push that finds it empty opens
|
|
723
|
+
* a channel. Ordering then comes from the array, which the language guarantees,
|
|
724
|
+
* rather than from delivery order across separate channels, which no
|
|
725
|
+
* specification does. That mattered because the storage engine below depends on
|
|
726
|
+
* a commit scheduled inside a slice running before the release scheduled at the
|
|
727
|
+
* end of that slice; separate channels do deliver in post order in Node,
|
|
728
|
+
* measured across 200 of them including one scheduled from inside another's
|
|
729
|
+
* callback, but a browser that chose otherwise would open the gate onto a
|
|
730
|
+
* transaction a previous event left open, and nothing would say so.
|
|
731
|
+
*
|
|
732
|
+
* A callback scheduled DURING a drain lands in the next batch, which is the
|
|
733
|
+
* semantics to want: one hand-off is one checkpoint end, and a slice that begins
|
|
734
|
+
* inside this drain gets its own. The drain runs every callback even if one
|
|
735
|
+
* throws, and rethrows the first exception afterwards, because abandoning the
|
|
736
|
+
* rest of a batch is how a gate wedges with nothing to see.
|
|
737
|
+
*
|
|
738
|
+
* A long-lived shared port was rejected for the reason it always is: it has to be
|
|
739
|
+
* closed on abort and `unref`'d so it cannot hold a test runner's event loop
|
|
740
|
+
* open, and an `unref`'d port can drop a release at exit — a wedged gate. A
|
|
741
|
+
* channel that lives for exactly one message cannot. Batching gets most of what
|
|
742
|
+
* a shared port was worth anyway: a slice that schedules a commit and a release
|
|
743
|
+
* now allocates one channel where it used to allocate two.
|
|
744
|
+
*
|
|
745
|
+
* **Exported because `kj::evalLater()` is this same point.** `ActorSqlite` opens
|
|
746
|
+
* its implicit transaction on the first write and commits it "on the next turn of
|
|
747
|
+
* the event loop" (`actor-sqlite.c++:352-357`); upstream's next turn is after the
|
|
748
|
+
* isolate run, which is after `js.runMicrotasks()`, which is after
|
|
749
|
+
* `currentInputLock` is cleared. Upstream's two boundaries are one boundary, and
|
|
750
|
+
* they stay one here only if the commit rides the same primitive as the release.
|
|
751
|
+
* Two consequences the storage engine depends on, both properties of this
|
|
752
|
+
* function rather than of `ActorSqlite`:
|
|
753
|
+
*
|
|
754
|
+
* 1. **Everything that holds the input lock across an await is a microtask
|
|
755
|
+
* chain.** `awaitIoWithInputLock` resumes through `#awaitIoImpl`'s `.then`
|
|
756
|
+
* into `run(func, lock)`, which never waits on the gate. So a whole run of
|
|
757
|
+
* held storage awaits finishes before the next hand-off and its writes are
|
|
758
|
+
* one transaction (§1.7.1 row 1).
|
|
759
|
+
* 2. **Everything that releases it needs at least one hand-off.** `awaitIo`
|
|
760
|
+
* resumes through `gate.wait()`, which cannot resolve until `#exit` runs
|
|
761
|
+
* here. So a timer or outbound await puts the commit between the two writes
|
|
762
|
+
* (§1.7.1 row 2).
|
|
763
|
+
*
|
|
764
|
+
*/
|
|
765
|
+
function atCheckpointEnd(run) {
|
|
766
|
+
pendingCheckpointEnds.push(run);
|
|
767
|
+
if (pendingCheckpointEnds.length > 1) return;
|
|
768
|
+
const channel = new MessageChannel();
|
|
769
|
+
channel.port1.onmessage = () => {
|
|
770
|
+
channel.port1.close();
|
|
771
|
+
channel.port2.close();
|
|
772
|
+
const batch = pendingCheckpointEnds.splice(0);
|
|
773
|
+
let failure;
|
|
774
|
+
for (const callback of batch) try {
|
|
775
|
+
callback();
|
|
776
|
+
} catch (exception) {
|
|
777
|
+
failure ??= { exception };
|
|
778
|
+
}
|
|
779
|
+
if (failure !== void 0) throw failure.exception;
|
|
780
|
+
};
|
|
781
|
+
channel.port2.postMessage(0);
|
|
782
|
+
}
|
|
783
|
+
/** The current batch. Its order is the hand-off order every consumer below relies on. */
|
|
784
|
+
var pendingCheckpointEnds = [];
|
|
785
|
+
/**
|
|
786
|
+
* ← `static thread_local IoContext* threadLocalRequest` (`io-context.c++:25`).
|
|
787
|
+
*
|
|
788
|
+
* **This is NOT an async context and must not become one.** It is set on entry
|
|
789
|
+
* to a slice's SYNCHRONOUS body and restored the instant that body returns —
|
|
790
|
+
* which for an `async` function is its first `await`. It propagates through
|
|
791
|
+
* exactly nothing. The package's "no async context is required" property
|
|
792
|
+
* (README, Part 4 mechanic 1) is undisturbed: nothing resolves a lock through
|
|
793
|
+
* this, and deleting it would change no gate behaviour.
|
|
794
|
+
*
|
|
795
|
+
* Upstream's scope is wider and cannot be matched. `runInContextScope` saves the
|
|
796
|
+
* previous context, installs itself, and restores at the end of the isolate run
|
|
797
|
+
* — which drains the whole microtask checkpoint synchronously, so upstream's
|
|
798
|
+
* `current()` covers a slice's continuations too. JS cannot drain a checkpoint
|
|
799
|
+
* synchronously, so covering continuations here would mean holding the value
|
|
800
|
+
* until `atCheckpointEnd`, and by then a SECOND actor's body can have run and
|
|
801
|
+
* left. That is not a hazard upstream has: two actors' slices genuinely overlap
|
|
802
|
+
* in this window (§1.10 gives every facet its own gates, so nothing serialises
|
|
803
|
+
* a parent against its child), and the value would be wrong with nothing to say
|
|
804
|
+
* so.
|
|
805
|
+
*
|
|
806
|
+
* So the port keeps only the half that is exact, and the one consumer is a
|
|
807
|
+
* tripwire that refuses on mismatch and stays quiet on `undefined` — never a
|
|
808
|
+
* resolver. See `requireOwnSlice` in `api/global-scope.ts`.
|
|
809
|
+
*/
|
|
810
|
+
var currentSlice;
|
|
811
|
+
/** ← `IoContext::tryCurrent()` (`io-context.c++:1416-1422`), over the narrowed scope above. */
|
|
812
|
+
function tryCurrentSlice() {
|
|
813
|
+
return currentSlice;
|
|
814
|
+
}
|
|
815
|
+
/**
|
|
816
|
+
* ← the `SuppressIoContextScope` constructor's `threadLocalRequest = this` half
|
|
817
|
+
* (`io-context.c++:1208`), as a function rather than an assignment in `#runImpl`.
|
|
818
|
+
*
|
|
819
|
+
* A function because `currentSlice = this` reads to a linter as a `this` alias — the
|
|
820
|
+
* ES5 `var self = this` habit — when it is the opposite: publishing the running
|
|
821
|
+
* context to a module scope, which is exactly what the C++ does to a thread local.
|
|
822
|
+
*/
|
|
823
|
+
function enterSlice(context) {
|
|
824
|
+
currentSlice = context;
|
|
825
|
+
}
|
|
826
|
+
/** ← `promiseForExceptionOrT()`: merge the rejection into the value so it survives the hop. */
|
|
827
|
+
function promiseForExceptionOrT(promise) {
|
|
828
|
+
return promise.then((value) => ({
|
|
829
|
+
ok: true,
|
|
830
|
+
value
|
|
831
|
+
}), (exception) => ({
|
|
832
|
+
ok: false,
|
|
833
|
+
exception
|
|
834
|
+
}));
|
|
835
|
+
}
|
|
836
|
+
/** ← `IdentityFunc<T>`. */
|
|
837
|
+
function identity(value) {
|
|
838
|
+
return value;
|
|
839
|
+
}
|
|
840
|
+
/**
|
|
841
|
+
* ← the `if (!msg.startsWith("broken."))` guard in `blockConcurrencyWhile`'s error
|
|
842
|
+
* handler. Upstream rewrites the exception's description in place, so this does
|
|
843
|
+
* too. `jsg`'s exception tunnelling — the `jsg.Error:` / `remote.` prefixes
|
|
844
|
+
* `annotateBroken()` also produces — has no port here, so only the brokenness tag
|
|
845
|
+
* it exists to carry survives.
|
|
846
|
+
*/
|
|
847
|
+
function annotateInputGateBroken(exception) {
|
|
848
|
+
if (!(exception instanceof Error)) return;
|
|
849
|
+
if (exception.message.startsWith("broken.") || exception.message.startsWith("remote.broken.")) return;
|
|
850
|
+
exception.message = INPUT_GATE_BROKEN_PREFIX + exception.message;
|
|
851
|
+
}
|
|
852
|
+
/**
|
|
853
|
+
* ← `jsg::isExceptionFromInputGateBroken` (`jsg/exception.c++:168-172`):
|
|
854
|
+
* "annotateBroken() produces 'broken.inputGateBroken; {message}', optionally
|
|
855
|
+
* prefixed with 'remote.' when crossing RPC boundaries. Strip the remote prefix
|
|
856
|
+
* first, then check the tag."
|
|
857
|
+
*
|
|
858
|
+
* Its writer is `annotateInputGateBroken` directly above, which is why the two
|
|
859
|
+
* live together rather than the reader moving to its consumer: `jsg/` has no
|
|
860
|
+
* module here, and a prefix known in two places is a prefix that drifts.
|
|
861
|
+
*/
|
|
862
|
+
function isExceptionFromInputGateBroken(exception) {
|
|
863
|
+
if (!(exception instanceof Error)) return false;
|
|
864
|
+
let description = exception.message;
|
|
865
|
+
while (description.startsWith(REMOTE_EXCEPTION_PREFIX)) description = description.slice(REMOTE_EXCEPTION_PREFIX.length);
|
|
866
|
+
return description.startsWith(INPUT_GATE_BROKEN_PREFIX);
|
|
867
|
+
}
|
|
868
|
+
/** ← `ERROR_REMOTE_PREFIX` (`jsg/exception.h`). */
|
|
869
|
+
var REMOTE_EXCEPTION_PREFIX = "remote.";
|
|
870
|
+
/**
|
|
871
|
+
* ← `jsg::EXCEPTION_IS_USER_ERROR` (`jsg/exception.h:160`), a
|
|
872
|
+
* `kj::Exception::DetailTypeId` attached to an arbitrary exception rather than a
|
|
873
|
+
* type of exception.
|
|
874
|
+
*
|
|
875
|
+
* A symbol-keyed property is the closest JS has: it rides any thrown object,
|
|
876
|
+
* survives a rethrow, and cannot collide with anything an application writes.
|
|
877
|
+
* `Symbol.for` rather than `Symbol()` so the detail is still legible after the
|
|
878
|
+
* exception crosses a realm — the mistake decision 18 records capnweb making.
|
|
879
|
+
*/
|
|
880
|
+
var EXCEPTION_IS_USER_ERROR = Symbol.for("workerd.exceptionIsUserError");
|
|
881
|
+
/** ← `error.setDetail(jsg::EXCEPTION_IS_USER_ERROR, kj::heapArray<byte>(0))`. */
|
|
882
|
+
function setUserErrorDetail(exception) {
|
|
883
|
+
if (typeof exception !== "object" || exception === null) return;
|
|
884
|
+
Object.defineProperty(exception, EXCEPTION_IS_USER_ERROR, {
|
|
885
|
+
value: true,
|
|
886
|
+
enumerable: false,
|
|
887
|
+
configurable: true
|
|
888
|
+
});
|
|
889
|
+
}
|
|
890
|
+
/** ← `e.getDetail(jsg::EXCEPTION_IS_USER_ERROR) != kj::none`. */
|
|
891
|
+
function hasUserErrorDetail(exception) {
|
|
892
|
+
if (typeof exception !== "object" || exception === null) return false;
|
|
893
|
+
return exception[EXCEPTION_IS_USER_ERROR] === true;
|
|
894
|
+
}
|
|
895
|
+
/**
|
|
896
|
+
* ← `IoContext::TimeoutManagerImpl` (`io-context.c++:40-140`, `:742-880`).
|
|
897
|
+
*
|
|
898
|
+
* **The one mechanic worth reading twice: a timer is NOT `awaitIo`.** Upstream
|
|
899
|
+
* says why in a comment on the very line (`io-context.c++:756-758`): "the manual
|
|
900
|
+
* use of run() here (including carrying over the critical section) is kind of
|
|
901
|
+
* ugly, but using awaitIo() doesn't work here because we need the ability to
|
|
902
|
+
* cancel the timer, so we don't want to addTask() it, which awaitIo() does
|
|
903
|
+
* implicitly." So the shape is `cs = ctx.getCriticalSection()` captured at the
|
|
904
|
+
* call, then `ctx.run(callback, cs)` when it fires. The captured section is what
|
|
905
|
+
* makes a timer armed inside `blockConcurrencyWhile` run INSIDE that section
|
|
906
|
+
* rather than queueing on the root gate behind it.
|
|
907
|
+
*
|
|
908
|
+
* **Substrate divergence: one `Timer.afterDelay` per timeout, where upstream
|
|
909
|
+
* keeps a sorted `timeoutTimes` map and a single `timerTask` for the nearest.**
|
|
910
|
+
* That structure exists because `kj::TimerChannel::atTime` supports one pending
|
|
911
|
+
* wait, so upstream has to multiplex; `Timer.afterDelay` takes as many
|
|
912
|
+
* concurrent waits as are asked for. The observable properties it produced —
|
|
913
|
+
* timers fire in deadline order, ties broken by arming order — are the ones both
|
|
914
|
+
* lane timers already have, since both are `setTimeout` underneath. Nothing else
|
|
915
|
+
* in `resetTimerTask` is observable, so nothing else is ported.
|
|
916
|
+
*
|
|
917
|
+
* Not ported: `TimeoutId::Generator` and its cross-`ServiceWorkerGlobalScope`
|
|
918
|
+
* assertion (`io-context.c++:52-60`), which exists to catch an IoContext being
|
|
919
|
+
* current for a different V8 context — a confusion with no shape here, since a
|
|
920
|
+
* timeout id is minted by the manager that owns it; `registerPendingEvent`,
|
|
921
|
+
* which needs the isolate's own idea of pending work; and `getNextTimeout`,
|
|
922
|
+
* whose only caller is the limit enforcer.
|
|
923
|
+
*/
|
|
924
|
+
var TimeoutManager = class {
|
|
925
|
+
#timer;
|
|
926
|
+
#timeouts = /* @__PURE__ */ new Map();
|
|
927
|
+
#nextId = 1;
|
|
928
|
+
constructor(timer) {
|
|
929
|
+
this.#timer = timer;
|
|
930
|
+
}
|
|
931
|
+
/** ← `TimeoutManagerImpl::setTimeout` (`io-context.c++:51-67`). */
|
|
932
|
+
setTimeout(ctx, params) {
|
|
933
|
+
const id = this.#nextId++;
|
|
934
|
+
const state = {
|
|
935
|
+
params,
|
|
936
|
+
isCanceled: false,
|
|
937
|
+
armed: void 0
|
|
938
|
+
};
|
|
939
|
+
this.#timeouts.set(id, state);
|
|
940
|
+
this.#arm(ctx, id, state);
|
|
941
|
+
return id;
|
|
942
|
+
}
|
|
943
|
+
/** ← `TimeoutManagerImpl::clearTimeout` (`io-context.c++:874-883`). */
|
|
944
|
+
clearTimeout(id) {
|
|
945
|
+
const state = this.#timeouts.get(id);
|
|
946
|
+
if (state === void 0) return;
|
|
947
|
+
this.#cancel(id, state);
|
|
948
|
+
}
|
|
949
|
+
/** ← `TimeoutManagerImpl::getTimeoutCount` (`io-context.c++:71-73`). */
|
|
950
|
+
getTimeoutCount() {
|
|
951
|
+
return this.#timeouts.size;
|
|
952
|
+
}
|
|
953
|
+
/** ← `TimeoutManagerImpl::cancelAll` (`io-context.c++:83-87`). */
|
|
954
|
+
cancelAll() {
|
|
955
|
+
for (const [id, state] of [...this.#timeouts]) this.#cancel(id, state);
|
|
956
|
+
}
|
|
957
|
+
/** ← `TimeoutState::cancel` — clear the flag, drop the callback reference, disarm. */
|
|
958
|
+
#cancel(id, state) {
|
|
959
|
+
state.isCanceled = true;
|
|
960
|
+
state.params.callback = void 0;
|
|
961
|
+
state.armed?.abort();
|
|
962
|
+
state.armed = void 0;
|
|
963
|
+
this.#timeouts.delete(id);
|
|
964
|
+
}
|
|
965
|
+
/** ← `TimeoutManagerImpl::setTimeoutImpl` (`io-context.c++:742-853`). */
|
|
966
|
+
#arm(ctx, id, state) {
|
|
967
|
+
const criticalSection = ctx.getCriticalSection();
|
|
968
|
+
const wake = new AbortController();
|
|
969
|
+
state.armed = wake;
|
|
970
|
+
const fired = this.#timer.afterDelay(state.params.msDelay, wake.signal).then(async () => {
|
|
971
|
+
if (state.isCanceled) return;
|
|
972
|
+
state.armed = void 0;
|
|
973
|
+
await this.#fire(ctx, id, state, criticalSection);
|
|
974
|
+
}, (exception) => {
|
|
975
|
+
if (!state.isCanceled) throw exception;
|
|
976
|
+
});
|
|
977
|
+
ctx.addWaitUntil(fired);
|
|
978
|
+
}
|
|
979
|
+
/** ← the body of the `.then` at `io-context.c++:759-816`, which is one `context.run`. */
|
|
980
|
+
async #fire(ctx, id, state, criticalSection) {
|
|
981
|
+
await ctx.run(() => {
|
|
982
|
+
if (state.isCanceled) return;
|
|
983
|
+
const callback = state.params.callback;
|
|
984
|
+
if (callback === void 0) return;
|
|
985
|
+
if (!state.params.repeat) {
|
|
986
|
+
state.params.callback = void 0;
|
|
987
|
+
this.#timeouts.delete(id);
|
|
988
|
+
}
|
|
989
|
+
try {
|
|
990
|
+
callback();
|
|
991
|
+
} finally {
|
|
992
|
+
if (state.params.repeat && !state.isCanceled) this.#arm(ctx, id, state);
|
|
993
|
+
}
|
|
994
|
+
}, criticalSection);
|
|
995
|
+
}
|
|
996
|
+
};
|
|
997
|
+
/**
|
|
998
|
+
* A set of background promises, with the one behaviour `kj::TaskSet` adds over an
|
|
999
|
+
* array: a failing task reports to `taskFailed` instead of becoming an unhandled
|
|
1000
|
+
* rejection, and the set can be waited on until empty.
|
|
1001
|
+
*/
|
|
1002
|
+
var TaskSet = class {
|
|
1003
|
+
#tasks = /* @__PURE__ */ new Set();
|
|
1004
|
+
#taskFailed;
|
|
1005
|
+
constructor(taskFailed) {
|
|
1006
|
+
this.#taskFailed = taskFailed;
|
|
1007
|
+
}
|
|
1008
|
+
add(promise) {
|
|
1009
|
+
const task = promise.then(() => {
|
|
1010
|
+
this.#tasks.delete(task);
|
|
1011
|
+
}, (exception) => {
|
|
1012
|
+
this.#tasks.delete(task);
|
|
1013
|
+
this.#taskFailed(exception);
|
|
1014
|
+
});
|
|
1015
|
+
this.#tasks.add(task);
|
|
1016
|
+
}
|
|
1017
|
+
/** ← `kj::TaskSet::onEmpty()`. Re-checks, since a task can add another. */
|
|
1018
|
+
async onEmpty() {
|
|
1019
|
+
while (this.#tasks.size > 0) await Promise.all([...this.#tasks]);
|
|
1020
|
+
}
|
|
1021
|
+
};
|
|
1022
|
+
var IoContext = class {
|
|
1023
|
+
#actor;
|
|
1024
|
+
#timer;
|
|
1025
|
+
/**
|
|
1026
|
+
* ← `kj::Maybe<InputGate::Lock> currentInputLock`, made a stack.
|
|
1027
|
+
*
|
|
1028
|
+
* The top entry is the lock of the slice that is running right now. Entries are
|
|
1029
|
+
* removed by identity rather than popped, so `#exit` never depends on the order
|
|
1030
|
+
* the overlapping slices happen to leave in.
|
|
1031
|
+
*/
|
|
1032
|
+
#currentInputLocks = [];
|
|
1033
|
+
/**
|
|
1034
|
+
* Where this context's gate was last deliberately engaged, for
|
|
1035
|
+
* `describeLostLock`. One slot, overwritten on every engagement — the gate
|
|
1036
|
+
* serialises slices, so the latest note is the best available ancestor of
|
|
1037
|
+
* whatever continuation is running lockless now.
|
|
1038
|
+
*/
|
|
1039
|
+
#lastGateUse;
|
|
1040
|
+
#transformGateUses = 0;
|
|
1041
|
+
#transformGateStack;
|
|
1042
|
+
#abortException;
|
|
1043
|
+
#abortPromise;
|
|
1044
|
+
#rejectAbort;
|
|
1045
|
+
/**
|
|
1046
|
+
* Two sets, as upstream: `abortWhen()` always uses `tasks` so that a monitor
|
|
1047
|
+
* which never completes cannot hold up a drain, while `addTask()` in an actor
|
|
1048
|
+
* is a waitUntil task.
|
|
1049
|
+
*/
|
|
1050
|
+
#tasks;
|
|
1051
|
+
#waitUntilTasks;
|
|
1052
|
+
#addTaskCounter = 0;
|
|
1053
|
+
#waitUntilStatus;
|
|
1054
|
+
/** ← `kj::Own<TimeoutManager> timeoutManager` (`io-context.h:1043`). */
|
|
1055
|
+
#timeouts;
|
|
1056
|
+
constructor(actor, timer) {
|
|
1057
|
+
this.#actor = actor;
|
|
1058
|
+
this.#timer = timer;
|
|
1059
|
+
this.#timeouts = new TimeoutManager(timer);
|
|
1060
|
+
const { promise, reject } = Promise.withResolvers();
|
|
1061
|
+
this.#abortPromise = promise;
|
|
1062
|
+
this.#rejectAbort = reject;
|
|
1063
|
+
promise.catch(() => {});
|
|
1064
|
+
this.#tasks = new TaskSet((exception) => this.#taskFailed(exception));
|
|
1065
|
+
this.#waitUntilTasks = new TaskSet((exception) => this.#taskFailed(exception));
|
|
1066
|
+
this.abortWhen(actor.getInputGate().onBroken());
|
|
1067
|
+
this.abortWhen(actor.getOutputGate().onBroken());
|
|
1068
|
+
}
|
|
1069
|
+
/**
|
|
1070
|
+
* Get the current input lock. Throws an exception if no input lock is held (e.g. because
|
|
1071
|
+
* this is not an actor request).
|
|
1072
|
+
*
|
|
1073
|
+
* ← `KJ_ASSERT_NONNULL(currentInputLock, ...).addRef()`. The `addRef` IS the §1.2
|
|
1074
|
+
* distinction: it is the only way to hold the gate past the end of this slice.
|
|
1075
|
+
*/
|
|
1076
|
+
getInputLock() {
|
|
1077
|
+
return this.#requireCurrent().addRef();
|
|
1078
|
+
}
|
|
1079
|
+
/** Get the current CriticalSection, if there is one, or returns null if not. */
|
|
1080
|
+
getCriticalSection() {
|
|
1081
|
+
return this.#currentInputLocks.at(-1)?.getCriticalSection();
|
|
1082
|
+
}
|
|
1083
|
+
/** Is a gated slice running? The question `IoContext::hasCurrent()` answers upstream. */
|
|
1084
|
+
hasCurrent() {
|
|
1085
|
+
return this.#currentInputLocks.length > 0;
|
|
1086
|
+
}
|
|
1087
|
+
/**
|
|
1088
|
+
* ← `IoContext::isCurrent()` (`io-context.c++:1428-1430`), over the narrowed
|
|
1089
|
+
* scope `currentSlice` documents: true only while a synchronous body of THIS
|
|
1090
|
+
* context is on the JS stack.
|
|
1091
|
+
*
|
|
1092
|
+
* Distinct from `hasCurrent()` above, which asks whether this context holds a
|
|
1093
|
+
* lock at all — true throughout an outstanding held await, and true for a
|
|
1094
|
+
* parent whose slice is awaiting a facet while the facet's body runs. This one
|
|
1095
|
+
* is the question a shared global has to answer: is the code calling me this
|
|
1096
|
+
* actor's?
|
|
1097
|
+
*/
|
|
1098
|
+
isCurrentSlice() {
|
|
1099
|
+
return currentSlice === this;
|
|
1100
|
+
}
|
|
1101
|
+
/**
|
|
1102
|
+
* Record that user code just engaged this context's gate — an `awaitIo`, an
|
|
1103
|
+
* `entry` dispatch, a re-entry callback firing. No upstream analogue, because
|
|
1104
|
+
* upstream cannot lose the lock; here a continuation that awaits a promise
|
|
1105
|
+
* the runtime does not own comes back lockless, the throw lands at the next
|
|
1106
|
+
* storage call three layers later, and the gap between "where the code last
|
|
1107
|
+
* verifiably ran gated" and the throw site is exactly where the foreign await
|
|
1108
|
+
* hides. This is that first coordinate. Always on: the capture rides calls
|
|
1109
|
+
* that already allocate promise machinery, and a stack costs microseconds
|
|
1110
|
+
* against the diagnosis it replaces.
|
|
1111
|
+
*/
|
|
1112
|
+
noteGateUse(what, stack) {
|
|
1113
|
+
this.#lastGateUse = {
|
|
1114
|
+
what,
|
|
1115
|
+
stack,
|
|
1116
|
+
at: this.now()
|
|
1117
|
+
};
|
|
1118
|
+
}
|
|
1119
|
+
/**
|
|
1120
|
+
* The suffix `requireInputLock` appends when the invocation stack is empty:
|
|
1121
|
+
* where this context's gate was last engaged, and how long before the throw.
|
|
1122
|
+
*
|
|
1123
|
+
* "Last engaged" is the honest claim, not "this continuation's ancestor" —
|
|
1124
|
+
* once the offending chain went lockless the gate reopened, so another slice
|
|
1125
|
+
* may have run in between and be the note this reports. In practice the loss
|
|
1126
|
+
* is discovered within the same event storm and the note is the parent; when
|
|
1127
|
+
* it is not, an engagement of this actor moments earlier is still the right
|
|
1128
|
+
* neighbourhood to search.
|
|
1129
|
+
*/
|
|
1130
|
+
describeLostLock() {
|
|
1131
|
+
const use = this.#lastGateUse;
|
|
1132
|
+
if (use === void 0) return " (this context has never held its gate: the call arrived from outside any actor invocation)";
|
|
1133
|
+
const age = Math.round(this.now() - use.at);
|
|
1134
|
+
const stackSuffix = use.stack === void 0 ? "" : `, at:\n${use.stack}`;
|
|
1135
|
+
return ` (an await after the last gated point resumed from a promise the runtime does not own; the gate was last engaged by ${use.what} ${age}ms before this call${stackSuffix})`;
|
|
1136
|
+
}
|
|
1137
|
+
/**
|
|
1138
|
+
* ← `IoContext::getActorOrThrow()`. Upstream's throws when the request is not
|
|
1139
|
+
* an actor request; there is no such request here, so it is a plain accessor.
|
|
1140
|
+
*/
|
|
1141
|
+
getActorOrThrow() {
|
|
1142
|
+
return this.#actor;
|
|
1143
|
+
}
|
|
1144
|
+
/** ← `IoContext::now()` (`io-context.h:703`), which reads the same timer. */
|
|
1145
|
+
now() {
|
|
1146
|
+
return this.#timer.now();
|
|
1147
|
+
}
|
|
1148
|
+
/**
|
|
1149
|
+
* ← `IoContext::setTimeoutImpl` (`io-context.c++:885-899`), clamp included.
|
|
1150
|
+
*
|
|
1151
|
+
* The generator parameter is gone with `TimeoutId::Generator` — see
|
|
1152
|
+
* `TimeoutManager`'s header — so the signature is upstream's minus that one
|
|
1153
|
+
* argument.
|
|
1154
|
+
*/
|
|
1155
|
+
setTimeoutImpl(repeat, callback, msDelay) {
|
|
1156
|
+
const delay = msDelay <= 0 || Number.isNaN(msDelay) ? 0 : msDelay >= MAX_TIMEOUT_MS ? MAX_TIMEOUT_MS : Math.trunc(msDelay);
|
|
1157
|
+
return this.#timeouts.setTimeout(this, {
|
|
1158
|
+
repeat,
|
|
1159
|
+
msDelay: delay,
|
|
1160
|
+
callback
|
|
1161
|
+
});
|
|
1162
|
+
}
|
|
1163
|
+
/** ← `IoContext::clearTimeoutImpl` (`io-context.c++:901-903`). */
|
|
1164
|
+
clearTimeoutImpl(id) {
|
|
1165
|
+
this.#timeouts.clearTimeout(id);
|
|
1166
|
+
}
|
|
1167
|
+
/** ← `IoContext::getTimeoutCount` (`io-context.c++:905-907`). */
|
|
1168
|
+
getTimeoutCount() {
|
|
1169
|
+
return this.#timeouts.getTimeoutCount();
|
|
1170
|
+
}
|
|
1171
|
+
/**
|
|
1172
|
+
* Wait until all outstanding output locks have been unlocked. Does not wait for future
|
|
1173
|
+
* output locks, even if they are created before past locks are unlocked.
|
|
1174
|
+
*/
|
|
1175
|
+
waitForOutputLocks() {
|
|
1176
|
+
return this.#actor.getOutputGate().wait();
|
|
1177
|
+
}
|
|
1178
|
+
/**
|
|
1179
|
+
* Check if the output gate is currently broken. This indicates that there was a problem
|
|
1180
|
+
* with committing storage writes.
|
|
1181
|
+
*/
|
|
1182
|
+
isOutputGateBroken() {
|
|
1183
|
+
return this.#actor.getOutputGate().isBroken();
|
|
1184
|
+
}
|
|
1185
|
+
/** Lock output until the given promise completes. */
|
|
1186
|
+
lockOutputWhile(promise, signal) {
|
|
1187
|
+
return this.#actor.getOutputGate().lockWhile(promise, signal);
|
|
1188
|
+
}
|
|
1189
|
+
/**
|
|
1190
|
+
* Rejects if and when the context should be aborted, e.g. because a gate broke. This
|
|
1191
|
+
* promise never resolves, only rejects.
|
|
1192
|
+
*/
|
|
1193
|
+
onAbort() {
|
|
1194
|
+
return this.#abortPromise;
|
|
1195
|
+
}
|
|
1196
|
+
/** Force context abort now. */
|
|
1197
|
+
abort(exception) {
|
|
1198
|
+
if (this.#abortException !== void 0) return;
|
|
1199
|
+
this.#abortException = { exception };
|
|
1200
|
+
this.#actor.shutdownActorCache(exception);
|
|
1201
|
+
this.#timeouts.cancelAll();
|
|
1202
|
+
this.#rejectAbort(exception);
|
|
1203
|
+
}
|
|
1204
|
+
/**
|
|
1205
|
+
* Await the given promise and, if it throws, call `abort()` with the exception. The promise
|
|
1206
|
+
* given here should just be a monitoring promise, it should not represent any sort of
|
|
1207
|
+
* background work beyond monitoring.
|
|
1208
|
+
*/
|
|
1209
|
+
abortWhen(promise) {
|
|
1210
|
+
if (this.#abortException === void 0) this.#tasks.add(promise.then(() => {}, (exception) => {
|
|
1211
|
+
this.abort(exception);
|
|
1212
|
+
}));
|
|
1213
|
+
}
|
|
1214
|
+
/**
|
|
1215
|
+
* Arrange for the given promise to execute as part of this request.
|
|
1216
|
+
*
|
|
1217
|
+
* "In Actors, we treat all tasks as wait-until tasks, because it's perfectly legit to start
|
|
1218
|
+
* a task under one request and then expect some other request to handle it later." Every
|
|
1219
|
+
* context here is an actor context, so that branch is the only branch.
|
|
1220
|
+
*/
|
|
1221
|
+
addTask(promise) {
|
|
1222
|
+
++this.#addTaskCounter;
|
|
1223
|
+
this.addWaitUntil(promise);
|
|
1224
|
+
}
|
|
1225
|
+
/**
|
|
1226
|
+
* Indicates that the script has requested that it stay active until the given promise
|
|
1227
|
+
* resolves. `drainWaitUntil()` waits until all such promises have completed. Touches
|
|
1228
|
+
* neither gate (§1.9).
|
|
1229
|
+
*/
|
|
1230
|
+
addWaitUntil(promise) {
|
|
1231
|
+
this.#waitUntilTasks.add(promise);
|
|
1232
|
+
}
|
|
1233
|
+
/** Returns the number of times addTask() has been called (even if the tasks have completed). */
|
|
1234
|
+
taskCount() {
|
|
1235
|
+
return this.#addTaskCounter;
|
|
1236
|
+
}
|
|
1237
|
+
/**
|
|
1238
|
+
* The first exception a background task failed with, if any.
|
|
1239
|
+
*
|
|
1240
|
+
* ← `waitUntilStatus()`, which returns an `EventOutcome` derived from the exception by
|
|
1241
|
+
* `RequestObserver`. There is no observer here, so the exception itself is the status —
|
|
1242
|
+
* and keeping it is what stops a failed background task from being swallowed, since
|
|
1243
|
+
* upstream's other half of `taskFailed()` is a log this package has no port for.
|
|
1244
|
+
*/
|
|
1245
|
+
waitUntilStatus() {
|
|
1246
|
+
return this.#waitUntilStatus?.exception;
|
|
1247
|
+
}
|
|
1248
|
+
/**
|
|
1249
|
+
* ← `IncomingRequest::drain()`, actor branch. "For actors, all promises are canceled on
|
|
1250
|
+
* actor shutdown, not on a fixed timeout, because work doesn't necessarily happen on a
|
|
1251
|
+
* per-request basis in actors."
|
|
1252
|
+
*/
|
|
1253
|
+
async drainWaitUntil() {
|
|
1254
|
+
await Promise.race([this.#waitUntilTasks.onEmpty(), this.#abortPromise.catch(() => {})]);
|
|
1255
|
+
}
|
|
1256
|
+
/**
|
|
1257
|
+
* Run the given callback within this context, holding an input lock.
|
|
1258
|
+
*
|
|
1259
|
+
* ← the two `IoContext::run()` overloads: given a CriticalSection it waits on that, given
|
|
1260
|
+
* an already-held Lock it runs under it, and given neither it takes a fresh lock from the
|
|
1261
|
+
* gate. The third case is what a new external event does, and it is the reason inheritance
|
|
1262
|
+
* cannot be read from gate state — see `makeReentryCallback`.
|
|
1263
|
+
*/
|
|
1264
|
+
async run(func, ilOrCs) {
|
|
1265
|
+
const aborted = this.#abortException;
|
|
1266
|
+
if (aborted !== void 0) throw aborted.exception;
|
|
1267
|
+
let lock;
|
|
1268
|
+
if (ilOrCs === void 0) lock = await this.#actor.getInputGate().wait();
|
|
1269
|
+
else if (ilOrCs instanceof CriticalSection) lock = await ilOrCs.wait();
|
|
1270
|
+
else lock = ilOrCs;
|
|
1271
|
+
return await this.#runImpl(func, lock);
|
|
1272
|
+
}
|
|
1273
|
+
/**
|
|
1274
|
+
* Make a function which, when called, re-enters this IoContext to run some code.
|
|
1275
|
+
*
|
|
1276
|
+
* Upstream, on why the critical section travels with the callback at all:
|
|
1277
|
+
*
|
|
1278
|
+
* > "What if the call was made within blockConcurrencyWhile()? The callback will be blocked
|
|
1279
|
+
* > until the critical section ends, which could lead to deadlock if the critical section
|
|
1280
|
+
* > code is waiting on it? ... The callback is allowed to run within the critical section
|
|
1281
|
+
* > (blockConcurrencyWhile()) from which it was called."
|
|
1282
|
+
*
|
|
1283
|
+
* The section is read here, at the point of capture, and never on invocation: a new
|
|
1284
|
+
* external event that inherited the running section would skip the queue and
|
|
1285
|
+
* `blockConcurrencyWhile` would silently block nothing (Part 4, mechanic 2).
|
|
1286
|
+
*
|
|
1287
|
+
* The returned function can be called multiple times.
|
|
1288
|
+
*
|
|
1289
|
+
* It does not route through `io-gate.ts`'s `makeReentryCallback`, which is the same idea
|
|
1290
|
+
* expressed at the gate. Upstream's `IoContext::makeReentryCallback` is literally
|
|
1291
|
+
* `ctx.run(func, cs)`, and going through the gate helper instead would take a lock this
|
|
1292
|
+
* file then has to make current a second time. The gate copy stays: it is the shape a
|
|
1293
|
+
* consumer holding only a gate needs, and Section 1's tests cover it.
|
|
1294
|
+
*/
|
|
1295
|
+
makeReentryCallback(func) {
|
|
1296
|
+
this.#requireCurrent();
|
|
1297
|
+
const criticalSection = this.getCriticalSection();
|
|
1298
|
+
const registrationStack = captureGateStack();
|
|
1299
|
+
return async (...args) => {
|
|
1300
|
+
this.noteGateUse("a re-entry callback registered at the site below", registrationStack);
|
|
1301
|
+
const call = this.run((lock) => func(lock, ...args), criticalSection);
|
|
1302
|
+
this.addTask(call.then(() => {}, () => {}));
|
|
1303
|
+
return await call;
|
|
1304
|
+
};
|
|
1305
|
+
}
|
|
1306
|
+
awaitIo(promise, func = identity) {
|
|
1307
|
+
this.noteGateUse("awaitIo", captureGateStack());
|
|
1308
|
+
return this.#awaitIoImpl(promise, this.getCriticalSection(), func);
|
|
1309
|
+
}
|
|
1310
|
+
/**
|
|
1311
|
+
* The await transform's hot path. It preserves provenance without capturing
|
|
1312
|
+
* an Error stack for every await: the first call and every 64th call refresh
|
|
1313
|
+
* the sampled site, while intervening calls keep that stack and say so.
|
|
1314
|
+
*/
|
|
1315
|
+
awaitIoFromTransform(promise) {
|
|
1316
|
+
const shouldSample = this.#transformGateUses++ % 64 === 0 || this.#transformGateStack === void 0;
|
|
1317
|
+
if (shouldSample) this.#transformGateStack = captureGateStack();
|
|
1318
|
+
this.#lastGateUse = {
|
|
1319
|
+
what: shouldSample ? "a transformed await sampled at the site below" : "a transformed await (stack from the most recent sampled transformed await)",
|
|
1320
|
+
stack: this.#transformGateStack,
|
|
1321
|
+
at: this.now()
|
|
1322
|
+
};
|
|
1323
|
+
return this.#awaitIoImpl(promise, this.getCriticalSection(), identity);
|
|
1324
|
+
}
|
|
1325
|
+
awaitIoWithInputLock(promise, func = identity) {
|
|
1326
|
+
let inputLock;
|
|
1327
|
+
try {
|
|
1328
|
+
inputLock = this.getInputLock();
|
|
1329
|
+
} catch (exception) {
|
|
1330
|
+
return Promise.reject(exception);
|
|
1331
|
+
}
|
|
1332
|
+
this.noteGateUse("awaitIoWithInputLock", captureGateStack());
|
|
1333
|
+
return this.#awaitIoImpl(promise, inputLock, func);
|
|
1334
|
+
}
|
|
1335
|
+
/**
|
|
1336
|
+
* Runs `callback` within its own critical section, returning its final result. If
|
|
1337
|
+
* `callback` throws, the input lock will break, resetting the actor.
|
|
1338
|
+
*
|
|
1339
|
+
* Three behaviours live here rather than in `io-gate.ts`, which has no timer, and rather
|
|
1340
|
+
* than in `api/actor-state.ts`, whose own `blockConcurrencyWhile` is a one-line forward:
|
|
1341
|
+
* the 30-second deadline, the brokenness annotation, and the fact that on failure the
|
|
1342
|
+
* returned promise is never settled at all.
|
|
1343
|
+
*/
|
|
1344
|
+
blockConcurrencyWhile(callback) {
|
|
1345
|
+
const lock = this.getInputLock();
|
|
1346
|
+
this.noteGateUse("blockConcurrencyWhile", captureGateStack());
|
|
1347
|
+
const criticalSection = lock.startCriticalSection();
|
|
1348
|
+
const { promise: result, resolve } = Promise.withResolvers();
|
|
1349
|
+
this.addTask((async () => {
|
|
1350
|
+
try {
|
|
1351
|
+
const value = await this.#runCriticalSection(criticalSection, callback);
|
|
1352
|
+
await this.#runImpl(() => {
|
|
1353
|
+
resolve(value);
|
|
1354
|
+
}, criticalSection.succeeded());
|
|
1355
|
+
} catch (exception) {
|
|
1356
|
+
annotateInputGateBroken(exception);
|
|
1357
|
+
criticalSection.failed(exception);
|
|
1358
|
+
throw exception;
|
|
1359
|
+
} finally {
|
|
1360
|
+
criticalSection.drop();
|
|
1361
|
+
}
|
|
1362
|
+
})());
|
|
1363
|
+
lock.release();
|
|
1364
|
+
return result;
|
|
1365
|
+
}
|
|
1366
|
+
/**
|
|
1367
|
+
* ← `runImpl()` + `runInContextScope()`: check the lock belongs to this actor, make it the
|
|
1368
|
+
* current one, run, and let `KJ_DEFER` clear it.
|
|
1369
|
+
*
|
|
1370
|
+
* The defer fires after the microtask checkpoint, not when `func` returns — see
|
|
1371
|
+
* `atCheckpointEnd`. Everything else about the scope is isolate machinery with no port.
|
|
1372
|
+
*/
|
|
1373
|
+
async #runImpl(func, lock) {
|
|
1374
|
+
if (!lock.isFor(this.#actor.getInputGate())) throw new Error("IoContext::runImpl() was given a lock belonging to another actor");
|
|
1375
|
+
this.#currentInputLocks.push(lock);
|
|
1376
|
+
let result;
|
|
1377
|
+
const previousSlice = currentSlice;
|
|
1378
|
+
enterSlice(this);
|
|
1379
|
+
try {
|
|
1380
|
+
result = func(lock);
|
|
1381
|
+
} finally {
|
|
1382
|
+
enterSlice(previousSlice);
|
|
1383
|
+
atCheckpointEnd(() => {
|
|
1384
|
+
this.#exit(lock);
|
|
1385
|
+
});
|
|
1386
|
+
}
|
|
1387
|
+
return await result;
|
|
1388
|
+
}
|
|
1389
|
+
/**
|
|
1390
|
+
* ← `requireCurrent()`. Upstream asks whether this IoContext is the thread's current one;
|
|
1391
|
+
* with no thread-local there is only one question left, and it is the one every caller of
|
|
1392
|
+
* `requireCurrent()` actually depends on: is a gated slice running?
|
|
1393
|
+
*/
|
|
1394
|
+
#requireCurrent() {
|
|
1395
|
+
const lock = this.#currentInputLocks.at(-1);
|
|
1396
|
+
if (lock === void 0) throw new Error(`no input lock available in this context${this.describeLostLock()}`);
|
|
1397
|
+
return lock;
|
|
1398
|
+
}
|
|
1399
|
+
/** ← the far side of `runInContextScope`'s `KJ_DEFER`. */
|
|
1400
|
+
#exit(lock) {
|
|
1401
|
+
const at = this.#currentInputLocks.lastIndexOf(lock);
|
|
1402
|
+
if (at < 0) throw new Error("IoContext invocation stack lost a lock it was holding");
|
|
1403
|
+
this.#currentInputLocks.splice(at, 1);
|
|
1404
|
+
lock.release();
|
|
1405
|
+
}
|
|
1406
|
+
/**
|
|
1407
|
+
* ← `awaitIoImpl()`.
|
|
1408
|
+
*
|
|
1409
|
+
* The KJ-side rejection is merged into the value so a single continuation handles both, the
|
|
1410
|
+
* continuation re-enters through `run(func, ilOrCs)`, and the whole thing rides `addTask()`.
|
|
1411
|
+
* When `ilOrCs` is a Lock this is `awaitIoWithInputLock` and the gate never opened; when it
|
|
1412
|
+
* is a CriticalSection or nothing this is `awaitIo` and the resumption queues for a fresh
|
|
1413
|
+
* lock like any other event.
|
|
1414
|
+
*/
|
|
1415
|
+
#awaitIoImpl(promise, ilOrCs, func) {
|
|
1416
|
+
const { promise: result, resolve, reject } = Promise.withResolvers();
|
|
1417
|
+
this.addTask(promiseForExceptionOrT(promise).then(async (outcome) => {
|
|
1418
|
+
try {
|
|
1419
|
+
await this.run(() => {
|
|
1420
|
+
if (outcome.ok) try {
|
|
1421
|
+
resolve(func(outcome.value));
|
|
1422
|
+
} catch (exception) {
|
|
1423
|
+
reject(exception);
|
|
1424
|
+
}
|
|
1425
|
+
else reject(outcome.exception);
|
|
1426
|
+
}, ilOrCs);
|
|
1427
|
+
} catch (exception) {
|
|
1428
|
+
if (ilOrCs instanceof Lock) ilOrCs.release();
|
|
1429
|
+
throw exception;
|
|
1430
|
+
}
|
|
1431
|
+
}));
|
|
1432
|
+
return result;
|
|
1433
|
+
}
|
|
1434
|
+
/**
|
|
1435
|
+
* ← the first `.then()` of `blockConcurrencyWhile`: start the section, run the callback
|
|
1436
|
+
* under its first nested lock, and race the deadline.
|
|
1437
|
+
*/
|
|
1438
|
+
async #runCriticalSection(criticalSection, callback) {
|
|
1439
|
+
const inputLock = await criticalSection.wait();
|
|
1440
|
+
return await this.#runImpl((lock) => {
|
|
1441
|
+
const running = callback(lock);
|
|
1442
|
+
const deadline = new AbortController();
|
|
1443
|
+
const timeout = this.#timer.afterDelay(BLOCK_CONCURRENCY_WHILE_TIMEOUT_MS, deadline.signal).then(() => {
|
|
1444
|
+
throw new Error(BLOCK_CONCURRENCY_WHILE_TIMEOUT_MESSAGE);
|
|
1445
|
+
});
|
|
1446
|
+
return Promise.race([Promise.resolve(running), timeout]).finally(() => {
|
|
1447
|
+
deadline.abort();
|
|
1448
|
+
});
|
|
1449
|
+
}, inputLock);
|
|
1450
|
+
}
|
|
1451
|
+
/** ← `IoContext::taskFailed()`, minus the logging half, which has no port. */
|
|
1452
|
+
#taskFailed(exception) {
|
|
1453
|
+
if (this.#waitUntilStatus === void 0) this.#waitUntilStatus = { exception };
|
|
1454
|
+
}
|
|
1455
|
+
};
|
|
1456
|
+
//#endregion
|
|
1457
|
+
export { isExceptionFromInputGateBroken as a, tryCurrentSlice as c, hasUserErrorDetail as i, InputGate as l, atCheckpointEnd as n, requireInputLock as o, captureGateStack as r, setUserErrorDetail as s, IoContext as t, OutputGate as u };
|
|
1458
|
+
|
|
1459
|
+
//# sourceMappingURL=io-context-Ci3Rf6U5.js.map
|