@nimbus-sh/worker 0.2.3 → 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/dist/_shared/session-router.d.ts +8 -0
- package/dist/_shared/session-router.d.ts.map +1 -1
- package/dist/_shared/session-router.js +8 -0
- package/dist/facets/cirrus-real.js +1 -1
- package/dist/facets/manager.d.ts +40 -126
- package/dist/facets/manager.d.ts.map +1 -1
- package/dist/facets/manager.js +97 -313
- package/dist/facets/opencode-staging.d.ts +1 -1
- package/dist/facets/opencode-staging.d.ts.map +1 -1
- package/dist/facets/opencode-staging.js +4 -0
- package/dist/facets/vite-dev-server.d.ts +2 -2
- package/dist/facets/vite-dev-server.js +4 -4
- package/dist/git/commands.d.ts +8 -0
- package/dist/git/commands.d.ts.map +1 -1
- package/dist/git/commands.js +41 -9
- package/dist/git/network-facet.d.ts +1 -1
- package/dist/git/network-facet.d.ts.map +1 -1
- package/dist/git/network-facet.js +35 -12
- package/dist/git-bundle.generated.d.ts +2 -2
- package/dist/git-bundle.generated.d.ts.map +1 -1
- package/dist/git-bundle.generated.js +3 -3
- package/dist/index.js +1 -1
- package/dist/loaders/child-process/spawn-facet.js +1 -1
- package/dist/loaders/child-process/spawn-pool.d.ts.map +1 -1
- package/dist/loaders/child-process/spawn-pool.js +2 -2
- package/dist/loaders/generated-workers.d.ts +1 -1
- package/dist/loaders/generated-workers.d.ts.map +1 -1
- package/dist/loaders/generated-workers.js +2 -2
- package/dist/loaders/npm-resolve-preamble.d.ts +3 -3
- package/dist/loaders/npm-resolve-preamble.js +3 -3
- package/dist/loaders/pre-bundle-preamble.d.ts +5 -5
- package/dist/loaders/pre-bundle-preamble.js +6 -6
- package/dist/loaders/process-host.d.ts +6 -107
- package/dist/loaders/process-host.d.ts.map +1 -1
- package/dist/loaders/process-host.js +6 -405
- package/dist/npm/install-batch-facet.js +1 -1
- package/dist/npm/installer.d.ts +4 -4
- package/dist/npm/installer.js +18 -18
- package/dist/npm/pre-bundle-facet.d.ts +2 -2
- package/dist/npm/pre-bundle-facet.js +5 -5
- package/dist/npm/resolve-facet.d.ts +1 -1
- package/dist/npm/resolve-facet.js +1 -1
- package/dist/npm/resolve-one-facet.d.ts +5 -5
- package/dist/npm/resolve-one-facet.js +5 -5
- package/dist/router/index.js +1 -1
- package/dist/router/remote-api.js +8 -0
- package/dist/runtime/bun-repl.d.ts +1 -1
- package/dist/runtime/bun-repl.js +3 -3
- package/dist/runtime/facet-loader-host.d.ts +5 -8
- package/dist/runtime/facet-loader-host.d.ts.map +1 -1
- package/dist/runtime/facet-loader-host.js +7 -11
- package/dist/runtime/node-repl.js +2 -2
- package/dist/runtime/python-repl.js +3 -3
- package/dist/runtime/ruby-repl.js +2 -2
- package/dist/runtime/ruby-resident.js +1 -1
- package/dist/session/hibernation.d.ts +18 -51
- package/dist/session/hibernation.d.ts.map +1 -1
- package/dist/session/hibernation.js +49 -186
- package/dist/session/init-phases.d.ts +2 -2
- package/dist/session/init-phases.d.ts.map +1 -1
- package/dist/session/init-phases.js +1 -1
- package/dist/session/init.d.ts.map +1 -1
- package/dist/session/init.js +6 -3
- package/dist/session/keys.d.ts +14 -37
- package/dist/session/keys.d.ts.map +1 -1
- package/dist/session/keys.js +17 -37
- package/dist/session/nimbus-session.d.ts +10 -4
- package/dist/session/nimbus-session.d.ts.map +1 -1
- package/dist/session/nimbus-session.js +20 -11
- package/dist/session/port-capability.d.ts +43 -0
- package/dist/session/port-capability.d.ts.map +1 -0
- package/dist/session/port-capability.js +49 -0
- package/dist/session/programmatic.d.ts +41 -5
- package/dist/session/programmatic.d.ts.map +1 -1
- package/dist/session/programmatic.js +147 -15
- package/dist/session/routes.d.ts +2 -0
- package/dist/session/routes.d.ts.map +1 -1
- package/dist/session/routes.js +91 -16
- package/dist/session/rpc.d.ts +35 -8
- package/dist/session/rpc.d.ts.map +1 -1
- package/dist/session/rpc.js +73 -14
- package/dist/session/start-real-vite.d.ts.map +1 -1
- package/dist/session/start-real-vite.js +2 -0
- package/dist/session/state-store.d.ts +3 -2
- package/dist/session/state-store.d.ts.map +1 -1
- package/dist/session/state-store.js +3 -2
- package/dist/session/supervisor-rpc.d.ts.map +1 -1
- package/dist/session/supervisor-rpc.js +5 -0
- package/dist/session/ws.d.ts +2 -2
- package/dist/session/ws.d.ts.map +1 -1
- package/dist/session/ws.js +2 -2
- package/dist/wrangler/nimbus-wrangler.js +1 -1
- package/package.json +3 -2
- package/dist/facets/inner-do-registry.d.ts +0 -41
- package/dist/facets/inner-do-registry.d.ts.map +0 -1
- package/dist/facets/inner-do-registry.js +0 -51
- package/dist/facets/launch-pacer.d.ts +0 -111
- package/dist/facets/launch-pacer.d.ts.map +0 -1
- package/dist/facets/launch-pacer.js +0 -130
- package/dist/loaders/fanout-pool.d.ts +0 -207
- package/dist/loaders/fanout-pool.d.ts.map +0 -1
- package/dist/loaders/fanout-pool.js +0 -363
- package/dist/loaders/loader-pool.d.ts +0 -294
- package/dist/loaders/loader-pool.d.ts.map +0 -1
- package/dist/loaders/loader-pool.js +0 -642
- package/dist/loaders/process-fabric.d.ts +0 -509
- package/dist/loaders/process-fabric.d.ts.map +0 -1
- package/dist/loaders/process-fabric.js +0 -360
- package/dist/loaders/vendor/errors.d.ts +0 -24
- package/dist/loaders/vendor/errors.d.ts.map +0 -1
- package/dist/loaders/vendor/errors.js +0 -46
- package/dist/loaders/vendor/serialize.d.ts +0 -3
- package/dist/loaders/vendor/serialize.d.ts.map +0 -1
- package/dist/loaders/vendor/serialize.js +0 -25
- package/dist/loaders/vendor/types.d.ts +0 -69
- package/dist/loaders/vendor/types.d.ts.map +0 -1
- package/dist/loaders/vendor/types.js +0 -4
- package/dist/loaders/workerd-facet-host.d.ts +0 -141
- package/dist/loaders/workerd-facet-host.d.ts.map +0 -1
- package/dist/loaders/workerd-facet-host.js +0 -284
- package/dist/session/bindings.d.ts +0 -209
- package/dist/session/bindings.d.ts.map +0 -1
- package/dist/session/bindings.js +0 -680
- package/dist/session/ctx-exports.d.ts +0 -16
- package/dist/session/ctx-exports.d.ts.map +0 -1
- package/dist/session/ctx-exports.js +0 -22
- package/dist/session/ws-hibernation-config.d.ts +0 -64
- package/dist/session/ws-hibernation-config.d.ts.map +0 -1
- package/dist/session/ws-hibernation-config.js +0 -89
|
@@ -1,207 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Two-tier fan-out primitive for work that must execute in Worker Loader
|
|
3
|
-
* facets without tripping workerd's per-DO dynamic-worker ceiling.
|
|
4
|
-
*
|
|
5
|
-
* A single Durable Object method can drive at most four concurrent
|
|
6
|
-
* Worker Loader fetches before extra dispatches serialize or fail. Small
|
|
7
|
-
* batches therefore run in the coordinator DO through NimbusLoaderPool.
|
|
8
|
-
* Wider batches are sharded across sibling NimbusSession DOs, each of
|
|
9
|
-
* which owns its own four-loader budget.
|
|
10
|
-
*
|
|
11
|
-
* Routing is deterministic: each task has a stable key, and the key maps
|
|
12
|
-
* to a sibling DO shard. There is no silent fallback to width-1 execution;
|
|
13
|
-
* missing LOADER or NIMBUS_SESSION bindings fail loudly so install and
|
|
14
|
-
* runtime operations do not appear successful after partial dispatch.
|
|
15
|
-
*/
|
|
16
|
-
/**
|
|
17
|
-
* Threshold at which routing switches from coordinator-local loaders to
|
|
18
|
-
* sibling Durable Objects.
|
|
19
|
-
*
|
|
20
|
-
* Set to **5** so the in-DO path stays below the V8 4-loaders-per-method
|
|
21
|
-
* cap by construction. width < 5 stays local; width >= 5 uses sibling DOs.
|
|
22
|
-
*/
|
|
23
|
-
export declare const IN_DO_THRESHOLD = 5;
|
|
24
|
-
/**
|
|
25
|
-
* Hard cap on concurrent peer DOs per single submitMany call. Throughput stays
|
|
26
|
-
* flat through this width while keeping per-request scheduler pressure bounded.
|
|
27
|
-
*/
|
|
28
|
-
export declare const MAX_PEER_FANOUT = 32;
|
|
29
|
-
/**
|
|
30
|
-
* Bounded retries for a peer-DO shard dispatch that rejects with a
|
|
31
|
-
* transient platform reset (code roll-over, storage cold-start hiccup).
|
|
32
|
-
* Sibling DOs are addressed by stable name, so the retry re-dispatches
|
|
33
|
-
* the SAME shard to the re-provisioning object; the fanned-out work
|
|
34
|
-
* (packument resolution, tarball materialisation) is idempotent, so
|
|
35
|
-
* re-running a shard is safe. Budget mirrors the resolve-facet's own
|
|
36
|
-
* per-fetch retry policy so a single flaky cold start no longer fails a
|
|
37
|
-
* whole install. The same budget covers an overloaded peer, on the longer
|
|
38
|
-
* schedule below. Non-transient rejections (OOM, count mismatch, genuine
|
|
39
|
-
* task throw) are NOT retried — they propagate on the first hit.
|
|
40
|
-
*/
|
|
41
|
-
export declare const PEER_TRANSIENT_RESET_RETRIES = 3;
|
|
42
|
-
export declare const PEER_RETRY_BACKOFF_MS: number[];
|
|
43
|
-
/**
|
|
44
|
-
* Backoff for a shard whose peer DO was shed as overloaded. The object is
|
|
45
|
-
* alive and the shard never ran; what it needs is time for the input-gate
|
|
46
|
-
* queue to drain, so the schedule is an order of magnitude longer than the
|
|
47
|
-
* reset schedule. A whole-batch abort here used to fail an entire install.
|
|
48
|
-
*/
|
|
49
|
-
export declare const PEER_OVERLOAD_BACKOFF_MS: number[];
|
|
50
|
-
/**
|
|
51
|
-
* Peer shards dispatched per phase. Each phase is a barrier that costs its
|
|
52
|
-
* slowest member, so a wide fan-out pays ⌈shards / FANOUT_PHASE_SIZE⌉ serial
|
|
53
|
-
* round-trips; the size trades that serialization against simultaneous cold
|
|
54
|
-
* sibling DO starts.
|
|
55
|
-
*
|
|
56
|
-
* The six-barrier profile once measured on a 123-package install (21 shards of
|
|
57
|
-
* ~6 packages, 10.6/6.8/21.8/7.9/34.6/4.8 s) came from the shard count, not
|
|
58
|
-
* from this width. Capping install shards at INSTALL_PEER_CAP fixed it at the
|
|
59
|
-
* source and that install now clears in two phases. Widening to 8 on top of
|
|
60
|
-
* that bought one further barrier and doubled the simultaneous cold sibling-DO
|
|
61
|
-
* starts, which is the account-level pressure the phasing exists for: twelve
|
|
62
|
-
* concurrent Markflow installs went from 48 simultaneous peer starts to 96 and
|
|
63
|
-
* began timing out. Phasing does not change how many peers start, only how
|
|
64
|
-
* many start at once, so this width is set by the burst the scheduler
|
|
65
|
-
* tolerates rather than by the barrier count.
|
|
66
|
-
*/
|
|
67
|
-
export declare const FANOUT_PHASE_SIZE = 4;
|
|
68
|
-
/** Argument shape for `submitMany`. */
|
|
69
|
-
export interface FanoutTask<A> {
|
|
70
|
-
/**
|
|
71
|
-
* Routing key for the stable-id router. Same key → same peer DO
|
|
72
|
-
* (when on the peer-DO path). Tests use this to predict placement.
|
|
73
|
-
*/
|
|
74
|
-
key: string;
|
|
75
|
-
/** Argument passed to the user fn. */
|
|
76
|
-
args: A;
|
|
77
|
-
}
|
|
78
|
-
/** Options handed to NimbusFanoutPool's constructor. */
|
|
79
|
-
export interface NimbusFanoutPoolOptions {
|
|
80
|
-
/**
|
|
81
|
-
* Tag prepended to peer-DO ids and in-DO loader ids for debugging
|
|
82
|
-
* (e.g. "npm-install-batch"). Affects neither isolate identity (in-DO
|
|
83
|
-
* path uses the existing NimbusLoaderPool's tag-fold) nor peer-DO
|
|
84
|
-
* deterministic placement (peer ids fold tag + key).
|
|
85
|
-
*/
|
|
86
|
-
tag: string;
|
|
87
|
-
/**
|
|
88
|
-
* Per-task timeout in ms. Default 60_000. Forwarded to the in-DO
|
|
89
|
-
* NimbusLoaderPool's submit calls and to the peer-DO RPC's own
|
|
90
|
-
* NimbusLoaderPool.
|
|
91
|
-
*/
|
|
92
|
-
timeoutMs?: number;
|
|
93
|
-
/**
|
|
94
|
-
* Preamble bundled into every facet (in-DO and inside each peer
|
|
95
|
-
* DO). Same semantics as NimbusLoaderPool's preamble option.
|
|
96
|
-
*/
|
|
97
|
-
preamble?: string;
|
|
98
|
-
/**
|
|
99
|
-
* Wasm modules forwarded to every facet. Same semantics as
|
|
100
|
-
* NimbusLoaderPool's wasmModules option.
|
|
101
|
-
*/
|
|
102
|
-
wasmModules?: Record<string, ArrayBuffer>;
|
|
103
|
-
/**
|
|
104
|
-
* Extra bindings forwarded to every facet. Same semantics as
|
|
105
|
-
* NimbusLoaderPool's extraBindings option.
|
|
106
|
-
*/
|
|
107
|
-
extraBindings?: Record<string, unknown>;
|
|
108
|
-
/**
|
|
109
|
-
* If set, skip the supervisor-RPC binding injection (mirrors
|
|
110
|
-
* NimbusLoaderPool's omitSupervisor flag).
|
|
111
|
-
*/
|
|
112
|
-
omitSupervisor?: boolean;
|
|
113
|
-
/**
|
|
114
|
-
* Invoking process pid, baked into each facet's SUPERVISOR binding so
|
|
115
|
-
* filesystem RPCs (writeBatchStream) are authorized under the caller's
|
|
116
|
-
* credential (mirrors NimbusLoaderPool's supervisorPid). Threaded to both
|
|
117
|
-
* the in-DO loader pool and, via `_rpcFanoutExecute`, the peer-DO pools.
|
|
118
|
-
* npm install passes the shell command's `ctx.pid`; resolve leaves it 0.
|
|
119
|
-
*/
|
|
120
|
-
supervisorPid?: number;
|
|
121
|
-
/**
|
|
122
|
-
* Called once per completed peer-DO dispatch phase with that phase's shard
|
|
123
|
-
* count and elapsed ms. Phases are barriers, so this is what tells a caller
|
|
124
|
-
* whether its fan-out is bounded by shard work or by the number of barriers.
|
|
125
|
-
* Not called on the in-DO path, which has no phases.
|
|
126
|
-
*/
|
|
127
|
-
onDispatchPhase?: (width: number, elapsedMs: number) => void;
|
|
128
|
-
/**
|
|
129
|
-
* Cap on peer DOs this pool will spread one submitMany across. Defaults to
|
|
130
|
-
* MAX_PEER_FANOUT. Tasks beyond the cap bucket into the peers that exist and
|
|
131
|
-
* run through their in-peer pool, so lowering it trades peers for barriers
|
|
132
|
-
* without lowering total concurrency: each peer runs its bucket at
|
|
133
|
-
* concurrency 4, so N peers still resolve 4N tasks at once.
|
|
134
|
-
*
|
|
135
|
-
* A caller sets this when its per-task work is small enough that a peer per
|
|
136
|
-
* task buys nothing but round-trips — one task per peer costs ⌈tasks/
|
|
137
|
-
* FANOUT_PHASE_SIZE⌉ barriers, and each barrier costs a cold sibling start.
|
|
138
|
-
*/
|
|
139
|
-
maxPeers?: number;
|
|
140
|
-
}
|
|
141
|
-
/**
|
|
142
|
-
* Two-tier fan-out pool. Constructed by the supervisor DO; routes
|
|
143
|
-
* each `submitMany` call automatically based on width.
|
|
144
|
-
*
|
|
145
|
-
* Lifetime: cheap to construct (no async init). Multiple submitMany
|
|
146
|
-
* calls share NO state — each is dispatched fresh. The class
|
|
147
|
-
* exists primarily as a clean API surface; per-call dispatch state
|
|
148
|
-
* lives only inside submitMany's promise.
|
|
149
|
-
*/
|
|
150
|
-
export declare class NimbusFanoutPool {
|
|
151
|
-
private readonly env;
|
|
152
|
-
private readonly ctx;
|
|
153
|
-
private readonly opts;
|
|
154
|
-
private readonly coordDoId;
|
|
155
|
-
private readonly coordDoIdShort;
|
|
156
|
-
constructor(env: any, ctx: DurableObjectState, opts: NimbusFanoutPoolOptions);
|
|
157
|
-
/**
|
|
158
|
-
* Dispatch `tasks` across the appropriate topology and return
|
|
159
|
-
* results in input order.
|
|
160
|
-
*
|
|
161
|
-
* Routing:
|
|
162
|
-
* tasks.length < 5 -> coordinator-local NimbusLoaderPool
|
|
163
|
-
* tasks.length >= 5 -> sibling NimbusSession DOs
|
|
164
|
-
*
|
|
165
|
-
* Backpressure: if `tasks.length > MAX_PEER_FANOUT (32)`, tasks
|
|
166
|
-
* are sharded modulo `MAX_PEER_FANOUT` and each shard's bucket
|
|
167
|
-
* runs serially inside its assigned peer DO via the in-peer
|
|
168
|
-
* NimbusLoaderPool's concurrency (capped at 4 there too). A
|
|
169
|
-
* single submitMany call returns when ALL tasks complete (or any
|
|
170
|
-
* throws).
|
|
171
|
-
*
|
|
172
|
-
* `fn` is the user function executed per task. It runs INSIDE a
|
|
173
|
-
* Worker Loader isolate (in the in-DO path) or inside a peer DO's
|
|
174
|
-
* Worker Loader isolate (in the peer-DO path); same trust posture
|
|
175
|
-
* as NimbusLoaderPool.submit. The function is serialized via
|
|
176
|
-
* the vendored serializeFunction (same as NimbusLoaderPool#prepare).
|
|
177
|
-
*/
|
|
178
|
-
submitMany<A, R>(tasks: FanoutTask<A>[], fn: (item: A, env: any) => R | Promise<R>): Promise<R[]>;
|
|
179
|
-
/** Report which topology a task count uses without dispatching. */
|
|
180
|
-
topologyFor(taskCount: number): 'in-do' | 'peer-do' | 'empty';
|
|
181
|
-
/**
|
|
182
|
-
* Compute the deterministic peer-DO id for a task key and peer count.
|
|
183
|
-
*
|
|
184
|
-
* Shape: `nbf:${tag}:${coordDoIdShort}:${shard}` where
|
|
185
|
-
* `shard = hash(key) mod peerCount`. Peer count is
|
|
186
|
-
* `min(tasks.length, MAX_PEER_FANOUT)`.
|
|
187
|
-
*/
|
|
188
|
-
peerSiblingId(key: string, peerCount: number): string;
|
|
189
|
-
private _dispatchInDo;
|
|
190
|
-
private _dispatchPeerDo;
|
|
191
|
-
}
|
|
192
|
-
/**
|
|
193
|
-
* Stable hash → shard. Uses a fresh djb2 over the key (NOT
|
|
194
|
-
* hashSource) and modulos by peerCount.
|
|
195
|
-
*
|
|
196
|
-
* Why not reuse hashSource: hashSource returns a base-36 string,
|
|
197
|
-
* NOT hex — its alphabet is `[0-9a-z]`. parseInt(str, 16) on a
|
|
198
|
-
* base-36 string aborts at the first non-hex char (any of g-z),
|
|
199
|
-
* which produces extremely poor distribution: keys with the same
|
|
200
|
-
* leading-hex-prefix collide regardless of their suffix. (Seen in
|
|
201
|
-
* the wild: `task-0 .. task-7` all collided onto shard 4.)
|
|
202
|
-
*
|
|
203
|
-
* Deterministic: same key + same peerCount → same shard, every run.
|
|
204
|
-
* Tests use this to predict placement.
|
|
205
|
-
*/
|
|
206
|
-
export declare function hashKeyToShard(key: string, peerCount: number): number;
|
|
207
|
-
//# sourceMappingURL=fanout-pool.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"fanout-pool.d.ts","sourceRoot":"","sources":["../../src/loaders/fanout-pool.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAQH;;;;;;GAMG;AACH,eAAO,MAAM,eAAe,IAAI,CAAC;AAEjC;;;GAGG;AACH,eAAO,MAAM,eAAe,KAAK,CAAC;AAElC;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,4BAA4B,IAAI,CAAC;AAC9C,eAAO,MAAM,qBAAqB,UAAmB,CAAC;AAEtD;;;;;GAKG;AACH,eAAO,MAAM,wBAAwB,UAAqB,CAAC;AAE3D;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,iBAAiB,IAAI,CAAC;AAEnC,uCAAuC;AACvC,MAAM,WAAW,UAAU,CAAC,CAAC;IAC3B;;;OAGG;IACH,GAAG,EAAE,MAAM,CAAC;IACZ,sCAAsC;IACtC,IAAI,EAAE,CAAC,CAAC;CACT;AAED,wDAAwD;AACxD,MAAM,WAAW,uBAAuB;IACtC;;;;;OAKG;IACH,GAAG,EAAE,MAAM,CAAC;IACZ;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;IAC1C;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACxC;;;OAGG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB;;;;;;OAMG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;;;OAKG;IACH,eAAe,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7D;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AA4BD;;;;;;;;GAQG;AACH,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAM;IAC1B,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAqB;IACzC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAA0B;IAC/C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAS;gBAE5B,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,kBAAkB,EAAE,IAAI,EAAE,uBAAuB;IAiB5E;;;;;;;;;;;;;;;;;;;;OAoBG;IACG,UAAU,CAAC,CAAC,EAAE,CAAC,EACnB,KAAK,EAAE,UAAU,CAAC,CAAC,CAAC,EAAE,EACtB,EAAE,EAAE,CAAC,IAAI,EAAE,CAAC,EAAE,GAAG,EAAE,GAAG,KAAK,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,GACxC,OAAO,CAAC,CAAC,EAAE,CAAC;IASf,mEAAmE;IACnE,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS,GAAG,OAAO;IAK7D;;;;;;OAMG;IACH,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM;YAOvC,aAAa;YAoCb,eAAe;CAyJ9B;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAUrE"}
|
|
@@ -1,363 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Two-tier fan-out primitive for work that must execute in Worker Loader
|
|
3
|
-
* facets without tripping workerd's per-DO dynamic-worker ceiling.
|
|
4
|
-
*
|
|
5
|
-
* A single Durable Object method can drive at most four concurrent
|
|
6
|
-
* Worker Loader fetches before extra dispatches serialize or fail. Small
|
|
7
|
-
* batches therefore run in the coordinator DO through NimbusLoaderPool.
|
|
8
|
-
* Wider batches are sharded across sibling NimbusSession DOs, each of
|
|
9
|
-
* which owns its own four-loader budget.
|
|
10
|
-
*
|
|
11
|
-
* Routing is deterministic: each task has a stable key, and the key maps
|
|
12
|
-
* to a sibling DO shard. There is no silent fallback to width-1 execution;
|
|
13
|
-
* missing LOADER or NIMBUS_SESSION bindings fail loudly so install and
|
|
14
|
-
* runtime operations do not appear successful after partial dispatch.
|
|
15
|
-
*/
|
|
16
|
-
import { serializeFunction } from './vendor/serialize.js';
|
|
17
|
-
import { BindingError } from './vendor/errors.js';
|
|
18
|
-
import { NimbusLoaderPool } from './loader-pool.js';
|
|
19
|
-
import { disposeRpcResource } from '@nimbus-sh/core/_shared/rpc-dispose.js';
|
|
20
|
-
import { describeError, isDoOverloaded, isTransientDoReset } from '@nimbus-sh/core/observability/oom-classify.js';
|
|
21
|
-
/**
|
|
22
|
-
* Threshold at which routing switches from coordinator-local loaders to
|
|
23
|
-
* sibling Durable Objects.
|
|
24
|
-
*
|
|
25
|
-
* Set to **5** so the in-DO path stays below the V8 4-loaders-per-method
|
|
26
|
-
* cap by construction. width < 5 stays local; width >= 5 uses sibling DOs.
|
|
27
|
-
*/
|
|
28
|
-
export const IN_DO_THRESHOLD = 5;
|
|
29
|
-
/**
|
|
30
|
-
* Hard cap on concurrent peer DOs per single submitMany call. Throughput stays
|
|
31
|
-
* flat through this width while keeping per-request scheduler pressure bounded.
|
|
32
|
-
*/
|
|
33
|
-
export const MAX_PEER_FANOUT = 32;
|
|
34
|
-
/**
|
|
35
|
-
* Bounded retries for a peer-DO shard dispatch that rejects with a
|
|
36
|
-
* transient platform reset (code roll-over, storage cold-start hiccup).
|
|
37
|
-
* Sibling DOs are addressed by stable name, so the retry re-dispatches
|
|
38
|
-
* the SAME shard to the re-provisioning object; the fanned-out work
|
|
39
|
-
* (packument resolution, tarball materialisation) is idempotent, so
|
|
40
|
-
* re-running a shard is safe. Budget mirrors the resolve-facet's own
|
|
41
|
-
* per-fetch retry policy so a single flaky cold start no longer fails a
|
|
42
|
-
* whole install. The same budget covers an overloaded peer, on the longer
|
|
43
|
-
* schedule below. Non-transient rejections (OOM, count mismatch, genuine
|
|
44
|
-
* task throw) are NOT retried — they propagate on the first hit.
|
|
45
|
-
*/
|
|
46
|
-
export const PEER_TRANSIENT_RESET_RETRIES = 3;
|
|
47
|
-
export const PEER_RETRY_BACKOFF_MS = [250, 750, 1500];
|
|
48
|
-
/**
|
|
49
|
-
* Backoff for a shard whose peer DO was shed as overloaded. The object is
|
|
50
|
-
* alive and the shard never ran; what it needs is time for the input-gate
|
|
51
|
-
* queue to drain, so the schedule is an order of magnitude longer than the
|
|
52
|
-
* reset schedule. A whole-batch abort here used to fail an entire install.
|
|
53
|
-
*/
|
|
54
|
-
export const PEER_OVERLOAD_BACKOFF_MS = [1000, 3000, 6000];
|
|
55
|
-
/**
|
|
56
|
-
* Peer shards dispatched per phase. Each phase is a barrier that costs its
|
|
57
|
-
* slowest member, so a wide fan-out pays ⌈shards / FANOUT_PHASE_SIZE⌉ serial
|
|
58
|
-
* round-trips; the size trades that serialization against simultaneous cold
|
|
59
|
-
* sibling DO starts.
|
|
60
|
-
*
|
|
61
|
-
* The six-barrier profile once measured on a 123-package install (21 shards of
|
|
62
|
-
* ~6 packages, 10.6/6.8/21.8/7.9/34.6/4.8 s) came from the shard count, not
|
|
63
|
-
* from this width. Capping install shards at INSTALL_PEER_CAP fixed it at the
|
|
64
|
-
* source and that install now clears in two phases. Widening to 8 on top of
|
|
65
|
-
* that bought one further barrier and doubled the simultaneous cold sibling-DO
|
|
66
|
-
* starts, which is the account-level pressure the phasing exists for: twelve
|
|
67
|
-
* concurrent Markflow installs went from 48 simultaneous peer starts to 96 and
|
|
68
|
-
* began timing out. Phasing does not change how many peers start, only how
|
|
69
|
-
* many start at once, so this width is set by the burst the scheduler
|
|
70
|
-
* tolerates rather than by the barrier count.
|
|
71
|
-
*/
|
|
72
|
-
export const FANOUT_PHASE_SIZE = 4;
|
|
73
|
-
function isNimbusFanoutPeerStub(value) {
|
|
74
|
-
if ((typeof value !== 'object' && typeof value !== 'function') || value === null) {
|
|
75
|
-
return false;
|
|
76
|
-
}
|
|
77
|
-
const execute = Reflect.get(value, '_rpcFanoutExecute');
|
|
78
|
-
return typeof execute === 'function';
|
|
79
|
-
}
|
|
80
|
-
function fanoutPeerStub(value) {
|
|
81
|
-
if ((typeof value !== 'object' && typeof value !== 'function') || value === null) {
|
|
82
|
-
throw new BindingError('NimbusFanoutPool: NIMBUS_SESSION.get() did not return a peer stub.');
|
|
83
|
-
}
|
|
84
|
-
if (!isNimbusFanoutPeerStub(value)) {
|
|
85
|
-
throw new BindingError('NimbusFanoutPool: peer stub does not expose _rpcFanoutExecute().');
|
|
86
|
-
}
|
|
87
|
-
return value;
|
|
88
|
-
}
|
|
89
|
-
/**
|
|
90
|
-
* Two-tier fan-out pool. Constructed by the supervisor DO; routes
|
|
91
|
-
* each `submitMany` call automatically based on width.
|
|
92
|
-
*
|
|
93
|
-
* Lifetime: cheap to construct (no async init). Multiple submitMany
|
|
94
|
-
* calls share NO state — each is dispatched fresh. The class
|
|
95
|
-
* exists primarily as a clean API surface; per-call dispatch state
|
|
96
|
-
* lives only inside submitMany's promise.
|
|
97
|
-
*/
|
|
98
|
-
export class NimbusFanoutPool {
|
|
99
|
-
env;
|
|
100
|
-
ctx;
|
|
101
|
-
opts;
|
|
102
|
-
coordDoId;
|
|
103
|
-
coordDoIdShort;
|
|
104
|
-
constructor(env, ctx, opts) {
|
|
105
|
-
// Hard-fail on missing LOADER. NimbusLoaderPool also enforces this,
|
|
106
|
-
// but we check up front so the diagnostic points at the fanout-pool
|
|
107
|
-
// construction site rather than the deferred loader-pool one.
|
|
108
|
-
if (!env?.LOADER || typeof env.LOADER.get !== 'function') {
|
|
109
|
-
throw new BindingError('NimbusFanoutPool: env.LOADER binding missing or invalid. ' +
|
|
110
|
-
'Add a [[worker_loaders]] entry to wrangler.jsonc.');
|
|
111
|
-
}
|
|
112
|
-
this.env = env;
|
|
113
|
-
this.ctx = ctx;
|
|
114
|
-
this.opts = opts;
|
|
115
|
-
this.coordDoId = ctx.id.toString();
|
|
116
|
-
this.coordDoIdShort = this.coordDoId.slice(0, 12);
|
|
117
|
-
}
|
|
118
|
-
/**
|
|
119
|
-
* Dispatch `tasks` across the appropriate topology and return
|
|
120
|
-
* results in input order.
|
|
121
|
-
*
|
|
122
|
-
* Routing:
|
|
123
|
-
* tasks.length < 5 -> coordinator-local NimbusLoaderPool
|
|
124
|
-
* tasks.length >= 5 -> sibling NimbusSession DOs
|
|
125
|
-
*
|
|
126
|
-
* Backpressure: if `tasks.length > MAX_PEER_FANOUT (32)`, tasks
|
|
127
|
-
* are sharded modulo `MAX_PEER_FANOUT` and each shard's bucket
|
|
128
|
-
* runs serially inside its assigned peer DO via the in-peer
|
|
129
|
-
* NimbusLoaderPool's concurrency (capped at 4 there too). A
|
|
130
|
-
* single submitMany call returns when ALL tasks complete (or any
|
|
131
|
-
* throws).
|
|
132
|
-
*
|
|
133
|
-
* `fn` is the user function executed per task. It runs INSIDE a
|
|
134
|
-
* Worker Loader isolate (in the in-DO path) or inside a peer DO's
|
|
135
|
-
* Worker Loader isolate (in the peer-DO path); same trust posture
|
|
136
|
-
* as NimbusLoaderPool.submit. The function is serialized via
|
|
137
|
-
* the vendored serializeFunction (same as NimbusLoaderPool#prepare).
|
|
138
|
-
*/
|
|
139
|
-
async submitMany(tasks, fn) {
|
|
140
|
-
if (tasks.length === 0)
|
|
141
|
-
return [];
|
|
142
|
-
if (tasks.length < IN_DO_THRESHOLD) {
|
|
143
|
-
return this._dispatchInDo(tasks, fn);
|
|
144
|
-
}
|
|
145
|
-
return this._dispatchPeerDo(tasks, fn);
|
|
146
|
-
}
|
|
147
|
-
/** Report which topology a task count uses without dispatching. */
|
|
148
|
-
topologyFor(taskCount) {
|
|
149
|
-
if (taskCount === 0)
|
|
150
|
-
return 'empty';
|
|
151
|
-
return taskCount < IN_DO_THRESHOLD ? 'in-do' : 'peer-do';
|
|
152
|
-
}
|
|
153
|
-
/**
|
|
154
|
-
* Compute the deterministic peer-DO id for a task key and peer count.
|
|
155
|
-
*
|
|
156
|
-
* Shape: `nbf:${tag}:${coordDoIdShort}:${shard}` where
|
|
157
|
-
* `shard = hash(key) mod peerCount`. Peer count is
|
|
158
|
-
* `min(tasks.length, MAX_PEER_FANOUT)`.
|
|
159
|
-
*/
|
|
160
|
-
peerSiblingId(key, peerCount) {
|
|
161
|
-
const shard = hashKeyToShard(key, peerCount);
|
|
162
|
-
return `nbf:${this.opts.tag}:${this.coordDoIdShort}:${shard}`;
|
|
163
|
-
}
|
|
164
|
-
// ── Private: in-DO dispatch (in-DO fanout) ──────────────────────────────
|
|
165
|
-
async _dispatchInDo(tasks, fn) {
|
|
166
|
-
// Use the existing NimbusLoaderPool. Concurrency = task count
|
|
167
|
-
// (capped at 4 by constructor — tasks.length is already < 5
|
|
168
|
-
// here, so the cap won't bite). Each task = one pool.submit;
|
|
169
|
-
// pool.map runs them with stable-slot reuse.
|
|
170
|
-
const concurrency = Math.min(tasks.length, IN_DO_THRESHOLD - 1);
|
|
171
|
-
const pool = new NimbusLoaderPool(this.env, this.ctx, {
|
|
172
|
-
concurrency,
|
|
173
|
-
timeoutMs: this.opts.timeoutMs,
|
|
174
|
-
tag: this.opts.tag,
|
|
175
|
-
preamble: this.opts.preamble,
|
|
176
|
-
wasmModules: this.opts.wasmModules,
|
|
177
|
-
extraBindings: this.opts.extraBindings,
|
|
178
|
-
omitSupervisor: this.opts.omitSupervisor,
|
|
179
|
-
supervisorPid: this.opts.supervisorPid,
|
|
180
|
-
});
|
|
181
|
-
try {
|
|
182
|
-
// pool.map runs the function over `items` with concurrency-bounded
|
|
183
|
-
// slot reuse. Each slot is one warm loader isolate; we get exactly
|
|
184
|
-
// `concurrency` loader isolates total — well under the 4-cap.
|
|
185
|
-
const items = tasks.map((t) => t.args);
|
|
186
|
-
const results = await pool.map(fn, items);
|
|
187
|
-
// pool.map returns Array<R | null> (null on per-item failure with
|
|
188
|
-
// onError='null'/'skip'). Default onError='throw' rejects on
|
|
189
|
-
// first failure, so successful settle here implies all R values.
|
|
190
|
-
return results;
|
|
191
|
-
}
|
|
192
|
-
finally {
|
|
193
|
-
try {
|
|
194
|
-
pool.dispose();
|
|
195
|
-
}
|
|
196
|
-
catch { /* best-effort */ }
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
// ── Private: peer-DO dispatch (peer-DO fanout) ────────────────────────────
|
|
200
|
-
async _dispatchPeerDo(tasks, fn) {
|
|
201
|
-
const ns = this.env?.NIMBUS_SESSION;
|
|
202
|
-
if (!ns || typeof ns.idFromName !== 'function' || typeof ns.get !== 'function') {
|
|
203
|
-
throw new BindingError('NimbusFanoutPool: env.NIMBUS_SESSION binding missing or invalid. ' +
|
|
204
|
-
'The peer-DO topology requires it. ' +
|
|
205
|
-
'Add the binding via durable_objects.bindings in wrangler.jsonc.');
|
|
206
|
-
}
|
|
207
|
-
// Serialize the user function ONCE here on the supervisor side.
|
|
208
|
-
// Each peer DO receives the same fnSource string; warm peer
|
|
209
|
-
// loader isolates (keyed on fnHash) reuse across calls with
|
|
210
|
-
// identical fns.
|
|
211
|
-
const fnSource = serializeFunction(fn);
|
|
212
|
-
// Cap peer count at MAX_PEER_FANOUT. Tasks beyond N=32 are
|
|
213
|
-
// bucketed into existing shards — each shard's peer DO then
|
|
214
|
-
// runs its bucket through its in-DO NimbusLoaderPool.map
|
|
215
|
-
// (concurrency capped at 4 there).
|
|
216
|
-
const peerCount = Math.min(tasks.length, this.opts.maxPeers ?? MAX_PEER_FANOUT);
|
|
217
|
-
// Group tasks by deterministic shard. Same key → same shard, so
|
|
218
|
-
// tests can predict which peer handles which task.
|
|
219
|
-
const shards = new Map();
|
|
220
|
-
for (const t of tasks) {
|
|
221
|
-
const shard = hashKeyToShard(t.key, peerCount);
|
|
222
|
-
let bucket = shards.get(shard);
|
|
223
|
-
if (!bucket) {
|
|
224
|
-
bucket = [];
|
|
225
|
-
shards.set(shard, bucket);
|
|
226
|
-
}
|
|
227
|
-
bucket.push(t);
|
|
228
|
-
}
|
|
229
|
-
// Dispatch each shard to its peer DO. Build a map from
|
|
230
|
-
// task → its place in the original tasks array so we can
|
|
231
|
-
// reassemble results in input order.
|
|
232
|
-
const taskIndex = new Map();
|
|
233
|
-
tasks.forEach((t, i) => taskIndex.set(t, i));
|
|
234
|
-
const results = new Array(tasks.length);
|
|
235
|
-
// Build one async dispatcher per shard (closure capturing siblingName,
|
|
236
|
-
// bucket). NOT eagerly-started — wrapped in a thunk so we can stagger
|
|
237
|
-
// dispatch via Promise chains without forcing all shards to start
|
|
238
|
-
// simultaneously.
|
|
239
|
-
const dispatchers = [];
|
|
240
|
-
for (const [shard, bucket] of shards) {
|
|
241
|
-
const siblingName = `nbf:${this.opts.tag}:${this.coordDoIdShort}:${shard}`;
|
|
242
|
-
const id = ns.idFromName(siblingName);
|
|
243
|
-
const peerArgs = bucket.map((t) => t.args);
|
|
244
|
-
dispatchers.push(async () => {
|
|
245
|
-
for (let attempt = 0;; attempt++) {
|
|
246
|
-
// Fresh stub per attempt: after a transient reset the previous
|
|
247
|
-
// stub points at a torn-down object, so a retry re-resolves the
|
|
248
|
-
// sibling by its stable id.
|
|
249
|
-
const peerStub = ns.get(id);
|
|
250
|
-
const stub = fanoutPeerStub(peerStub);
|
|
251
|
-
try {
|
|
252
|
-
// Each peer DO RPC call uses ONE LOADER worker on its side.
|
|
253
|
-
// Supervisor → peer DO is a stub.fetch / RPC method call,
|
|
254
|
-
// NOT an env.LOADER.get(); that's the cap-sidestep that
|
|
255
|
-
// makes peer-DO fanout work.
|
|
256
|
-
const rpcResp = await stub._rpcFanoutExecute(fnSource, peerArgs, {
|
|
257
|
-
tag: this.opts.tag,
|
|
258
|
-
timeoutMs: this.opts.timeoutMs,
|
|
259
|
-
preamble: this.opts.preamble,
|
|
260
|
-
wasmModules: this.opts.wasmModules,
|
|
261
|
-
extraBindings: this.opts.extraBindings,
|
|
262
|
-
omitSupervisor: this.opts.omitSupervisor,
|
|
263
|
-
// INSTALL-HONESTY: forward the COORDINATOR's full doId so
|
|
264
|
-
// the peer's NimbusLoaderPool can mint a SUPERVISOR
|
|
265
|
-
// binding that routes back HERE (the user's session DO),
|
|
266
|
-
// not to the peer DO itself. Without this, peer DOs'
|
|
267
|
-
// env.SUPERVISOR.writeBatch / writeBatchStream / stdout /
|
|
268
|
-
// ... write into the peer's own VFS — invisible to the
|
|
269
|
-
// user. See INSTALL-HONESTY-retro.md.
|
|
270
|
-
coordinatorDoId: this.coordDoId,
|
|
271
|
-
// Credential source for peer-side writeBatchStream — the
|
|
272
|
-
// invoking process pid, so package writes are authorized
|
|
273
|
-
// as the user (not rejected as pid:0).
|
|
274
|
-
supervisorPid: this.opts.supervisorPid,
|
|
275
|
-
});
|
|
276
|
-
try {
|
|
277
|
-
const peerResults = rpcResp.results ?? [];
|
|
278
|
-
if (peerResults.length !== bucket.length) {
|
|
279
|
-
throw new Error(`peer DO returned ${peerResults.length} results for ${bucket.length} tasks ` +
|
|
280
|
-
`(siblingName=${siblingName})`);
|
|
281
|
-
}
|
|
282
|
-
// Place each result back into its original input slot.
|
|
283
|
-
for (let i = 0; i < bucket.length; i++) {
|
|
284
|
-
const origIdx = taskIndex.get(bucket[i]);
|
|
285
|
-
if (origIdx === undefined) {
|
|
286
|
-
throw new Error(`peer DO result had no original task index (siblingName=${siblingName})`);
|
|
287
|
-
}
|
|
288
|
-
results[origIdx] = peerResults[i];
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
finally {
|
|
292
|
-
disposeRpcResource(rpcResp);
|
|
293
|
-
}
|
|
294
|
-
return;
|
|
295
|
-
}
|
|
296
|
-
catch (err) {
|
|
297
|
-
const schedule = isTransientDoReset(err) ? PEER_RETRY_BACKOFF_MS
|
|
298
|
-
: isDoOverloaded(err) ? PEER_OVERLOAD_BACKOFF_MS
|
|
299
|
-
: null;
|
|
300
|
-
if (schedule && attempt < PEER_TRANSIENT_RESET_RETRIES) {
|
|
301
|
-
const backoff = schedule[Math.min(attempt, schedule.length - 1)];
|
|
302
|
-
await new Promise((r) => setTimeout(r, backoff));
|
|
303
|
-
continue;
|
|
304
|
-
}
|
|
305
|
-
// Re-throwing bare loses everything only this frame knows: which
|
|
306
|
-
// sibling ran the shard, how wide it was, and how many attempts
|
|
307
|
-
// it already cost. Callers report the message, so a rejection the
|
|
308
|
-
// platform words as `internal error` arrived at the user with no
|
|
309
|
-
// way to tell a one-off peer from a shard that had exhausted its
|
|
310
|
-
// retries. `cause` keeps the original for anything that inspects
|
|
311
|
-
// errors rather than reads them.
|
|
312
|
-
throw new Error(`peer shard ${siblingName} (${bucket.length} task${bucket.length === 1 ? '' : 's'}) `
|
|
313
|
-
+ `failed after ${attempt + 1} attempt${attempt === 0 ? '' : 's'}: ${describeError(err)}`, { cause: err });
|
|
314
|
-
}
|
|
315
|
-
finally {
|
|
316
|
-
disposeRpcResource(peerStub);
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
});
|
|
320
|
-
}
|
|
321
|
-
// Dispatch peer shards in bounded phases. A single Promise.all across all
|
|
322
|
-
// shards can create too many simultaneous cold sibling DO starts under
|
|
323
|
-
// concurrent installs. Promise-chain phasing limits scheduler pressure
|
|
324
|
-
// without sleeps, timers, or idle gaps between phases.
|
|
325
|
-
//
|
|
326
|
-
// A phase is a hard barrier: no shard in phase N+1 starts until every
|
|
327
|
-
// shard in phase N returns, so `width@ms` per phase is what separates
|
|
328
|
-
// "the shards are slow" from "there are too many barriers".
|
|
329
|
-
for (let i = 0; i < dispatchers.length; i += FANOUT_PHASE_SIZE) {
|
|
330
|
-
const phase = dispatchers.slice(i, i + FANOUT_PHASE_SIZE);
|
|
331
|
-
const phaseStartedAt = Date.now();
|
|
332
|
-
await Promise.all(phase.map((d) => d()));
|
|
333
|
-
this.opts.onDispatchPhase?.(phase.length, Date.now() - phaseStartedAt);
|
|
334
|
-
}
|
|
335
|
-
return results;
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
/**
|
|
339
|
-
* Stable hash → shard. Uses a fresh djb2 over the key (NOT
|
|
340
|
-
* hashSource) and modulos by peerCount.
|
|
341
|
-
*
|
|
342
|
-
* Why not reuse hashSource: hashSource returns a base-36 string,
|
|
343
|
-
* NOT hex — its alphabet is `[0-9a-z]`. parseInt(str, 16) on a
|
|
344
|
-
* base-36 string aborts at the first non-hex char (any of g-z),
|
|
345
|
-
* which produces extremely poor distribution: keys with the same
|
|
346
|
-
* leading-hex-prefix collide regardless of their suffix. (Seen in
|
|
347
|
-
* the wild: `task-0 .. task-7` all collided onto shard 4.)
|
|
348
|
-
*
|
|
349
|
-
* Deterministic: same key + same peerCount → same shard, every run.
|
|
350
|
-
* Tests use this to predict placement.
|
|
351
|
-
*/
|
|
352
|
-
export function hashKeyToShard(key, peerCount) {
|
|
353
|
-
if (peerCount <= 1)
|
|
354
|
-
return 0;
|
|
355
|
-
// djb2, returning an unsigned 32-bit integer — full 2^32 range,
|
|
356
|
-
// no string-format conversion gotchas. peerCount <= MAX_PEER_FANOUT
|
|
357
|
-
// (32) << 2^32, so the modulo distributes uniformly for any input.
|
|
358
|
-
let h = 5381;
|
|
359
|
-
for (let i = 0; i < key.length; i++) {
|
|
360
|
-
h = ((h << 5) + h + key.charCodeAt(i)) | 0;
|
|
361
|
-
}
|
|
362
|
-
return (h >>> 0) % peerCount;
|
|
363
|
-
}
|