@nimbus-sh/fabric 0.7.0 → 0.8.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 +19 -1
- package/dist/bindings.d.ts +11 -0
- package/dist/bindings.d.ts.map +1 -1
- package/dist/bindings.js +19 -8
- package/dist/connections.d.ts +1 -1
- package/dist/connections.js +1 -1
- package/dist/do-calls.d.ts +71 -9
- package/dist/do-calls.d.ts.map +1 -1
- package/dist/do-calls.js +148 -22
- package/dist/facet-pool.d.ts +3 -0
- package/dist/facet-pool.d.ts.map +1 -1
- package/dist/facet-pool.js +3 -0
- package/dist/fanout.d.ts.map +1 -1
- package/dist/fanout.js +6 -3
- package/dist/fenced-work.d.ts +3 -3
- package/dist/host-dispatch.d.ts +12 -4
- package/dist/host-dispatch.d.ts.map +1 -1
- package/dist/host-dispatch.js +11 -4
- package/dist/image-store.js +2 -2
- package/dist/inner-do-registry.d.ts +9 -0
- package/dist/inner-do-registry.d.ts.map +1 -1
- package/dist/inner-do-registry.js +35 -0
- package/dist/isolate-pool.d.ts +8 -1
- package/dist/isolate-pool.d.ts.map +1 -1
- package/dist/isolate-pool.js +9 -7
- package/dist/process-fabric.d.ts +16 -5
- package/dist/process-fabric.d.ts.map +1 -1
- package/dist/process-fabric.js +18 -6
- package/dist/process-host.d.ts +5 -0
- package/dist/process-host.d.ts.map +1 -1
- package/dist/process-host.js +13 -11
- package/dist/supervisor-props.d.ts +47 -0
- package/dist/supervisor-props.d.ts.map +1 -0
- package/dist/supervisor-props.js +37 -0
- package/dist/workerd-facet-host.d.ts +4 -17
- package/dist/workerd-facet-host.d.ts.map +1 -1
- package/dist/workerd-facet-host.js +98 -32
- package/package.json +6 -6
- package/src/bindings.ts +25 -10
- package/src/connections.ts +1 -1
- package/src/do-calls.ts +182 -25
- package/src/facet-pool.ts +5 -0
- package/src/fanout.ts +6 -3
- package/src/fenced-work.ts +3 -3
- package/src/host-dispatch.ts +22 -7
- package/src/image-store.ts +2 -2
- package/src/inner-do-registry.ts +31 -0
- package/src/isolate-pool.ts +16 -8
- package/src/process-fabric.ts +33 -10
- package/src/process-host.ts +18 -11
- package/src/supervisor-props.ts +56 -0
- package/src/workerd-facet-host.ts +99 -37
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import { disposeRpcResource } from '@nimbus-sh/platform/rpc-dispose.js';
|
|
17
|
+
import { StorageLedger, forgetFacetStorage } from '@nimbus-sh/core/runtime/storage-ledger.js';
|
|
18
|
+
import type { SqlDatabase } from '@nimbus-sh/core/runtime/os-contracts.js';
|
|
17
19
|
import {
|
|
18
20
|
getCtxExports,
|
|
19
21
|
stagedBootAssembler,
|
|
@@ -41,6 +43,7 @@ import {
|
|
|
41
43
|
type ResidentDiskReader,
|
|
42
44
|
type ResidentSupervisorProps,
|
|
43
45
|
} from './process-fabric.js';
|
|
46
|
+
import { supervisorLoaderKey } from './supervisor-props.js';
|
|
44
47
|
|
|
45
48
|
// ── Loaded-worker entrypoint plumbing ───────────────────────────────────────
|
|
46
49
|
|
|
@@ -56,7 +59,7 @@ export interface NimbusCtxExports {
|
|
|
56
59
|
key: string;
|
|
57
60
|
name: string | null;
|
|
58
61
|
depth: number;
|
|
59
|
-
supervisor:
|
|
62
|
+
supervisor: ResidentSupervisorProps;
|
|
60
63
|
stage?: unknown;
|
|
61
64
|
};
|
|
62
65
|
}) => LoadedWorkerEntrypointStub;
|
|
@@ -78,7 +81,7 @@ export function getNimbusCtxExports(): NimbusCtxExports {
|
|
|
78
81
|
*/
|
|
79
82
|
export async function createLoadedWorkerEntrypoint(
|
|
80
83
|
ctxExports: NimbusCtxExports,
|
|
81
|
-
supervisor:
|
|
84
|
+
supervisor: ResidentSupervisorProps,
|
|
82
85
|
stage: unknown,
|
|
83
86
|
name: string | null = null,
|
|
84
87
|
): Promise<LoadedWorkerEntrypointStub> {
|
|
@@ -87,7 +90,9 @@ export async function createLoadedWorkerEntrypoint(
|
|
|
87
90
|
}
|
|
88
91
|
return await ctxExports.NimbusLoadedEntrypoint({
|
|
89
92
|
props: {
|
|
90
|
-
|
|
93
|
+
// The entrypoint's loader outlives this instance, and a warm worker keeps
|
|
94
|
+
// the SUPERVISOR binding it was built with.
|
|
95
|
+
key: supervisorLoaderKey(`nimbus-process:${supervisor.doId}:${supervisor.pid}`, supervisor),
|
|
91
96
|
name,
|
|
92
97
|
depth: 0,
|
|
93
98
|
supervisor,
|
|
@@ -262,26 +267,25 @@ interface SlotBook {
|
|
|
262
267
|
next: number;
|
|
263
268
|
/** Slot held by each live pid, so release can find it. */
|
|
264
269
|
held: Map<number, number>;
|
|
270
|
+
/** Explicit (`app-slot-`) names this incarnation started a process under and has not released. */
|
|
271
|
+
live: Set<string>;
|
|
265
272
|
}
|
|
266
273
|
|
|
267
274
|
/**
|
|
268
275
|
* Slot books, per hosting actor, because the facet index is per Durable
|
|
269
276
|
* Object.
|
|
270
277
|
*
|
|
271
|
-
* Keyed weakly off `ctx`,
|
|
272
|
-
*
|
|
273
|
-
*
|
|
274
|
-
*
|
|
275
|
-
*
|
|
276
|
-
*
|
|
277
|
-
*
|
|
278
|
-
* VFS epoch, in which case `invalidatedSince` can only answer poison and the
|
|
279
|
-
* whole store is dropped. A process therefore cannot boot onto a previous
|
|
280
|
-
* tenant's filesystem even when release never ran.
|
|
278
|
+
* Keyed weakly off `ctx`, so a book describes one incarnation. A facet that
|
|
279
|
+
* is still running when that incarnation ends outlives it (measured: a timer
|
|
280
|
+
* or an outgoing call keeps it going), and a `get` of its name with a new
|
|
281
|
+
* class then resets the whole object. So a fresh incarnation's first get of
|
|
282
|
+
* each name ends whatever runs there first: a minted `proc-slot-` name is
|
|
283
|
+
* deleted, which also wipes the storage a previous incarnation left, and an
|
|
284
|
+
* explicit name is aborted, which keeps it.
|
|
281
285
|
*
|
|
282
|
-
* The book
|
|
283
|
-
* allocated against DO storage instead (their owner survives a reset), so
|
|
284
|
-
* fresh incarnation's `next` starting at 0 can never collide with them even
|
|
286
|
+
* The book allocates only the `proc-slot-` space. Durable `app-slot-` names
|
|
287
|
+
* are allocated against DO storage instead (their owner survives a reset), so
|
|
288
|
+
* a fresh incarnation's `next` starting at 0 can never collide with them even
|
|
285
289
|
* before the durable ledger is adopted.
|
|
286
290
|
*/
|
|
287
291
|
const slotBooks = new WeakMap<DurableObjectState, SlotBook>();
|
|
@@ -289,24 +293,28 @@ const slotBooks = new WeakMap<DurableObjectState, SlotBook>();
|
|
|
289
293
|
function slotBook(ctx: DurableObjectState): SlotBook {
|
|
290
294
|
let book = slotBooks.get(ctx);
|
|
291
295
|
if (!book) {
|
|
292
|
-
book = { free: [], next: 0, held: new Map() };
|
|
296
|
+
book = { free: [], next: 0, held: new Map(), live: new Set() };
|
|
293
297
|
slotBooks.set(ctx, book);
|
|
294
298
|
}
|
|
295
299
|
return book;
|
|
296
300
|
}
|
|
297
301
|
|
|
298
|
-
/**
|
|
299
|
-
|
|
302
|
+
/**
|
|
303
|
+
* Take a slot for `pid`, reusing a returned one before minting a new name.
|
|
304
|
+
* `minted` names may still hold storage a previous incarnation of this actor
|
|
305
|
+
* left there, so the caller deletes it before the first get.
|
|
306
|
+
*/
|
|
307
|
+
function acquireSlot(ctx: DurableObjectState, pid: number): { slot: number; minted: boolean } {
|
|
300
308
|
const book = slotBook(ctx);
|
|
301
309
|
const existing = book.held.get(pid);
|
|
302
|
-
if (existing !== undefined) return existing;
|
|
310
|
+
if (existing !== undefined) return { slot: existing, minted: false };
|
|
303
311
|
const reused = book.free.length > 0;
|
|
304
312
|
const slot = reused ? book.free.shift()! : book.next++;
|
|
305
313
|
book.held.set(pid, slot);
|
|
306
314
|
// A fresh name is a permanently consumed facet ID; the durable count lives
|
|
307
315
|
// in the budgets ledger (see budgets.ts).
|
|
308
316
|
if (!reused) recordFacetNameMinted(ctx, book.next);
|
|
309
|
-
return slot;
|
|
317
|
+
return { slot, minted: !reused };
|
|
310
318
|
}
|
|
311
319
|
|
|
312
320
|
/** Return `pid`'s slot to the free list. */
|
|
@@ -320,14 +328,36 @@ function releaseSlot(ctx: DurableObjectState, pid: number): void {
|
|
|
320
328
|
}
|
|
321
329
|
|
|
322
330
|
/**
|
|
323
|
-
* Drop one facet's SQLite by name
|
|
324
|
-
*
|
|
325
|
-
*
|
|
326
|
-
*
|
|
327
|
-
*
|
|
331
|
+
* Drop one facet's SQLite by name, and its row in the session's storage
|
|
332
|
+
* ledger (N18) in the same step: the only way a facet database is deleted.
|
|
333
|
+
* `spawnResident` releases ephemeral processes with abort+delete (storage is
|
|
334
|
+
* slot-reuse hygiene) and durable ones with abort alone (the storage IS the
|
|
335
|
+
* durable application's state); explicit removal arrives here through the
|
|
336
|
+
* coordinator's durable-slot book, owner-checked.
|
|
328
337
|
*/
|
|
338
|
+
/** The session's storage ledger (N18), over this actor's SQL; null where it has none. */
|
|
339
|
+
function sessionLedger(ctx: DurableObjectState): StorageLedger | null {
|
|
340
|
+
const sql = (ctx as { storage?: { sql?: SqlDatabase } }).storage?.sql;
|
|
341
|
+
return sql ? new StorageLedger(sql) : null;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const facetNames = new WeakMap<object, Map<number, string>>();
|
|
345
|
+
|
|
346
|
+
function facetOfPid(ctx: DurableObjectState): Map<number, string> {
|
|
347
|
+
let names = facetNames.get(ctx);
|
|
348
|
+
if (!names) facetNames.set(ctx, names = new Map());
|
|
349
|
+
return names;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/** The facet a running resident process `pid` lives in on this actor, for its storage ledger row. */
|
|
353
|
+
export function residentFacetOf(ctx: DurableObjectState, pid: number): string | undefined {
|
|
354
|
+
return facetNames.get(ctx)?.get(pid);
|
|
355
|
+
}
|
|
356
|
+
|
|
329
357
|
export function deleteFacetStorage(ctx: DurableObjectState, name: string): void {
|
|
330
358
|
facetContainer(ctx).delete(name);
|
|
359
|
+
const sql = (ctx as { storage?: { sql?: SqlDatabase } }).storage?.sql;
|
|
360
|
+
if (sql) forgetFacetStorage(sql, name);
|
|
331
361
|
}
|
|
332
362
|
|
|
333
363
|
|
|
@@ -416,8 +446,12 @@ function spawnResident(
|
|
|
416
446
|
+ `prefix, got '${explicit.name}'`,
|
|
417
447
|
);
|
|
418
448
|
}
|
|
419
|
-
const
|
|
449
|
+
const grant = explicit ? undefined : acquireSlot(ctx, params.pid);
|
|
450
|
+
const slot = grant?.slot;
|
|
420
451
|
const name = explicit ? explicit.name : residentFacetName(slot!);
|
|
452
|
+
if (grant?.minted) {
|
|
453
|
+
try { deleteFacetStorage(ctx, name); } catch { /* nothing stored under this name */ }
|
|
454
|
+
}
|
|
421
455
|
// The start callback is the ONLY way this facet is ever created, and it
|
|
422
456
|
// fires AT MOST ONCE. Every later use goes through the stub below, so the
|
|
423
457
|
// callback running a second time means the facet was released or died —
|
|
@@ -439,27 +473,40 @@ function spawnResident(
|
|
|
439
473
|
evaluated = true;
|
|
440
474
|
return { class: residentProcessClass(ctx, env, disk, supervisor, params) };
|
|
441
475
|
};
|
|
476
|
+
const book = slotBook(ctx);
|
|
477
|
+
const ledger = sessionLedger(ctx);
|
|
442
478
|
let facet: ResidentFacetStub;
|
|
443
479
|
try {
|
|
480
|
+
// N18: the fill is admitted, and recorded under the facet's name, before
|
|
481
|
+
// the facet exists; a refusal leaves no facet.
|
|
482
|
+
if (ledger !== null && params.storageBytes !== undefined) ledger.fill(name, params.storageBytes);
|
|
483
|
+
// get() with a new class on a facet an earlier incarnation left running resets this object.
|
|
484
|
+
if (explicit && !book.live.has(name)) {
|
|
485
|
+
facets.abort(name, new Error('Nimbus: a new incarnation takes this facet name'));
|
|
486
|
+
}
|
|
444
487
|
facet = facets.get(name, start);
|
|
445
488
|
} catch (error) {
|
|
446
489
|
if (slot !== undefined) releaseSlot(ctx, params.pid);
|
|
447
490
|
throw withFacetBudgetNamed(facetNameCount(ctx), error);
|
|
448
491
|
}
|
|
492
|
+
if (explicit) book.live.add(name);
|
|
493
|
+
facetOfPid(ctx).set(params.pid, name);
|
|
449
494
|
|
|
450
495
|
let disposed = false;
|
|
451
496
|
const release = async () => {
|
|
452
497
|
if (disposed) return;
|
|
453
498
|
disposed = true;
|
|
454
499
|
released = true;
|
|
500
|
+
facetOfPid(ctx).delete(params.pid);
|
|
455
501
|
try { facets.abort(name, new Error('Nimbus: resident process released')); } catch { /* already gone */ }
|
|
502
|
+
if (explicit) book.live.delete(name);
|
|
456
503
|
// The two release classes: an ephemeral facet's SQLite is slot-reuse
|
|
457
504
|
// hygiene — the name is handed out again, so the store must not be — and
|
|
458
505
|
// a durable one's is the application itself: abort ends the process, the
|
|
459
506
|
// data stays for the next boot, and only removeDurableApp's explicit
|
|
460
507
|
// deleteFacetStorage call ever drops it.
|
|
461
508
|
if (!explicit?.durable) {
|
|
462
|
-
try {
|
|
509
|
+
try { deleteFacetStorage(ctx, name); } catch { /* already gone */ }
|
|
463
510
|
}
|
|
464
511
|
// Only after the facet is gone. A slot handed out while its previous
|
|
465
512
|
// tenant were still being torn down would have two processes on one name.
|
|
@@ -468,7 +515,11 @@ function spawnResident(
|
|
|
468
515
|
|
|
469
516
|
let started: Promise<unknown>;
|
|
470
517
|
try {
|
|
471
|
-
|
|
518
|
+
// The allowance the ledger admitted, for the facet's store to keep under.
|
|
519
|
+
const startArgs = ledger !== null && params.storageBytes !== undefined && params.startArgs !== null && typeof params.startArgs === 'object'
|
|
520
|
+
? { ...(params.startArgs as Record<string, unknown>), storage: { facet: name, grant: params.storageBytes } }
|
|
521
|
+
: params.startArgs;
|
|
522
|
+
started = facet.startProcess(startArgs);
|
|
472
523
|
} catch (error) {
|
|
473
524
|
void release();
|
|
474
525
|
throw withFacetBudgetNamed(facetNameCount(ctx), error);
|
|
@@ -477,7 +528,17 @@ function spawnResident(
|
|
|
477
528
|
// this one, and it is annotated AFTER awaiting the ledger — the first
|
|
478
529
|
// failure of a fresh incarnation must compare against the persisted count,
|
|
479
530
|
// not the zero its adoption read has not yet replaced.
|
|
480
|
-
started = started.
|
|
531
|
+
started = started.then((payload) => {
|
|
532
|
+
// Once the facet is up (N18) its row is the cap its store keeps under
|
|
533
|
+
// (what it measures plus what it may still grow into), or what it
|
|
534
|
+
// measures if that is more (overshoot).
|
|
535
|
+
const { databaseSize: size, storageCap: cap } = (payload ?? {}) as { databaseSize?: unknown; storageCap?: unknown };
|
|
536
|
+
const measured = typeof size === 'number' && Number.isFinite(size) ? size : null;
|
|
537
|
+
const capped = typeof cap === 'number' && Number.isFinite(cap) ? cap : null;
|
|
538
|
+
const row = capped !== null ? Math.max(capped, measured ?? 0) : measured;
|
|
539
|
+
if (ledger !== null && row !== null) ledger.reportSize(name, row);
|
|
540
|
+
return payload;
|
|
541
|
+
}, async (error) => {
|
|
481
542
|
throw withFacetBudgetNamed(await facetNameCountDurable(ctx), error);
|
|
482
543
|
});
|
|
483
544
|
// A caller reads whichever of `started` and the lifecycle it needs, so keep
|
|
@@ -516,11 +577,14 @@ function residentProcessClass(
|
|
|
516
577
|
+ 'the Worker Loader binding; add it via worker_loaders in wrangler.jsonc.',
|
|
517
578
|
);
|
|
518
579
|
}
|
|
580
|
+
// A warm worker keeps the SUPERVISOR binding it was built with, and the
|
|
581
|
+
// loader outlives this instance.
|
|
582
|
+
const loaderKey = supervisorLoaderKey(params.workerKey, supervisor);
|
|
519
583
|
try {
|
|
520
584
|
const worker = loader
|
|
521
|
-
.get(
|
|
585
|
+
.get(loaderKey, () => residentWorkerConfig(env, disk, supervisor, params.boot))
|
|
522
586
|
.getDurableObjectClass(RESIDENT_PROCESS_CLASS);
|
|
523
|
-
recordLoaderId(ctx,
|
|
587
|
+
recordLoaderId(ctx, loaderKey);
|
|
524
588
|
return worker;
|
|
525
589
|
} catch (error) {
|
|
526
590
|
throw withDynamicWorkerCapNamed(ctx, error);
|
|
@@ -541,7 +605,7 @@ async function runOneShot<T>(
|
|
|
541
605
|
+ 'the Worker Loader binding; add it via worker_loaders in wrangler.jsonc.',
|
|
542
606
|
);
|
|
543
607
|
}
|
|
544
|
-
const supervisorRpc = supervisorEntrypoint();
|
|
608
|
+
const supervisorRpc = supervisorEntrypoint(undefined, supervisor.route?.supervisorEntrypoint);
|
|
545
609
|
let supervisorBinding: unknown;
|
|
546
610
|
let worker: LoadedWorkerStub | undefined;
|
|
547
611
|
let entrypoint: LoadedWorkerEntrypointStub | undefined;
|
|
@@ -626,11 +690,9 @@ export async function residentWorkerConfig(
|
|
|
626
690
|
? await stagedBootAssembler()(env, boot.stage)
|
|
627
691
|
: await residentLoaderConfig(boot.code, disk());
|
|
628
692
|
assertModuleMapWithinCodeLimit(configModules(config));
|
|
629
|
-
const supervisorRpc = supervisorEntrypoint();
|
|
693
|
+
const supervisorRpc = supervisorEntrypoint(undefined, supervisor.route?.supervisorEntrypoint);
|
|
630
694
|
if (!supervisorRpc) {
|
|
631
|
-
throw new Error(
|
|
632
|
-
`Nimbus: ctx.exports.${supervisorEntrypointName() ?? '<supervisor entrypoint>'} unavailable`,
|
|
633
|
-
);
|
|
695
|
+
throw new Error(`Nimbus: ctx.exports.${supervisor.route?.supervisorEntrypoint ?? supervisorEntrypointName() ?? '<supervisor entrypoint>'} unavailable`);
|
|
634
696
|
}
|
|
635
697
|
return { ...config, env: { SUPERVISOR: supervisorRpc({ props: supervisor }) } };
|
|
636
698
|
}
|