knitting 0.1.70 → 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 +101 -10
- 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/package.json +8 -3
- 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/src/api.js +58 -34
- package/src/connections/node-addons.js +11 -1
- 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/memory/lock.d.ts +48 -6
- package/src/memory/lock.js +350 -142
- package/src/memory/payloadCodec.js +31 -11
- package/src/permission/protocol.d.ts +1 -0
- package/src/permission/protocol.js +8 -3
- package/src/runtime/dispatcher.d.ts +6 -1
- package/src/runtime/dispatcher.js +24 -8
- package/src/runtime/inline-executor.js +2 -1
- package/src/runtime/pool.d.ts +13 -4
- package/src/runtime/pool.js +107 -47
- package/src/runtime/process-worker.js +10 -1
- package/src/runtime/tx-queue.d.ts +2 -1
- package/src/runtime/tx-queue.js +11 -0
- package/src/runtime/worker-common.d.ts +7 -0
- package/src/runtime/worker-common.js +28 -2
- package/src/types.d.ts +35 -11
- package/src/worker/loop.js +17 -4
- 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/timers.js +25 -3
|
@@ -40,7 +40,7 @@ const objectGetPrototypeOf = Object.getPrototypeOf;
|
|
|
40
40
|
const objectHasOwn = Object.prototype.hasOwnProperty;
|
|
41
41
|
const arrayIsArray = Array.isArray;
|
|
42
42
|
const objectPrototype = Object.prototype;
|
|
43
|
-
const UNSUPPORTED_OBJECT_DETAIL = "Unsupported object type. Allowed: plain object, array, Error, Date, Envelope, Buffer, ArrayBuffer, DataView,
|
|
43
|
+
const UNSUPPORTED_OBJECT_DETAIL = "Unsupported object type. Allowed: plain object, array, Error, Date, Envelope, Buffer, ArrayBuffer, DataView, Uint8Array, Int32Array, Float64Array, BigInt64Array, BigUint64Array, and registered external payloads. Serialize it yourself.";
|
|
44
44
|
const ENVELOPE_PAYLOAD_DETAIL = "Envelope payload must be an ArrayBuffer, SharedArrayBuffer, " +
|
|
45
45
|
"ProcessSharedBuffer, or BufferReference.";
|
|
46
46
|
const ENVELOPE_HEADER_DETAIL = "Envelope header must be a JSON-like value or string.";
|
|
@@ -252,18 +252,23 @@ const toErrorPayload = (error) => {
|
|
|
252
252
|
}
|
|
253
253
|
return payload;
|
|
254
254
|
};
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
255
|
+
// The shape `toErrorCause` writes for an Error cause. A plain-object cause
|
|
256
|
+
// with any other key is left as the object it was.
|
|
257
|
+
const isErrorPayload = (value) => {
|
|
258
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
259
|
+
return false;
|
|
259
260
|
}
|
|
260
|
-
|
|
261
|
-
|
|
261
|
+
const payload = value;
|
|
262
|
+
if (typeof payload.name !== "string" || typeof payload.message !== "string") {
|
|
263
|
+
return false;
|
|
262
264
|
}
|
|
263
|
-
|
|
264
|
-
|
|
265
|
+
for (const key in payload) {
|
|
266
|
+
if (key !== "name" && key !== "message" && key !== "stack" && key !== "cause")
|
|
267
|
+
return false;
|
|
265
268
|
}
|
|
266
|
-
|
|
269
|
+
return true;
|
|
270
|
+
};
|
|
271
|
+
const reviveErrorPayload = (payload) => {
|
|
267
272
|
const err = new Error(typeof payload.message === "string" ? payload.message : "");
|
|
268
273
|
if (typeof payload.name === "string" && payload.name.length > 0) {
|
|
269
274
|
err.name = payload.name;
|
|
@@ -276,10 +281,25 @@ const parseErrorPayload = (raw) => {
|
|
|
276
281
|
}
|
|
277
282
|
}
|
|
278
283
|
if (objectHasOwn.call(payload, "cause")) {
|
|
279
|
-
err.cause = payload.cause
|
|
284
|
+
err.cause = isErrorPayload(payload.cause)
|
|
285
|
+
? reviveErrorPayload(payload.cause)
|
|
286
|
+
: payload.cause;
|
|
280
287
|
}
|
|
281
288
|
return err;
|
|
282
289
|
};
|
|
290
|
+
const parseErrorPayload = (raw) => {
|
|
291
|
+
let parsed;
|
|
292
|
+
try {
|
|
293
|
+
parsed = parseJSON(raw);
|
|
294
|
+
}
|
|
295
|
+
catch {
|
|
296
|
+
return new Error(raw);
|
|
297
|
+
}
|
|
298
|
+
if (parsed == null || typeof parsed !== "object") {
|
|
299
|
+
return new Error(String(parsed));
|
|
300
|
+
}
|
|
301
|
+
return reviveErrorPayload(parsed);
|
|
302
|
+
};
|
|
283
303
|
const decodeBigIntBinary = (bytes) => {
|
|
284
304
|
const sign = bytes[0];
|
|
285
305
|
let value = 0n;
|
|
@@ -2,6 +2,7 @@ type PermissionPath = string | URL;
|
|
|
2
2
|
type NodePermissionSettings = {
|
|
3
3
|
allowWorker?: boolean;
|
|
4
4
|
allowChildProcess?: boolean;
|
|
5
|
+
/** Allows task code to load Node native addons inside worker threads. */
|
|
5
6
|
allowAddons?: boolean;
|
|
6
7
|
allowFfi?: boolean;
|
|
7
8
|
allowWasi?: boolean;
|
|
@@ -281,9 +281,14 @@ const resolveDenoLock = (input, cwd, home) => {
|
|
|
281
281
|
};
|
|
282
282
|
const resolveNodePermissionActivationFlag = () => {
|
|
283
283
|
try {
|
|
284
|
-
const
|
|
285
|
-
const
|
|
286
|
-
|
|
284
|
+
const version = String(getNodeProcess()?.versions?.node ?? "");
|
|
285
|
+
const [majorPart, minorPart] = version.split(".");
|
|
286
|
+
const major = Number.parseInt(majorPart ?? "", 10);
|
|
287
|
+
const minor = Number.parseInt(minorPart ?? "", 10);
|
|
288
|
+
const permissionIsStable = major > 23 ||
|
|
289
|
+
(major === 23 && minor >= 5) ||
|
|
290
|
+
(major === 22 && minor >= 13);
|
|
291
|
+
if (Number.isFinite(major) && major > 0 && !permissionIsStable) {
|
|
287
292
|
return "--experimental-permission";
|
|
288
293
|
}
|
|
289
294
|
}
|
|
@@ -2,12 +2,17 @@ import { type MultiQueue } from "./tx-queue.js";
|
|
|
2
2
|
import { type MainSignal } from "../ipc/transport/shared-memory.js";
|
|
3
3
|
import { type RuntimeMessageChannelLike, type RuntimeMessagePortLike } from "../common/worker-runtime.js";
|
|
4
4
|
import type { DispatcherSettings } from "../types.js";
|
|
5
|
-
export declare const hostDispatcherLoop: ({ signalBox: { opView, txStatus, rxStatus, }, queue: { completeFrame, hasPendingFrames, flushToWorker, txIdle, waitForCompletion, armCompletionNotifier, setCompletionWaiterArmed, }, channelHandler, dispatcherOptions, notifySignal, crossProcess, nativeCompletionDoorbell, processCompletionDoorbell, }: {
|
|
5
|
+
export declare const hostDispatcherLoop: ({ signalBox: { opView, txStatus, rxStatus, }, queue: { completeFrame, hasPendingFrames, flushToWorker, txIdle, waitForCompletion, armCompletionNotifier, setCompletionWaiterArmed, }, channelHandler, dispatcherOptions, notifySignal, wakeAfterPass, crossProcess, nativeCompletionDoorbell, processCompletionDoorbell, }: {
|
|
6
6
|
queue: MultiQueue;
|
|
7
7
|
signalBox: MainSignal;
|
|
8
8
|
channelHandler: ChannelHandler;
|
|
9
9
|
dispatcherOptions?: DispatcherSettings;
|
|
10
10
|
notifySignal?: () => void;
|
|
11
|
+
/**
|
|
12
|
+
* Runs at the end of every drain pass. Defaults to waking this lane's worker
|
|
13
|
+
* if it is parked; a shared queue supplies its own policy.
|
|
14
|
+
*/
|
|
15
|
+
wakeAfterPass?: () => void;
|
|
11
16
|
/** Workers live in other processes; disables only the atomic doorbell. */
|
|
12
17
|
crossProcess?: boolean;
|
|
13
18
|
/** A runtime-native completion ring that wakes the host event loop. */
|
|
@@ -7,9 +7,9 @@ const IMMEDIATE_PUMP = RUNTIME === "deno" || RUNTIME === "node"
|
|
|
7
7
|
// Polling tolerates a wider free window; a doorbell should arm promptly.
|
|
8
8
|
const POLL_STALL_FREE_LOOPS = 128;
|
|
9
9
|
const DOORBELL_STALL_FREE_LOOPS = 1;
|
|
10
|
-
export const hostDispatcherLoop = ({ signalBox: { opView, txStatus, rxStatus, }, queue: { completeFrame, hasPendingFrames, flushToWorker, txIdle, waitForCompletion, armCompletionNotifier, setCompletionWaiterArmed, }, channelHandler, dispatcherOptions, notifySignal, crossProcess, nativeCompletionDoorbell, processCompletionDoorbell, }) => {
|
|
10
|
+
export const hostDispatcherLoop = ({ signalBox: { opView, txStatus, rxStatus, }, queue: { completeFrame, hasPendingFrames, flushToWorker, txIdle, waitForCompletion, armCompletionNotifier, setCompletionWaiterArmed, }, channelHandler, dispatcherOptions, notifySignal, wakeAfterPass, crossProcess, nativeCompletionDoorbell, processCompletionDoorbell, }) => {
|
|
11
|
+
const a_add = Atomics.add;
|
|
11
12
|
const a_load = Atomics.load;
|
|
12
|
-
const a_store = Atomics.store;
|
|
13
13
|
const a_notify = Atomics.notify;
|
|
14
14
|
const canNotifySignal = opView.buffer instanceof SharedArrayBuffer;
|
|
15
15
|
const wakeSignal = notifySignal ??
|
|
@@ -17,6 +17,16 @@ export const hostDispatcherLoop = ({ signalBox: { opView, txStatus, rxStatus, },
|
|
|
17
17
|
if (canNotifySignal)
|
|
18
18
|
a_notify(opView, 0, 1);
|
|
19
19
|
});
|
|
20
|
+
// Bump rather than store a constant: the worker parks on the value it last
|
|
21
|
+
// read, so only a changed word makes a ring that lands before its wait count.
|
|
22
|
+
const wakeLane = () => {
|
|
23
|
+
a_add(opView, 0, 1);
|
|
24
|
+
wakeSignal();
|
|
25
|
+
};
|
|
26
|
+
const wakeIfParked = wakeAfterPass ?? (() => {
|
|
27
|
+
if (a_load(rxStatus, 0) === 0)
|
|
28
|
+
wakeLane();
|
|
29
|
+
});
|
|
20
30
|
const notify = () => channelHandler.notify();
|
|
21
31
|
// Atomics waiters are process-local, so cross-process workers cannot use this
|
|
22
32
|
// doorbell.
|
|
@@ -149,12 +159,11 @@ export const hostDispatcherLoop = ({ signalBox: { opView, txStatus, rxStatus, },
|
|
|
149
159
|
do {
|
|
150
160
|
check.rerun = false;
|
|
151
161
|
txStatus[0] = 1;
|
|
152
|
-
// Wake a parked worker whenever its receive status is clear
|
|
153
|
-
// frames are not a reliable gate
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
}
|
|
162
|
+
// Wake a parked worker whenever its receive status is clear, so it is
|
|
163
|
+
// up by the time the frames land. Pending frames are not a reliable gate
|
|
164
|
+
// because the queue may have room.
|
|
165
|
+
if (a_load(rxStatus, 0) === 0)
|
|
166
|
+
wakeLane();
|
|
158
167
|
// Only completed frames count as progress for backoff purposes.
|
|
159
168
|
let completed = false;
|
|
160
169
|
let progressed = true;
|
|
@@ -170,6 +179,13 @@ export const hostDispatcherLoop = ({ signalBox: { opView, txStatus, rxStatus, },
|
|
|
170
179
|
progressed = true;
|
|
171
180
|
}
|
|
172
181
|
}
|
|
182
|
+
// The ring above was decided before the frames flushed here existed, and
|
|
183
|
+
// calls publish straight from enqueue without passing through this loop.
|
|
184
|
+
// A worker that parked in between saw neither the frames nor a ring and
|
|
185
|
+
// slept out its whole parkMs. Publishing ends in a seq-cst store and the
|
|
186
|
+
// worker re-checks after clearing rxStatus, so re-reading rxStatus after
|
|
187
|
+
// the pass means either the worker sees the frames or this sees it parked.
|
|
188
|
+
wakeIfParked();
|
|
173
189
|
txStatus[0] = 0;
|
|
174
190
|
if (!txIdle()) {
|
|
175
191
|
// Queued frames indicate back-pressure, not progress; only a completion
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { withResolvers } from "../common/with-resolvers.js";
|
|
2
|
+
import { KnittingError } from "../error.js";
|
|
2
3
|
import RingQueue from "../ipc/tools/ring-queue.js";
|
|
3
4
|
import { createRuntimeMessageChannel } from "../common/worker-runtime.js";
|
|
4
5
|
// const objects, not `enum`s: Andromeda's Nova engine can't parse `enum`. Same
|
|
@@ -270,7 +271,7 @@ export const createInlineExecutor = ({ tasks, genTaskID, batchSize, }) => {
|
|
|
270
271
|
if (stateByIndex[index] !== SlotStateMacro.Pending)
|
|
271
272
|
continue;
|
|
272
273
|
try {
|
|
273
|
-
deferredByIndex[index]?.reject("Thread closed");
|
|
274
|
+
deferredByIndex[index]?.reject(new KnittingError("THREAD_CLOSED", "Thread closed"));
|
|
274
275
|
}
|
|
275
276
|
catch {
|
|
276
277
|
}
|
package/src/runtime/pool.d.ts
CHANGED
|
@@ -10,7 +10,11 @@ import "../worker/loop.js";
|
|
|
10
10
|
import { type PayloadBufferOptions } from "../memory/payload-config.js";
|
|
11
11
|
/** Build the shared submit and private return buffers for a stealing pool. */
|
|
12
12
|
export declare const resolveStealRegionLanes: (consumers: number) => number;
|
|
13
|
-
/**
|
|
13
|
+
/**
|
|
14
|
+
* Return the widest region allowed by the selected claim discipline. Only
|
|
15
|
+
* Dekker needs a region per consumer; `ticket` has no regions at all and uses
|
|
16
|
+
* the width purely as a claim batch size.
|
|
17
|
+
*/
|
|
14
18
|
export declare const resolveMaxStealRegionLanes: (consumers: number, stealClaim?: StealClaimDiscipline) => number;
|
|
15
19
|
/** Maximum claimants that leave the protocol's required spare region. */
|
|
16
20
|
export declare const MAX_STEAL_CONSUMERS: number;
|
|
@@ -91,7 +95,8 @@ export declare const createStealPoolBuffers: ({ threads, payload, sharedArgs, re
|
|
|
91
95
|
setPromiseHandler: (handler?: import("../memory/lock.js").PromisePayloadHandler) => void;
|
|
92
96
|
}[];
|
|
93
97
|
sharedQueue: {
|
|
94
|
-
|
|
98
|
+
close: (reason: unknown) => void;
|
|
99
|
+
rejectAll: (reason: unknown) => void;
|
|
95
100
|
hasPendingFrames: () => boolean;
|
|
96
101
|
txIdle: () => boolean;
|
|
97
102
|
completeFrame: () => number;
|
|
@@ -104,12 +109,12 @@ export declare const createStealPoolBuffers: ({ threads, payload, sharedArgs, re
|
|
|
104
109
|
settlePromisePayload: (task: Task, isRejected: boolean, value: unknown) => boolean;
|
|
105
110
|
};
|
|
106
111
|
regionLanes: number;
|
|
107
|
-
stealClaim:
|
|
112
|
+
stealClaim: "dekker" | "ticket";
|
|
108
113
|
processMemory: ProcessStealMemoryLayout;
|
|
109
114
|
abortSignalSAB: SharedArrayBuffer | import("../types.js").SharedBufferRegion;
|
|
110
115
|
abortSignalMax: number;
|
|
111
116
|
};
|
|
112
|
-
export declare const spawnWorkerContext: ({ list, ids, names, sab, thread, debug, hostDebug, totalNumberOfThread, workerCount, source, at, workerOptions, workerExecArgv, permission, host, payload, sharedBytesEnabled, abortSignalCapacity, usesAbortSignal, sharedChannelHandler, denoCompletionDoorbell, stealPool, }: {
|
|
117
|
+
export declare 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, }: {
|
|
113
118
|
list: string[];
|
|
114
119
|
ids: number[];
|
|
115
120
|
names: string[];
|
|
@@ -117,6 +122,7 @@ export declare const spawnWorkerContext: ({ list, ids, names, sab, thread, debug
|
|
|
117
122
|
sab?: Sab;
|
|
118
123
|
thread: number;
|
|
119
124
|
debug?: DebugOptions;
|
|
125
|
+
debugEpoch?: number;
|
|
120
126
|
hostDebug?: (message: string) => void;
|
|
121
127
|
totalNumberOfThread: number;
|
|
122
128
|
/**
|
|
@@ -128,6 +134,8 @@ export declare const spawnWorkerContext: ({ list, ids, names, sab, thread, debug
|
|
|
128
134
|
source?: string;
|
|
129
135
|
workerOptions?: WorkerSettings;
|
|
130
136
|
workerExecArgv?: string[];
|
|
137
|
+
/** The caller's own `workerExecArgv`, before defaults and filtering. */
|
|
138
|
+
requestedExecArgv?: string[];
|
|
131
139
|
permission?: WorkerData["permission"];
|
|
132
140
|
host?: DispatcherSettings;
|
|
133
141
|
payload?: PayloadBufferOptions;
|
|
@@ -171,6 +179,7 @@ export declare const spawnWorkerContext: ({ list, ids, names, sab, thread, debug
|
|
|
171
179
|
processSharedMemoryBackings?: readonly ProcessSharedMemoryBacking[];
|
|
172
180
|
dispatcherCheck?: DispatcherCheck;
|
|
173
181
|
laneWake?: () => void;
|
|
182
|
+
laneAwake?: () => boolean;
|
|
174
183
|
bindSend?: (fn: () => void) => void;
|
|
175
184
|
bindCompletionWake?: (fn: () => void) => void;
|
|
176
185
|
processCompletionDoorbell?: boolean;
|
package/src/runtime/pool.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { createHostTxQueue } from "./tx-queue.js";
|
|
2
2
|
import { cleanupProcessWorkerMemoryQuietly, createProcessSharedMemoryAllocator, createProcessStealMemoryLayout, createProcessWorkerMemoryLayout, createProcessWorkerNativeSignalNotifier, processWorkerUsesIpc, readProcessSharedMemorySettings, readProcessWorkerCommandPrefix, readProcessWorkerRuntime, spawnProcessWorker, toProcessWorkerBootPayload, } from "./process-worker.js";
|
|
3
|
-
import { serializeWorkerBootstrapData, terminateWorkerQuietly,
|
|
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 { KnittingError } from "../error.js";
|
|
6
7
|
import { createNodeCompletionDoorbell } from "./node-doorbell.js";
|
|
7
|
-
import { HEADER_SLOT_STRIDE_U32, lock2, LOCK_SECTOR_BYTE_LENGTH, LockBound, } from "../memory/lock.js";
|
|
8
|
+
import { DEFAULT_STEAL_CLAIM, HEADER_SLOT_STRIDE_U32, lock2, LOCK_SECTOR_BYTE_LENGTH, LockBound, } from "../memory/lock.js";
|
|
8
9
|
// Registers the payload codec before any lock is built.
|
|
9
10
|
import "../memory/payloadCodec.js";
|
|
10
11
|
import "../worker/loop.js";
|
|
@@ -25,8 +26,12 @@ const WORKER_STOP_ACK_TIMEOUT_MS = 50;
|
|
|
25
26
|
// registry. Give its worker loop a chance to drain deferred producer pins
|
|
26
27
|
// before the host falls back to terminate().
|
|
27
28
|
const WORKER_STOP_NEEDS_ACK = RUNTIME === "deno" || RUNTIME === "node";
|
|
28
|
-
//
|
|
29
|
-
|
|
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;
|
|
30
35
|
const SINGLE_WORKER_SPIN_US = 50;
|
|
31
36
|
const DEFAULT_ABORT_SIGNAL_CAPACITY = 258;
|
|
32
37
|
const sanitizePositiveInteger = (value) => {
|
|
@@ -38,7 +43,10 @@ const sanitizePositiveInteger = (value) => {
|
|
|
38
43
|
const resolveAbortSignalCapacity = (value) => sanitizePositiveInteger(value) ?? DEFAULT_ABORT_SIGNAL_CAPACITY;
|
|
39
44
|
const abortSignalByteLength = (capacity) => Math.max(1, Math.ceil(capacity / 32)) * Uint32Array.BYTES_PER_ELEMENT;
|
|
40
45
|
const withDefaultWorkerTimers = (options, workerCount) => {
|
|
41
|
-
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);
|
|
42
50
|
// `??` and not `||`: an explicit `spinMicroseconds: 0` is the whole point of
|
|
43
51
|
// the multi-worker default and must not fall through to it.
|
|
44
52
|
const spinMicroseconds = options?.timers?.spinMicroseconds ??
|
|
@@ -67,10 +75,14 @@ export const resolveStealRegionLanes = (consumers) => {
|
|
|
67
75
|
}
|
|
68
76
|
throw new RangeError(`${consumers} stealing workers need more than ${LockBound.slots} lanes`);
|
|
69
77
|
};
|
|
70
|
-
/**
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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;
|
|
74
86
|
/** Maximum claimants that leave the protocol's required spare region. */
|
|
75
87
|
export const MAX_STEAL_CONSUMERS = LockBound.slots - 1;
|
|
76
88
|
export const createStealPoolBuffers = ({ threads, payload, sharedArgs, regionLanes, stealClaim, abortSignalCapacity, usesAbortSignal, processWorker, }) => {
|
|
@@ -97,7 +109,7 @@ export const createStealPoolBuffers = ({ threads, payload, sharedArgs, regionLan
|
|
|
97
109
|
payload: payloadSab,
|
|
98
110
|
}),
|
|
99
111
|
});
|
|
100
|
-
// Keep the default width valid for Dekker; wider
|
|
112
|
+
// Keep the default width valid for Dekker; wider regions are explicit.
|
|
101
113
|
const maxLanes = resolveMaxStealRegionLanes(threads, stealClaim);
|
|
102
114
|
const lanes = regionLanes === undefined
|
|
103
115
|
? resolveStealRegionLanes(threads)
|
|
@@ -188,7 +200,7 @@ export const createStealPoolBuffers = ({ threads, payload, sharedArgs, regionLan
|
|
|
188
200
|
(abortSignalSAB === undefined ? undefined : resolvedAbortSignalCapacity),
|
|
189
201
|
};
|
|
190
202
|
};
|
|
191
|
-
export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostDebug, totalNumberOfThread, workerCount, source, at, workerOptions, workerExecArgv, permission, host, payload, sharedBytesEnabled, abortSignalCapacity, usesAbortSignal, sharedChannelHandler, denoCompletionDoorbell, stealPool, }) => {
|
|
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, }) => {
|
|
192
204
|
if (workerOptions?.runtime === "compiled") {
|
|
193
205
|
return spawnCompiledWorkerContext({
|
|
194
206
|
list,
|
|
@@ -380,10 +392,20 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostD
|
|
|
380
392
|
const ownsChannel = sharedChannelHandler === undefined &&
|
|
381
393
|
stealPool === undefined;
|
|
382
394
|
const ownChannel = sharedChannelHandler ?? new ChannelHandler();
|
|
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
|
+
}
|
|
383
406
|
// Create the Node doorbell before bootstrapping the worker.
|
|
384
407
|
let nodeCompletionWake;
|
|
385
|
-
const nodeCompletionDoorbell =
|
|
386
|
-
host?.doorbell !== false && host?.nativeDoorbell === true
|
|
408
|
+
const nodeCompletionDoorbell = useNativeNodeDoorbell
|
|
387
409
|
? createNodeCompletionDoorbell(() => nodeCompletionWake?.())
|
|
388
410
|
: undefined;
|
|
389
411
|
const canUseNodeCompletionDoorbell = nodeCompletionDoorbell !== undefined;
|
|
@@ -438,6 +460,7 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostD
|
|
|
438
460
|
at,
|
|
439
461
|
thread,
|
|
440
462
|
debug,
|
|
463
|
+
debugEpoch,
|
|
441
464
|
workerOptions: resolvedWorkerOptions,
|
|
442
465
|
totalNumberOfThread,
|
|
443
466
|
startAt: signalBox.startAt,
|
|
@@ -476,6 +499,11 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostD
|
|
|
476
499
|
const withExecArgv = workerExecArgv && workerExecArgv.length > 0
|
|
477
500
|
? { ...baseWorkerOptions, execArgv: workerExecArgv }
|
|
478
501
|
: baseWorkerOptions;
|
|
502
|
+
const requiredNodePermissionFlags = RUNTIME === "node" &&
|
|
503
|
+
permission?.enabled === true &&
|
|
504
|
+
permission.unsafe !== true
|
|
505
|
+
? permission.node.flags.filter(isNodePermissionExecFlag)
|
|
506
|
+
: [];
|
|
479
507
|
if (processWorkerMemory !== undefined) {
|
|
480
508
|
worker = spawnProcessWorker({
|
|
481
509
|
workerUrl,
|
|
@@ -488,43 +516,59 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostD
|
|
|
488
516
|
}
|
|
489
517
|
else if (HAS_NODE_WORKER_THREADS) {
|
|
490
518
|
try {
|
|
491
|
-
worker
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
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.");
|
|
499
533
|
}
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
}
|
|
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)}`);
|
|
511
544
|
}
|
|
512
|
-
|
|
545
|
+
if (fallbackError?.code ===
|
|
546
|
+
"ERR_WORKER_INVALID_EXEC_ARGV") {
|
|
513
547
|
worker = new WorkerCtor(workerUrl, baseWorkerOptions);
|
|
514
548
|
}
|
|
549
|
+
else {
|
|
550
|
+
throw fallbackError;
|
|
551
|
+
}
|
|
515
552
|
}
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
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);
|
|
519
561
|
}
|
|
520
562
|
}
|
|
521
563
|
else {
|
|
522
|
-
|
|
564
|
+
throw error;
|
|
523
565
|
}
|
|
524
566
|
}
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
567
|
+
warnDroppedWorkerExecArgv(requestedExecArgv, appliedExecArgv);
|
|
568
|
+
}
|
|
569
|
+
catch (error) {
|
|
570
|
+
nodeCompletionDoorbell?.close();
|
|
571
|
+
throw error;
|
|
528
572
|
}
|
|
529
573
|
}
|
|
530
574
|
else {
|
|
@@ -553,11 +597,24 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostD
|
|
|
553
597
|
deactivateStealConsumer();
|
|
554
598
|
}
|
|
555
599
|
};
|
|
556
|
-
const markWorkerClosed = (reason) => {
|
|
600
|
+
const markWorkerClosed = (code, reason, cause) => {
|
|
557
601
|
if (closedReason)
|
|
558
602
|
return;
|
|
559
603
|
closedReason = reason;
|
|
560
|
-
|
|
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
|
+
}
|
|
561
618
|
channelHandler?.close();
|
|
562
619
|
};
|
|
563
620
|
const onWorkerMessage = (message) => {
|
|
@@ -567,12 +624,12 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostD
|
|
|
567
624
|
}
|
|
568
625
|
if (!isWorkerFatalMessage(message))
|
|
569
626
|
return;
|
|
570
|
-
markWorkerClosed(`Worker startup failed: ${message[WORKER_FATAL_MESSAGE_KEY]}`);
|
|
627
|
+
markWorkerClosed("WORKER_STARTUP_FAILED", `Worker startup failed: ${message[WORKER_FATAL_MESSAGE_KEY]}`);
|
|
571
628
|
terminateFailedWorker();
|
|
572
629
|
};
|
|
573
630
|
const onWorkerError = (error) => {
|
|
574
631
|
const message = String(error?.message ?? error);
|
|
575
|
-
markWorkerClosed(`Worker crashed: ${message}
|
|
632
|
+
markWorkerClosed("WORKER_CRASHED", `Worker crashed: ${message}`, error);
|
|
576
633
|
terminateFailedWorker();
|
|
577
634
|
};
|
|
578
635
|
const nodeWorker = worker;
|
|
@@ -585,11 +642,11 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostD
|
|
|
585
642
|
if (closedReason !== undefined)
|
|
586
643
|
return;
|
|
587
644
|
if (stopRequested) {
|
|
588
|
-
markWorkerClosed("Thread closed");
|
|
645
|
+
markWorkerClosed("THREAD_CLOSED", "Thread closed");
|
|
589
646
|
return;
|
|
590
647
|
}
|
|
591
648
|
const normalized = typeof code === "number" ? code : -1;
|
|
592
|
-
markWorkerClosed(`Worker exited with code ${normalized}`);
|
|
649
|
+
markWorkerClosed("WORKER_EXITED", `Worker exited with code ${normalized}`);
|
|
593
650
|
});
|
|
594
651
|
}
|
|
595
652
|
else {
|
|
@@ -651,7 +708,7 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostD
|
|
|
651
708
|
call,
|
|
652
709
|
requestStop: requestWorkerStop,
|
|
653
710
|
kills: async () => {
|
|
654
|
-
markWorkerClosed("Thread closed");
|
|
711
|
+
markWorkerClosed("THREAD_CLOSED", "Thread closed");
|
|
655
712
|
// Process workers must exit before their shared memory is unmapped.
|
|
656
713
|
const awaitExit = processWorkerMemory !== undefined;
|
|
657
714
|
const termination = terminateWorkerQuietly(worker, awaitExit);
|
|
@@ -665,6 +722,9 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostD
|
|
|
665
722
|
laneWake: sharedChannelHandler !== undefined || stealPool !== undefined
|
|
666
723
|
? laneWake
|
|
667
724
|
: undefined,
|
|
725
|
+
laneAwake: stealPool !== undefined
|
|
726
|
+
? () => a_load(signalBox.rxStatus, 0) !== 0
|
|
727
|
+
: undefined,
|
|
668
728
|
bindSend: sharedChannelHandler !== undefined || stealPool !== undefined
|
|
669
729
|
? ((fn) => void (dispatchSend = fn))
|
|
670
730
|
: undefined,
|
|
@@ -681,7 +681,16 @@ const spawnNodeHostedProcessWorker = ({ workerUrl, bootPayload, memory, processR
|
|
|
681
681
|
child.on("message", events.emitMessage);
|
|
682
682
|
queueMicrotask(() => child.send?.(bootPayload));
|
|
683
683
|
}
|
|
684
|
-
child.on("error",
|
|
684
|
+
child.on("error", (error) => {
|
|
685
|
+
if (error?.code !== "ENOENT") {
|
|
686
|
+
events.emitError(error);
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
// A bare "spawn deno ENOENT" does not say which option chose the binary.
|
|
690
|
+
events.emitError(new Error(`process worker binary "${command}" was not found (spawn ENOENT). ` +
|
|
691
|
+
`Install ${processRuntime} or set worker.processRuntime to "node", ` +
|
|
692
|
+
`"deno" or "bun"; it defaults to "deno".`, { cause: error }));
|
|
693
|
+
});
|
|
685
694
|
let resolveExit = () => { };
|
|
686
695
|
const exited = new Promise((resolve) => {
|
|
687
696
|
resolveExit = resolve;
|
|
@@ -26,7 +26,8 @@ type CreateHostTxQueueArgs = {
|
|
|
26
26
|
now?: () => number;
|
|
27
27
|
};
|
|
28
28
|
export declare function createHostTxQueue({ max, lock, returnLock, extraReturnLocks, abortSignals, now, }: CreateHostTxQueueArgs): {
|
|
29
|
-
|
|
29
|
+
close: (reason: unknown) => void;
|
|
30
|
+
rejectAll: (reason: unknown) => void;
|
|
30
31
|
hasPendingFrames: () => boolean;
|
|
31
32
|
txIdle: () => boolean;
|
|
32
33
|
completeFrame: () => number;
|
package/src/runtime/tx-queue.js
CHANGED
|
@@ -34,6 +34,7 @@ export function createHostTxQueue({ max, lock, returnLock, extraReturnLocks, abo
|
|
|
34
34
|
const queuePush = (task) => queue.push(task);
|
|
35
35
|
const { publish, flushPending, hasPendingFrames, getPendingFrameCount, getPendingPromiseCount, resetPendingState, } = lock;
|
|
36
36
|
let inUsed = 0 | 0;
|
|
37
|
+
let closedReason;
|
|
37
38
|
const resetSignal = abortSignals?.resetSignal;
|
|
38
39
|
const nowTime = now ?? p_now;
|
|
39
40
|
const onReturnResolved = (task) => {
|
|
@@ -182,11 +183,19 @@ export function createHostTxQueue({ max, lock, returnLock, extraReturnLocks, abo
|
|
|
182
183
|
resetPendingState();
|
|
183
184
|
inUsed = 0 | 0;
|
|
184
185
|
};
|
|
186
|
+
/** Reject everything in flight, and every later call, with `reason`. */
|
|
187
|
+
const close = (reason) => {
|
|
188
|
+
if (closedReason !== undefined)
|
|
189
|
+
return;
|
|
190
|
+
closedReason = reason;
|
|
191
|
+
rejectAll(reason);
|
|
192
|
+
};
|
|
185
193
|
const flushToWorker = () => flushPending();
|
|
186
194
|
const enqueueKnown = (task) => {
|
|
187
195
|
return publish(task);
|
|
188
196
|
};
|
|
189
197
|
return {
|
|
198
|
+
close,
|
|
190
199
|
rejectAll,
|
|
191
200
|
hasPendingFrames,
|
|
192
201
|
txIdle,
|
|
@@ -200,6 +209,8 @@ export function createHostTxQueue({ max, lock, returnLock, extraReturnLocks, abo
|
|
|
200
209
|
const USE_SIGNAL = abortSignal !== undefined &&
|
|
201
210
|
abortSignals !== undefined;
|
|
202
211
|
return (rawArgs) => {
|
|
212
|
+
if (closedReason !== undefined)
|
|
213
|
+
return Promise.reject(closedReason);
|
|
203
214
|
if (inUsed === queue.length) {
|
|
204
215
|
const newSize = inUsed + 32;
|
|
205
216
|
let current = queue.length;
|
|
@@ -7,8 +7,15 @@ export type SpawnedWorker = {
|
|
|
7
7
|
export type NodeWorkerLike = {
|
|
8
8
|
on?: (event: "error" | "exit" | "message", listener: (...args: unknown[]) => void) => void;
|
|
9
9
|
};
|
|
10
|
+
export declare const isNodePermissionExecFlag: (flag: string) => boolean;
|
|
10
11
|
export declare const toWorkerSafeExecArgv: (flags: string[] | undefined) => string[] | undefined;
|
|
12
|
+
/** Preserve process-worker runtime flags while replacing inherited permissions. */
|
|
11
13
|
export declare const toWorkerCompatExecArgv: (flags: string[] | undefined) => string[] | undefined;
|
|
14
|
+
/**
|
|
15
|
+
* Warn once per flag set when a `workerExecArgv` flag the caller asked for did
|
|
16
|
+
* not reach the worker thread, instead of dropping it silently.
|
|
17
|
+
*/
|
|
18
|
+
export declare const warnDroppedWorkerExecArgv: (requested: string[] | undefined, applied: string[] | undefined) => void;
|
|
12
19
|
export declare const serializeWorkerBootstrapData: (options: WorkerSettings) => WorkerSettings;
|
|
13
20
|
/** Terminate a worker, optionally waiting for it to exit. */
|
|
14
21
|
export declare const terminateWorkerQuietly: (worker: SpawnedWorker, awaitExit?: boolean) => Promise<void>;
|