@nimbus-sh/fabric 0.1.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/LICENSE +21 -0
- package/README.md +487 -0
- package/dist/alarms.d.ts +134 -0
- package/dist/alarms.d.ts.map +1 -0
- package/dist/alarms.js +214 -0
- package/dist/bindings.d.ts +316 -0
- package/dist/bindings.d.ts.map +1 -0
- package/dist/bindings.js +678 -0
- package/dist/ctx-exports.d.ts +47 -0
- package/dist/ctx-exports.d.ts.map +1 -0
- package/dist/ctx-exports.js +54 -0
- package/dist/facet-image-store.d.ts +112 -0
- package/dist/facet-image-store.d.ts.map +1 -0
- package/dist/facet-image-store.js +181 -0
- package/dist/fanout-pool.d.ts +223 -0
- package/dist/fanout-pool.d.ts.map +1 -0
- package/dist/fanout-pool.js +368 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +25 -0
- package/dist/inner-do-registry.d.ts +41 -0
- package/dist/inner-do-registry.d.ts.map +1 -0
- package/dist/inner-do-registry.js +51 -0
- package/dist/launch-journal.d.ts +170 -0
- package/dist/launch-journal.d.ts.map +1 -0
- package/dist/launch-journal.js +154 -0
- package/dist/launch-pacer.d.ts +173 -0
- package/dist/launch-pacer.d.ts.map +1 -0
- package/dist/launch-pacer.js +193 -0
- package/dist/loader-ledger.d.ts +57 -0
- package/dist/loader-ledger.d.ts.map +1 -0
- package/dist/loader-ledger.js +91 -0
- package/dist/loader-pool.d.ts +315 -0
- package/dist/loader-pool.d.ts.map +1 -0
- package/dist/loader-pool.js +666 -0
- package/dist/process-fabric.d.ts +524 -0
- package/dist/process-fabric.d.ts.map +1 -0
- package/dist/process-fabric.js +388 -0
- package/dist/process-host.d.ts +132 -0
- package/dist/process-host.d.ts.map +1 -0
- package/dist/process-host.js +444 -0
- package/dist/vendor/errors.d.ts +24 -0
- package/dist/vendor/errors.d.ts.map +1 -0
- package/dist/vendor/errors.js +46 -0
- package/dist/vendor/serialize.d.ts +3 -0
- package/dist/vendor/serialize.d.ts.map +1 -0
- package/dist/vendor/serialize.js +25 -0
- package/dist/vendor/types.d.ts +69 -0
- package/dist/vendor/types.d.ts.map +1 -0
- package/dist/vendor/types.js +4 -0
- package/dist/workerd-facet-host.d.ts +207 -0
- package/dist/workerd-facet-host.d.ts.map +1 -0
- package/dist/workerd-facet-host.js +508 -0
- package/dist/ws-hibernation-config.d.ts +73 -0
- package/dist/ws-hibernation-config.d.ts.map +1 -0
- package/dist/ws-hibernation-config.js +93 -0
- package/package.json +62 -0
- package/src/alarms.ts +275 -0
- package/src/bindings.ts +871 -0
- package/src/ctx-exports.ts +77 -0
- package/src/facet-image-store.ts +196 -0
- package/src/fanout-pool.ts +503 -0
- package/src/index.ts +26 -0
- package/src/inner-do-registry.ts +58 -0
- package/src/launch-journal.ts +229 -0
- package/src/launch-pacer.ts +231 -0
- package/src/loader-ledger.ts +112 -0
- package/src/loader-pool.ts +984 -0
- package/src/process-fabric.ts +729 -0
- package/src/process-host.ts +566 -0
- package/src/vendor/errors.ts +56 -0
- package/src/vendor/serialize.ts +37 -0
- package/src/vendor/types.ts +75 -0
- package/src/workerd-facet-host.ts +694 -0
- package/src/ws-hibernation-config.ts +123 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* launch-journal.ts — durable record of the resident launches a Durable Object
|
|
3
|
+
* owes, and their recovery after an instance reset.
|
|
4
|
+
*
|
|
5
|
+
* The platform resets a session Durable Object over what one turn has
|
|
6
|
+
* outstanding in storage ("Internal error in Durable Object storage caused
|
|
7
|
+
* object to be reset"), and a resident launch is the largest writer a session
|
|
8
|
+
* has. Everything a launch holds is in memory, so the process it is building
|
|
9
|
+
* and the terminal watching it both go with the instance — the journal is what
|
|
10
|
+
* a LATER instance reads to know that happened, and this module is the whole
|
|
11
|
+
* of that mechanism: the put→sync durability barrier on the way in, the
|
|
12
|
+
* delete→sync release on the way out, and the once-per-instance recovery pump
|
|
13
|
+
* that re-drives what a previous generation left behind.
|
|
14
|
+
*
|
|
15
|
+
* What a launch IS stays the embedder's: the journal stores the record it is
|
|
16
|
+
* given and hands it back on recovery. The mechanism reads only the fields in
|
|
17
|
+
* {@link ResidentLaunchRecord}; everything else in the record rides through
|
|
18
|
+
* opaquely.
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* Prefix for the resident-process journal: one row per resident this session
|
|
22
|
+
* owes the user, keyed by the pid it was built for.
|
|
23
|
+
*
|
|
24
|
+
* A resident holds its state in memory — the process table entry, the facet
|
|
25
|
+
* handle, the terminal — so an instance reset destroys it silently. The row
|
|
26
|
+
* is what a LATER instance reads to know a resident ended that way rather
|
|
27
|
+
* than on purpose: a pid at or below the reader's own pid base was allocated
|
|
28
|
+
* by a previous generation (PID_GEN_STRIDE, core's process-table). Written
|
|
29
|
+
* (and synced) before the launch's first byte of work, rewritten as `running`
|
|
30
|
+
* when the launch settles, and released only when the PROCESS ends — because
|
|
31
|
+
* the resets this row survives strike after the launch as often as during it
|
|
32
|
+
* (measured live, staging 2026-08-13: every observed reset landed seconds
|
|
33
|
+
* AFTER settle).
|
|
34
|
+
*
|
|
35
|
+
* The VALUE is live production DO storage and must never change — renaming a
|
|
36
|
+
* storage key is a migration, and orphaned rows are the least of what it
|
|
37
|
+
* breaks.
|
|
38
|
+
*/
|
|
39
|
+
export declare const RESIDENT_LAUNCH_KEY_PREFIX = "resident-launch:";
|
|
40
|
+
/** A launch is re-driven once. A reset that recurs is not the transient one. */
|
|
41
|
+
export declare const RESIDENT_LAUNCH_MAX_ATTEMPT = 1;
|
|
42
|
+
/**
|
|
43
|
+
* A resident process this session owes the user, as a later instance would
|
|
44
|
+
* have to re-drive it.
|
|
45
|
+
*
|
|
46
|
+
* The launch's own inputs and nothing derived from them: everything a launch
|
|
47
|
+
* builds is a pure function of these, and the images it writes are content-
|
|
48
|
+
* addressed, so re-driving is the same work again rather than a repair. The
|
|
49
|
+
* inputs themselves are the embedder's — a record type extends this base with
|
|
50
|
+
* whatever its `redrive` needs, and the journal never reads those fields.
|
|
51
|
+
*
|
|
52
|
+
* The row lives for the PROCESS's lifetime, not the launch's. Measured live
|
|
53
|
+
* (staging, 2026-08-13): every observed reset struck seconds AFTER the launch
|
|
54
|
+
* settled — the platform kills the object while the resident runs, which is
|
|
55
|
+
* when a launch-scoped row had already been deleted and recovery had nothing
|
|
56
|
+
* to find. A resident's facet cannot outlive its session instance (the
|
|
57
|
+
* process host's held-open leg dies with it), so a row from a previous
|
|
58
|
+
* generation always names a process that is genuinely gone.
|
|
59
|
+
*/
|
|
60
|
+
export interface ResidentLaunchRecord {
|
|
61
|
+
pid: number;
|
|
62
|
+
command: string;
|
|
63
|
+
/** 0 for a launch the user asked for; 1 for the one re-drive it may get. */
|
|
64
|
+
attempt: number;
|
|
65
|
+
/** Where the resident was when its instance died: still being built, or
|
|
66
|
+
* booted and running. Running residents re-drive with a fresh attempt
|
|
67
|
+
* budget — their launch already proved itself once. */
|
|
68
|
+
phase: 'starting' | 'running';
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* The slice of Durable Object storage the journal writes through. Exactly a
|
|
72
|
+
* `DurableObjectStorage`, narrowed to what the mechanism performs — `sync()`
|
|
73
|
+
* is load-bearing, see {@link ResidentLaunchJournal.journal}.
|
|
74
|
+
*/
|
|
75
|
+
export interface LaunchJournalStorage {
|
|
76
|
+
put(key: string, value: unknown): Promise<void>;
|
|
77
|
+
delete(key: string): Promise<boolean>;
|
|
78
|
+
list<T = unknown>(options: {
|
|
79
|
+
prefix: string;
|
|
80
|
+
}): Promise<Map<string, T>>;
|
|
81
|
+
sync(): Promise<void>;
|
|
82
|
+
}
|
|
83
|
+
/** What the journal's recovery needs from its embedder. */
|
|
84
|
+
export interface LaunchJournalHost<R extends ResidentLaunchRecord> {
|
|
85
|
+
/**
|
|
86
|
+
* The current instance generation's pid floor. A pid at or below it was
|
|
87
|
+
* allocated by a PREVIOUS instance (core's process-table, PID_GEN_STRIDE),
|
|
88
|
+
* so its launch never finished; above it is this instance's own, still
|
|
89
|
+
* running. The journal takes the base rather than the predicate so the one
|
|
90
|
+
* definition of what a prior-generation pid is stays in the process table.
|
|
91
|
+
*/
|
|
92
|
+
generationBase(): number;
|
|
93
|
+
/**
|
|
94
|
+
* Root a recovery re-drive on the instance (`ctx.waitUntil`) so it is not
|
|
95
|
+
* an abandoned promise between turns.
|
|
96
|
+
*/
|
|
97
|
+
waitUntil(promise: Promise<unknown>): void;
|
|
98
|
+
/**
|
|
99
|
+
* Re-drive an interrupted launch from its journalled inputs. `attempt` is
|
|
100
|
+
* the budget the re-drive spends — the mechanism computes it, the embedder
|
|
101
|
+
* carries it into the launch it starts. The result is discarded: a re-drive
|
|
102
|
+
* owns its own process, and nobody is waiting on the pid it allocates.
|
|
103
|
+
*/
|
|
104
|
+
redrive(record: R, attempt: number): Promise<unknown>;
|
|
105
|
+
/** A re-drive is being started for this record. */
|
|
106
|
+
onRedrive?(record: R): void;
|
|
107
|
+
/** The record's re-drive budget is spent; the resident stays stopped. */
|
|
108
|
+
onAbandoned?(record: R): void;
|
|
109
|
+
/** The re-drive itself failed. */
|
|
110
|
+
onRedriveFailed?(record: R, error: unknown): void;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* The resident-launch journal of one Durable Object instance.
|
|
114
|
+
*
|
|
115
|
+
* In-memory state here is per-instance on purpose: `journalledPids` tracks the
|
|
116
|
+
* rows THIS instance wrote, and `recovered` whether this instance has already
|
|
117
|
+
* read the journal a reset leaves behind. Rows from a previous instance are
|
|
118
|
+
* recovery's to consume, never the release path's.
|
|
119
|
+
*/
|
|
120
|
+
export declare class ResidentLaunchJournal<R extends ResidentLaunchRecord> {
|
|
121
|
+
private readonly storage;
|
|
122
|
+
private readonly host;
|
|
123
|
+
/**
|
|
124
|
+
* Pids THIS instance holds journal rows for. What keeps the terminal hook —
|
|
125
|
+
* which fires for every process, shells and one-shots included — from
|
|
126
|
+
* paying a storage delete for pids that never had a row.
|
|
127
|
+
*/
|
|
128
|
+
private journalledPids;
|
|
129
|
+
/** Whether this instance has already read the journal a reset leaves behind. */
|
|
130
|
+
private recovered;
|
|
131
|
+
constructor(storage: LaunchJournalStorage, host: LaunchJournalHost<R>);
|
|
132
|
+
/**
|
|
133
|
+
* Record a launch as in flight, so an instance that replaces this one knows
|
|
134
|
+
* it never finished. Best-effort: a launch that cannot be journalled still
|
|
135
|
+
* runs, and a reset then costs exactly what it cost before the journal.
|
|
136
|
+
*
|
|
137
|
+
* Synced, not merely put: `await put()` resolves before durability, and the
|
|
138
|
+
* reset this journal exists for destroys every write its turn still had
|
|
139
|
+
* outstanding — measured live, a launch killed in its first chunks left NO
|
|
140
|
+
* row for the replacement instance to find, which is how the recovery this
|
|
141
|
+
* feeds sat inert while its own test stayed green. `sync()` is the storage
|
|
142
|
+
* layer's durability barrier: the row is on disk before the launch performs
|
|
143
|
+
* its first byte of real work. What remains is a reset between the put and
|
|
144
|
+
* the sync's completion — and a launch that dies there has not started, so
|
|
145
|
+
* losing its row costs a retype, not a recovery.
|
|
146
|
+
*/
|
|
147
|
+
journal(record: R): Promise<void>;
|
|
148
|
+
/** True while this instance holds a journal row for `pid`. */
|
|
149
|
+
has(pid: number): boolean;
|
|
150
|
+
/**
|
|
151
|
+
* The journal row's one release: the process is over, nothing is owed.
|
|
152
|
+
* Synced so an instance reset moments later cannot roll the delete back and
|
|
153
|
+
* resurrect a process the user watched end.
|
|
154
|
+
*/
|
|
155
|
+
release(pid: number): Promise<void>;
|
|
156
|
+
/**
|
|
157
|
+
* Re-drive the launches a previous instance was building when it was reset.
|
|
158
|
+
*
|
|
159
|
+
* Sited on the launch-turn pump because the pump is what an alarm calls, and
|
|
160
|
+
* a launch that was suspended has an alarm armed for it — a reset during a
|
|
161
|
+
* chunk fails that alarm, and the platform re-delivers it to the instance
|
|
162
|
+
* that replaces this one. So the first turn after a reset is already this
|
|
163
|
+
* one.
|
|
164
|
+
*
|
|
165
|
+
* Runs once per instance: the journal only changes when a launch of THIS
|
|
166
|
+
* instance starts or settles, and those are rows this instance wrote.
|
|
167
|
+
*/
|
|
168
|
+
recoverInterrupted(): Promise<void>;
|
|
169
|
+
}
|
|
170
|
+
//# sourceMappingURL=launch-journal.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"launch-journal.d.ts","sourceRoot":"","sources":["../src/launch-journal.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,0BAA0B,qBAAqB,CAAC;AAE7D,gFAAgF;AAChF,eAAO,MAAM,2BAA2B,IAAI,CAAC;AAE7C;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,WAAW,oBAAoB;IACnC,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,4EAA4E;IAC5E,OAAO,EAAE,MAAM,CAAC;IAChB;;4DAEwD;IACxD,KAAK,EAAE,UAAU,GAAG,SAAS,CAAC;CAC/B;AAED;;;;GAIG;AACH,MAAM,WAAW,oBAAoB;IACnC,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAChD,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACtC,IAAI,CAAC,CAAC,GAAG,OAAO,EAAE,OAAO,EAAE;QAAE,MAAM,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,CAAC;IACxE,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACvB;AAED,2DAA2D;AAC3D,MAAM,WAAW,iBAAiB,CAAC,CAAC,SAAS,oBAAoB;IAC/D;;;;;;OAMG;IACH,cAAc,IAAI,MAAM,CAAC;IACzB;;;OAGG;IACH,SAAS,CAAC,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,GAAG,IAAI,CAAC;IAC3C;;;;;OAKG;IACH,OAAO,CAAC,MAAM,EAAE,CAAC,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACtD,mDAAmD;IACnD,SAAS,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,IAAI,CAAC;IAC5B,yEAAyE;IACzE,WAAW,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,IAAI,CAAC;IAC9B,kCAAkC;IAClC,eAAe,CAAC,CAAC,MAAM,EAAE,CAAC,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI,CAAC;CACnD;AAED;;;;;;;GAOG;AACH,qBAAa,qBAAqB,CAAC,CAAC,SAAS,oBAAoB;IAW7D,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,IAAI;IAXvB;;;;OAIG;IACH,OAAO,CAAC,cAAc,CAAqB;IAC3C,gFAAgF;IAChF,OAAO,CAAC,SAAS,CAAS;gBAGP,OAAO,EAAE,oBAAoB,EAC7B,IAAI,EAAE,iBAAiB,CAAC,CAAC,CAAC;IAG7C;;;;;;;;;;;;;;OAcG;IACG,OAAO,CAAC,MAAM,EAAE,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAUvC,8DAA8D;IAC9D,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO;IAIzB;;;;OAIG;IACG,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAUzC;;;;;;;;;;;OAWG;IACG,kBAAkB,IAAI,OAAO,CAAC,IAAI,CAAC;CA6B1C"}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* launch-journal.ts — durable record of the resident launches a Durable Object
|
|
3
|
+
* owes, and their recovery after an instance reset.
|
|
4
|
+
*
|
|
5
|
+
* The platform resets a session Durable Object over what one turn has
|
|
6
|
+
* outstanding in storage ("Internal error in Durable Object storage caused
|
|
7
|
+
* object to be reset"), and a resident launch is the largest writer a session
|
|
8
|
+
* has. Everything a launch holds is in memory, so the process it is building
|
|
9
|
+
* and the terminal watching it both go with the instance — the journal is what
|
|
10
|
+
* a LATER instance reads to know that happened, and this module is the whole
|
|
11
|
+
* of that mechanism: the put→sync durability barrier on the way in, the
|
|
12
|
+
* delete→sync release on the way out, and the once-per-instance recovery pump
|
|
13
|
+
* that re-drives what a previous generation left behind.
|
|
14
|
+
*
|
|
15
|
+
* What a launch IS stays the embedder's: the journal stores the record it is
|
|
16
|
+
* given and hands it back on recovery. The mechanism reads only the fields in
|
|
17
|
+
* {@link ResidentLaunchRecord}; everything else in the record rides through
|
|
18
|
+
* opaquely.
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* Prefix for the resident-process journal: one row per resident this session
|
|
22
|
+
* owes the user, keyed by the pid it was built for.
|
|
23
|
+
*
|
|
24
|
+
* A resident holds its state in memory — the process table entry, the facet
|
|
25
|
+
* handle, the terminal — so an instance reset destroys it silently. The row
|
|
26
|
+
* is what a LATER instance reads to know a resident ended that way rather
|
|
27
|
+
* than on purpose: a pid at or below the reader's own pid base was allocated
|
|
28
|
+
* by a previous generation (PID_GEN_STRIDE, core's process-table). Written
|
|
29
|
+
* (and synced) before the launch's first byte of work, rewritten as `running`
|
|
30
|
+
* when the launch settles, and released only when the PROCESS ends — because
|
|
31
|
+
* the resets this row survives strike after the launch as often as during it
|
|
32
|
+
* (measured live, staging 2026-08-13: every observed reset landed seconds
|
|
33
|
+
* AFTER settle).
|
|
34
|
+
*
|
|
35
|
+
* The VALUE is live production DO storage and must never change — renaming a
|
|
36
|
+
* storage key is a migration, and orphaned rows are the least of what it
|
|
37
|
+
* breaks.
|
|
38
|
+
*/
|
|
39
|
+
export const RESIDENT_LAUNCH_KEY_PREFIX = 'resident-launch:';
|
|
40
|
+
/** A launch is re-driven once. A reset that recurs is not the transient one. */
|
|
41
|
+
export const RESIDENT_LAUNCH_MAX_ATTEMPT = 1;
|
|
42
|
+
/**
|
|
43
|
+
* The resident-launch journal of one Durable Object instance.
|
|
44
|
+
*
|
|
45
|
+
* In-memory state here is per-instance on purpose: `journalledPids` tracks the
|
|
46
|
+
* rows THIS instance wrote, and `recovered` whether this instance has already
|
|
47
|
+
* read the journal a reset leaves behind. Rows from a previous instance are
|
|
48
|
+
* recovery's to consume, never the release path's.
|
|
49
|
+
*/
|
|
50
|
+
export class ResidentLaunchJournal {
|
|
51
|
+
storage;
|
|
52
|
+
host;
|
|
53
|
+
/**
|
|
54
|
+
* Pids THIS instance holds journal rows for. What keeps the terminal hook —
|
|
55
|
+
* which fires for every process, shells and one-shots included — from
|
|
56
|
+
* paying a storage delete for pids that never had a row.
|
|
57
|
+
*/
|
|
58
|
+
journalledPids = new Set();
|
|
59
|
+
/** Whether this instance has already read the journal a reset leaves behind. */
|
|
60
|
+
recovered = false;
|
|
61
|
+
constructor(storage, host) {
|
|
62
|
+
this.storage = storage;
|
|
63
|
+
this.host = host;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Record a launch as in flight, so an instance that replaces this one knows
|
|
67
|
+
* it never finished. Best-effort: a launch that cannot be journalled still
|
|
68
|
+
* runs, and a reset then costs exactly what it cost before the journal.
|
|
69
|
+
*
|
|
70
|
+
* Synced, not merely put: `await put()` resolves before durability, and the
|
|
71
|
+
* reset this journal exists for destroys every write its turn still had
|
|
72
|
+
* outstanding — measured live, a launch killed in its first chunks left NO
|
|
73
|
+
* row for the replacement instance to find, which is how the recovery this
|
|
74
|
+
* feeds sat inert while its own test stayed green. `sync()` is the storage
|
|
75
|
+
* layer's durability barrier: the row is on disk before the launch performs
|
|
76
|
+
* its first byte of real work. What remains is a reset between the put and
|
|
77
|
+
* the sync's completion — and a launch that dies there has not started, so
|
|
78
|
+
* losing its row costs a retype, not a recovery.
|
|
79
|
+
*/
|
|
80
|
+
async journal(record) {
|
|
81
|
+
try {
|
|
82
|
+
this.journalledPids.add(record.pid);
|
|
83
|
+
await this.storage.put(`${RESIDENT_LAUNCH_KEY_PREFIX}${record.pid}`, record);
|
|
84
|
+
await this.storage.sync();
|
|
85
|
+
}
|
|
86
|
+
catch (e) {
|
|
87
|
+
console.warn('[nimbus] resident launch journal write failed:', errorMessage(e));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/** True while this instance holds a journal row for `pid`. */
|
|
91
|
+
has(pid) {
|
|
92
|
+
return this.journalledPids.has(pid);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* The journal row's one release: the process is over, nothing is owed.
|
|
96
|
+
* Synced so an instance reset moments later cannot roll the delete back and
|
|
97
|
+
* resurrect a process the user watched end.
|
|
98
|
+
*/
|
|
99
|
+
async release(pid) {
|
|
100
|
+
if (!this.journalledPids.delete(pid))
|
|
101
|
+
return;
|
|
102
|
+
try {
|
|
103
|
+
await this.storage.delete(`${RESIDENT_LAUNCH_KEY_PREFIX}${pid}`);
|
|
104
|
+
await this.storage.sync();
|
|
105
|
+
}
|
|
106
|
+
catch (e) {
|
|
107
|
+
console.warn('[nimbus] resident launch journal delete failed:', errorMessage(e));
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Re-drive the launches a previous instance was building when it was reset.
|
|
112
|
+
*
|
|
113
|
+
* Sited on the launch-turn pump because the pump is what an alarm calls, and
|
|
114
|
+
* a launch that was suspended has an alarm armed for it — a reset during a
|
|
115
|
+
* chunk fails that alarm, and the platform re-delivers it to the instance
|
|
116
|
+
* that replaces this one. So the first turn after a reset is already this
|
|
117
|
+
* one.
|
|
118
|
+
*
|
|
119
|
+
* Runs once per instance: the journal only changes when a launch of THIS
|
|
120
|
+
* instance starts or settles, and those are rows this instance wrote.
|
|
121
|
+
*/
|
|
122
|
+
async recoverInterrupted() {
|
|
123
|
+
if (this.recovered)
|
|
124
|
+
return;
|
|
125
|
+
this.recovered = true;
|
|
126
|
+
const journal = await this.storage.list({ prefix: RESIDENT_LAUNCH_KEY_PREFIX });
|
|
127
|
+
const base = this.host.generationBase();
|
|
128
|
+
for (const [key, record] of journal) {
|
|
129
|
+
// A pid at or below this instance's base was allocated by a PREVIOUS one
|
|
130
|
+
// (process-table.ts, PID_GEN_STRIDE), so its launch never finished; above
|
|
131
|
+
// the base is this instance's own, still running. Same predicate as
|
|
132
|
+
// `session/rpc.ts` uses to attribute a prior generation's pid.
|
|
133
|
+
if (!(record.pid > 0 && record.pid <= base))
|
|
134
|
+
continue;
|
|
135
|
+
await this.storage.delete(key);
|
|
136
|
+
if (record.attempt >= RESIDENT_LAUNCH_MAX_ATTEMPT) {
|
|
137
|
+
this.host.onAbandoned?.(record);
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
this.host.onRedrive?.(record);
|
|
141
|
+
// Not awaited: this call is running inside the alarm that granted the
|
|
142
|
+
// turn, and the launch it starts asks for turns of its own through that
|
|
143
|
+
// same alarm — awaiting it here would be waiting on an alarm that cannot
|
|
144
|
+
// be scheduled until this one returns.
|
|
145
|
+
this.host.waitUntil(this.host.redrive(record, record.attempt + 1)
|
|
146
|
+
.catch((e) => {
|
|
147
|
+
this.host.onRedriveFailed?.(record, e);
|
|
148
|
+
}));
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
function errorMessage(error) {
|
|
153
|
+
return error instanceof Error ? error.message : String(error);
|
|
154
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* launch-pacer.ts — spreading a resident launch across Durable Object turns.
|
|
3
|
+
*
|
|
4
|
+
* Building a resident process is the largest single span of computation this
|
|
5
|
+
* session performs: for pi it walks a 17 MB source tree through eight
|
|
6
|
+
* enrichment passes, serializes a 22.9 MB module map, and writes that map into
|
|
7
|
+
* the image store. Done in one turn it occupied the session DO's only thread
|
|
8
|
+
* for 15-35 s, and a session that cannot reach its thread cannot service the
|
|
9
|
+
* terminal WebSocket — the launch turn finished `outcome=ok` and the terminal
|
|
10
|
+
* died anyway, painting "[process terminal closed]" over a process that was
|
|
11
|
+
* still running.
|
|
12
|
+
*
|
|
13
|
+
* A faster launch does not fix that. A launch half the length still blocks the
|
|
14
|
+
* thread for as long as it runs, and the socket is dropped inside that window
|
|
15
|
+
* whether or not the work succeeds. What fixes it is never holding the thread
|
|
16
|
+
* for long in the first place, which means suspending the launch at bounded
|
|
17
|
+
* intervals and resuming it on a fresh turn. Responsiveness stops depending on
|
|
18
|
+
* how long the total work takes.
|
|
19
|
+
*
|
|
20
|
+
* A fresh turn is also a fresh CPU budget. The same launches that dropped the
|
|
21
|
+
* socket were also being killed with `exceededCpu` at 31.8 s and 32.5 s
|
|
22
|
+
* against a 30 s ceiling, and no amount of yielding *within* one invocation
|
|
23
|
+
* moves that: CPU accrues to the invocation, not to the pause. Only genuinely
|
|
24
|
+
* re-entering the object resets it.
|
|
25
|
+
*
|
|
26
|
+
* Progress is measured in bytes rather than milliseconds because workerd's
|
|
27
|
+
* clock does not advance without I/O — a wall-clock guard inside a span of
|
|
28
|
+
* pure computation reads zero however many seconds it burns, which is why the
|
|
29
|
+
* phase costs behind this module had to be recovered from per-turn `cpuTime`
|
|
30
|
+
* rather than measured in place. Bytes are what the work is actually
|
|
31
|
+
* proportional to, and they are exact. The same reasoning is why
|
|
32
|
+
* `git/network-facet.ts` bounds its checkout chunks by entries and decoded
|
|
33
|
+
* bytes and treats its wall guard as coarse.
|
|
34
|
+
*/
|
|
35
|
+
/** How a paced launch gets back onto a fresh Durable Object turn. */
|
|
36
|
+
export interface LaunchTurnScheduler {
|
|
37
|
+
/**
|
|
38
|
+
* Suspend until a fresh turn is running this launch again.
|
|
39
|
+
*
|
|
40
|
+
* `chunkEnded` settles when the resumed launch reaches its next suspension
|
|
41
|
+
* point or finishes, so whoever grants the turn can await the work it just
|
|
42
|
+
* released rather than letting it run detached in a handler's microtask
|
|
43
|
+
* drain.
|
|
44
|
+
*/
|
|
45
|
+
nextTurn(chunkEnded: Promise<void>): Promise<void>;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Bytes of launch work one turn may perform before it must yield.
|
|
49
|
+
*
|
|
50
|
+
* Sized so a chunk stays far below both the CPU ceiling and the span in which
|
|
51
|
+
* a terminal socket is at risk, while keeping the number of turn handoffs —
|
|
52
|
+
* each an alarm round trip — small enough not to dominate a launch. pi's
|
|
53
|
+
* 22.9 MB map crosses this about a dozen times per phase that handles it.
|
|
54
|
+
*/
|
|
55
|
+
export declare const LAUNCH_CHUNK_MAX_BYTES = 2000000;
|
|
56
|
+
/**
|
|
57
|
+
* Accounts launch progress and ends the turn when a chunk's worth has been
|
|
58
|
+
* spent.
|
|
59
|
+
*
|
|
60
|
+
* Callers report the work they are about to do or have just done and await
|
|
61
|
+
* the result; a pacer that is not yielding returns without suspending, so the
|
|
62
|
+
* one-shot exec path — which passes no pacer at all — keeps its exact
|
|
63
|
+
* behaviour and cost. Nothing here decides WHAT the launch does, only where it
|
|
64
|
+
* is allowed to stop.
|
|
65
|
+
*/
|
|
66
|
+
export declare class LaunchPacer {
|
|
67
|
+
private readonly scheduler;
|
|
68
|
+
private readonly maxChunkBytes;
|
|
69
|
+
private readonly stillWanted?;
|
|
70
|
+
/** Turn handoffs this launch has taken. Reported with the launch. */
|
|
71
|
+
chunks: number;
|
|
72
|
+
/** Total work accounted, for the same report. */
|
|
73
|
+
bytes: number;
|
|
74
|
+
private spent;
|
|
75
|
+
private chunkEnded;
|
|
76
|
+
/**
|
|
77
|
+
* @param stillWanted Checked every time the launch resumes. A launch spans
|
|
78
|
+
* many turns, so anything may have happened to what it is building for
|
|
79
|
+
* while it was suspended; throwing from here is how a launch stops instead
|
|
80
|
+
* of spending turn after turn on work nothing will use. Checked at the one
|
|
81
|
+
* place a launch can be interrupted, rather than at whichever phases
|
|
82
|
+
* remembered to ask.
|
|
83
|
+
*/
|
|
84
|
+
constructor(scheduler: LaunchTurnScheduler, maxChunkBytes?: number, stillWanted?: (() => void) | undefined);
|
|
85
|
+
/**
|
|
86
|
+
* Account `bytes` of completed work, ending the turn if a chunk is full.
|
|
87
|
+
*
|
|
88
|
+
* Safe to call anywhere the launch holds no state that a concurrent turn
|
|
89
|
+
* could invalidate — which is why the image store registers its whole root
|
|
90
|
+
* set before the first call rather than one entry at a time.
|
|
91
|
+
*/
|
|
92
|
+
spend(bytes: number): Promise<void>;
|
|
93
|
+
/**
|
|
94
|
+
* The launch has finished (or failed). Releases the turn still waiting on
|
|
95
|
+
* the chunk it resumed, so a launch that ends mid-chunk does not strand the
|
|
96
|
+
* handler that granted it.
|
|
97
|
+
*/
|
|
98
|
+
settle(): void;
|
|
99
|
+
}
|
|
100
|
+
/** What {@link LaunchTurnPump} needs from the Durable Object hosting it. */
|
|
101
|
+
export interface LaunchTurnPumpHost {
|
|
102
|
+
/**
|
|
103
|
+
* Arrange for {@link LaunchTurnPump.pump} to run on a fresh Durable Object
|
|
104
|
+
* turn.
|
|
105
|
+
*
|
|
106
|
+
* The embedder satisfies this with an alarm, which is the only primitive
|
|
107
|
+
* that genuinely re-enters the object: a fresh turn is both a released
|
|
108
|
+
* thread and a fresh CPU budget, and a launch needs each for a different
|
|
109
|
+
* reason. Without it the pump degrades to a same-context timer — see
|
|
110
|
+
* {@link LaunchTurnPump.nextTurn}.
|
|
111
|
+
*/
|
|
112
|
+
requestTurn?: () => void;
|
|
113
|
+
/**
|
|
114
|
+
* Awaited first on every pump, before any waiter resumes. Where the
|
|
115
|
+
* resident-launch journal's recovery sits: the pump is what an alarm calls,
|
|
116
|
+
* and the first turn after a reset is the re-delivered alarm of a launch
|
|
117
|
+
* the reset interrupted.
|
|
118
|
+
*/
|
|
119
|
+
recover?: () => Promise<void>;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* The granting side of {@link LaunchTurnScheduler}: parks suspended launches
|
|
123
|
+
* and resumes every one of them when the host grants a fresh turn.
|
|
124
|
+
*/
|
|
125
|
+
export declare class LaunchTurnPump implements LaunchTurnScheduler {
|
|
126
|
+
private readonly host;
|
|
127
|
+
/**
|
|
128
|
+
* Launches suspended between chunks, waiting for a turn of their own.
|
|
129
|
+
*
|
|
130
|
+
* In-memory on purpose: a launch is only meaningful while the process table
|
|
131
|
+
* entry it is building for exists, and both are lost together if the isolate
|
|
132
|
+
* resets. What survives a reset is the journal, which names the launch's
|
|
133
|
+
* INPUTS rather than its position — a resumed queue would be resurrecting
|
|
134
|
+
* half-built work for pids that no longer exist, where re-driving a launch
|
|
135
|
+
* from its inputs is the same idempotent work again.
|
|
136
|
+
*/
|
|
137
|
+
private waiters;
|
|
138
|
+
constructor(host: LaunchTurnPumpHost);
|
|
139
|
+
/**
|
|
140
|
+
* How a paced launch asks for a fresh turn.
|
|
141
|
+
*
|
|
142
|
+
* The host grants one by calling {@link pump} from a context that is
|
|
143
|
+
* genuinely a new invocation — the session's alarm. Without such a host
|
|
144
|
+
* there is no fresh turn to be had, and the launch continues on this one
|
|
145
|
+
* rather than hanging: that is exactly the single-turn launch this path has
|
|
146
|
+
* always performed, so a harness or a runtime without alarms loses the
|
|
147
|
+
* responsiveness but keeps the behaviour.
|
|
148
|
+
*/
|
|
149
|
+
nextTurn(chunkEnded: Promise<void>): Promise<void>;
|
|
150
|
+
/**
|
|
151
|
+
* Run one chunk of every launch waiting for a turn.
|
|
152
|
+
*
|
|
153
|
+
* Awaits the chunk each resumed launch then performs, so the invocation that
|
|
154
|
+
* granted the turn is the invocation that pays for the work — rather than
|
|
155
|
+
* releasing it into a handler's microtask drain, where nothing owns it and
|
|
156
|
+
* the runtime may tear the context down mid-chunk.
|
|
157
|
+
*/
|
|
158
|
+
pump(): Promise<void>;
|
|
159
|
+
/** Whether any launch is suspended waiting for a turn. */
|
|
160
|
+
get hasPending(): boolean;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Chunk bound for this session, honouring the verification knob.
|
|
164
|
+
*
|
|
165
|
+
* `NIMBUS_LAUNCH_CHUNK_BYTES` forces a small bound so an ordinary launch —
|
|
166
|
+
* not just a pathological one — crosses several turns and exercises every
|
|
167
|
+
* suspension point. Without it the multi-turn path would only ever be
|
|
168
|
+
* reached by the largest programs, which is the same reason
|
|
169
|
+
* `git/commands.ts` carries `NIMBUS_GIT_CHECKOUT_CHUNK_ENTRIES`. Unset in
|
|
170
|
+
* production, where the default applies.
|
|
171
|
+
*/
|
|
172
|
+
export declare function launchChunkMaxBytes(env: unknown): number;
|
|
173
|
+
//# sourceMappingURL=launch-pacer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"launch-pacer.d.ts","sourceRoot":"","sources":["../src/launch-pacer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAEH,qEAAqE;AACrE,MAAM,WAAW,mBAAmB;IAClC;;;;;;;OAOG;IACH,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACpD;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,sBAAsB,UAAY,CAAC;AAEhD;;;;;;;;;GASG;AACH,qBAAa,WAAW;IAkBpB,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,aAAa;IAC9B,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAC;IAnB/B,qEAAqE;IACrE,MAAM,SAAK;IACX,iDAAiD;IACjD,KAAK,SAAK;IAEV,OAAO,CAAC,KAAK,CAAK;IAClB,OAAO,CAAC,UAAU,CAA8D;IAEhF;;;;;;;OAOG;gBAEgB,SAAS,EAAE,mBAAmB,EAC9B,aAAa,GAAE,MAA+B,EAC9C,WAAW,CAAC,GAAE,MAAM,IAAI,aAAA;IAG3C;;;;;;OAMG;IACG,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAczC;;;;OAIG;IACH,MAAM,IAAI,IAAI;CAIf;AAQD,4EAA4E;AAC5E,MAAM,WAAW,kBAAkB;IACjC;;;;;;;;;OASG;IACH,WAAW,CAAC,EAAE,MAAM,IAAI,CAAC;IACzB;;;;;OAKG;IACH,OAAO,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC/B;AAED;;;GAGG;AACH,qBAAa,cAAe,YAAW,mBAAmB;IAa5C,OAAO,CAAC,QAAQ,CAAC,IAAI;IAZjC;;;;;;;;;OASG;IACH,OAAO,CAAC,OAAO,CAAgE;gBAElD,IAAI,EAAE,kBAAkB;IAErD;;;;;;;;;OASG;IACH,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAWlD;;;;;;;OAOG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAS3B,0DAA0D;IAC1D,IAAI,UAAU,IAAI,OAAO,CAExB;CACF;AAED;;;;;;;;;GASG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,CAMxD"}
|