@nimbus-sh/fabric 0.8.0 → 0.9.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 +26 -11
- package/dist/budgets.d.ts +50 -29
- package/dist/budgets.d.ts.map +1 -1
- package/dist/budgets.js +88 -40
- 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 +46 -50
- 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/isolate-pool.d.ts +31 -23
- package/dist/isolate-pool.d.ts.map +1 -1
- package/dist/isolate-pool.js +68 -46
- 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 +30 -30
- package/package.json +4 -4
- package/src/budgets.ts +96 -49
- package/src/do-calls.ts +45 -11
- package/src/fanout.ts +60 -53
- package/src/host-wasm.ts +41 -0
- package/src/image-store.ts +29 -2
- package/src/isolate-pool.ts +98 -66
- 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 +32 -31
package/README.md
CHANGED
|
@@ -22,9 +22,19 @@ npm install @nimbus-sh/fabric
|
|
|
22
22
|
|
|
23
23
|
## Requirements
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
dispatcher needs `AsyncLocalStorage`, which
|
|
27
|
-
|
|
25
|
+
Use a compatibility date of 2026-08-04 or later, or list `nodejs_compat` in
|
|
26
|
+
`compatibility_flags`. The timer dispatcher needs `AsyncLocalStorage`, which
|
|
27
|
+
workerd ships only under `nodejs_compat`, on by date from 2026-08-04. Without
|
|
28
|
+
it the module fails to load at deploy time.
|
|
29
|
+
|
|
30
|
+
`composeFabric` also needs `enhanced_error_serialization`: a compatibility
|
|
31
|
+
date of 2026-04-21 or later, or the flag in `compatibility_flags` on an older
|
|
32
|
+
date. A program's filesystem errors reach it across workerd RPC, and only
|
|
33
|
+
that flag carries their `code`. On workerd without it `composeFabric` throws,
|
|
34
|
+
naming both fixes, rather than every program seeing `EIO`. It throws where
|
|
35
|
+
it is called. At module scope the Worker fails at startup. A library host
|
|
36
|
+
that composes through `NimbusWorkspace.create({ fabric })` deploys and
|
|
37
|
+
starts, and its first create throws.
|
|
28
38
|
|
|
29
39
|
Import the root inside a Worker. Outside workerd, import subpaths such as
|
|
30
40
|
`@nimbus-sh/fabric/timers.js`, which are typed against plain objects and run
|
|
@@ -224,13 +234,18 @@ Warm isolates are scoped to one session. A pool may opt into
|
|
|
224
234
|
`cacheScope: 'global'` only if it takes no supervisor binding and keeps no
|
|
225
235
|
user state.
|
|
226
236
|
|
|
227
|
-
`Fanout` handles wider batches.
|
|
228
|
-
|
|
229
|
-
|
|
237
|
+
`Fanout` handles wider batches. A Durable Object may have 10 distinct Dynamic
|
|
238
|
+
Workers with in-flight requests at once (`DO_DYNAMIC_WORKER_LIMIT`), shared
|
|
239
|
+
across every concurrent request to it; repeated requests to one Dynamic
|
|
240
|
+
Worker count once. A batch that fits the coordinator's remaining headroom
|
|
241
|
+
runs there, one Dynamic Worker per task; a wider one shards across up to 32
|
|
242
|
+
sibling objects, 4 at a time, each spending its own headroom.
|
|
230
243
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
244
|
+
The loader ledger counts what is in flight per DO: pool and one-shot calls,
|
|
245
|
+
esbuild facet calls, git network ops, and every resident process for as long
|
|
246
|
+
as it lives. `dynamicWorkerHeadroom(ctx)` is what is left,
|
|
247
|
+
`claimDynamicWorkers(ctx, n)` reserves a width, `loaderLedgerStats(ctx)`
|
|
248
|
+
reports it all, and a limit refusal names the workers in flight.
|
|
234
249
|
|
|
235
250
|
## Process fabric
|
|
236
251
|
|
|
@@ -370,8 +385,8 @@ or left to you.
|
|
|
370
385
|
| Request-time `WebAssembly.compile`/`instantiate` CSP-blocked; wasm rides the loader modules map as `{ wasm: ArrayBuffer }`, compiled at module load | RPC of a compiled `Module` refused by structured clone; inlined bytes OOMed the supervisor |
|
|
371
386
|
| Module scope bans I/O; `new Function` succeeds at module scope and throws at request time | code reaches a facet through the module map or not at all |
|
|
372
387
|
| The facet start callback fires at most once | re-running it would re-execute the user's program |
|
|
373
|
-
|
|
|
374
|
-
| `ctx.facets.clone` is same-object only
|
|
388
|
+
| 10 distinct Dynamic Workers with in-flight requests per DO, shared across its concurrent requests; repeated requests to one Dynamic Worker count once ([changelog, 2026-08-28](https://developers.cloudflare.com/changelog/post/2026-08-28-durable-objects-dynamic-workers-limit/)) | `DO_DYNAMIC_WORKER_LIMIT`; `Fanout` sizes in-DO batches to the live headroom, and a refusal names the workers in flight |
|
|
389
|
+
| `ctx.facets.clone` is same-object only; declared by `@cloudflare/workers-types` 5 and present in workerd ≥ 1.20260926.1 and in production | 18–31 ms / 45.7 MB, 34–54 ms / 1 GB; an unresolvable `src` silently EMPTIES the destination and reports success — `cloneStorage` enforces the both-ends validation |
|
|
375
390
|
| A DO dies at ~200 MiB of live wasm linear memory; reserved and written pages die at the same ceiling | lazy growth buys nothing; bound guest memory by rewriting the memory section |
|
|
376
391
|
| A wasm stack suspended (JSPI) in one request cannot resume in another | 3 in-context resumes took 6 ms; the first cross-context one hit a 30 s timeout |
|
|
377
392
|
|
package/dist/budgets.d.ts
CHANGED
|
@@ -1,34 +1,39 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* budgets.ts — per-DO accounting for the platform budgets the fabric spends:
|
|
3
|
-
* the
|
|
4
|
-
* dynamic-worker module-map ceiling.
|
|
3
|
+
* the Durable Object's Dynamic Worker concurrency limit, the facet-ID
|
|
4
|
+
* lifetime budget, and the dynamic-worker module-map ceiling.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* (`IN_DO_THRESHOLD` = 5 in the fanout pool), which until now meant the slots
|
|
13
|
-
* were counted in prose. This ledger counts them at the fabric's loader call
|
|
14
|
-
* sites instead — the loader pool's slots, a resident process's keyed worker,
|
|
15
|
-
* a one-shot's load — so proximity is measurable and a cap failure can name
|
|
16
|
-
* the ids actually holding slots.
|
|
6
|
+
* The Dynamic Worker model is Cloudflare's documented one
|
|
7
|
+
* ({@link DO_DYNAMIC_WORKER_LIMIT}): a Durable Object may have a fixed number
|
|
8
|
+
* of DISTINCT Dynamic Workers with in-flight requests at once, shared across
|
|
9
|
+
* every concurrent request to that object (one I/O context), and any number
|
|
10
|
+
* of in-flight requests to the same Dynamic Worker count as one. Only
|
|
11
|
+
* in-flight requests count: a loader id with nothing in flight holds nothing.
|
|
17
12
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
13
|
+
* The ledger counts, per hosting actor, the distinct workers that are in
|
|
14
|
+
* flight right now, keyed by loader id (a fresh key per unkeyed `load`), plus
|
|
15
|
+
* the width fan-outs have claimed and not yet released. A fan-out spends only
|
|
16
|
+
* the {@link dynamicWorkerHeadroom} that leaves, so work a Durable Object
|
|
17
|
+
* already has in flight — a resident process, the esbuild facet, a git
|
|
18
|
+
* network op, another fan-out — keeps its slots.
|
|
21
19
|
*
|
|
22
20
|
* Keyed weakly off the hosting actor's `ctx`, like the facet slot books: the
|
|
23
|
-
*
|
|
21
|
+
* limit is per Durable Object, and dynamic workers die with the isolate that
|
|
24
22
|
* loaded them, so a ledger that goes away with its host describes nothing
|
|
25
23
|
* that still exists.
|
|
26
24
|
*/
|
|
27
|
-
/** Record a keyed `loader.get(id)` — a permanent slot if the id is new. */
|
|
28
|
-
export declare function recordLoaderId(ctx: object, id: string): void;
|
|
29
25
|
/**
|
|
30
|
-
*
|
|
31
|
-
*
|
|
26
|
+
* Distinct Dynamic Workers one Durable Object may have with in-flight
|
|
27
|
+
* requests at once, shared across all concurrent requests to that object;
|
|
28
|
+
* multiple in-flight requests to one Dynamic Worker count once.
|
|
29
|
+
* https://developers.cloudflare.com/changelog/post/2026-08-28-durable-objects-dynamic-workers-limit/
|
|
30
|
+
*/
|
|
31
|
+
export declare const DO_DYNAMIC_WORKER_LIMIT = 10;
|
|
32
|
+
/**
|
|
33
|
+
* Hold the Dynamic Worker `workerKey` in flight on this actor's ledger; the
|
|
34
|
+
* returned function ends the hold (idempotently), from the caller's own
|
|
35
|
+
* `finally`. Holds on one key nest: the worker counts once until the last
|
|
36
|
+
* one ends, as the platform counts it.
|
|
32
37
|
*
|
|
33
38
|
* A begin/end pair rather than a wrapper on purpose, and the shape is
|
|
34
39
|
* load-bearing: wrapping the stub call in a ledger-owned async frame
|
|
@@ -41,19 +46,35 @@ export declare function recordLoaderId(ctx: object, id: string): void;
|
|
|
41
46
|
* workers: an RPC stub call must stay a direct property call awaited by the
|
|
42
47
|
* frame that made it, so the ledger only brackets it.
|
|
43
48
|
*/
|
|
44
|
-
export declare function beginLoaderFetch(ctx: object): () => void;
|
|
49
|
+
export declare function beginLoaderFetch(ctx: object, workerKey: string): () => void;
|
|
50
|
+
/**
|
|
51
|
+
* Distinct Dynamic Workers this actor may still put in flight: the limit
|
|
52
|
+
* less what is held and claimed right now. Never negative.
|
|
53
|
+
*/
|
|
54
|
+
export declare function dynamicWorkerHeadroom(ctx: object): number;
|
|
55
|
+
/**
|
|
56
|
+
* Claim `width` distinct Dynamic Workers for one fan-out, or null when the
|
|
57
|
+
* headroom cannot hold it. The claim counts until `release` (idempotent), so
|
|
58
|
+
* a second fan-out sizing itself meanwhile sees it; the claimant's own
|
|
59
|
+
* dispatches are held as well while they run, which only ever over-counts
|
|
60
|
+
* toward sending that second fan-out elsewhere.
|
|
61
|
+
*/
|
|
62
|
+
export declare function claimDynamicWorkers(ctx: object, width: number): {
|
|
63
|
+
release(): void;
|
|
64
|
+
} | null;
|
|
45
65
|
/** Snapshot for the diag surface. Pure read; no I/O. */
|
|
46
66
|
export declare function loaderLedgerStats(ctx: object): {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
67
|
+
limit: number;
|
|
68
|
+
inFlightWorkers: string[];
|
|
69
|
+
claimed: number;
|
|
70
|
+
headroom: number;
|
|
71
|
+
peak: number;
|
|
50
72
|
};
|
|
51
73
|
/**
|
|
52
|
-
* Name the per-DO accounting on a "
|
|
74
|
+
* Name the per-DO accounting on a "Dynamic worker concurrency limit exceeded"
|
|
53
75
|
* failure; hand every other error back untouched. The platform's message
|
|
54
|
-
* says only that the
|
|
55
|
-
*
|
|
56
|
-
* shrink anything.
|
|
76
|
+
* says only that the limit was hit — which workers were in flight, and what
|
|
77
|
+
* fan-outs had claimed, is what the operator needs to know to shrink anything.
|
|
57
78
|
*/
|
|
58
79
|
export declare function withDynamicWorkerCapNamed<E>(ctx: object, error: E): E | Error;
|
|
59
80
|
/**
|
package/dist/budgets.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"budgets.d.ts","sourceRoot":"","sources":["../src/budgets.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"budgets.d.ts","sourceRoot":"","sources":["../src/budgets.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAKH;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,KAAK,CAAC;AA0B1C;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,IAAI,CAY3E;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAEzD;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG;IAAE,OAAO,IAAI,IAAI,CAAA;CAAE,GAAG,IAAI,CAa1F;AAED,wDAAwD;AACxD,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG;IAC9C,KAAK,EAAE,MAAM,CAAC;IACd,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;CACd,CASA;AAED;;;;;GAKG;AACH,wBAAgB,yBAAyB,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,GAAG,CAAC,GAAG,KAAK,CAU7E;AAID;;;;;;;GAOG;AACH,eAAO,MAAM,+BAA+B,WAAa,CAAC;AAE1D;;;;;;;;;;;;;GAaG;AACH,wBAAgB,8BAA8B,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAoBrF;AA4BD;;;;;GAKG;AACH,eAAO,MAAM,wBAAwB,QAAS,CAAC;AAE/C,sEAAsE;AACtE,eAAO,MAAM,yBAAyB,iCAAiC,CAAC;AAExE,mEAAmE;AACnE,UAAU,sBAAsB;IAC9B,OAAO,EAAE;QACP,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC;QAC7C,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;KACjD,CAAC;CACH;AAqCD;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,sBAAsB,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAatF;AAED,4EAA4E;AAC5E,wBAAgB,cAAc,CAAC,GAAG,EAAE,sBAAsB,GAAG,MAAM,CAGlE;AAED,4EAA4E;AAC5E,wBAAsB,qBAAqB,CAAC,GAAG,EAAE,sBAAsB,GAAG,OAAO,CAAC,MAAM,CAAC,CAIxF;AAED;;;;;;;;GAQG;AACH,wBAAsB,aAAa,CACjC,GAAG,EAAE,sBAAsB,GAC1B,OAAO,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAK/C;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAU9E"}
|
package/dist/budgets.js
CHANGED
|
@@ -1,46 +1,53 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* budgets.ts — per-DO accounting for the platform budgets the fabric spends:
|
|
3
|
-
* the
|
|
4
|
-
* dynamic-worker module-map ceiling.
|
|
3
|
+
* the Durable Object's Dynamic Worker concurrency limit, the facet-ID
|
|
4
|
+
* lifetime budget, and the dynamic-worker module-map ceiling.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* (`IN_DO_THRESHOLD` = 5 in the fanout pool), which until now meant the slots
|
|
13
|
-
* were counted in prose. This ledger counts them at the fabric's loader call
|
|
14
|
-
* sites instead — the loader pool's slots, a resident process's keyed worker,
|
|
15
|
-
* a one-shot's load — so proximity is measurable and a cap failure can name
|
|
16
|
-
* the ids actually holding slots.
|
|
6
|
+
* The Dynamic Worker model is Cloudflare's documented one
|
|
7
|
+
* ({@link DO_DYNAMIC_WORKER_LIMIT}): a Durable Object may have a fixed number
|
|
8
|
+
* of DISTINCT Dynamic Workers with in-flight requests at once, shared across
|
|
9
|
+
* every concurrent request to that object (one I/O context), and any number
|
|
10
|
+
* of in-flight requests to the same Dynamic Worker count as one. Only
|
|
11
|
+
* in-flight requests count: a loader id with nothing in flight holds nothing.
|
|
17
12
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
13
|
+
* The ledger counts, per hosting actor, the distinct workers that are in
|
|
14
|
+
* flight right now, keyed by loader id (a fresh key per unkeyed `load`), plus
|
|
15
|
+
* the width fan-outs have claimed and not yet released. A fan-out spends only
|
|
16
|
+
* the {@link dynamicWorkerHeadroom} that leaves, so work a Durable Object
|
|
17
|
+
* already has in flight — a resident process, the esbuild facet, a git
|
|
18
|
+
* network op, another fan-out — keeps its slots.
|
|
21
19
|
*
|
|
22
20
|
* Keyed weakly off the hosting actor's `ctx`, like the facet slot books: the
|
|
23
|
-
*
|
|
21
|
+
* limit is per Durable Object, and dynamic workers die with the isolate that
|
|
24
22
|
* loaded them, so a ledger that goes away with its host describes nothing
|
|
25
23
|
* that still exists.
|
|
26
24
|
*/
|
|
27
25
|
import { classifyError } from '@nimbus-sh/platform/oom-classify.js';
|
|
26
|
+
import { hostWasmIdentity } from './host-wasm.js';
|
|
27
|
+
/**
|
|
28
|
+
* Distinct Dynamic Workers one Durable Object may have with in-flight
|
|
29
|
+
* requests at once, shared across all concurrent requests to that object;
|
|
30
|
+
* multiple in-flight requests to one Dynamic Worker count once.
|
|
31
|
+
* https://developers.cloudflare.com/changelog/post/2026-08-28-durable-objects-dynamic-workers-limit/
|
|
32
|
+
*/
|
|
33
|
+
export const DO_DYNAMIC_WORKER_LIMIT = 10;
|
|
28
34
|
const ledgers = new WeakMap();
|
|
29
35
|
function ledger(ctx) {
|
|
30
36
|
let entry = ledgers.get(ctx);
|
|
31
37
|
if (!entry) {
|
|
32
|
-
entry = {
|
|
38
|
+
entry = { inFlight: new Map(), claimed: 0, peak: 0 };
|
|
33
39
|
ledgers.set(ctx, entry);
|
|
34
40
|
}
|
|
35
41
|
return entry;
|
|
36
42
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
ledger(ctx).ids.add(id);
|
|
43
|
+
function inUse(entry) {
|
|
44
|
+
return entry.inFlight.size + entry.claimed;
|
|
40
45
|
}
|
|
41
46
|
/**
|
|
42
|
-
*
|
|
43
|
-
* function ends
|
|
47
|
+
* Hold the Dynamic Worker `workerKey` in flight on this actor's ledger; the
|
|
48
|
+
* returned function ends the hold (idempotently), from the caller's own
|
|
49
|
+
* `finally`. Holds on one key nest: the worker counts once until the last
|
|
50
|
+
* one ends, as the platform counts it.
|
|
44
51
|
*
|
|
45
52
|
* A begin/end pair rather than a wrapper on purpose, and the shape is
|
|
46
53
|
* load-bearing: wrapping the stub call in a ledger-owned async frame
|
|
@@ -53,43 +60,77 @@ export function recordLoaderId(ctx, id) {
|
|
|
53
60
|
* workers: an RPC stub call must stay a direct property call awaited by the
|
|
54
61
|
* frame that made it, so the ledger only brackets it.
|
|
55
62
|
*/
|
|
56
|
-
export function beginLoaderFetch(ctx) {
|
|
63
|
+
export function beginLoaderFetch(ctx, workerKey) {
|
|
57
64
|
const entry = ledger(ctx);
|
|
58
|
-
entry.
|
|
59
|
-
entry.
|
|
65
|
+
entry.inFlight.set(workerKey, (entry.inFlight.get(workerKey) ?? 0) + 1);
|
|
66
|
+
entry.peak = Math.max(entry.peak, inUse(entry));
|
|
60
67
|
let ended = false;
|
|
61
68
|
return () => {
|
|
62
69
|
if (ended)
|
|
63
70
|
return;
|
|
64
71
|
ended = true;
|
|
65
|
-
entry.
|
|
72
|
+
const open = (entry.inFlight.get(workerKey) ?? 1) - 1;
|
|
73
|
+
if (open > 0)
|
|
74
|
+
entry.inFlight.set(workerKey, open);
|
|
75
|
+
else
|
|
76
|
+
entry.inFlight.delete(workerKey);
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Distinct Dynamic Workers this actor may still put in flight: the limit
|
|
81
|
+
* less what is held and claimed right now. Never negative.
|
|
82
|
+
*/
|
|
83
|
+
export function dynamicWorkerHeadroom(ctx) {
|
|
84
|
+
return Math.max(0, DO_DYNAMIC_WORKER_LIMIT - inUse(ledger(ctx)));
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Claim `width` distinct Dynamic Workers for one fan-out, or null when the
|
|
88
|
+
* headroom cannot hold it. The claim counts until `release` (idempotent), so
|
|
89
|
+
* a second fan-out sizing itself meanwhile sees it; the claimant's own
|
|
90
|
+
* dispatches are held as well while they run, which only ever over-counts
|
|
91
|
+
* toward sending that second fan-out elsewhere.
|
|
92
|
+
*/
|
|
93
|
+
export function claimDynamicWorkers(ctx, width) {
|
|
94
|
+
const entry = ledger(ctx);
|
|
95
|
+
if (width < 1 || width > DO_DYNAMIC_WORKER_LIMIT - inUse(entry))
|
|
96
|
+
return null;
|
|
97
|
+
entry.claimed += width;
|
|
98
|
+
entry.peak = Math.max(entry.peak, inUse(entry));
|
|
99
|
+
let released = false;
|
|
100
|
+
return {
|
|
101
|
+
release() {
|
|
102
|
+
if (released)
|
|
103
|
+
return;
|
|
104
|
+
released = true;
|
|
105
|
+
entry.claimed -= width;
|
|
106
|
+
},
|
|
66
107
|
};
|
|
67
108
|
}
|
|
68
109
|
/** Snapshot for the diag surface. Pure read; no I/O. */
|
|
69
110
|
export function loaderLedgerStats(ctx) {
|
|
70
111
|
const entry = ledger(ctx);
|
|
71
112
|
return {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
113
|
+
limit: DO_DYNAMIC_WORKER_LIMIT,
|
|
114
|
+
inFlightWorkers: [...entry.inFlight.keys()],
|
|
115
|
+
claimed: entry.claimed,
|
|
116
|
+
headroom: dynamicWorkerHeadroom(ctx),
|
|
117
|
+
peak: entry.peak,
|
|
75
118
|
};
|
|
76
119
|
}
|
|
77
120
|
/**
|
|
78
|
-
* Name the per-DO accounting on a "
|
|
121
|
+
* Name the per-DO accounting on a "Dynamic worker concurrency limit exceeded"
|
|
79
122
|
* failure; hand every other error back untouched. The platform's message
|
|
80
|
-
* says only that the
|
|
81
|
-
*
|
|
82
|
-
* shrink anything.
|
|
123
|
+
* says only that the limit was hit — which workers were in flight, and what
|
|
124
|
+
* fan-outs had claimed, is what the operator needs to know to shrink anything.
|
|
83
125
|
*/
|
|
84
126
|
export function withDynamicWorkerCapNamed(ctx, error) {
|
|
85
127
|
if (classifyError(error) !== 'dynamic_worker_cap')
|
|
86
128
|
return error;
|
|
87
129
|
const entry = ledger(ctx);
|
|
88
130
|
const platform = error instanceof Error ? error.message : String(error);
|
|
89
|
-
return new Error(`${platform} — this Durable Object
|
|
90
|
-
+ `
|
|
91
|
-
+
|
|
92
|
-
+ `peak ${entry.peakLiveFetches}`, { cause: error });
|
|
131
|
+
return new Error(`${platform} — this Durable Object had ${entry.inFlight.size} distinct dynamic worker(s) in flight `
|
|
132
|
+
+ `(${[...entry.inFlight.keys()].join(', ') || 'none recorded'}) and ${entry.claimed} claimed by fan-outs, `
|
|
133
|
+
+ `against a limit of ${DO_DYNAMIC_WORKER_LIMIT}; peak ${entry.peak}`, { cause: error });
|
|
93
134
|
}
|
|
94
135
|
// ── Dynamic-worker module-map ceiling ───────────────────────────────────────
|
|
95
136
|
/**
|
|
@@ -136,13 +177,18 @@ export function assertModuleMapWithinCodeLimit(modules) {
|
|
|
136
177
|
}
|
|
137
178
|
/**
|
|
138
179
|
* Bytes one module-map member carries, across the loader's content kinds
|
|
139
|
-
* (plain string, `{ js | cjs | py | text }`, `{ wasm | data }
|
|
140
|
-
* encoder, text is measured exactly; without
|
|
180
|
+
* (plain string, `{ js | cjs | py | text }`, `{ wasm | data }`, a bare
|
|
181
|
+
* WebAssembly.Module). With an encoder, text is measured exactly; without
|
|
182
|
+
* one, by code-unit length. A compiled module counts the wire size its host
|
|
183
|
+
* described (host-wasm.ts); one nobody described counts nothing here and is
|
|
184
|
+
* left to the platform's own refusal, as the text undercount is.
|
|
141
185
|
*/
|
|
142
186
|
function memberBytes(content, encoder) {
|
|
143
187
|
const textBytes = (text) => encoder ? encoder.encode(text).byteLength : text.length;
|
|
144
188
|
if (typeof content === 'string')
|
|
145
189
|
return textBytes(content);
|
|
190
|
+
if (content instanceof WebAssembly.Module)
|
|
191
|
+
return hostWasmIdentity(content)?.bytes ?? 0;
|
|
146
192
|
if (content !== null && typeof content === 'object') {
|
|
147
193
|
for (const value of Object.values(content)) {
|
|
148
194
|
if (typeof value === 'string')
|
|
@@ -151,6 +197,8 @@ function memberBytes(content, encoder) {
|
|
|
151
197
|
return value.byteLength;
|
|
152
198
|
if (ArrayBuffer.isView(value))
|
|
153
199
|
return value.byteLength;
|
|
200
|
+
if (value instanceof WebAssembly.Module)
|
|
201
|
+
return hostWasmIdentity(value)?.bytes ?? 0;
|
|
154
202
|
}
|
|
155
203
|
}
|
|
156
204
|
return 0;
|
package/dist/do-calls.d.ts
CHANGED
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
* namespace stubs, where the thunk shape is production-proven in Proteus.
|
|
41
41
|
*/
|
|
42
42
|
import { type DoCallClass } from '@nimbus-sh/platform/oom-classify.js';
|
|
43
|
+
import { type SpanRecorder } from '@nimbus-sh/platform/tracing.js';
|
|
43
44
|
export interface DoCallRetryPolicy {
|
|
44
45
|
maxAttempts?: number;
|
|
45
46
|
baseDelayMs?: number;
|
|
@@ -78,7 +79,26 @@ export interface DoCallRetryPolicy {
|
|
|
78
79
|
* its error.
|
|
79
80
|
*/
|
|
80
81
|
onRetry?(info: DoCallRetryInfo): void;
|
|
82
|
+
/**
|
|
83
|
+
* Where the call's telemetry goes: its span's recorder. Each attempt lost
|
|
84
|
+
* to a transient or overloaded failure is recorded as an exception whose
|
|
85
|
+
* `code` is the failure's class, and once the call has settled it gets
|
|
86
|
+
* `do_call.attempts` (started), `do_call.hedges` (started by a hedge),
|
|
87
|
+
* `do_call.answered_by` (the attempt whose answer the call took, absent
|
|
88
|
+
* when none answered) and `do_call.outcome` ({@link DoCallOutcome}).
|
|
89
|
+
* Nothing recorded can change the call's answer. Records nothing when
|
|
90
|
+
* absent.
|
|
91
|
+
*/
|
|
92
|
+
span?: SpanRecorder;
|
|
81
93
|
}
|
|
94
|
+
/**
|
|
95
|
+
* How an `idempotent` call ended: `answered` (an attempt succeeded),
|
|
96
|
+
* `callee_error` (the callee's own failure, which is an answer),
|
|
97
|
+
* `exhausted` (the last transient failure, no repeat left), `overloaded`
|
|
98
|
+
* (shed, nothing repeated after it), or `caller_error` (the resolver or
|
|
99
|
+
* `onRetry` threw).
|
|
100
|
+
*/
|
|
101
|
+
export type DoCallOutcome = 'answered' | 'callee_error' | 'exhausted' | 'overloaded' | 'caller_error';
|
|
82
102
|
/** What one retry is answering: which call, which platform class, which
|
|
83
103
|
* attempt just failed out of how many. */
|
|
84
104
|
export interface DoCallRetryInfo {
|
package/dist/do-calls.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"do-calls.d.ts","sourceRoot":"","sources":["../src/do-calls.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AAEH,OAAO,EAAqC,KAAK,WAAW,EAAE,MAAM,qCAAqC,CAAC;
|
|
1
|
+
{"version":3,"file":"do-calls.d.ts","sourceRoot":"","sources":["../src/do-calls.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AAEH,OAAO,EAAqC,KAAK,WAAW,EAAE,MAAM,qCAAqC,CAAC;AAE1G,OAAO,EAAY,KAAK,YAAY,EAAE,MAAM,gCAAgC,CAAC;AAS7E,MAAM,WAAW,iBAAiB;IAChC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;;OAMG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;;;;;;;;;;;;;OAeG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;;;;;OAQG;IACH,OAAO,CAAC,CAAC,IAAI,EAAE,eAAe,GAAG,IAAI,CAAC;IACtC;;;;;;;;;OASG;IACH,IAAI,CAAC,EAAE,YAAY,CAAC;CACrB;AAED;;;;;;GAMG;AACH,MAAM,MAAM,aAAa,GAAG,UAAU,GAAG,cAAc,GAAG,WAAW,GAAG,YAAY,GAAG,cAAc,CAAC;AAEtG;2CAC2C;AAC3C,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,WAAW,CAAC;IAC5B;;;OAGG;IACH,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,OAAO,CAAC;CAChB;AAED;;;;;;;;;GASG;AACH,MAAM,MAAM,cAAc,CAAC,CAAC,IAAI,MAAM,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;AAErD;;;;;GAKG;AACH,qBAAa,WAAY,SAAQ,KAAK;IAElC,QAAQ,CAAC,SAAS,EAAE,MAAM;IAC1B,QAAQ,CAAC,IAAI,EAAE,YAAY,GAAG,UAAU;IACxC,QAAQ,CAAC,cAAc,EAAE,WAAW;gBAF3B,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,YAAY,GAAG,UAAU,EAC/B,cAAc,EAAE,WAAW,EACpC,KAAK,EAAE,OAAO;CASjB;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,UAAU,CAAC,CAAC,EAAE,CAAC,EAC7B,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,cAAc,CAAC,CAAC,CAAC,EACvB,IAAI,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,EAC7B,MAAM,GAAE,iBAAsB,GAC7B,OAAO,CAAC,CAAC,CAAC,CAgIZ;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAsB,QAAQ,CAAC,CAAC,EAAE,CAAC,EACjC,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,cAAc,CAAC,CAAC,CAAC,EACvB,IAAI,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,GAC5B,OAAO,CAAC,CAAC,CAAC,CAUZ"}
|
package/dist/do-calls.js
CHANGED
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
*/
|
|
42
42
|
import { classifyDoCall, isRetryableDoCall } from '@nimbus-sh/platform/oom-classify.js';
|
|
43
43
|
import { disposeRpcResource } from '@nimbus-sh/platform/rpc-dispose.js';
|
|
44
|
+
import { untraced } from '@nimbus-sh/platform/tracing.js';
|
|
44
45
|
/** Total attempts. Two retries is what a dropped connection or a deploy
|
|
45
46
|
* bounce needs; beyond that the object is not coming back inside this
|
|
46
47
|
* request (the consumer's measured bound). */
|
|
@@ -87,7 +88,7 @@ export class DoCallError extends Error {
|
|
|
87
88
|
export function idempotent(operation, stub, call, policy = {}) {
|
|
88
89
|
const maxAttempts = policy.maxAttempts ?? MAX_ATTEMPTS;
|
|
89
90
|
const baseDelayMs = policy.baseDelayMs ?? BASE_DELAY_MS;
|
|
90
|
-
const { hedgeAfterMs, retryWindowMs } = policy;
|
|
91
|
+
const { hedgeAfterMs, retryWindowMs, span = untraced } = policy;
|
|
91
92
|
const startedAt = Date.now();
|
|
92
93
|
// The executor form: fabric's library target predates Promise.withResolvers.
|
|
93
94
|
return new Promise((resolve, reject) => {
|
|
@@ -99,7 +100,9 @@ export function idempotent(operation, stub, call, policy = {}) {
|
|
|
99
100
|
// An attempt was shed as overloaded: nothing is repeated after it.
|
|
100
101
|
let refused = false;
|
|
101
102
|
let settled = false;
|
|
102
|
-
|
|
103
|
+
// Attempts a hedge started rather than a retry or the first send.
|
|
104
|
+
let hedged = 0;
|
|
105
|
+
const settle = (outcome, answeredBy, answer) => {
|
|
103
106
|
if (settled)
|
|
104
107
|
return;
|
|
105
108
|
settled = true;
|
|
@@ -107,6 +110,12 @@ export function idempotent(operation, stub, call, policy = {}) {
|
|
|
107
110
|
clearTimeout(timer);
|
|
108
111
|
hedges.clear();
|
|
109
112
|
answer();
|
|
113
|
+
span.set({
|
|
114
|
+
'do_call.attempts': started,
|
|
115
|
+
'do_call.hedges': hedged,
|
|
116
|
+
'do_call.answered_by': answeredBy,
|
|
117
|
+
'do_call.outcome': outcome,
|
|
118
|
+
});
|
|
110
119
|
};
|
|
111
120
|
/** May another attempt start at `at`? */
|
|
112
121
|
const canRepeat = (at) => !settled && !refused && started < maxAttempts
|
|
@@ -114,13 +123,20 @@ export function idempotent(operation, stub, call, policy = {}) {
|
|
|
114
123
|
/** `error` ended an attempt that will not be repeated: the call's answer, once nothing else is live. */
|
|
115
124
|
const exhausted = (error) => {
|
|
116
125
|
if (live === 0)
|
|
117
|
-
settle(() => reject(error));
|
|
126
|
+
settle(refused ? 'overloaded' : 'exhausted', undefined, () => reject(error));
|
|
118
127
|
};
|
|
119
128
|
/** A failed attempt, numbered: repeat it after its backoff, or let it stand. */
|
|
120
129
|
const failed = async (number, error) => {
|
|
121
130
|
if (settled)
|
|
122
131
|
return;
|
|
123
132
|
const classification = classifyDoCall(error);
|
|
133
|
+
if (!isRetryableDoCall(classification) && classification !== 'overloaded') {
|
|
134
|
+
// The call ran and its answer is this error — ENOENT is a read's answer as much as bytes are.
|
|
135
|
+
settle('callee_error', number, () => reject(error));
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
// A lost attempt: the call's span says which, and why.
|
|
139
|
+
span.exception(error, classification, `attempt ${number} of ${maxAttempts}: `);
|
|
124
140
|
if (classification === 'overloaded') {
|
|
125
141
|
// A shed call is no answer: nothing more is sent, and an attempt
|
|
126
142
|
// still in flight may yet answer.
|
|
@@ -128,11 +144,6 @@ export function idempotent(operation, stub, call, policy = {}) {
|
|
|
128
144
|
exhausted(error);
|
|
129
145
|
return;
|
|
130
146
|
}
|
|
131
|
-
if (!isRetryableDoCall(classification)) {
|
|
132
|
-
// The call ran and its answer is this error — ENOENT is a read's answer as much as bytes are.
|
|
133
|
-
settle(() => reject(error));
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
136
147
|
const delayMs = Math.floor(Math.random() * 2 ** number * baseDelayMs);
|
|
137
148
|
if (!canRepeat(Date.now() + delayMs)) {
|
|
138
149
|
exhausted(error);
|
|
@@ -163,8 +174,10 @@ export function idempotent(operation, stub, call, policy = {}) {
|
|
|
163
174
|
const hedge = hedgeAfterMs === undefined ? undefined : setTimeout(() => {
|
|
164
175
|
if (hedge !== undefined)
|
|
165
176
|
hedges.delete(hedge);
|
|
166
|
-
if (canRepeat(Date.now()))
|
|
177
|
+
if (canRepeat(Date.now())) {
|
|
178
|
+
hedged++;
|
|
167
179
|
attempt();
|
|
180
|
+
}
|
|
168
181
|
}, hedgeAfterMs);
|
|
169
182
|
if (hedge !== undefined)
|
|
170
183
|
hedges.add(hedge);
|
|
@@ -193,11 +206,11 @@ export function idempotent(operation, stub, call, policy = {}) {
|
|
|
193
206
|
disposeRpcResource(result);
|
|
194
207
|
return;
|
|
195
208
|
}
|
|
196
|
-
settle(() => resolve(result));
|
|
209
|
+
settle('answered', number, () => resolve(result));
|
|
197
210
|
};
|
|
198
211
|
/** Start an attempt. Whatever it throws outside the call itself fails the call. */
|
|
199
212
|
const attempt = () => {
|
|
200
|
-
run().catch((error) => settle(() => reject(error)));
|
|
213
|
+
run().catch((error) => settle('caller_error', undefined, () => reject(error)));
|
|
201
214
|
};
|
|
202
215
|
attempt();
|
|
203
216
|
});
|