@nimbus-sh/fabric 0.1.0 → 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/README.md +208 -293
- package/dist/bindings.js +5 -5
- package/dist/budgets.d.ts +132 -0
- package/dist/budgets.d.ts.map +1 -0
- package/dist/budgets.js +248 -0
- package/dist/composition.d.ts +3 -0
- package/dist/composition.d.ts.map +1 -0
- package/dist/composition.js +2 -0
- package/dist/connections.d.ts +81 -0
- package/dist/connections.d.ts.map +1 -0
- package/dist/connections.js +114 -0
- package/dist/derived.d.ts +65 -0
- package/dist/derived.d.ts.map +1 -0
- package/dist/derived.js +95 -0
- package/dist/do-calls.d.ts +94 -0
- package/dist/do-calls.d.ts.map +1 -0
- package/dist/do-calls.js +111 -0
- package/dist/facet-pool.d.ts +90 -0
- package/dist/facet-pool.d.ts.map +1 -0
- package/dist/facet-pool.js +113 -0
- package/dist/{fanout-pool.d.ts → fanout.d.ts} +20 -20
- package/dist/fanout.d.ts.map +1 -0
- package/dist/{fanout-pool.js → fanout.js} +20 -20
- package/dist/{launch-journal.d.ts → fenced-work.d.ts} +58 -17
- package/dist/fenced-work.d.ts.map +1 -0
- package/dist/fenced-work.js +241 -0
- package/dist/generation.d.ts +69 -0
- package/dist/generation.d.ts.map +1 -0
- package/dist/generation.js +118 -0
- package/dist/{facet-image-store.d.ts → image-store.d.ts} +8 -8
- package/dist/image-store.d.ts.map +1 -0
- package/dist/{facet-image-store.js → image-store.js} +4 -4
- package/dist/index.d.ts +16 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +16 -8
- package/dist/{loader-pool.d.ts → isolate-pool.d.ts} +19 -19
- package/dist/isolate-pool.d.ts.map +1 -0
- package/dist/{loader-pool.js → isolate-pool.js} +20 -20
- package/dist/journal.d.ts +111 -0
- package/dist/journal.d.ts.map +1 -0
- package/dist/journal.js +177 -0
- package/dist/outbox.d.ts +249 -0
- package/dist/outbox.d.ts.map +1 -0
- package/dist/outbox.js +355 -0
- package/dist/process-fabric.d.ts +33 -15
- package/dist/process-fabric.d.ts.map +1 -1
- package/dist/process-fabric.js +25 -15
- package/dist/process-host.d.ts +1 -1
- package/dist/process-host.d.ts.map +1 -1
- package/dist/process-host.js +19 -11
- package/dist/sealed.d.ts +78 -0
- package/dist/sealed.d.ts.map +1 -0
- package/dist/sealed.js +145 -0
- package/dist/timers.d.ts +138 -0
- package/dist/timers.d.ts.map +1 -0
- package/dist/timers.js +231 -0
- package/dist/{launch-pacer.d.ts → turn-budget.d.ts} +24 -21
- package/dist/turn-budget.d.ts.map +1 -0
- package/dist/{launch-pacer.js → turn-budget.js} +24 -12
- package/dist/workerd-facet-host.d.ts +67 -70
- package/dist/workerd-facet-host.d.ts.map +1 -1
- package/dist/workerd-facet-host.js +129 -181
- package/examples/agent-core-adapter.ts +191 -0
- package/package.json +4 -2
- package/src/bindings.ts +6 -6
- package/src/budgets.ts +308 -0
- package/src/composition.ts +16 -0
- package/src/connections.ts +140 -0
- package/src/derived.ts +135 -0
- package/src/do-calls.ts +156 -0
- package/src/facet-pool.ts +157 -0
- package/src/{fanout-pool.ts → fanout.ts} +35 -35
- package/src/{launch-journal.ts → fenced-work.ts} +129 -42
- package/src/generation.ts +144 -0
- package/src/{facet-image-store.ts → image-store.ts} +9 -9
- package/src/index.ts +16 -8
- package/src/{loader-pool.ts → isolate-pool.ts} +34 -34
- package/src/journal.ts +242 -0
- package/src/node-async-hooks.d.ts +14 -0
- package/src/outbox.ts +520 -0
- package/src/process-fabric.ts +43 -34
- package/src/process-host.ts +22 -20
- package/src/sealed.ts +150 -0
- package/src/timers.ts +294 -0
- package/src/{launch-pacer.ts → turn-budget.ts} +34 -27
- package/src/workerd-facet-host.ts +159 -208
- package/dist/alarms.d.ts +0 -134
- package/dist/alarms.d.ts.map +0 -1
- package/dist/alarms.js +0 -214
- package/dist/ctx-exports.d.ts +0 -47
- package/dist/ctx-exports.d.ts.map +0 -1
- package/dist/ctx-exports.js +0 -54
- package/dist/facet-image-store.d.ts.map +0 -1
- package/dist/fanout-pool.d.ts.map +0 -1
- package/dist/launch-journal.d.ts.map +0 -1
- package/dist/launch-journal.js +0 -154
- package/dist/launch-pacer.d.ts.map +0 -1
- package/dist/loader-ledger.d.ts +0 -57
- package/dist/loader-ledger.d.ts.map +0 -1
- package/dist/loader-ledger.js +0 -91
- package/dist/loader-pool.d.ts.map +0 -1
- package/src/alarms.ts +0 -275
- package/src/ctx-exports.ts +0 -77
- package/src/loader-ledger.ts +0 -112
package/dist/loader-ledger.d.ts
DELETED
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* loader-ledger.ts — per-DO accounting for the Worker Loader's two caps.
|
|
3
|
-
*
|
|
4
|
-
* Measured on production workerd: a Durable Object admits ~5–6 concurrent
|
|
5
|
-
* dynamic workers before the platform refuses with "Too many concurrent
|
|
6
|
-
* dynamic workers", one DO method can drive at most 4 concurrent Loader
|
|
7
|
-
* fetches, and loader-cache entries are never released — every DISTINCT
|
|
8
|
-
* `loader.get(id)` permanently consumes one of the dynamic-worker slots for
|
|
9
|
-
* the object's lifetime. Nimbus stays under the caps by construction
|
|
10
|
-
* (`IN_DO_THRESHOLD` = 5 in the fanout pool), which until now meant the slots
|
|
11
|
-
* were counted in prose. This ledger counts them at the fabric's loader call
|
|
12
|
-
* sites instead — the loader pool's slots, a resident process's keyed worker,
|
|
13
|
-
* a one-shot's load — so proximity is measurable and a cap failure can name
|
|
14
|
-
* the ids actually holding slots.
|
|
15
|
-
*
|
|
16
|
-
* Measurement only: no admission control. The caps are the platform's, they
|
|
17
|
-
* are approximate ("~5–6"), and a gate on an approximate number would refuse
|
|
18
|
-
* work the platform would have run.
|
|
19
|
-
*
|
|
20
|
-
* Keyed weakly off the hosting actor's `ctx`, like the facet slot books: the
|
|
21
|
-
* caps are per Durable Object, and dynamic workers die with the isolate that
|
|
22
|
-
* loaded them, so a ledger that goes away with its host describes nothing
|
|
23
|
-
* that still exists.
|
|
24
|
-
*/
|
|
25
|
-
/** Record a keyed `loader.get(id)` — a permanent slot if the id is new. */
|
|
26
|
-
export declare function recordLoaderId(ctx: object, id: string): void;
|
|
27
|
-
/**
|
|
28
|
-
* Count one call into a dynamic worker as a live Loader fetch; the returned
|
|
29
|
-
* function ends it (idempotently), from the caller's own `finally`.
|
|
30
|
-
*
|
|
31
|
-
* A begin/end pair rather than a wrapper on purpose, and the shape is
|
|
32
|
-
* load-bearing: wrapping the stub call in a ledger-owned async frame
|
|
33
|
-
* (`trackLoaderFetch(ctx, () => entrypoint.execute(...))`) left the hosting
|
|
34
|
-
* Durable Object poisoned after every pooled dispatch — the next fabric
|
|
35
|
-
* activity hung the object or reset the instance outright (pid base jumped,
|
|
36
|
-
* every attached WebSocket dropped with no close frame), measured 7/7 on
|
|
37
|
-
* staging and gone 3/3 with the direct call restored. Same seam-quirk class
|
|
38
|
-
* as pipelined `fetch.call`, which workerd refuses for dynamically-loaded
|
|
39
|
-
* workers: an RPC stub call must stay a direct property call awaited by the
|
|
40
|
-
* frame that made it, so the ledger only brackets it.
|
|
41
|
-
*/
|
|
42
|
-
export declare function beginLoaderFetch(ctx: object): () => void;
|
|
43
|
-
/** Snapshot for the diag surface. Pure read; no I/O. */
|
|
44
|
-
export declare function loaderLedgerStats(ctx: object): {
|
|
45
|
-
idsEverGotten: string[];
|
|
46
|
-
liveFetches: number;
|
|
47
|
-
peakLiveFetches: number;
|
|
48
|
-
};
|
|
49
|
-
/**
|
|
50
|
-
* Name the per-DO accounting on a "Too many concurrent dynamic workers"
|
|
51
|
-
* failure; hand every other error back untouched. The platform's message
|
|
52
|
-
* says only that the cap was hit — which ids hold the slots, and that a
|
|
53
|
-
* keyed id can never give one back, is what the operator needs to know to
|
|
54
|
-
* shrink anything.
|
|
55
|
-
*/
|
|
56
|
-
export declare function withDynamicWorkerCapNamed<E>(ctx: object, error: E): E | Error;
|
|
57
|
-
//# sourceMappingURL=loader-ledger.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"loader-ledger.d.ts","sourceRoot":"","sources":["../src/loader-ledger.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAwBH,2EAA2E;AAC3E,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI,CAE5D;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,IAAI,CAUxD;AAED,wDAAwD;AACxD,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG;IAC9C,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,EAAE,MAAM,CAAC;CACzB,CAOA;AAED;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,GAAG,CAAC,GAAG,KAAK,CAW7E"}
|
package/dist/loader-ledger.js
DELETED
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* loader-ledger.ts — per-DO accounting for the Worker Loader's two caps.
|
|
3
|
-
*
|
|
4
|
-
* Measured on production workerd: a Durable Object admits ~5–6 concurrent
|
|
5
|
-
* dynamic workers before the platform refuses with "Too many concurrent
|
|
6
|
-
* dynamic workers", one DO method can drive at most 4 concurrent Loader
|
|
7
|
-
* fetches, and loader-cache entries are never released — every DISTINCT
|
|
8
|
-
* `loader.get(id)` permanently consumes one of the dynamic-worker slots for
|
|
9
|
-
* the object's lifetime. Nimbus stays under the caps by construction
|
|
10
|
-
* (`IN_DO_THRESHOLD` = 5 in the fanout pool), which until now meant the slots
|
|
11
|
-
* were counted in prose. This ledger counts them at the fabric's loader call
|
|
12
|
-
* sites instead — the loader pool's slots, a resident process's keyed worker,
|
|
13
|
-
* a one-shot's load — so proximity is measurable and a cap failure can name
|
|
14
|
-
* the ids actually holding slots.
|
|
15
|
-
*
|
|
16
|
-
* Measurement only: no admission control. The caps are the platform's, they
|
|
17
|
-
* are approximate ("~5–6"), and a gate on an approximate number would refuse
|
|
18
|
-
* work the platform would have run.
|
|
19
|
-
*
|
|
20
|
-
* Keyed weakly off the hosting actor's `ctx`, like the facet slot books: the
|
|
21
|
-
* caps are per Durable Object, and dynamic workers die with the isolate that
|
|
22
|
-
* loaded them, so a ledger that goes away with its host describes nothing
|
|
23
|
-
* that still exists.
|
|
24
|
-
*/
|
|
25
|
-
import { classifyError } from '@nimbus-sh/core/observability/oom-classify.js';
|
|
26
|
-
const ledgers = new WeakMap();
|
|
27
|
-
function ledger(ctx) {
|
|
28
|
-
let entry = ledgers.get(ctx);
|
|
29
|
-
if (!entry) {
|
|
30
|
-
entry = { ids: new Set(), liveFetches: 0, peakLiveFetches: 0 };
|
|
31
|
-
ledgers.set(ctx, entry);
|
|
32
|
-
}
|
|
33
|
-
return entry;
|
|
34
|
-
}
|
|
35
|
-
/** Record a keyed `loader.get(id)` — a permanent slot if the id is new. */
|
|
36
|
-
export function recordLoaderId(ctx, id) {
|
|
37
|
-
ledger(ctx).ids.add(id);
|
|
38
|
-
}
|
|
39
|
-
/**
|
|
40
|
-
* Count one call into a dynamic worker as a live Loader fetch; the returned
|
|
41
|
-
* function ends it (idempotently), from the caller's own `finally`.
|
|
42
|
-
*
|
|
43
|
-
* A begin/end pair rather than a wrapper on purpose, and the shape is
|
|
44
|
-
* load-bearing: wrapping the stub call in a ledger-owned async frame
|
|
45
|
-
* (`trackLoaderFetch(ctx, () => entrypoint.execute(...))`) left the hosting
|
|
46
|
-
* Durable Object poisoned after every pooled dispatch — the next fabric
|
|
47
|
-
* activity hung the object or reset the instance outright (pid base jumped,
|
|
48
|
-
* every attached WebSocket dropped with no close frame), measured 7/7 on
|
|
49
|
-
* staging and gone 3/3 with the direct call restored. Same seam-quirk class
|
|
50
|
-
* as pipelined `fetch.call`, which workerd refuses for dynamically-loaded
|
|
51
|
-
* workers: an RPC stub call must stay a direct property call awaited by the
|
|
52
|
-
* frame that made it, so the ledger only brackets it.
|
|
53
|
-
*/
|
|
54
|
-
export function beginLoaderFetch(ctx) {
|
|
55
|
-
const entry = ledger(ctx);
|
|
56
|
-
entry.liveFetches++;
|
|
57
|
-
entry.peakLiveFetches = Math.max(entry.peakLiveFetches, entry.liveFetches);
|
|
58
|
-
let ended = false;
|
|
59
|
-
return () => {
|
|
60
|
-
if (ended)
|
|
61
|
-
return;
|
|
62
|
-
ended = true;
|
|
63
|
-
entry.liveFetches--;
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
/** Snapshot for the diag surface. Pure read; no I/O. */
|
|
67
|
-
export function loaderLedgerStats(ctx) {
|
|
68
|
-
const entry = ledger(ctx);
|
|
69
|
-
return {
|
|
70
|
-
idsEverGotten: [...entry.ids],
|
|
71
|
-
liveFetches: entry.liveFetches,
|
|
72
|
-
peakLiveFetches: entry.peakLiveFetches,
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
/**
|
|
76
|
-
* Name the per-DO accounting on a "Too many concurrent dynamic workers"
|
|
77
|
-
* failure; hand every other error back untouched. The platform's message
|
|
78
|
-
* says only that the cap was hit — which ids hold the slots, and that a
|
|
79
|
-
* keyed id can never give one back, is what the operator needs to know to
|
|
80
|
-
* shrink anything.
|
|
81
|
-
*/
|
|
82
|
-
export function withDynamicWorkerCapNamed(ctx, error) {
|
|
83
|
-
if (classifyError(error) !== 'dynamic_worker_cap')
|
|
84
|
-
return error;
|
|
85
|
-
const entry = ledger(ctx);
|
|
86
|
-
const platform = error instanceof Error ? error.message : String(error);
|
|
87
|
-
return new Error(`${platform} — this Durable Object has ${entry.ids.size} loader id(s) permanently `
|
|
88
|
-
+ `holding dynamic-worker slots (a loader.get id is never released): `
|
|
89
|
-
+ `${[...entry.ids].join(', ') || '(none recorded)'}; live Loader fetches ${entry.liveFetches}, `
|
|
90
|
-
+ `peak ${entry.peakLiveFetches}`, { cause: error });
|
|
91
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"loader-pool.d.ts","sourceRoot":"","sources":["../src/loader-pool.ts"],"names":[],"mappings":"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"}
|
package/src/alarms.ts
DELETED
|
@@ -1,275 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* alarms.ts — Durable Object alarm multiplexing + isolate-generation
|
|
3
|
-
* machinery, persisted across hibernation.
|
|
4
|
-
*
|
|
5
|
-
* Workerd hibernates Durable Objects between requests to free memory. On
|
|
6
|
-
* wake, the new isolate must rebuild its in-memory state from SQL — but it
|
|
7
|
-
* also needs to know "is this the same lifecycle as before, or did workerd
|
|
8
|
-
* recycle me?" That distinction matters for recovery (warmJoin vs cold init)
|
|
9
|
-
* and is captured by the isolate generation, a counter persisted across
|
|
10
|
-
* hibernations.
|
|
11
|
-
*
|
|
12
|
-
* A Durable Object has ONE alarm, and a second `setAlarm()` silently
|
|
13
|
-
* overwrites the first — so every alarm-driven subsystem coordinates through
|
|
14
|
-
* a single reason→deadline map and one dispatcher. Reasons are plain strings
|
|
15
|
-
* registered by the embedder: `scheduleAlarm` arms one, and `dispatchAlarm`
|
|
16
|
-
* runs the embedder-supplied handler for every reason whose deadline has
|
|
17
|
-
* passed.
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
import { errorText } from '@nimbus-sh/core/_shared/error-text.js';
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* The storage the alarm map lives in. `setAlarm` is optional because
|
|
24
|
-
* `wrangler dev` serves a storage without it, which is the whole reason
|
|
25
|
-
* scheduling degrades to a no-op instead of throwing.
|
|
26
|
-
*/
|
|
27
|
-
export interface AlarmStorage {
|
|
28
|
-
get(key: string): Promise<unknown>;
|
|
29
|
-
put(key: string, value: unknown): Promise<void>;
|
|
30
|
-
delete(key: string): Promise<boolean>;
|
|
31
|
-
setAlarm?(scheduledTime: number): Promise<void>;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/** The hosting actor's context, as the alarm coordination reads it. */
|
|
35
|
-
export interface AlarmContext {
|
|
36
|
-
storage: AlarmStorage;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Multi-reason alarm coordination map.
|
|
41
|
-
*
|
|
42
|
-
* JSON-serialised `Record<reason, deadlineMsEpoch>` where keys are the
|
|
43
|
-
* embedder's canonical reason strings (e.g. 'w9-flush', 'log-janitor'). The
|
|
44
|
-
* alarm() dispatcher reads this on fire, dispatches every reason whose
|
|
45
|
-
* deadline has passed, and re-arms `ctx.storage.setAlarm` at the earliest
|
|
46
|
-
* remaining deadline.
|
|
47
|
-
*
|
|
48
|
-
* Why a map (not a single nextAlarmAt + reason): two subsystems can have
|
|
49
|
-
* distinct deadlines. Without the map, the later setAlarm() call would
|
|
50
|
-
* overwrite the earlier reason silently, breaking whichever subsystem
|
|
51
|
-
* expected its deadline.
|
|
52
|
-
*
|
|
53
|
-
* Forward-compat: the dispatcher silently drops unknown reasons so a
|
|
54
|
-
* rollback from a future deploy that added new reasons doesn't leave the
|
|
55
|
-
* alarm stuck.
|
|
56
|
-
*
|
|
57
|
-
* The VALUE is live production DO storage ('w1_next_alarm_reasons', from the
|
|
58
|
-
* workstream that introduced it) and must never change — renaming a storage
|
|
59
|
-
* key is a migration, and orphaned rows are the least of what it breaks.
|
|
60
|
-
*/
|
|
61
|
-
export const ALARM_REASONS_KEY = 'w1_next_alarm_reasons';
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Storage key for the isolate-generation counter (cold-start +
|
|
65
|
-
* post-hibernation wake; one increment per fresh isolate).
|
|
66
|
-
*
|
|
67
|
-
* The VALUE is live production DO storage ('w9_isolate_gen') and must never
|
|
68
|
-
* change, same contract as {@link ALARM_REASONS_KEY}.
|
|
69
|
-
*/
|
|
70
|
-
export const ISOLATE_GEN_KEY = 'w9_isolate_gen';
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* The host instance carrying the per-instance alarm chain. The field lives on
|
|
74
|
-
* the embedder's DO instance so one chain serializes every alarm-map
|
|
75
|
-
* read-modify-write for that instance (see {@link scheduleAlarm}).
|
|
76
|
-
*/
|
|
77
|
-
export interface AlarmHost {
|
|
78
|
-
_alarmChain?: Promise<unknown>;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/** The host instance carrying the isolate-generation state. */
|
|
82
|
-
export interface IsolateGenHost {
|
|
83
|
-
_isolateGen: number;
|
|
84
|
-
_isolateGenPersisted: boolean;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Schedule (or re-schedule) an alarm reason. Coordinated via a single map in
|
|
89
|
-
* DO storage so multiple subsystems don't clobber each other's `setAlarm()`
|
|
90
|
-
* calls.
|
|
91
|
-
*
|
|
92
|
-
* Semantics:
|
|
93
|
-
* - Reads the existing reasons map.
|
|
94
|
-
* - Sets `map[reason] = whenMs` IF `whenMs` is sooner than the
|
|
95
|
-
* currently-pending deadline for that reason (or no entry exists).
|
|
96
|
-
* Later-than-pending requests are silently ignored — the existing
|
|
97
|
-
* alarm will fire and re-arm anyway.
|
|
98
|
-
* - Writes the map back and calls `ctx.storage.setAlarm(min(deadlines))`.
|
|
99
|
-
*
|
|
100
|
-
* Cost: 1 storage read + 1 storage write + 1 setAlarm per call. setAlarm
|
|
101
|
-
* itself is billed as 1 row written per DO pricing. At a 60s janitor
|
|
102
|
-
* cadence, this is ~$0.05/mo/session at scale — dwarfed by the
|
|
103
|
-
* hibernation duration savings.
|
|
104
|
-
*
|
|
105
|
-
* Fail-soft: any throw is swallowed with a warn. On older runtimes /
|
|
106
|
-
* wrangler-dev where setAlarm is unavailable, this is a no-op (the
|
|
107
|
-
* subsystem's in-isolate setTimeout fallback continues to work).
|
|
108
|
-
*/
|
|
109
|
-
export function scheduleAlarm(
|
|
110
|
-
host: AlarmHost,
|
|
111
|
-
ctx: AlarmContext,
|
|
112
|
-
reason: string,
|
|
113
|
-
whenMs: number,
|
|
114
|
-
): Promise<boolean> {
|
|
115
|
-
// Serialize every read-modify-write of the reasons map through one
|
|
116
|
-
// per-instance chain: two schedulers firing back-to-back from one activity
|
|
117
|
-
// hook would otherwise interleave their get→put cycles and silently drop
|
|
118
|
-
// whichever reason wrote first.
|
|
119
|
-
const run = async (): Promise<boolean> => {
|
|
120
|
-
try {
|
|
121
|
-
const setAlarmFn = ctx?.storage?.setAlarm;
|
|
122
|
-
if (typeof setAlarmFn !== 'function') return false;
|
|
123
|
-
const existing = (await ctx.storage.get(ALARM_REASONS_KEY)) as
|
|
124
|
-
| Record<string, number>
|
|
125
|
-
| undefined;
|
|
126
|
-
const map: Record<string, number> = { ...(existing || {}) };
|
|
127
|
-
// Earliest-deadline-first: only update if new request is sooner or
|
|
128
|
-
// this reason has no pending entry.
|
|
129
|
-
if (!(reason in map) || whenMs < map[reason]) {
|
|
130
|
-
map[reason] = whenMs;
|
|
131
|
-
await ctx.storage.put(ALARM_REASONS_KEY, map);
|
|
132
|
-
}
|
|
133
|
-
const earliest = Math.min(...Object.values(map));
|
|
134
|
-
setAlarmFn.call(ctx.storage, earliest);
|
|
135
|
-
return true;
|
|
136
|
-
} catch (e) {
|
|
137
|
-
console.warn('[nimbus/W1] scheduleAlarm threw:', errorText(e));
|
|
138
|
-
return false;
|
|
139
|
-
}
|
|
140
|
-
};
|
|
141
|
-
const chained = (host._alarmChain ?? Promise.resolve()).then(run, run);
|
|
142
|
-
host._alarmChain = chained;
|
|
143
|
-
return chained;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* What one alarm handler may return: nothing, or a deadline this reason
|
|
148
|
-
* re-arms itself at. Re-arming through the return value keeps the map's
|
|
149
|
-
* read-modify-write inside the dispatcher, where it is serialized.
|
|
150
|
-
*/
|
|
151
|
-
export type AlarmHandlerResult = void | { rearmAt: number };
|
|
152
|
-
|
|
153
|
-
/** The embedder's reasons, each with the handler that answers it. */
|
|
154
|
-
export type AlarmHandlers = Record<
|
|
155
|
-
string,
|
|
156
|
-
(now: number) => AlarmHandlerResult | Promise<AlarmHandlerResult>
|
|
157
|
-
>;
|
|
158
|
-
|
|
159
|
-
/**
|
|
160
|
-
* Multi-reason alarm dispatcher. Called from the DO's `alarm()` handler with
|
|
161
|
-
* the embedder's handler map.
|
|
162
|
-
*
|
|
163
|
-
* For each pending reason whose deadline has passed, run its handler.
|
|
164
|
-
* Handlers are awaited in place: the alarm invocation is the fresh turn a
|
|
165
|
-
* re-entering subsystem asked for, and it has to stay the one paying for the
|
|
166
|
-
* work it just released.
|
|
167
|
-
*
|
|
168
|
-
* After running fireable reasons, re-arms `ctx.storage.setAlarm` at the
|
|
169
|
-
* earliest remaining deadline. If no reasons remain, deletes the map key and
|
|
170
|
-
* does NOT call setAlarm — the DO becomes hibernation-eligible after the 10s
|
|
171
|
-
* idle window.
|
|
172
|
-
*
|
|
173
|
-
* Forward/back-compat: unknown reasons silently dropped. `onLegacyAlarm`
|
|
174
|
-
* covers an alarm that fires with no map at all — a deploy from before the
|
|
175
|
-
* map existed left a bare `setAlarm` behind, and the embedder decides what
|
|
176
|
-
* that one-time fire means (one dispatch later the map is populated by the
|
|
177
|
-
* next scheduleAlarm call).
|
|
178
|
-
*/
|
|
179
|
-
export function dispatchAlarm(
|
|
180
|
-
host: AlarmHost,
|
|
181
|
-
ctx: AlarmContext,
|
|
182
|
-
handlers: AlarmHandlers,
|
|
183
|
-
onLegacyAlarm?: () => void,
|
|
184
|
-
): Promise<void> {
|
|
185
|
-
// Same serialization as scheduleAlarm: the dispatcher's read→handlers→write
|
|
186
|
-
// cycle must not interleave with an activity-hook scheduleAlarm.
|
|
187
|
-
const chained = (host._alarmChain ?? Promise.resolve()).then(
|
|
188
|
-
() => dispatchAlarmBody(ctx, handlers, onLegacyAlarm),
|
|
189
|
-
() => dispatchAlarmBody(ctx, handlers, onLegacyAlarm),
|
|
190
|
-
);
|
|
191
|
-
host._alarmChain = chained;
|
|
192
|
-
return chained;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
async function dispatchAlarmBody(
|
|
196
|
-
ctx: AlarmContext,
|
|
197
|
-
handlers: AlarmHandlers,
|
|
198
|
-
onLegacyAlarm?: () => void,
|
|
199
|
-
): Promise<void> {
|
|
200
|
-
try {
|
|
201
|
-
const now = Date.now();
|
|
202
|
-
const existing = (await ctx?.storage?.get?.(ALARM_REASONS_KEY)) as
|
|
203
|
-
| Record<string, number>
|
|
204
|
-
| undefined;
|
|
205
|
-
if (!existing || Object.keys(existing).length === 0) {
|
|
206
|
-
onLegacyAlarm?.();
|
|
207
|
-
return;
|
|
208
|
-
}
|
|
209
|
-
const map: Record<string, number> = { ...existing };
|
|
210
|
-
// Snapshot fireable reasons BEFORE running any of them, so a
|
|
211
|
-
// handler that schedules itself for the next cycle doesn't get
|
|
212
|
-
// immediately re-fired in the same dispatch.
|
|
213
|
-
const fired: string[] = [];
|
|
214
|
-
for (const [reason, when] of Object.entries(map)) {
|
|
215
|
-
if (when <= now) fired.push(reason);
|
|
216
|
-
}
|
|
217
|
-
for (const reason of fired) {
|
|
218
|
-
delete map[reason];
|
|
219
|
-
const handler = handlers[reason];
|
|
220
|
-
// Unknown reasons silently dropped (forward-compat).
|
|
221
|
-
if (!handler) continue;
|
|
222
|
-
try {
|
|
223
|
-
const result = await handler(now);
|
|
224
|
-
if (result && typeof result.rearmAt === 'number') {
|
|
225
|
-
map[reason] = result.rearmAt;
|
|
226
|
-
}
|
|
227
|
-
} catch (e) {
|
|
228
|
-
console.warn(`[nimbus/W1] dispatch ${reason} threw:`, errorText(e));
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
// Re-arm or clear.
|
|
232
|
-
const setAlarmFn = ctx?.storage?.setAlarm;
|
|
233
|
-
if (Object.keys(map).length > 0) {
|
|
234
|
-
await ctx.storage.put(ALARM_REASONS_KEY, map);
|
|
235
|
-
const earliest = Math.min(...Object.values(map));
|
|
236
|
-
if (typeof setAlarmFn === 'function') {
|
|
237
|
-
setAlarmFn.call(ctx.storage, earliest);
|
|
238
|
-
}
|
|
239
|
-
} else {
|
|
240
|
-
try { await ctx.storage.delete(ALARM_REASONS_KEY); } catch {}
|
|
241
|
-
// No remaining reasons → no setAlarm call → DO becomes
|
|
242
|
-
// hibernation-eligible after the 10s idle window.
|
|
243
|
-
}
|
|
244
|
-
} catch (e) {
|
|
245
|
-
console.warn('[nimbus/W1] dispatchAlarm threw:', errorText(e));
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
/** Increment + persist the isolate-gen counter once per fresh isolate. */
|
|
250
|
-
export async function maybeBumpIsolateGen(host: IsolateGenHost, ctx: AlarmContext): Promise<void> {
|
|
251
|
-
if (host._isolateGenPersisted) return;
|
|
252
|
-
host._isolateGenPersisted = true;
|
|
253
|
-
try {
|
|
254
|
-
const prev = (await ctx.storage.get(ISOLATE_GEN_KEY)) as number | undefined;
|
|
255
|
-
// Adopt the persisted truth first, and adopt the bump only after the
|
|
256
|
-
// put resolves. An unpersisted `next` would be re-read as `prev` by the
|
|
257
|
-
// NEXT boot and re-issued — two instances sharing one generation is
|
|
258
|
-
// exactly the pid-aliasing this counter exists to prevent. Running on
|
|
259
|
-
// the previous persisted generation is the lesser lapse, and the
|
|
260
|
-
// put-failure case is replica-only in practice (replicas never spawn).
|
|
261
|
-
//
|
|
262
|
-
// What holds the guarantee is the output gate, not this await: measured,
|
|
263
|
-
// the block body resolves in 0 ms even with a confirmed put, because
|
|
264
|
-
// `await storage.put()` returns before durability. The gate is what
|
|
265
|
-
// keeps a pid from generation N from escaping before N is durable, which
|
|
266
|
-
// is why marking this put `allowUnconfirmed` is not a free speedup — see
|
|
267
|
-
// scratchpad/coldstart-s1.md.
|
|
268
|
-
host._isolateGen = typeof prev === 'number' ? prev : 0;
|
|
269
|
-
const next = host._isolateGen + 1;
|
|
270
|
-
await ctx.storage.put(ISOLATE_GEN_KEY, next);
|
|
271
|
-
host._isolateGen = next;
|
|
272
|
-
} catch (e) {
|
|
273
|
-
console.warn('[nimbus/W9] isolate-gen bump failed:', errorText(e));
|
|
274
|
-
}
|
|
275
|
-
}
|
package/src/ctx-exports.ts
DELETED
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ctx-exports.ts — leaf module holding the ctx.exports reference.
|
|
3
|
-
*
|
|
4
|
-
* Isolated from the embedder's entry module so helpers (notably
|
|
5
|
-
* loader-pool.ts) can read `ctx.exports` without transitively importing the
|
|
6
|
-
* Durable Object classes. Keeping this a leaf (no imports) lets the pool be
|
|
7
|
-
* unit-tested in a plain Node/Bun process.
|
|
8
|
-
*
|
|
9
|
-
* The embedder's fetch handler calls `setCtxExports(ctx.exports)` on the
|
|
10
|
-
* first request; callers like the loader pool read via `getCtxExports()`.
|
|
11
|
-
* If the pool is constructed before the first fetch (unlikely) it just gets
|
|
12
|
-
* null — the caller decides how to degrade.
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* One entry of `ctx.exports`: a top-level entrypoint's loopback factory, which
|
|
17
|
-
* mints a Service Binding stub for that entrypoint when called with props.
|
|
18
|
-
*
|
|
19
|
-
* The stub's RPC surface belongs to the entrypoint CLASS, which this leaf
|
|
20
|
-
* cannot see — `Cloudflare.Exports` is derived from the embedder's own main
|
|
21
|
-
* module, so for a library it evaluates to `{}`. A caller that knows the class
|
|
22
|
-
* names the surface it expects (`factory<MySupervisorRpc>({ props })`); one
|
|
23
|
-
* that does not gets `unknown` and has to narrow, same as
|
|
24
|
-
* `DurableObjectNamespace<T>` and `RpcStub<T>` in @cloudflare/workers-types.
|
|
25
|
-
*/
|
|
26
|
-
export type EntrypointLoopbackFactory = <Stub = unknown>(options: { props: object }) => Stub;
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* `ctx.exports` itself — one factory per top-level entrypoint export, keyed by
|
|
30
|
-
* export name. Absent names read as undefined, which is how a caller finds out
|
|
31
|
-
* the embedder's entry module does not re-export the class it needs.
|
|
32
|
-
*/
|
|
33
|
-
export type CtxExports = Record<string, EntrypointLoopbackFactory | undefined>;
|
|
34
|
-
|
|
35
|
-
let _ctxExports: CtxExports | null = null;
|
|
36
|
-
|
|
37
|
-
export function setCtxExports(value: CtxExports): void {
|
|
38
|
-
if (_ctxExports) return; // first-write-wins, same as the prior inline impl
|
|
39
|
-
_ctxExports = value;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export function getCtxExports(): CtxExports | null {
|
|
43
|
-
return _ctxExports;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* The fabric mints supervisor bindings for the programs it hosts, but the
|
|
48
|
-
* entrypoint class that answers them belongs to the embedder, so its
|
|
49
|
-
* ctx.exports name is registered once at composition time rather than
|
|
50
|
-
* hardcoded here. First-write-wins, same as the ctx.exports holder above.
|
|
51
|
-
*/
|
|
52
|
-
let _supervisorEntrypointName: string | null = null;
|
|
53
|
-
|
|
54
|
-
export function setSupervisorEntrypointName(name: string): void {
|
|
55
|
-
if (_supervisorEntrypointName) return;
|
|
56
|
-
_supervisorEntrypointName = name;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Resolve the registered supervisor entrypoint on an exports object —
|
|
61
|
-
* `exportsObj` when given (a WorkerEntrypoint reads its own ctx.exports),
|
|
62
|
-
* the held ctx.exports otherwise. Calling the result with props mints one
|
|
63
|
-
* supervisor binding (`env.SUPERVISOR`) for one hosted program. Null when
|
|
64
|
-
* either half is missing; the caller decides whether that degrades or throws.
|
|
65
|
-
*/
|
|
66
|
-
export function supervisorEntrypoint(exportsObj?: unknown): EntrypointLoopbackFactory | null {
|
|
67
|
-
const exports = exportsObj ?? _ctxExports;
|
|
68
|
-
if (!_supervisorEntrypointName) return null;
|
|
69
|
-
if ((typeof exports !== 'object' && typeof exports !== 'function') || exports === null) return null;
|
|
70
|
-
const factory = (exports as Record<string, unknown>)[_supervisorEntrypointName];
|
|
71
|
-
return typeof factory === 'function' ? (factory as EntrypointLoopbackFactory) : null;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/** The registered name, for error messages that point at the missing export. */
|
|
75
|
-
export function supervisorEntrypointName(): string | null {
|
|
76
|
-
return _supervisorEntrypointName;
|
|
77
|
-
}
|
package/src/loader-ledger.ts
DELETED
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* loader-ledger.ts — per-DO accounting for the Worker Loader's two caps.
|
|
3
|
-
*
|
|
4
|
-
* Measured on production workerd: a Durable Object admits ~5–6 concurrent
|
|
5
|
-
* dynamic workers before the platform refuses with "Too many concurrent
|
|
6
|
-
* dynamic workers", one DO method can drive at most 4 concurrent Loader
|
|
7
|
-
* fetches, and loader-cache entries are never released — every DISTINCT
|
|
8
|
-
* `loader.get(id)` permanently consumes one of the dynamic-worker slots for
|
|
9
|
-
* the object's lifetime. Nimbus stays under the caps by construction
|
|
10
|
-
* (`IN_DO_THRESHOLD` = 5 in the fanout pool), which until now meant the slots
|
|
11
|
-
* were counted in prose. This ledger counts them at the fabric's loader call
|
|
12
|
-
* sites instead — the loader pool's slots, a resident process's keyed worker,
|
|
13
|
-
* a one-shot's load — so proximity is measurable and a cap failure can name
|
|
14
|
-
* the ids actually holding slots.
|
|
15
|
-
*
|
|
16
|
-
* Measurement only: no admission control. The caps are the platform's, they
|
|
17
|
-
* are approximate ("~5–6"), and a gate on an approximate number would refuse
|
|
18
|
-
* work the platform would have run.
|
|
19
|
-
*
|
|
20
|
-
* Keyed weakly off the hosting actor's `ctx`, like the facet slot books: the
|
|
21
|
-
* caps are per Durable Object, and dynamic workers die with the isolate that
|
|
22
|
-
* loaded them, so a ledger that goes away with its host describes nothing
|
|
23
|
-
* that still exists.
|
|
24
|
-
*/
|
|
25
|
-
|
|
26
|
-
import { classifyError } from '@nimbus-sh/core/observability/oom-classify.js';
|
|
27
|
-
|
|
28
|
-
interface LoaderLedger {
|
|
29
|
-
/** Distinct loader ids ever gotten — each one a permanently consumed slot. */
|
|
30
|
-
ids: Set<string>;
|
|
31
|
-
/** In-flight calls into dynamic workers, right now. */
|
|
32
|
-
liveFetches: number;
|
|
33
|
-
/** The most that were ever in flight at once. */
|
|
34
|
-
peakLiveFetches: number;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const ledgers = new WeakMap<object, LoaderLedger>();
|
|
38
|
-
|
|
39
|
-
function ledger(ctx: object): LoaderLedger {
|
|
40
|
-
let entry = ledgers.get(ctx);
|
|
41
|
-
if (!entry) {
|
|
42
|
-
entry = { ids: new Set(), liveFetches: 0, peakLiveFetches: 0 };
|
|
43
|
-
ledgers.set(ctx, entry);
|
|
44
|
-
}
|
|
45
|
-
return entry;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/** Record a keyed `loader.get(id)` — a permanent slot if the id is new. */
|
|
49
|
-
export function recordLoaderId(ctx: object, id: string): void {
|
|
50
|
-
ledger(ctx).ids.add(id);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Count one call into a dynamic worker as a live Loader fetch; the returned
|
|
55
|
-
* function ends it (idempotently), from the caller's own `finally`.
|
|
56
|
-
*
|
|
57
|
-
* A begin/end pair rather than a wrapper on purpose, and the shape is
|
|
58
|
-
* load-bearing: wrapping the stub call in a ledger-owned async frame
|
|
59
|
-
* (`trackLoaderFetch(ctx, () => entrypoint.execute(...))`) left the hosting
|
|
60
|
-
* Durable Object poisoned after every pooled dispatch — the next fabric
|
|
61
|
-
* activity hung the object or reset the instance outright (pid base jumped,
|
|
62
|
-
* every attached WebSocket dropped with no close frame), measured 7/7 on
|
|
63
|
-
* staging and gone 3/3 with the direct call restored. Same seam-quirk class
|
|
64
|
-
* as pipelined `fetch.call`, which workerd refuses for dynamically-loaded
|
|
65
|
-
* workers: an RPC stub call must stay a direct property call awaited by the
|
|
66
|
-
* frame that made it, so the ledger only brackets it.
|
|
67
|
-
*/
|
|
68
|
-
export function beginLoaderFetch(ctx: object): () => void {
|
|
69
|
-
const entry = ledger(ctx);
|
|
70
|
-
entry.liveFetches++;
|
|
71
|
-
entry.peakLiveFetches = Math.max(entry.peakLiveFetches, entry.liveFetches);
|
|
72
|
-
let ended = false;
|
|
73
|
-
return () => {
|
|
74
|
-
if (ended) return;
|
|
75
|
-
ended = true;
|
|
76
|
-
entry.liveFetches--;
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/** Snapshot for the diag surface. Pure read; no I/O. */
|
|
81
|
-
export function loaderLedgerStats(ctx: object): {
|
|
82
|
-
idsEverGotten: string[];
|
|
83
|
-
liveFetches: number;
|
|
84
|
-
peakLiveFetches: number;
|
|
85
|
-
} {
|
|
86
|
-
const entry = ledger(ctx);
|
|
87
|
-
return {
|
|
88
|
-
idsEverGotten: [...entry.ids],
|
|
89
|
-
liveFetches: entry.liveFetches,
|
|
90
|
-
peakLiveFetches: entry.peakLiveFetches,
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Name the per-DO accounting on a "Too many concurrent dynamic workers"
|
|
96
|
-
* failure; hand every other error back untouched. The platform's message
|
|
97
|
-
* says only that the cap was hit — which ids hold the slots, and that a
|
|
98
|
-
* keyed id can never give one back, is what the operator needs to know to
|
|
99
|
-
* shrink anything.
|
|
100
|
-
*/
|
|
101
|
-
export function withDynamicWorkerCapNamed<E>(ctx: object, error: E): E | Error {
|
|
102
|
-
if (classifyError(error) !== 'dynamic_worker_cap') return error;
|
|
103
|
-
const entry = ledger(ctx);
|
|
104
|
-
const platform = error instanceof Error ? error.message : String(error);
|
|
105
|
-
return new Error(
|
|
106
|
-
`${platform} — this Durable Object has ${entry.ids.size} loader id(s) permanently `
|
|
107
|
-
+ `holding dynamic-worker slots (a loader.get id is never released): `
|
|
108
|
-
+ `${[...entry.ids].join(', ') || '(none recorded)'}; live Loader fetches ${entry.liveFetches}, `
|
|
109
|
-
+ `peak ${entry.peakLiveFetches}`,
|
|
110
|
-
{ cause: error },
|
|
111
|
-
);
|
|
112
|
-
}
|