@nimbus-sh/fabric 0.8.0 → 0.10.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/README.md +98 -11
- package/dist/bindings.d.ts +31 -33
- package/dist/bindings.d.ts.map +1 -1
- package/dist/bindings.js +108 -97
- package/dist/budgets.d.ts +102 -29
- package/dist/budgets.d.ts.map +1 -1
- package/dist/budgets.js +266 -44
- package/dist/do-calls.d.ts +20 -0
- package/dist/do-calls.d.ts.map +1 -1
- package/dist/do-calls.js +24 -11
- package/dist/fanout.d.ts +40 -32
- package/dist/fanout.d.ts.map +1 -1
- package/dist/fanout.js +48 -51
- package/dist/fenced-work.d.ts +3 -3
- package/dist/fenced-work.js +3 -3
- package/dist/host-wasm.d.ts +29 -0
- package/dist/host-wasm.d.ts.map +1 -0
- package/dist/host-wasm.js +31 -0
- package/dist/image-store.d.ts +1 -1
- package/dist/image-store.d.ts.map +1 -1
- package/dist/image-store.js +33 -1
- package/dist/inner-do-env.d.ts +83 -0
- package/dist/inner-do-env.d.ts.map +1 -0
- package/dist/inner-do-env.js +181 -0
- package/dist/isolate-pool.d.ts +40 -23
- package/dist/isolate-pool.d.ts.map +1 -1
- package/dist/isolate-pool.js +105 -55
- package/dist/process-fabric.d.ts +26 -11
- package/dist/process-fabric.d.ts.map +1 -1
- package/dist/process-fabric.js +44 -0
- package/dist/timers.d.ts +12 -0
- package/dist/timers.d.ts.map +1 -1
- package/dist/timers.js +44 -9
- package/dist/vendor/types.d.ts +11 -5
- package/dist/vendor/types.d.ts.map +1 -1
- package/dist/workerd-facet-host.d.ts.map +1 -1
- package/dist/workerd-facet-host.js +35 -30
- package/package.json +4 -4
- package/src/bindings.ts +121 -98
- package/src/budgets.ts +311 -53
- package/src/do-calls.ts +45 -11
- package/src/fanout.ts +62 -53
- package/src/fenced-work.ts +3 -3
- package/src/host-wasm.ts +41 -0
- package/src/image-store.ts +29 -2
- package/src/inner-do-env.ts +213 -0
- package/src/isolate-pool.ts +145 -75
- package/src/process-fabric.ts +55 -13
- package/src/timers.ts +45 -9
- package/src/vendor/types.ts +11 -5
- package/src/workerd-facet-host.ts +36 -31
package/src/isolate-pool.ts
CHANGED
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
* running 67 npm tarball extractions (cold-start dominates). We pin
|
|
8
8
|
* each job to `slot = cursor % concurrency` and use stable loader
|
|
9
9
|
* IDs `nfp:${fnHash}:slot-${i}:g${generation}`, so a pool of
|
|
10
|
-
* concurrency=
|
|
10
|
+
* concurrency=N keeps at most N warm isolates rather than one per job.
|
|
11
11
|
* 2. **Nimbus defaults**: compatibilityDate = CF_COMPAT_DATE (matches
|
|
12
|
-
* the supervisor worker), compatibilityFlags =
|
|
12
|
+
* the supervisor worker), compatibilityFlags = GUEST_COMPAT_FLAGS,
|
|
13
13
|
* globalOutbound = undefined (inherit parent network so the facet can
|
|
14
14
|
* reach https://registry.npmjs.org without a proxy binding).
|
|
15
15
|
* 3. **Supervisor autoinjection**. The pool grabs the embedder's
|
|
@@ -24,12 +24,18 @@
|
|
|
24
24
|
* and binding types used by this implementation.
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
|
-
import { CF_COMPAT_DATE } from '@nimbus-sh/core/constants.js';
|
|
27
|
+
import { CF_COMPAT_DATE, GUEST_COMPAT_FLAGS } from '@nimbus-sh/core/constants.js';
|
|
28
28
|
import { supervisorEntrypoint, type HostRoute } from './composition.js';
|
|
29
29
|
import { supervisorBindingProps, supervisorLoaderKey } from './supervisor-props.js';
|
|
30
30
|
import { disposeRpcResource } from '@nimbus-sh/platform/rpc-dispose.js';
|
|
31
31
|
import { serializeFunction, hashSource } from './vendor/serialize.js';
|
|
32
|
-
import {
|
|
32
|
+
import {
|
|
33
|
+
beginLoaderFetch,
|
|
34
|
+
beginLoaderFetchWhenFree,
|
|
35
|
+
withDynamicWorkerCapNamed,
|
|
36
|
+
type DynamicWorkerClaim,
|
|
37
|
+
type EndLoaderFetch,
|
|
38
|
+
} from './budgets.js';
|
|
33
39
|
import { assertModuleMapWithinCodeLimit } from './budgets.js';
|
|
34
40
|
import { recordFailure, setLastFacetId, getLastRpcFrame } from '@nimbus-sh/platform/oom-discriminator.js';
|
|
35
41
|
import { classifyError } from '@nimbus-sh/platform/oom-classify.js';
|
|
@@ -41,6 +47,7 @@ import {
|
|
|
41
47
|
} from './vendor/errors.js';
|
|
42
48
|
import type { FacetBindings } from '@nimbus-sh/core/runtime/facet-host.js';
|
|
43
49
|
import type { ModuleContent, WorkerLoader } from './vendor/types.js';
|
|
50
|
+
import { hostWasmIdentity } from './host-wasm.js';
|
|
44
51
|
|
|
45
52
|
/**
|
|
46
53
|
* A function dispatched into a facet isolate, with the bindings that facet was
|
|
@@ -62,8 +69,18 @@ export interface IsolatePoolEnv {
|
|
|
62
69
|
|
|
63
70
|
/** Options handed to IsolatePool's constructor. */
|
|
64
71
|
export interface IsolatePoolOptions {
|
|
65
|
-
/**
|
|
72
|
+
/**
|
|
73
|
+
* Maximum concurrent in-flight facets, each a distinct Dynamic Worker
|
|
74
|
+
* spent from the hosting DO's `DO_DYNAMIC_WORKER_LIMIT`. Default 1; a
|
|
75
|
+
* caller that wants more sizes it against that budget (Fanout does).
|
|
76
|
+
*/
|
|
66
77
|
concurrency?: number;
|
|
78
|
+
/**
|
|
79
|
+
* The width the caller claimed for this pool's dispatches
|
|
80
|
+
* (`claimDynamicWorkers`): they are held inside it rather than on top of
|
|
81
|
+
* it, and a refused one waits for a slot of the claim.
|
|
82
|
+
*/
|
|
83
|
+
claim?: DynamicWorkerClaim;
|
|
67
84
|
/** Per-task timeout in ms. Default 60_000. */
|
|
68
85
|
timeoutMs?: number;
|
|
69
86
|
/**
|
|
@@ -157,15 +174,19 @@ export interface IsolatePoolOptions {
|
|
|
157
174
|
/**
|
|
158
175
|
* WebAssembly modules to ship into the facet via the LOADER's
|
|
159
176
|
* `modules` map. Map keys are module specifier paths (e.g.
|
|
160
|
-
* `'esbuild.wasm'`); values are the raw bytes
|
|
177
|
+
* `'esbuild.wasm'`); values are the raw bytes, or a module the host
|
|
178
|
+
* already holds compiled.
|
|
161
179
|
*
|
|
162
|
-
* Workerd registers each entry as `{ wasm
|
|
163
|
-
*
|
|
180
|
+
* Workerd registers each entry as `{ wasm }` in the worker's modules
|
|
181
|
+
* map. The pool prepends a static
|
|
164
182
|
* `import __NIMBUS_WASM_<id> from './<key>';` to the generated
|
|
165
|
-
* worker.js
|
|
166
|
-
* where wasm code generation is permitted)
|
|
167
|
-
*
|
|
168
|
-
*
|
|
183
|
+
* worker.js; bytes are compiled at the facet's module-load (startup
|
|
184
|
+
* phase, where wasm code generation is permitted), and a compiled
|
|
185
|
+
* module is shared with the facet as is, its compiled code included
|
|
186
|
+
* (workerd src/workerd/api/worker-loader.c++,
|
|
187
|
+
* extractWasmModuleContent). The Modules are exposed via
|
|
188
|
+
* `globalThis.__NIMBUS_WASM[<key>]` for the user function to read at
|
|
189
|
+
* request time.
|
|
169
190
|
*
|
|
170
191
|
* Why this works when other paths don't:
|
|
171
192
|
* - request-time `WebAssembly.compile()` — disallowed by workerd
|
|
@@ -174,15 +195,15 @@ export interface IsolatePoolOptions {
|
|
|
174
195
|
* structured-clone refuses ("Unable to deserialize cloned data").
|
|
175
196
|
* - inlining bytes in the preamble — 16 MiB string per dispatch
|
|
176
197
|
* OOMs the supervisor at module-source allocation time.
|
|
177
|
-
* - LOADER modules-map (this) —
|
|
178
|
-
* blob
|
|
179
|
-
*
|
|
198
|
+
* - LOADER modules-map (this) — the module rides INSIDE the worker
|
|
199
|
+
* code blob, never crossing structured-clone, never executing JS
|
|
200
|
+
* eval.
|
|
180
201
|
*
|
|
181
|
-
*
|
|
182
|
-
*
|
|
183
|
-
*
|
|
202
|
+
* A compiled module must be described (host-wasm.ts describeHostWasm):
|
|
203
|
+
* its identity keys warm slots, as the bytes' fingerprint does, and its
|
|
204
|
+
* size counts toward the dynamic-worker code limit.
|
|
184
205
|
*/
|
|
185
|
-
wasmModules?: Record<string, ArrayBuffer>;
|
|
206
|
+
wasmModules?: Record<string, ArrayBuffer | WebAssembly.Module>;
|
|
186
207
|
}
|
|
187
208
|
|
|
188
209
|
/** Per-call override (merged with pool defaults). */
|
|
@@ -240,6 +261,15 @@ interface ResolvedResilience {
|
|
|
240
261
|
retries: number;
|
|
241
262
|
}
|
|
242
263
|
|
|
264
|
+
/**
|
|
265
|
+
* How long one call waits, in all, on the Dynamic Worker ledger after
|
|
266
|
+
* "Dynamic worker concurrency limit exceeded" before the refusal surfaces
|
|
267
|
+
* (beginLoaderFetchWhenFree: let in when a hold ends or the refusal's pause
|
|
268
|
+
* passes). A deployed Durable Object admitted the refused batch after a 6 s
|
|
269
|
+
* pause; 15 s bounds a call that would never be admitted.
|
|
270
|
+
*/
|
|
271
|
+
const CAP_REFUSAL_WAIT_MS = 15_000;
|
|
272
|
+
|
|
243
273
|
/**
|
|
244
274
|
* esbuild runtime helpers re-declared at the top of every generated facet
|
|
245
275
|
* module. esbuild emits `__name(fn, "fn")` wrappers around every named
|
|
@@ -367,7 +397,7 @@ export function assembleLoaderWorkerModuleSource(
|
|
|
367
397
|
* Typical use:
|
|
368
398
|
*
|
|
369
399
|
* const pool = new IsolatePool(env, ctx, {
|
|
370
|
-
* concurrency:
|
|
400
|
+
* concurrency: 2,
|
|
371
401
|
* tag: 'npm-install',
|
|
372
402
|
* });
|
|
373
403
|
* const results = await pool.map(
|
|
@@ -379,6 +409,8 @@ export class IsolatePool {
|
|
|
379
409
|
private readonly loader: WorkerLoader;
|
|
380
410
|
/** The hosting actor, as the loader budget ledger's per-DO key. */
|
|
381
411
|
private readonly ctx: DurableObjectState;
|
|
412
|
+
/** The width this pool's dispatches are held inside (IsolatePoolOptions.claim). */
|
|
413
|
+
private readonly claim: DynamicWorkerClaim | undefined;
|
|
382
414
|
private readonly concurrency: number;
|
|
383
415
|
private readonly defaultTimeoutMs: number;
|
|
384
416
|
private readonly defaultRetries: number;
|
|
@@ -412,13 +444,13 @@ export class IsolatePool {
|
|
|
412
444
|
/** Identifier used inside the generated worker for both the static
|
|
413
445
|
* import binding and the globalThis exposure. Sanitised from `name`. */
|
|
414
446
|
id: string;
|
|
415
|
-
|
|
447
|
+
wasm: ArrayBuffer | WebAssembly.Module;
|
|
416
448
|
}>;
|
|
417
|
-
/** Hash of
|
|
418
|
-
*
|
|
419
|
-
*
|
|
420
|
-
*
|
|
421
|
-
*
|
|
449
|
+
/** Hash of every constructor-time wasm module, folded into the loader
|
|
450
|
+
* cache key so changes invalidate warm slots: a compiled module by the
|
|
451
|
+
* identity its host described, bytes by name + length + first/last
|
|
452
|
+
* byte. Hashing the FULL bytes would be O(20+ MiB) per dispatch and is
|
|
453
|
+
* unnecessary — they are pinned at deploy time. */
|
|
422
454
|
private readonly wasmHash: string;
|
|
423
455
|
/**
|
|
424
456
|
* Short prefix of the owning DO's id, baked into the loader.get()
|
|
@@ -457,7 +489,8 @@ export class IsolatePool {
|
|
|
457
489
|
}
|
|
458
490
|
this.loader = loader;
|
|
459
491
|
this.ctx = ctx;
|
|
460
|
-
this.
|
|
492
|
+
this.claim = opts?.claim;
|
|
493
|
+
this.concurrency = Math.max(1, opts?.concurrency ?? 1);
|
|
461
494
|
this.defaultTimeoutMs = opts?.timeoutMs ?? 60_000;
|
|
462
495
|
this.defaultRetries = Math.max(0, opts?.retries ?? 0);
|
|
463
496
|
this.tag = opts?.tag ?? 'facet';
|
|
@@ -475,17 +508,36 @@ export class IsolatePool {
|
|
|
475
508
|
// (e.g. 'esbuild.wasm' and 'esbuild_wasm' both sanitise to
|
|
476
509
|
// 'esbuild_wasm') are rejected loudly because the generated worker
|
|
477
510
|
// would otherwise have duplicate imports. Order is preserved.
|
|
478
|
-
const wasmEntries: Array<{ name: string; id: string;
|
|
511
|
+
const wasmEntries: Array<{ name: string; id: string; wasm: ArrayBuffer | WebAssembly.Module }> = [];
|
|
512
|
+
const fingerprints: string[] = [];
|
|
479
513
|
const seenIds = new Set<string>();
|
|
480
514
|
if (opts?.wasmModules) {
|
|
481
|
-
for (const [name,
|
|
482
|
-
|
|
515
|
+
for (const [name, wasm] of Object.entries(opts.wasmModules)) {
|
|
516
|
+
let fingerprint: string;
|
|
517
|
+
if (wasm instanceof ArrayBuffer) {
|
|
518
|
+
// Name + length + first/last byte: hashing 20+ MiB of wasm per
|
|
519
|
+
// dispatch would be wasteful, and these bytes change only with
|
|
520
|
+
// the deployed bundle.
|
|
521
|
+
const u = new Uint8Array(wasm);
|
|
522
|
+
const len = u.byteLength;
|
|
523
|
+
fingerprint = `${name}:${len}:${len > 0 ? u[0] : 0}:${len > 0 ? u[len - 1] : 0}`;
|
|
524
|
+
} else if (wasm instanceof WebAssembly.Module) {
|
|
525
|
+
const identity = hostWasmIdentity(wasm);
|
|
526
|
+
if (!identity) {
|
|
527
|
+
throw new BindingError(
|
|
528
|
+
`IsolatePool: wasmModules['${name}'] is a WebAssembly.Module nobody described; ` +
|
|
529
|
+
'pass it through describeHostWasm (@nimbus-sh/fabric/host-wasm.js) so warm slots ' +
|
|
530
|
+
'can be keyed by it and the code limit can count it.',
|
|
531
|
+
);
|
|
532
|
+
}
|
|
533
|
+
fingerprint = `${name}:host:${identity.id}:${identity.bytes}`;
|
|
534
|
+
} else {
|
|
483
535
|
// Reached only when a caller broke the declared option type, so the
|
|
484
|
-
// value is whatever it really was rather than the
|
|
485
|
-
const got = (
|
|
536
|
+
// value is whatever it really was rather than the union here.
|
|
537
|
+
const got = (wasm as { constructor?: { name?: string } } | null | undefined)?.constructor?.name;
|
|
486
538
|
throw new BindingError(
|
|
487
|
-
`IsolatePool: wasmModules['${name}'] must be ArrayBuffer ` +
|
|
488
|
-
`(got ${got || typeof
|
|
539
|
+
`IsolatePool: wasmModules['${name}'] must be an ArrayBuffer or a WebAssembly.Module ` +
|
|
540
|
+
`(got ${got || typeof wasm}).`,
|
|
489
541
|
);
|
|
490
542
|
}
|
|
491
543
|
const id = name.replace(/[^A-Za-z0-9_]/g, '_').replace(/^[^A-Za-z_]/, '_');
|
|
@@ -496,28 +548,12 @@ export class IsolatePool {
|
|
|
496
548
|
);
|
|
497
549
|
}
|
|
498
550
|
seenIds.add(id);
|
|
499
|
-
wasmEntries.push({ name, id,
|
|
551
|
+
wasmEntries.push({ name, id, wasm });
|
|
552
|
+
fingerprints.push(fingerprint);
|
|
500
553
|
}
|
|
501
554
|
}
|
|
502
555
|
this.wasmModules = wasmEntries;
|
|
503
|
-
|
|
504
|
-
// Hashing 20+ MiB of wasm per dispatch would be wasteful; this
|
|
505
|
-
// fingerprint is bytes-stable for a given deployed bundle and only
|
|
506
|
-
// changes when the wasm itself changes (deploy-time event).
|
|
507
|
-
if (wasmEntries.length === 0) {
|
|
508
|
-
this.wasmHash = '0';
|
|
509
|
-
} else {
|
|
510
|
-
const fp = wasmEntries
|
|
511
|
-
.map((w) => {
|
|
512
|
-
const u = new Uint8Array(w.bytes);
|
|
513
|
-
const len = u.byteLength;
|
|
514
|
-
const first = len > 0 ? u[0] : 0;
|
|
515
|
-
const last = len > 0 ? u[len - 1] : 0;
|
|
516
|
-
return `${w.name}:${len}:${first}:${last}`;
|
|
517
|
-
})
|
|
518
|
-
.join('|');
|
|
519
|
-
this.wasmHash = hashSource(fp);
|
|
520
|
-
}
|
|
556
|
+
this.wasmHash = fingerprints.length === 0 ? '0' : hashSource(fingerprints.join('|'));
|
|
521
557
|
|
|
522
558
|
const bindings: Record<string, unknown> = { ...(opts?.extraBindings ?? {}) };
|
|
523
559
|
this.supervisorKey = 's-none';
|
|
@@ -579,9 +615,9 @@ export class IsolatePool {
|
|
|
579
615
|
*/
|
|
580
616
|
#materialisePerCallWasm(
|
|
581
617
|
perCall: Record<string, ArrayBuffer> | undefined,
|
|
582
|
-
): Array<{ name: string; id: string;
|
|
618
|
+
): Array<{ name: string; id: string; wasm: ArrayBuffer }> {
|
|
583
619
|
if (!perCall) return [];
|
|
584
|
-
const out: Array<{ name: string; id: string;
|
|
620
|
+
const out: Array<{ name: string; id: string; wasm: ArrayBuffer }> = [];
|
|
585
621
|
const ctorIds = new Set(this.wasmModules.map((w) => w.id));
|
|
586
622
|
const seen = new Set<string>();
|
|
587
623
|
for (const [name, bytes] of Object.entries(perCall)) {
|
|
@@ -607,7 +643,7 @@ export class IsolatePool {
|
|
|
607
643
|
);
|
|
608
644
|
}
|
|
609
645
|
seen.add(id);
|
|
610
|
-
out.push({ name, id, bytes });
|
|
646
|
+
out.push({ name, id, wasm: bytes });
|
|
611
647
|
}
|
|
612
648
|
return out;
|
|
613
649
|
}
|
|
@@ -640,12 +676,12 @@ export class IsolatePool {
|
|
|
640
676
|
* hashing the wasm were marginal; the correctness cost was severe.
|
|
641
677
|
*/
|
|
642
678
|
#fingerprintWasm(
|
|
643
|
-
entries: Array<{ name: string;
|
|
679
|
+
entries: Array<{ name: string; wasm: ArrayBuffer }>,
|
|
644
680
|
): string {
|
|
645
681
|
if (entries.length === 0) return '0';
|
|
646
682
|
const parts: string[] = [];
|
|
647
683
|
for (const w of entries) {
|
|
648
|
-
const u = new Uint8Array(w.
|
|
684
|
+
const u = new Uint8Array(w.wasm);
|
|
649
685
|
const len = u.byteLength;
|
|
650
686
|
// djb2 over the bytes. Faster than crypto.subtle.digest at small
|
|
651
687
|
// sizes, deterministic, and good enough for cache-key
|
|
@@ -672,11 +708,11 @@ export class IsolatePool {
|
|
|
672
708
|
*/
|
|
673
709
|
#buildCode(
|
|
674
710
|
fnSource: string,
|
|
675
|
-
perCallWasmEntries?: Array<{ name: string; id: string;
|
|
711
|
+
perCallWasmEntries?: Array<{ name: string; id: string; wasm: ArrayBuffer }>,
|
|
676
712
|
) {
|
|
677
713
|
const workerOpts = {
|
|
678
714
|
compatibilityDate: CF_COMPAT_DATE,
|
|
679
|
-
compatibilityFlags: [
|
|
715
|
+
compatibilityFlags: [...GUEST_COMPAT_FLAGS],
|
|
680
716
|
// Inherit parent network so the facet can reach registry.npmjs.org.
|
|
681
717
|
globalOutbound: undefined,
|
|
682
718
|
env: this.bindings,
|
|
@@ -722,7 +758,7 @@ export class IsolatePool {
|
|
|
722
758
|
// (matters only for human-readable diffs; workerd doesn't care).
|
|
723
759
|
const modules: Record<string, ModuleContent> = { 'worker.js': moduleSource };
|
|
724
760
|
for (const w of allWasmEntries) {
|
|
725
|
-
modules[w.name] = { wasm: w.
|
|
761
|
+
modules[w.name] = { wasm: w.wasm };
|
|
726
762
|
}
|
|
727
763
|
assertModuleMapWithinCodeLimit(modules);
|
|
728
764
|
|
|
@@ -758,7 +794,7 @@ export class IsolatePool {
|
|
|
758
794
|
invoke: (entrypoint: { execute(...args: unknown[]): Promise<unknown>; fetch(input: RequestInfo, init?: RequestInit): Promise<Response> }, attempt: number) => Promise<T>,
|
|
759
795
|
resilience: ResolvedResilience,
|
|
760
796
|
perCallWasm?: Record<string, ArrayBuffer>,
|
|
761
|
-
|
|
797
|
+
signal?: AbortSignal,
|
|
762
798
|
): Promise<T> {
|
|
763
799
|
// A warm slot executes one dispatch at a time: queue behind the
|
|
764
800
|
// previous owner, then record this dispatch as the new tail. The
|
|
@@ -776,7 +812,7 @@ export class IsolatePool {
|
|
|
776
812
|
}
|
|
777
813
|
const inFlight: Promise<unknown>[] = [];
|
|
778
814
|
try {
|
|
779
|
-
return await this.#dispatchSlotOwned(fnSource, fnHash, slotIndex, invoke, resilience, perCallWasm, inFlight,
|
|
815
|
+
return await this.#dispatchSlotOwned(fnSource, fnHash, slotIndex, invoke, resilience, perCallWasm, inFlight, signal);
|
|
780
816
|
} finally {
|
|
781
817
|
// Do not delay the caller's own outcome — the tail releases when
|
|
782
818
|
// the RPCs the body launched have actually settled.
|
|
@@ -793,7 +829,7 @@ export class IsolatePool {
|
|
|
793
829
|
resilience: ResolvedResilience,
|
|
794
830
|
perCallWasm: Record<string, ArrayBuffer> | undefined,
|
|
795
831
|
inFlight: Promise<unknown>[],
|
|
796
|
-
|
|
832
|
+
signal?: AbortSignal,
|
|
797
833
|
): Promise<T> {
|
|
798
834
|
// Per-call wasm fingerprint. Mixed into the cache key so two calls
|
|
799
835
|
// with different bytes hit different slots (no cache poisoning).
|
|
@@ -817,6 +853,9 @@ export class IsolatePool {
|
|
|
817
853
|
// slot updated on every dispatch.
|
|
818
854
|
try { setLastFacetId(id, slotIndex); } catch { /* best-effort */ }
|
|
819
855
|
|
|
856
|
+
// A hold the ledger already took for the next attempt, when a refused
|
|
857
|
+
// call waited on it for room; otherwise the attempt begins its own.
|
|
858
|
+
let admitted: EndLoaderFetch | undefined;
|
|
820
859
|
const runOnce = async (): Promise<T> => {
|
|
821
860
|
// loader.get() is synchronous from the caller's POV; the callback
|
|
822
861
|
// is only invoked on cache miss. We wrap the callback tightly so a
|
|
@@ -842,15 +881,20 @@ export class IsolatePool {
|
|
|
842
881
|
// fine and stays — it only tears down the long-lived SUPERVISOR
|
|
843
882
|
// binding stub once the whole pool is done, which does NOT
|
|
844
883
|
// invalidate any in-flight slot's entrypoint reference.
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
const endFetch = beginLoaderFetch(this.ctx);
|
|
884
|
+
//
|
|
885
|
+
// The hold comes first, so it ends whatever setup throws: a retry's
|
|
886
|
+
// was taken when the ledger let it in.
|
|
887
|
+
const endFetch = admitted ?? beginLoaderFetch(this.ctx, id, this.claim);
|
|
888
|
+
admitted = undefined;
|
|
851
889
|
try {
|
|
890
|
+
const stub = this.loader.get(id, async () => code);
|
|
891
|
+
const entrypoint = stub.getEntrypoint();
|
|
892
|
+
// Direct property call, awaited by this frame — bracketed, never
|
|
893
|
+
// wrapped. See beginLoaderFetch for the measured DO-poisoning hazard.
|
|
852
894
|
return await invoke(entrypoint, attempt);
|
|
853
895
|
} catch (err) {
|
|
896
|
+
// The ledger learns a limit refusal from the hold it ends.
|
|
897
|
+
endFetch(err);
|
|
854
898
|
if (err instanceof Error) {
|
|
855
899
|
throw new ExecutionError(err.message, err.stack);
|
|
856
900
|
}
|
|
@@ -863,6 +907,8 @@ export class IsolatePool {
|
|
|
863
907
|
const maxAttempts = 1 + resilience.retries;
|
|
864
908
|
let lastError: Error | undefined;
|
|
865
909
|
let retriedCloneRefusal = false;
|
|
910
|
+
// When this call's waits for room after a limit refusal run out.
|
|
911
|
+
let capDeadline: number | undefined;
|
|
866
912
|
let attempt = 0;
|
|
867
913
|
while (attempt < maxAttempts) {
|
|
868
914
|
try {
|
|
@@ -921,7 +967,7 @@ export class IsolatePool {
|
|
|
921
967
|
message: lastError.message,
|
|
922
968
|
});
|
|
923
969
|
} catch { /* fail-soft */ }
|
|
924
|
-
if (
|
|
970
|
+
if (signal?.aborted) {
|
|
925
971
|
// The caller aborted this dispatch's Request. workerd cancelled
|
|
926
972
|
// the isolate's execution context wherever it was suspended —
|
|
927
973
|
// mid-syscall, mid-stream — so the interpreter's heap may hold
|
|
@@ -944,6 +990,30 @@ export class IsolatePool {
|
|
|
944
990
|
// reverse direction. This refresh targets the stale-loader case.
|
|
945
991
|
continue;
|
|
946
992
|
}
|
|
993
|
+
if (cause === 'dynamic_worker_cap') {
|
|
994
|
+
// The platform refused to start this call. Nothing ran, so the
|
|
995
|
+
// call waits, as the platform asks, and is sent again without
|
|
996
|
+
// spending an attempt: on the ledger, which lets it in when a
|
|
997
|
+
// hold ends, or when the pause this refusal started has passed
|
|
998
|
+
// (the platform still counts a worker the ledger has given back:
|
|
999
|
+
// a fan-out's workers stay counted for a moment after their
|
|
1000
|
+
// calls return).
|
|
1001
|
+
capDeadline ??= Date.now() + CAP_REFUSAL_WAIT_MS;
|
|
1002
|
+
const remainingMs = capDeadline - Date.now();
|
|
1003
|
+
if (remainingMs > 0) {
|
|
1004
|
+
// The deadline's timer is cleared once the wait settles: a
|
|
1005
|
+
// pending timer keeps the hosting object from hibernating.
|
|
1006
|
+
const deadline = new AbortController();
|
|
1007
|
+
const timer = setTimeout(() => deadline.abort(), remainingMs);
|
|
1008
|
+
const waitFor = signal ? AbortSignal.any([deadline.signal, signal]) : deadline.signal;
|
|
1009
|
+
admitted = await beginLoaderFetchWhenFree(this.ctx, id, { claim: this.claim, signal: waitFor })
|
|
1010
|
+
.catch(() => undefined)
|
|
1011
|
+
.finally(() => clearTimeout(timer));
|
|
1012
|
+
if (admitted) continue;
|
|
1013
|
+
// The caller aborted while waiting: surface its abort as above.
|
|
1014
|
+
if (signal?.aborted) throw lastError;
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
947
1017
|
if (attempt < maxAttempts - 1) {
|
|
948
1018
|
// 100 * 2^attempt, capped at 2s so retries don't compound waiting.
|
|
949
1019
|
const delay = Math.min(2000, 100 * Math.pow(2, attempt));
|
|
@@ -952,9 +1022,9 @@ export class IsolatePool {
|
|
|
952
1022
|
attempt++;
|
|
953
1023
|
}
|
|
954
1024
|
}
|
|
955
|
-
// On the way out only, so retries do not stack the annotation: a
|
|
956
|
-
// carries the ledger — which
|
|
957
|
-
//
|
|
1025
|
+
// On the way out only, so retries do not stack the annotation: a limit
|
|
1026
|
+
// hit carries the ledger — which workers were in flight — instead of the
|
|
1027
|
+
// platform's bare message.
|
|
958
1028
|
const named = withDynamicWorkerCapNamed(this.ctx, lastError!);
|
|
959
1029
|
if (maxAttempts > 1) {
|
|
960
1030
|
throw new RetryExhaustedError(maxAttempts, named);
|
|
@@ -1021,7 +1091,7 @@ export class IsolatePool {
|
|
|
1021
1091
|
(entrypoint) => entrypoint.fetch(request.clone()),
|
|
1022
1092
|
resilience,
|
|
1023
1093
|
opts?.wasmModules,
|
|
1024
|
-
|
|
1094
|
+
request.signal,
|
|
1025
1095
|
);
|
|
1026
1096
|
}
|
|
1027
1097
|
|
package/src/process-fabric.ts
CHANGED
|
@@ -133,6 +133,14 @@ export const ResidentCodeSpecSchema = z.object({
|
|
|
133
133
|
* image store below and the spec names it.
|
|
134
134
|
*/
|
|
135
135
|
vfsTextModules: z.record(z.string(), z.string()).optional(),
|
|
136
|
+
/**
|
|
137
|
+
* VFS paths of generated CommonJS PACKS (encodeCommonJsPack): each image
|
|
138
|
+
* carries many `{ cjs }` modules, and the map gains every one of them at
|
|
139
|
+
* load. A node process's module cells travel this way — one module per
|
|
140
|
+
* file, so the guest's registry compiles only what the program requires,
|
|
141
|
+
* and one image per launch, so the boot spec names a path and not thousands.
|
|
142
|
+
*/
|
|
143
|
+
vfsCommonJsPacks: z.array(z.string()).optional(),
|
|
136
144
|
/**
|
|
137
145
|
* The isolate's exact `env`: one entry per binding the embedder minted,
|
|
138
146
|
* carried by reference so loopback stubs survive untouched. Defined —
|
|
@@ -265,7 +273,7 @@ export async function residentLoaderConfig(
|
|
|
265
273
|
spec: ResidentCodeSpec,
|
|
266
274
|
disk: ResidentDiskReader,
|
|
267
275
|
): Promise<Record<string, unknown>> {
|
|
268
|
-
const resolved: Record<string, string | { wasm: ArrayBuffer }> = {};
|
|
276
|
+
const resolved: Record<string, string | { wasm: ArrayBuffer } | { cjs: string }> = {};
|
|
269
277
|
for (const [moduleName, path] of Object.entries(spec.vfsWasmModules ?? {})) {
|
|
270
278
|
const bytes = await disk.readFile(path);
|
|
271
279
|
// The read's own buffer when it fits exactly, and only otherwise a copy.
|
|
@@ -283,6 +291,9 @@ export async function residentLoaderConfig(
|
|
|
283
291
|
for (const [moduleName, path] of Object.entries(spec.vfsTextModules ?? {})) {
|
|
284
292
|
resolved[moduleName] = await readFacetImage(disk, path);
|
|
285
293
|
}
|
|
294
|
+
for (const path of spec.vfsCommonJsPacks ?? []) {
|
|
295
|
+
Object.assign(resolved, decodeCommonJsPack(await readFacetImage(disk, path)));
|
|
296
|
+
}
|
|
286
297
|
return {
|
|
287
298
|
compatibilityDate: spec.compatibilityDate,
|
|
288
299
|
compatibilityFlags: spec.compatibilityFlags,
|
|
@@ -293,6 +304,40 @@ export async function residentLoaderConfig(
|
|
|
293
304
|
};
|
|
294
305
|
}
|
|
295
306
|
|
|
307
|
+
/**
|
|
308
|
+
* One image holding many `{ cjs }` modules: a JSON index of `[name, length]`
|
|
309
|
+
* rows, a newline, and the module texts back to back. Lengths are UTF-16 code
|
|
310
|
+
* units, the unit the decoded text is sliced in, so decoding copies nothing:
|
|
311
|
+
* each module is a slice of the one string read.
|
|
312
|
+
*
|
|
313
|
+
* Encoded as its parts, in order, never joined: the image store encodes them
|
|
314
|
+
* straight into the image's bytes, and a joined copy would be a second full
|
|
315
|
+
* copy of the program's code on the coordinator.
|
|
316
|
+
*/
|
|
317
|
+
export function encodeCommonJsPack(modules: Record<string, string>): string[] {
|
|
318
|
+
const index: [string, number][] = [];
|
|
319
|
+
const parts: string[] = [''];
|
|
320
|
+
for (const [name, text] of Object.entries(modules)) {
|
|
321
|
+
index.push([name, text.length]);
|
|
322
|
+
parts.push(text);
|
|
323
|
+
}
|
|
324
|
+
parts[0] = JSON.stringify(index) + '\n';
|
|
325
|
+
return parts;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
export function decodeCommonJsPack(pack: string): Record<string, { cjs: string }> {
|
|
329
|
+
const newline = pack.indexOf('\n');
|
|
330
|
+
const index = z.array(z.tuple([z.string(), z.number().int().nonnegative()])).parse(JSON.parse(pack.slice(0, newline)));
|
|
331
|
+
const modules: Record<string, { cjs: string }> = {};
|
|
332
|
+
let offset = newline + 1;
|
|
333
|
+
for (const [name, length] of index) {
|
|
334
|
+
modules[name] = { cjs: pack.slice(offset, offset + length) };
|
|
335
|
+
offset += length;
|
|
336
|
+
}
|
|
337
|
+
if (offset !== pack.length) throw new Error(`Nimbus: CommonJS pack holds ${pack.length - offset} bytes its index does not name`);
|
|
338
|
+
return modules;
|
|
339
|
+
}
|
|
340
|
+
|
|
296
341
|
/**
|
|
297
342
|
* Read one content-addressed facet image and verify it against the digest its
|
|
298
343
|
* path claims. Content addressing is only a guarantee if the bytes are checked
|
|
@@ -428,17 +473,14 @@ export interface ProcessImageDelivery {
|
|
|
428
473
|
* across one. A peer-hosted process can only ever receive an image
|
|
429
474
|
* through `moduleCeilingBytes` below, or by streaming it.
|
|
430
475
|
*
|
|
431
|
-
*
|
|
432
|
-
*
|
|
433
|
-
*
|
|
434
|
-
*
|
|
435
|
-
* workerd
|
|
436
|
-
*
|
|
437
|
-
*
|
|
438
|
-
*
|
|
439
|
-
* destination of a DIFFERENT class had all 500 seeded files readable from the
|
|
440
|
-
* destination's CONSTRUCTOR. No compat-date gate. So calling it is a
|
|
441
|
-
* lockfile-and-types problem, not a platform one.
|
|
476
|
+
* Present in production, and since the pins moved to
|
|
477
|
+
* `@cloudflare/workers-types` 5.20260928.1 and workerd 1.20260926.1 also in
|
|
478
|
+
* the type checker and `wrangler dev` (DurableObjectFacets.clone;
|
|
479
|
+
* src/workerd/api/actor-state.h). Before that the types declared no `clone`
|
|
480
|
+
* and the pinned workerd 1.20260603.1 lacked it, while on a live Worker the
|
|
481
|
+
* binding enumerated `["abort","clone","constructor","delete","get"]` and a
|
|
482
|
+
* clone into a destination of a DIFFERENT class had all 500 seeded files
|
|
483
|
+
* readable from the destination's CONSTRUCTOR. No compat-date gate.
|
|
442
484
|
*
|
|
443
485
|
* The hazard that comes with it, measured rather than assumed: ANY `src`
|
|
444
486
|
* that does not resolve to a populated facet — a typo, a name not created
|
|
@@ -488,7 +530,7 @@ export interface OneShotCodeSpec {
|
|
|
488
530
|
compatibilityDate: string;
|
|
489
531
|
compatibilityFlags: string[];
|
|
490
532
|
mainModule: string;
|
|
491
|
-
modules: Record<string, string | { wasm: ArrayBuffer }>;
|
|
533
|
+
modules: Record<string, string | { wasm: ArrayBuffer } | { cjs: string }>;
|
|
492
534
|
}
|
|
493
535
|
|
|
494
536
|
/**
|