knitting 0.1.63 → 0.1.73
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 +623 -342
- package/knitting.browser.d.ts +3 -1
- package/knitting.browser.js +1 -1
- package/knitting.d.ts +3 -1
- package/knitting.js +2 -1
- package/map.md +0 -6
- package/package.json +10 -5
- package/prebuilds/darwin-arm64-node-127/knitting_buffer_pointer.node +0 -0
- package/prebuilds/darwin-arm64-node-127/knitting_doorbell.node +0 -0
- package/prebuilds/darwin-arm64-node-137/knitting_buffer_pointer.node +0 -0
- package/prebuilds/darwin-arm64-node-137/knitting_doorbell.node +0 -0
- package/prebuilds/darwin-x64-node-127/knitting_buffer_pointer.node +0 -0
- package/prebuilds/darwin-x64-node-127/knitting_doorbell.node +0 -0
- package/prebuilds/darwin-x64-node-137/knitting_buffer_pointer.node +0 -0
- package/prebuilds/darwin-x64-node-137/knitting_doorbell.node +0 -0
- package/prebuilds/linux-x64-node-127/knitting_buffer_pointer.node +0 -0
- package/prebuilds/linux-x64-node-127/knitting_doorbell.node +0 -0
- package/prebuilds/linux-x64-node-137/knitting_buffer_pointer.node +0 -0
- package/prebuilds/linux-x64-node-137/knitting_doorbell.node +0 -0
- package/prebuilds/win32-x64/knitting_windows_shared_memory.dll +0 -0
- package/prebuilds/win32-x64-node-127/knitting_buffer_pointer.node +0 -0
- package/prebuilds/win32-x64-node-127/knitting_doorbell.node +0 -0
- package/prebuilds/win32-x64-node-127/knitting_shared_memory.node +0 -0
- package/prebuilds/win32-x64-node-127/knitting_shm.node +0 -0
- package/prebuilds/win32-x64-node-137/knitting_buffer_pointer.node +0 -0
- package/prebuilds/win32-x64-node-137/knitting_doorbell.node +0 -0
- package/prebuilds/win32-x64-node-137/knitting_shared_memory.node +0 -0
- package/prebuilds/win32-x64-node-137/knitting_shm.node +0 -0
- package/scripts/build-native-addons.ts +5 -0
- package/shared-memory.d.ts +3 -0
- package/shared-memory.js +3 -0
- package/src/api.js +158 -71
- package/src/common/with-resolvers.js +2 -5
- package/src/common/worker-runtime.d.ts +7 -0
- package/src/common/worker-runtime.js +7 -0
- package/src/connections/buffer-reference.d.ts +10 -36
- package/src/connections/buffer-reference.js +15 -170
- package/src/connections/node-addons.d.ts +1 -1
- package/src/connections/node-addons.js +11 -1
- package/src/connections/shared-array-buffer-payload.d.ts +7 -0
- package/src/connections/shared-array-buffer-payload.js +27 -11
- package/src/debug/gate.js +1 -1
- package/src/debug/handle.d.ts +6 -1
- package/src/debug/handle.js +14 -6
- package/src/error.d.ts +9 -0
- package/src/error.js +16 -2
- package/src/knitting_buffer_pointer.cc +57 -2
- package/src/knitting_doorbell.cc +220 -0
- package/src/memory/knitting-body.d.ts +44 -0
- package/src/memory/knitting-body.js +51 -0
- package/src/memory/knitting-buffer-http.d.ts +116 -0
- package/src/memory/knitting-buffer-http.js +255 -0
- package/src/memory/knitting-buffer.d.ts +250 -0
- package/src/memory/knitting-buffer.js +695 -0
- package/src/memory/lazy-region-registry.d.ts +83 -0
- package/src/memory/lazy-region-registry.js +355 -0
- package/src/memory/lock.d.ts +80 -15
- package/src/memory/lock.js +473 -139
- package/src/memory/payloadCodec.d.ts +18 -2
- package/src/memory/payloadCodec.js +340 -76
- package/src/memory/regionRegistry.d.ts +6 -0
- package/src/memory/regionRegistry.js +125 -240
- package/src/memory/shared-buffer-io.d.ts +7 -0
- package/src/memory/shared-buffer-io.js +34 -8
- package/src/permission/protocol.d.ts +1 -0
- package/src/permission/protocol.js +8 -3
- package/src/runtime/deno-doorbell.d.ts +26 -0
- package/src/runtime/deno-doorbell.js +117 -0
- package/src/runtime/dispatcher.d.ts +13 -6
- package/src/runtime/dispatcher.js +101 -63
- package/src/runtime/host-arg-arena.d.ts +3 -0
- package/src/runtime/host-arg-arena.js +16 -0
- package/src/runtime/inline-executor.js +2 -1
- package/src/runtime/node-doorbell.d.ts +14 -0
- package/src/runtime/node-doorbell.js +84 -0
- package/src/runtime/pool.d.ts +30 -15
- package/src/runtime/pool.js +199 -151
- package/src/runtime/process-worker.d.ts +9 -0
- package/src/runtime/process-worker.js +32 -3
- package/src/runtime/tx-queue.d.ts +4 -6
- package/src/runtime/tx-queue.js +63 -48
- package/src/runtime/worker-common.d.ts +7 -0
- package/src/runtime/worker-common.js +28 -2
- package/src/types.d.ts +66 -78
- package/src/worker/loop.js +95 -60
- package/src/worker/rx-queue.d.ts +2 -3
- package/src/worker/rx-queue.js +34 -40
- package/src/worker/safety/index.d.ts +1 -1
- package/src/worker/safety/index.js +1 -1
- package/src/worker/safety/process.d.ts +2 -0
- package/src/worker/safety/process.js +8 -1
- package/src/worker/safety/startup.js +11 -6
- package/src/worker/shared-return.d.ts +9 -0
- package/src/worker/shared-return.js +22 -0
- package/src/worker/task-loader.js +1 -2
- package/src/worker/timers.d.ts +2 -6
- package/src/worker/timers.js +39 -22
- package/unsafe.d.ts +2 -1
- package/unsafe.js +2 -1
package/src/runtime/pool.js
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
import { createHostTxQueue } from "./tx-queue.js";
|
|
2
|
-
import { cleanupProcessWorkerMemoryQuietly, createProcessSharedMemoryAllocator, createProcessStealMemoryLayout, createProcessWorkerMemoryLayout, createProcessWorkerNativeSignalNotifier, readProcessSharedMemorySettings, readProcessWorkerCommandPrefix, readProcessWorkerRuntime, spawnProcessWorker, toProcessWorkerBootPayload, } from "./process-worker.js";
|
|
3
|
-
import { serializeWorkerBootstrapData, terminateWorkerQuietly,
|
|
2
|
+
import { cleanupProcessWorkerMemoryQuietly, createProcessSharedMemoryAllocator, createProcessStealMemoryLayout, createProcessWorkerMemoryLayout, createProcessWorkerNativeSignalNotifier, processWorkerUsesIpc, readProcessSharedMemorySettings, readProcessWorkerCommandPrefix, readProcessWorkerRuntime, spawnProcessWorker, toProcessWorkerBootPayload, } from "./process-worker.js";
|
|
3
|
+
import { serializeWorkerBootstrapData, terminateWorkerQuietly, isNodePermissionExecFlag, toWorkerSafeExecArgv, warnDroppedWorkerExecArgv, } from "./worker-common.js";
|
|
4
4
|
import { createSharedMemoryTransport, TRANSPORT_SIGNAL_BYTES, WORKER_STOP, } from "../ipc/transport/shared-memory.js";
|
|
5
5
|
import { ChannelHandler, hostDispatcherLoop, } from "./dispatcher.js";
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
import { KnittingError } from "../error.js";
|
|
7
|
+
import { createNodeCompletionDoorbell } from "./node-doorbell.js";
|
|
8
|
+
import { DEFAULT_STEAL_CLAIM, HEADER_SLOT_STRIDE_U32, lock2, LOCK_SECTOR_BYTE_LENGTH, LockBound, } from "../memory/lock.js";
|
|
9
|
+
// Registers the payload codec before any lock is built.
|
|
9
10
|
import "../memory/payloadCodec.js";
|
|
10
11
|
import "../worker/loop.js";
|
|
11
12
|
import { createSharedArrayBuffer, createWasmSharedArrayBuffer, IS_BROWSER, RUNTIME, } from "../common/runtime.js";
|
|
12
|
-
import { HAS_NODE_WORKER_THREADS, RUNTIME_WORKER, } from "../common/worker-runtime.js";
|
|
13
|
+
import { HAS_NODE_WORKER_THREADS, isProcessCompletionDoorbell, RUNTIME_WORKER, } from "../common/worker-runtime.js";
|
|
13
14
|
import { probeLockBufferTextCompat } from "../common/shared-buffer-text.js";
|
|
14
15
|
import { signalAbortFactory } from "../shared/abortSignal.js";
|
|
15
16
|
import { createLockControlCarpet } from "../memory/byte-carpet.js";
|
|
16
17
|
import { resolvePayloadBufferOptions, } from "../memory/payload-config.js";
|
|
17
|
-
import { createBufferReferenceReturnReleaseMessage, detachArrayBufferBestEffort, } from "../connections/buffer-reference.js";
|
|
18
18
|
import { spawnCompiledWorkerContext } from "./compiled-worker.js";
|
|
19
19
|
const WORKER_FATAL_MESSAGE_KEY = "__knittingWorkerFatal";
|
|
20
20
|
const isWorkerFatalMessage = (value) => !!value &&
|
|
@@ -22,9 +22,16 @@ const isWorkerFatalMessage = (value) => !!value &&
|
|
|
22
22
|
typeof value[WORKER_FATAL_MESSAGE_KEY] === "string";
|
|
23
23
|
// Bound the wait for a worker to acknowledge loop exit.
|
|
24
24
|
const WORKER_STOP_ACK_TIMEOUT_MS = 50;
|
|
25
|
-
|
|
26
|
-
//
|
|
27
|
-
|
|
25
|
+
// Node owns returned BufferReference backing stores in a process-global native
|
|
26
|
+
// registry. Give its worker loop a chance to drain deferred producer pins
|
|
27
|
+
// before the host falls back to terminate().
|
|
28
|
+
const WORKER_STOP_NEEDS_ACK = RUNTIME === "deno" || RUNTIME === "node";
|
|
29
|
+
// The park timeout is only a safety net: the host wakes a parked thread worker
|
|
30
|
+
// after every publish, so a long park costs no latency and idles at ~0 CPU.
|
|
31
|
+
const DEFAULT_WORKER_PARK_MS = 1000;
|
|
32
|
+
// A process worker can only be rung across processes by a Node host waking a
|
|
33
|
+
// Node child; every other pairing rediscovers work on this timeout.
|
|
34
|
+
const DEFAULT_PROCESS_WORKER_PARK_MS = 1;
|
|
28
35
|
const SINGLE_WORKER_SPIN_US = 50;
|
|
29
36
|
const DEFAULT_ABORT_SIGNAL_CAPACITY = 258;
|
|
30
37
|
const sanitizePositiveInteger = (value) => {
|
|
@@ -36,7 +43,10 @@ const sanitizePositiveInteger = (value) => {
|
|
|
36
43
|
const resolveAbortSignalCapacity = (value) => sanitizePositiveInteger(value) ?? DEFAULT_ABORT_SIGNAL_CAPACITY;
|
|
37
44
|
const abortSignalByteLength = (capacity) => Math.max(1, Math.ceil(capacity / 32)) * Uint32Array.BYTES_PER_ELEMENT;
|
|
38
45
|
const withDefaultWorkerTimers = (options, workerCount) => {
|
|
39
|
-
const parkMs = options?.timers?.parkMs ??
|
|
46
|
+
const parkMs = options?.timers?.parkMs ??
|
|
47
|
+
(options?.runtime === "process"
|
|
48
|
+
? DEFAULT_PROCESS_WORKER_PARK_MS
|
|
49
|
+
: DEFAULT_WORKER_PARK_MS);
|
|
40
50
|
// `??` and not `||`: an explicit `spinMicroseconds: 0` is the whole point of
|
|
41
51
|
// the multi-worker default and must not fall through to it.
|
|
42
52
|
const spinMicroseconds = options?.timers?.spinMicroseconds ??
|
|
@@ -57,16 +67,7 @@ const withFixedPayloadConfig = (config) => ({
|
|
|
57
67
|
mode: "fixed",
|
|
58
68
|
payloadInitialBytes: config.payloadMaxByteLength,
|
|
59
69
|
});
|
|
60
|
-
/**
|
|
61
|
-
* Build the buffers, host-side locks and pending registry that a stealing pool
|
|
62
|
-
* shares. One submit region for every worker to claim from, one private return
|
|
63
|
-
* region per worker, and a single pool-global queue so a response arriving on
|
|
64
|
-
* any lane settles the right promise.
|
|
65
|
-
*
|
|
66
|
-
* Region width follows paper §6.1: the largest power-of-two `g` leaving a spare
|
|
67
|
-
* region for a delayed claimant (`slots / g >= consumers + 1`). At 32 slots that
|
|
68
|
-
* caps how wide a region can be well before it caps the worker count.
|
|
69
|
-
*/
|
|
70
|
+
/** Build the shared submit and private return buffers for a stealing pool. */
|
|
70
71
|
export const resolveStealRegionLanes = (consumers) => {
|
|
71
72
|
for (let lanes = LockBound.slots; lanes >= 1; lanes >>= 1) {
|
|
72
73
|
if (LockBound.slots / lanes >= consumers + 1)
|
|
@@ -74,9 +75,17 @@ export const resolveStealRegionLanes = (consumers) => {
|
|
|
74
75
|
}
|
|
75
76
|
throw new RangeError(`${consumers} stealing workers need more than ${LockBound.slots} lanes`);
|
|
76
77
|
};
|
|
78
|
+
/**
|
|
79
|
+
* Return the widest region allowed by the selected claim discipline. Only
|
|
80
|
+
* Dekker needs a region per consumer; `ticket` has no regions at all and uses
|
|
81
|
+
* the width purely as a claim batch size.
|
|
82
|
+
*/
|
|
83
|
+
export const resolveMaxStealRegionLanes = (consumers, stealClaim = DEFAULT_STEAL_CLAIM) => stealClaim === "dekker"
|
|
84
|
+
? resolveStealRegionLanes(consumers)
|
|
85
|
+
: LockBound.slots;
|
|
77
86
|
/** Maximum claimants that leave the protocol's required spare region. */
|
|
78
87
|
export const MAX_STEAL_CONSUMERS = LockBound.slots - 1;
|
|
79
|
-
export const createStealPoolBuffers = ({ threads, payload, regionLanes, abortSignalCapacity, usesAbortSignal, processWorker, }) => {
|
|
88
|
+
export const createStealPoolBuffers = ({ threads, payload, sharedArgs, regionLanes, stealClaim, abortSignalCapacity, usesAbortSignal, processWorker, }) => {
|
|
80
89
|
const basePayloadConfig = resolvePayloadBufferOptions({ options: payload });
|
|
81
90
|
const payloadConfig = processWorker === undefined
|
|
82
91
|
? basePayloadConfig
|
|
@@ -100,9 +109,10 @@ export const createStealPoolBuffers = ({ threads, payload, regionLanes, abortSig
|
|
|
100
109
|
payload: payloadSab,
|
|
101
110
|
}),
|
|
102
111
|
});
|
|
103
|
-
|
|
112
|
+
// Keep the default width valid for Dekker; wider regions are explicit.
|
|
113
|
+
const maxLanes = resolveMaxStealRegionLanes(threads, stealClaim);
|
|
104
114
|
const lanes = regionLanes === undefined
|
|
105
|
-
?
|
|
115
|
+
? resolveStealRegionLanes(threads)
|
|
106
116
|
: Math.min(Math.max(1, regionLanes | 0), maxLanes);
|
|
107
117
|
const resolvedAbortSignalCapacity = resolveAbortSignalCapacity(abortSignalCapacity);
|
|
108
118
|
const processMemory = processWorker === undefined ? undefined : createProcessStealMemoryLayout({
|
|
@@ -133,7 +143,11 @@ export const createStealPoolBuffers = ({ threads, payload, regionLanes, abortSig
|
|
|
133
143
|
textCompat: submitBuffers.textCompat,
|
|
134
144
|
consumers: threads,
|
|
135
145
|
regionLanes: lanes,
|
|
146
|
+
stealClaim,
|
|
136
147
|
processBoundary: processMemory !== undefined,
|
|
148
|
+
// Shared arguments are opt-in because the receiving task may outlive the
|
|
149
|
+
// borrowed region.
|
|
150
|
+
sharedReturn: sharedArgs === true,
|
|
137
151
|
});
|
|
138
152
|
const returnBuffers = [];
|
|
139
153
|
const hostReturnLocks = [];
|
|
@@ -179,13 +193,14 @@ export const createStealPoolBuffers = ({ threads, payload, regionLanes, abortSig
|
|
|
179
193
|
hostReturnLocks,
|
|
180
194
|
sharedQueue,
|
|
181
195
|
regionLanes: lanes,
|
|
196
|
+
stealClaim,
|
|
182
197
|
processMemory,
|
|
183
198
|
abortSignalSAB,
|
|
184
199
|
abortSignalMax: processMemory?.abortSignalMax ??
|
|
185
200
|
(abortSignalSAB === undefined ? undefined : resolvedAbortSignalCapacity),
|
|
186
201
|
};
|
|
187
202
|
};
|
|
188
|
-
export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostDebug, totalNumberOfThread, workerCount, source, at, workerOptions, workerExecArgv, permission, host, payload,
|
|
203
|
+
export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, debugEpoch, hostDebug, totalNumberOfThread, workerCount, source, at, workerOptions, workerExecArgv, requestedExecArgv, permission, host, payload, sharedBytesEnabled, abortSignalCapacity, usesAbortSignal, sharedChannelHandler, denoCompletionDoorbell, stealPool, }) => {
|
|
189
204
|
if (workerOptions?.runtime === "compiled") {
|
|
190
205
|
return spawnCompiledWorkerContext({
|
|
191
206
|
list,
|
|
@@ -200,12 +215,20 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostD
|
|
|
200
215
|
const poliWorker = RUNTIME_WORKER;
|
|
201
216
|
const resolvedWorkerOptions = serializeWorkerBootstrapData(withDefaultWorkerTimers(workerOptions, workerCount ?? totalNumberOfThread));
|
|
202
217
|
const useProcessWorkerRuntime = resolvedWorkerOptions.runtime === "process";
|
|
218
|
+
const canUseDenoCompletionDoorbell = !useProcessWorkerRuntime &&
|
|
219
|
+
denoCompletionDoorbell !== undefined;
|
|
203
220
|
const processWorkerRuntime = useProcessWorkerRuntime
|
|
204
221
|
? readProcessWorkerRuntime(resolvedWorkerOptions)
|
|
205
222
|
: undefined;
|
|
206
223
|
const processWorkerCommandPrefix = useProcessWorkerRuntime
|
|
207
224
|
? readProcessWorkerCommandPrefix(resolvedWorkerOptions)
|
|
208
225
|
: undefined;
|
|
226
|
+
const canUseProcessCompletionDoorbell = useProcessWorkerRuntime &&
|
|
227
|
+
host?.doorbell !== false &&
|
|
228
|
+
processWorkerUsesIpc({
|
|
229
|
+
processRuntime: processWorkerRuntime,
|
|
230
|
+
commandPrefix: processWorkerCommandPrefix,
|
|
231
|
+
});
|
|
209
232
|
const processSharedMemorySettings = useProcessWorkerRuntime
|
|
210
233
|
? readProcessSharedMemorySettings(resolvedWorkerOptions)
|
|
211
234
|
: undefined;
|
|
@@ -238,10 +261,7 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostD
|
|
|
238
261
|
const abortBytes = stealPool === undefined && usesAbortSignal === true
|
|
239
262
|
? abortSignalByteLength(resolvedAbortSignalCapacity)
|
|
240
263
|
: 0;
|
|
241
|
-
//
|
|
242
|
-
// `thread` must find its slice there. Falling back to a private layout would
|
|
243
|
-
// silently give this lane its own submit region — it would boot and then
|
|
244
|
-
// never see a task the host published to the shared one.
|
|
264
|
+
// Stealing process workers use their slice of the pool-wide mapping.
|
|
245
265
|
const stealProcessMemory = stealPool?.processMemory;
|
|
246
266
|
const processWorkerMemory = !useProcessWorkerRuntime
|
|
247
267
|
? undefined
|
|
@@ -264,18 +284,14 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostD
|
|
|
264
284
|
createWasmSharedArrayBuffer;
|
|
265
285
|
const createPayloadBuffer = processSharedMemory?.createBuffer;
|
|
266
286
|
const makePayloadBuffer = () => createPayloadBuffer
|
|
267
|
-
//
|
|
268
|
-
// payload ceiling instead of relying on SAB growth.
|
|
287
|
+
// Process-shared buffers are fixed-size.
|
|
269
288
|
? createPayloadBuffer(resolvedPayloadConfig.payloadMaxByteLength)
|
|
270
289
|
: resolvedPayloadConfig.mode === "growable"
|
|
271
290
|
? createSharedArrayBuffer(resolvedPayloadConfig.payloadInitialBytes, resolvedPayloadConfig.payloadMaxByteLength)
|
|
272
291
|
: createSharedArrayBuffer(resolvedPayloadConfig.payloadInitialBytes);
|
|
273
292
|
const makeLockControlLayout = () => {
|
|
274
|
-
//
|
|
275
|
-
//
|
|
276
|
-
// Request/return headers stay in separate contiguous slabs to preserve
|
|
277
|
-
// sequential batching locality.
|
|
278
|
-
// Abort bitmap stays at the tail.
|
|
293
|
+
// Place hot control words first; keep headers contiguous and the abort bitmap
|
|
294
|
+
// at the tail.
|
|
279
295
|
return createLockControlCarpet({
|
|
280
296
|
signalBytes,
|
|
281
297
|
abortBytes,
|
|
@@ -350,68 +366,11 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostD
|
|
|
350
366
|
processRuntime: processWorkerRuntime,
|
|
351
367
|
signal: signalBox.opView,
|
|
352
368
|
});
|
|
353
|
-
const outstandingBorrowedReturns = bufferReferenceReturn === "borrow" && typeof WeakRef === "function"
|
|
354
|
-
? new Map()
|
|
355
|
-
: undefined;
|
|
356
|
-
const releaseBorrowedReturnToken = (token) => {
|
|
357
|
-
outstandingBorrowedReturns?.delete(token);
|
|
358
|
-
worker?.postMessage?.(createBufferReferenceReturnReleaseMessage(token));
|
|
359
|
-
};
|
|
360
|
-
const revokeOutstandingBorrowedReturns = (copyBytes) => {
|
|
361
|
-
if (outstandingBorrowedReturns === undefined)
|
|
362
|
-
return;
|
|
363
|
-
for (const [token, record] of outstandingBorrowedReturns) {
|
|
364
|
-
try {
|
|
365
|
-
const ref = record.ref.deref();
|
|
366
|
-
if (ref !== undefined) {
|
|
367
|
-
ref.revokeBorrow(copyBytes);
|
|
368
|
-
continue;
|
|
369
|
-
}
|
|
370
|
-
const aliasBuffer = record.aliasBuffer?.deref();
|
|
371
|
-
if (aliasBuffer !== undefined &&
|
|
372
|
-
detachArrayBufferBestEffort(record.runtime, aliasBuffer) &&
|
|
373
|
-
copyBytes) {
|
|
374
|
-
releaseBorrowedReturnToken(token);
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
catch {
|
|
378
|
-
// best effort
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
outstandingBorrowedReturns.clear();
|
|
382
|
-
};
|
|
383
|
-
const returnHooks = bufferReferenceReturn === "borrow"
|
|
384
|
-
? {
|
|
385
|
-
release: releaseBorrowedReturnToken,
|
|
386
|
-
track: (ref, token, aliasBuffer) => {
|
|
387
|
-
if (outstandingBorrowedReturns === undefined)
|
|
388
|
-
return;
|
|
389
|
-
const record = outstandingBorrowedReturns.get(token);
|
|
390
|
-
if (record !== undefined) {
|
|
391
|
-
if (aliasBuffer !== undefined) {
|
|
392
|
-
record.aliasBuffer = new WeakRef(aliasBuffer);
|
|
393
|
-
}
|
|
394
|
-
return;
|
|
395
|
-
}
|
|
396
|
-
outstandingBorrowedReturns.set(token, {
|
|
397
|
-
ref: new WeakRef(ref),
|
|
398
|
-
aliasBuffer: aliasBuffer === undefined
|
|
399
|
-
? undefined
|
|
400
|
-
: new WeakRef(aliasBuffer),
|
|
401
|
-
runtime: ref.runtime,
|
|
402
|
-
});
|
|
403
|
-
},
|
|
404
|
-
}
|
|
405
|
-
: undefined;
|
|
406
369
|
const queue = stealPool?.sharedQueue ?? createHostTxQueue({
|
|
407
370
|
lock,
|
|
408
371
|
returnLock,
|
|
409
372
|
abortSignals,
|
|
410
|
-
releaseBufferReferenceReturn: returnHooks,
|
|
411
373
|
});
|
|
412
|
-
if (stealPool !== undefined) {
|
|
413
|
-
stealPool.sharedQueue.setReturnHooks(stealPool.consumerId, returnHooks);
|
|
414
|
-
}
|
|
415
374
|
const { enqueue, rejectAll, txIdle, } = queue;
|
|
416
375
|
const thisSignal = signalBox.opView;
|
|
417
376
|
const a_add = Atomics.add;
|
|
@@ -420,8 +379,7 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostD
|
|
|
420
379
|
const canNotifySignal = thisSignal.buffer instanceof SharedArrayBuffer;
|
|
421
380
|
const notifySignal = nativeNotifySignal ??
|
|
422
381
|
(canNotifySignal ? (() => a_notify(thisSignal, 0, 1)) : undefined);
|
|
423
|
-
//
|
|
424
|
-
// (new work just arrived) — the dispatcher drain wakes it the same way.
|
|
382
|
+
// Wake this lane when its worker is parked.
|
|
425
383
|
const laneWake = () => {
|
|
426
384
|
if (a_load(signalBox.rxStatus, 0) === 0) {
|
|
427
385
|
a_add(thisSignal, 0, 1);
|
|
@@ -434,10 +392,26 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostD
|
|
|
434
392
|
const ownsChannel = sharedChannelHandler === undefined &&
|
|
435
393
|
stealPool === undefined;
|
|
436
394
|
const ownChannel = sharedChannelHandler ?? new ChannelHandler();
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
const
|
|
440
|
-
|
|
395
|
+
const threadCanLoadNodeAddons = permission?.enabled !== true ||
|
|
396
|
+
permission.unsafe === true || permission.node.allowAddons === true;
|
|
397
|
+
const useNativeNodeDoorbell = RUNTIME === "node" &&
|
|
398
|
+
!useProcessWorkerRuntime && host?.doorbell !== false &&
|
|
399
|
+
host?.nativeDoorbell === true && threadCanLoadNodeAddons;
|
|
400
|
+
if (RUNTIME === "node" && !useProcessWorkerRuntime &&
|
|
401
|
+
host?.doorbell !== false && host?.nativeDoorbell === true &&
|
|
402
|
+
!threadCanLoadNodeAddons) {
|
|
403
|
+
hostDebug?.("native Node doorbell disabled by worker permissions; using the " +
|
|
404
|
+
"portable wake path (set permission.node.allowAddons=true to enable)");
|
|
405
|
+
}
|
|
406
|
+
// Create the Node doorbell before bootstrapping the worker.
|
|
407
|
+
let nodeCompletionWake;
|
|
408
|
+
const nodeCompletionDoorbell = useNativeNodeDoorbell
|
|
409
|
+
? createNodeCompletionDoorbell(() => nodeCompletionWake?.())
|
|
410
|
+
: undefined;
|
|
411
|
+
const canUseNodeCompletionDoorbell = nodeCompletionDoorbell !== undefined;
|
|
412
|
+
// A stealing pool has one dispatcher for its shared queue.
|
|
413
|
+
const { check: dispatcherCheck, wakeCompletion: directCompletionWake, } = stealPool !== undefined
|
|
414
|
+
? { check: undefined, wakeCompletion: undefined }
|
|
441
415
|
: hostDispatcherLoop({
|
|
442
416
|
signalBox,
|
|
443
417
|
queue,
|
|
@@ -445,7 +419,15 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostD
|
|
|
445
419
|
dispatcherOptions: host,
|
|
446
420
|
notifySignal: nativeNotifySignal,
|
|
447
421
|
crossProcess: useProcessWorkerRuntime,
|
|
422
|
+
nativeCompletionDoorbell: canUseDenoCompletionDoorbell ||
|
|
423
|
+
canUseNodeCompletionDoorbell,
|
|
424
|
+
processCompletionDoorbell: canUseProcessCompletionDoorbell,
|
|
448
425
|
});
|
|
426
|
+
if (canUseDenoCompletionDoorbell && dispatcherCheck !== undefined) {
|
|
427
|
+
denoCompletionDoorbell.listen(thread, directCompletionWake);
|
|
428
|
+
}
|
|
429
|
+
nodeCompletionWake = directCompletionWake;
|
|
430
|
+
let processCompletionWake = directCompletionWake;
|
|
449
431
|
if (ownsChannel && dispatcherCheck !== undefined) {
|
|
450
432
|
ownChannel.open(dispatcherCheck);
|
|
451
433
|
channelHandler = ownChannel;
|
|
@@ -453,7 +435,7 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostD
|
|
|
453
435
|
if (dispatcherCheck.isRunning === true)
|
|
454
436
|
return;
|
|
455
437
|
dispatcherCheck.isRunning = true;
|
|
456
|
-
|
|
438
|
+
queueMicrotask(dispatcherCheck);
|
|
457
439
|
laneWake();
|
|
458
440
|
};
|
|
459
441
|
}
|
|
@@ -478,24 +460,34 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostD
|
|
|
478
460
|
at,
|
|
479
461
|
thread,
|
|
480
462
|
debug,
|
|
463
|
+
debugEpoch,
|
|
481
464
|
workerOptions: resolvedWorkerOptions,
|
|
482
465
|
totalNumberOfThread,
|
|
483
466
|
startAt: signalBox.startAt,
|
|
484
467
|
lock: lockBuffers,
|
|
485
468
|
returnLock: returnLockBuffers,
|
|
486
469
|
payloadConfig: resolvedPayloadConfig,
|
|
487
|
-
|
|
470
|
+
sharedReturn: sharedBytesEnabled === true,
|
|
488
471
|
permission,
|
|
489
|
-
//
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
472
|
+
// Select the completion doorbell supported by the worker runtime.
|
|
473
|
+
notifyOnHostPublish: host?.doorbell !== false &&
|
|
474
|
+
(canUseProcessCompletionDoorbell ||
|
|
475
|
+
canUseNodeCompletionDoorbell ||
|
|
476
|
+
(!useProcessWorkerRuntime &&
|
|
477
|
+
(RUNTIME === "bun" || RUNTIME === "node") &&
|
|
478
|
+
typeof Atomics.waitAsync === "function")),
|
|
479
|
+
processCompletionDoorbell: canUseProcessCompletionDoorbell,
|
|
480
|
+
denoCompletionDoorbell: canUseDenoCompletionDoorbell
|
|
481
|
+
? denoCompletionDoorbell.pointer
|
|
482
|
+
: undefined,
|
|
483
|
+
nodeCompletionDoorbell: canUseNodeCompletionDoorbell
|
|
484
|
+
? nodeCompletionDoorbell.pointer
|
|
485
|
+
: undefined,
|
|
495
486
|
steal: stealPool === undefined ? undefined : {
|
|
496
487
|
consumers: stealPool.consumers,
|
|
497
488
|
consumerId: stealPool.consumerId,
|
|
498
489
|
regionLanes: stealPool.regionLanes,
|
|
490
|
+
claim: stealPool.stealClaim,
|
|
499
491
|
},
|
|
500
492
|
};
|
|
501
493
|
const baseWorkerOptions = {
|
|
@@ -507,6 +499,11 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostD
|
|
|
507
499
|
const withExecArgv = workerExecArgv && workerExecArgv.length > 0
|
|
508
500
|
? { ...baseWorkerOptions, execArgv: workerExecArgv }
|
|
509
501
|
: baseWorkerOptions;
|
|
502
|
+
const requiredNodePermissionFlags = RUNTIME === "node" &&
|
|
503
|
+
permission?.enabled === true &&
|
|
504
|
+
permission.unsafe !== true
|
|
505
|
+
? permission.node.flags.filter(isNodePermissionExecFlag)
|
|
506
|
+
: [];
|
|
510
507
|
if (processWorkerMemory !== undefined) {
|
|
511
508
|
worker = spawnProcessWorker({
|
|
512
509
|
workerUrl,
|
|
@@ -519,43 +516,59 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostD
|
|
|
519
516
|
}
|
|
520
517
|
else if (HAS_NODE_WORKER_THREADS) {
|
|
521
518
|
try {
|
|
522
|
-
worker
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
519
|
+
// The flags the worker actually started with.
|
|
520
|
+
let appliedExecArgv;
|
|
521
|
+
try {
|
|
522
|
+
worker = new WorkerCtor(workerUrl, withExecArgv);
|
|
523
|
+
appliedExecArgv = withExecArgv.execArgv;
|
|
524
|
+
}
|
|
525
|
+
catch (error) {
|
|
526
|
+
if (error?.code === "ERR_WORKER_INVALID_EXEC_ARGV") {
|
|
527
|
+
const fallbackExecArgv = toWorkerSafeExecArgv(withExecArgv.execArgv);
|
|
528
|
+
const missingPermissionFlags = requiredNodePermissionFlags.filter((flag) => !fallbackExecArgv?.includes(flag));
|
|
529
|
+
if (missingPermissionFlags.length > 0) {
|
|
530
|
+
throw new Error("Node rejected the permission flags required by Knitting's " +
|
|
531
|
+
`thread worker policy (${missingPermissionFlags.join(", ")}). ` +
|
|
532
|
+
"Refusing to retry without them.");
|
|
530
533
|
}
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
}
|
|
534
|
+
if (fallbackExecArgv && fallbackExecArgv.length > 0) {
|
|
535
|
+
try {
|
|
536
|
+
worker = new WorkerCtor(workerUrl, { ...baseWorkerOptions, execArgv: fallbackExecArgv });
|
|
537
|
+
appliedExecArgv = fallbackExecArgv;
|
|
538
|
+
}
|
|
539
|
+
catch (fallbackError) {
|
|
540
|
+
if (requiredNodePermissionFlags.length > 0) {
|
|
541
|
+
throw new Error("Node could not start a thread worker with Knitting's " +
|
|
542
|
+
"permission policy. Refusing to start it without those " +
|
|
543
|
+
`permissions. ${String(fallbackError)}`);
|
|
542
544
|
}
|
|
543
|
-
|
|
545
|
+
if (fallbackError?.code ===
|
|
546
|
+
"ERR_WORKER_INVALID_EXEC_ARGV") {
|
|
544
547
|
worker = new WorkerCtor(workerUrl, baseWorkerOptions);
|
|
545
548
|
}
|
|
549
|
+
else {
|
|
550
|
+
throw fallbackError;
|
|
551
|
+
}
|
|
546
552
|
}
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
553
|
+
}
|
|
554
|
+
else if (requiredNodePermissionFlags.length > 0) {
|
|
555
|
+
throw new Error("Node rejected the permission flags required by Knitting's " +
|
|
556
|
+
"thread worker policy. Refusing to start the worker without " +
|
|
557
|
+
"those permissions.");
|
|
558
|
+
}
|
|
559
|
+
else {
|
|
560
|
+
worker = new WorkerCtor(workerUrl, baseWorkerOptions);
|
|
550
561
|
}
|
|
551
562
|
}
|
|
552
563
|
else {
|
|
553
|
-
|
|
564
|
+
throw error;
|
|
554
565
|
}
|
|
555
566
|
}
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
567
|
+
warnDroppedWorkerExecArgv(requestedExecArgv, appliedExecArgv);
|
|
568
|
+
}
|
|
569
|
+
catch (error) {
|
|
570
|
+
nodeCompletionDoorbell?.close();
|
|
571
|
+
throw error;
|
|
559
572
|
}
|
|
560
573
|
}
|
|
561
574
|
else {
|
|
@@ -565,6 +578,11 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostD
|
|
|
565
578
|
worker.postMessage?.(workerDataPayload);
|
|
566
579
|
}
|
|
567
580
|
let closedReason;
|
|
581
|
+
// A worker that exits because the host asked it to stop is a closed thread,
|
|
582
|
+
// not a crash. The stop is acknowledged in shared memory before the worker
|
|
583
|
+
// unwinds, so on a slow host the `exit` event can land ahead of `kills()`
|
|
584
|
+
// and label an orderly shutdown "Worker exited with code 0".
|
|
585
|
+
let stopRequested = false;
|
|
568
586
|
const deactivateStealConsumer = () => {
|
|
569
587
|
stealPool?.hostSubmitLock.deactivateStealConsumer(stealPool.consumerId);
|
|
570
588
|
};
|
|
@@ -579,26 +597,39 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostD
|
|
|
579
597
|
deactivateStealConsumer();
|
|
580
598
|
}
|
|
581
599
|
};
|
|
582
|
-
|
|
583
|
-
// can be copied out; after a crash/exit they are gone and borrows are
|
|
584
|
-
// detached so stale reads fail loud instead of aliasing freed memory.
|
|
585
|
-
const markWorkerClosed = (reason, workerBytesReadable = false) => {
|
|
600
|
+
const markWorkerClosed = (code, reason, cause) => {
|
|
586
601
|
if (closedReason)
|
|
587
602
|
return;
|
|
588
603
|
closedReason = reason;
|
|
589
|
-
|
|
590
|
-
|
|
604
|
+
const error = new KnittingError(code, reason, cause);
|
|
605
|
+
if (stealPool !== undefined && stealPool.stealClaim !== "ticket") {
|
|
606
|
+
// Dekker's shared queue outlives one consumer: the surviving workers
|
|
607
|
+
// keep claiming from it, so only the calls in flight are rejected.
|
|
608
|
+
rejectAll(error);
|
|
609
|
+
}
|
|
610
|
+
else {
|
|
611
|
+
// A lane's own queue has no other consumer, so a call accepted after
|
|
612
|
+
// this point would stay pending forever: close it and reject them all.
|
|
613
|
+
// Under ticket stealing a claimed ticket may be lost at any point
|
|
614
|
+
// between CAS and retirement, so the whole shared registry closes and
|
|
615
|
+
// task-id/slot reuse is prohibited.
|
|
616
|
+
queue.close(error);
|
|
617
|
+
}
|
|
591
618
|
channelHandler?.close();
|
|
592
619
|
};
|
|
593
620
|
const onWorkerMessage = (message) => {
|
|
621
|
+
if (isProcessCompletionDoorbell(message)) {
|
|
622
|
+
processCompletionWake?.();
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
594
625
|
if (!isWorkerFatalMessage(message))
|
|
595
626
|
return;
|
|
596
|
-
markWorkerClosed(`Worker startup failed: ${message[WORKER_FATAL_MESSAGE_KEY]}
|
|
627
|
+
markWorkerClosed("WORKER_STARTUP_FAILED", `Worker startup failed: ${message[WORKER_FATAL_MESSAGE_KEY]}`);
|
|
597
628
|
terminateFailedWorker();
|
|
598
629
|
};
|
|
599
630
|
const onWorkerError = (error) => {
|
|
600
631
|
const message = String(error?.message ?? error);
|
|
601
|
-
markWorkerClosed(`Worker crashed: ${message}
|
|
632
|
+
markWorkerClosed("WORKER_CRASHED", `Worker crashed: ${message}`, error);
|
|
602
633
|
terminateFailedWorker();
|
|
603
634
|
};
|
|
604
635
|
const nodeWorker = worker;
|
|
@@ -606,13 +637,16 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostD
|
|
|
606
637
|
nodeWorker.on("message", onWorkerMessage);
|
|
607
638
|
nodeWorker.on("error", onWorkerError);
|
|
608
639
|
nodeWorker.on("exit", (code) => {
|
|
609
|
-
// Exit is the point at which this endpoint can no longer write WANT, so
|
|
610
|
-
// survivors may safely ignore any intent it left behind.
|
|
611
640
|
deactivateStealConsumer();
|
|
641
|
+
nodeCompletionDoorbell?.close();
|
|
612
642
|
if (closedReason !== undefined)
|
|
613
643
|
return;
|
|
644
|
+
if (stopRequested) {
|
|
645
|
+
markWorkerClosed("THREAD_CLOSED", "Thread closed");
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
614
648
|
const normalized = typeof code === "number" ? code : -1;
|
|
615
|
-
markWorkerClosed(`Worker exited with code ${normalized}`);
|
|
649
|
+
markWorkerClosed("WORKER_EXITED", `Worker exited with code ${normalized}`);
|
|
616
650
|
});
|
|
617
651
|
}
|
|
618
652
|
else {
|
|
@@ -646,34 +680,35 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostD
|
|
|
646
680
|
return pending;
|
|
647
681
|
};
|
|
648
682
|
};
|
|
649
|
-
// Ask the worker to leave its loop before termination.
|
|
650
683
|
const requestWorkerStop = async () => {
|
|
684
|
+
stopRequested = true;
|
|
651
685
|
const stopView = signalBox.stopView;
|
|
652
686
|
if (stopView === undefined)
|
|
653
|
-
return;
|
|
687
|
+
return true;
|
|
654
688
|
try {
|
|
655
689
|
Atomics.store(stopView, 0, WORKER_STOP.requested);
|
|
656
690
|
laneWake();
|
|
657
691
|
notifySignal?.();
|
|
658
692
|
}
|
|
659
693
|
catch {
|
|
660
|
-
return;
|
|
694
|
+
return false;
|
|
661
695
|
}
|
|
662
696
|
if (!WORKER_STOP_NEEDS_ACK)
|
|
663
|
-
return;
|
|
697
|
+
return true;
|
|
664
698
|
const deadline = Date.now() + WORKER_STOP_ACK_TIMEOUT_MS;
|
|
665
699
|
while (Atomics.load(stopView, 0) !== WORKER_STOP.acknowledged) {
|
|
666
700
|
if (Date.now() >= deadline)
|
|
667
|
-
return;
|
|
701
|
+
return false;
|
|
668
702
|
await new Promise((resolve) => setTimeout(resolve, 1));
|
|
669
703
|
}
|
|
704
|
+
return true;
|
|
670
705
|
};
|
|
671
706
|
const context = {
|
|
672
707
|
txIdle,
|
|
673
708
|
call,
|
|
674
709
|
requestStop: requestWorkerStop,
|
|
675
710
|
kills: async () => {
|
|
676
|
-
markWorkerClosed("Thread closed"
|
|
711
|
+
markWorkerClosed("THREAD_CLOSED", "Thread closed");
|
|
677
712
|
// Process workers must exit before their shared memory is unmapped.
|
|
678
713
|
const awaitExit = processWorkerMemory !== undefined;
|
|
679
714
|
const termination = terminateWorkerQuietly(worker, awaitExit);
|
|
@@ -687,9 +722,22 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostD
|
|
|
687
722
|
laneWake: sharedChannelHandler !== undefined || stealPool !== undefined
|
|
688
723
|
? laneWake
|
|
689
724
|
: undefined,
|
|
725
|
+
laneAwake: stealPool !== undefined
|
|
726
|
+
? () => a_load(signalBox.rxStatus, 0) !== 0
|
|
727
|
+
: undefined,
|
|
690
728
|
bindSend: sharedChannelHandler !== undefined || stealPool !== undefined
|
|
691
729
|
? ((fn) => void (dispatchSend = fn))
|
|
692
730
|
: undefined,
|
|
731
|
+
bindCompletionWake: (canUseProcessCompletionDoorbell ||
|
|
732
|
+
canUseNodeCompletionDoorbell) &&
|
|
733
|
+
stealPool !== undefined
|
|
734
|
+
? ((fn) => {
|
|
735
|
+
processCompletionWake = fn;
|
|
736
|
+
nodeCompletionWake = fn;
|
|
737
|
+
})
|
|
738
|
+
: undefined,
|
|
739
|
+
processCompletionDoorbell: canUseProcessCompletionDoorbell,
|
|
740
|
+
nodeCompletionDoorbell: canUseNodeCompletionDoorbell,
|
|
693
741
|
};
|
|
694
742
|
return context;
|
|
695
743
|
};
|
|
@@ -92,6 +92,15 @@ export declare const createProcessWorkerNativeSignalNotifier: ({ processRuntime,
|
|
|
92
92
|
processRuntime: ProcessWorkerRuntime | undefined;
|
|
93
93
|
signal: Int32Array;
|
|
94
94
|
}) => (() => void) | undefined;
|
|
95
|
+
/**
|
|
96
|
+
* Whether this host/child pair has the Node-compatible IPC channel that the
|
|
97
|
+
* process completion-doorbell prototype uses. Deno process workers currently
|
|
98
|
+
* boot through environment data and do not have that channel.
|
|
99
|
+
*/
|
|
100
|
+
export declare const processWorkerUsesIpc: ({ processRuntime, commandPrefix, }: {
|
|
101
|
+
processRuntime: ProcessWorkerRuntime | undefined;
|
|
102
|
+
commandPrefix?: ProcessWorkerCommandPrefix;
|
|
103
|
+
}) => boolean;
|
|
95
104
|
export declare const spawnProcessWorker: (options: {
|
|
96
105
|
workerUrl: string | URL;
|
|
97
106
|
bootPayload: ProcessWorkerBootPayload;
|