@nxtedition/scheduler 3.0.4 → 3.0.6
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/lib/index.js +38 -34
- package/package.json +4 -3
package/lib/index.js
CHANGED
|
@@ -1,15 +1,8 @@
|
|
|
1
|
-
import os from 'node:os'
|
|
2
|
-
|
|
3
1
|
const RUNNING_INDEX = 0
|
|
4
2
|
const CONCURRENCY_INDEX = 1
|
|
5
3
|
|
|
6
4
|
const maxInt = 2147483647
|
|
7
5
|
|
|
8
|
-
// On x86/x64, aligned 32-bit reads do not tear, so a plain array access is safe
|
|
9
|
-
// and avoids the overhead of Atomics.load() in V8. On other architectures (e.g. ARM),
|
|
10
|
-
// use Atomics.load() to prevent tearing.
|
|
11
|
-
const useAtomicLoad = os.arch() !== 'x64' && os.arch() !== 'ia32'
|
|
12
|
-
|
|
13
6
|
class FastQueue {
|
|
14
7
|
idx = 0
|
|
15
8
|
cnt = 0
|
|
@@ -110,17 +103,17 @@ export class Scheduler {
|
|
|
110
103
|
throw new Error('Invalid concurrency')
|
|
111
104
|
}
|
|
112
105
|
const stateBuffer = new SharedArrayBuffer(64)
|
|
113
|
-
const stateView = new
|
|
106
|
+
const stateView = new Int32Array(stateBuffer)
|
|
114
107
|
Atomics.store(stateView, CONCURRENCY_INDEX, concurrency ?? 0)
|
|
115
108
|
return stateBuffer
|
|
116
109
|
}
|
|
117
110
|
|
|
118
111
|
constructor(opts ) {
|
|
119
112
|
if (opts instanceof SharedArrayBuffer) {
|
|
120
|
-
this.#stateView = new
|
|
113
|
+
this.#stateView = new Int32Array(opts)
|
|
121
114
|
this.#concurrency = Atomics.load(this.#stateView, CONCURRENCY_INDEX) || Infinity
|
|
122
115
|
} else {
|
|
123
|
-
this.#concurrency = opts?.concurrency
|
|
116
|
+
this.#concurrency = opts?.concurrency ?? Infinity
|
|
124
117
|
}
|
|
125
118
|
}
|
|
126
119
|
|
|
@@ -152,22 +145,21 @@ export class Scheduler {
|
|
|
152
145
|
const queue = this.#queues[p + 3]
|
|
153
146
|
|
|
154
147
|
if (this.#stateView) {
|
|
155
|
-
//
|
|
156
|
-
|
|
157
|
-
// proceed, briefly exceeding it by up to N-1 (where N is the number of workers).
|
|
158
|
-
// This is by design — the concurrency limit is a soft / best-effort constraint.
|
|
159
|
-
// Small, transient over-subscriptions are acceptable and self-correcting on the
|
|
160
|
-
// next release() cycle.
|
|
161
|
-
// Plain array read on x86 (aligned 32-bit reads don't tear); Atomics.load elsewhere.
|
|
162
|
-
const running = useAtomicLoad
|
|
163
|
-
? Atomics.load(this.#stateView, RUNNING_INDEX)
|
|
164
|
-
: this.#stateView[RUNNING_INDEX]
|
|
165
|
-
if (this.#running < 1 || running < this.#concurrency) {
|
|
148
|
+
// Make sure we are always running at least one local job even if we might globally over subscribe.
|
|
149
|
+
if (this.#running < 1) {
|
|
166
150
|
Atomics.add(this.#stateView, RUNNING_INDEX, 1)
|
|
167
151
|
this.#running += 1
|
|
168
152
|
fn(opaque)
|
|
169
153
|
return
|
|
170
154
|
}
|
|
155
|
+
|
|
156
|
+
if (Atomics.add(this.#stateView, RUNNING_INDEX, 1) >= this.#concurrency) {
|
|
157
|
+
Atomics.sub(this.#stateView, RUNNING_INDEX, 1)
|
|
158
|
+
} else {
|
|
159
|
+
this.#running += 1
|
|
160
|
+
fn(opaque)
|
|
161
|
+
return
|
|
162
|
+
}
|
|
171
163
|
} else if ((this.#running < 1 && this.#concurrency > 0) || this.#running < this.#concurrency) {
|
|
172
164
|
this.#running += 1
|
|
173
165
|
fn(opaque)
|
|
@@ -182,10 +174,16 @@ export class Scheduler {
|
|
|
182
174
|
}
|
|
183
175
|
|
|
184
176
|
release() {
|
|
185
|
-
let running
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
177
|
+
let running
|
|
178
|
+
if (this.#running > 0) {
|
|
179
|
+
running = this.#stateView
|
|
180
|
+
? Atomics.sub(this.#stateView, RUNNING_INDEX, 1) - 1
|
|
181
|
+
: this.#running - 1
|
|
182
|
+
this.#running -= 1
|
|
183
|
+
} else {
|
|
184
|
+
// Gracefully handle user error...
|
|
185
|
+
running = this.#stateView ? Atomics.load(this.#stateView, RUNNING_INDEX) : this.#running
|
|
186
|
+
}
|
|
189
187
|
|
|
190
188
|
if (this.#pending === 0 || this.#releasing) {
|
|
191
189
|
return
|
|
@@ -230,33 +228,39 @@ export class Scheduler {
|
|
|
230
228
|
throw new Error('Invariant violation: pending > 0 but no tasks in queues')
|
|
231
229
|
}
|
|
232
230
|
|
|
233
|
-
const fn = queue.arr[queue.idx
|
|
234
|
-
|
|
231
|
+
const fn = queue.arr[queue.idx]
|
|
232
|
+
queue.arr[queue.idx++] = null
|
|
233
|
+
const opaque = queue.arr[queue.idx]
|
|
234
|
+
queue.arr[queue.idx++] = null
|
|
235
235
|
queue.cnt -= 1
|
|
236
236
|
|
|
237
237
|
if (queue.cnt === 0) {
|
|
238
238
|
queue.idx = 0
|
|
239
239
|
queue.arr.length = 0
|
|
240
240
|
} else if (queue.idx > 1024) {
|
|
241
|
-
queue.idx = 0
|
|
242
241
|
queue.arr.splice(0, queue.idx)
|
|
242
|
+
queue.idx = 0
|
|
243
243
|
}
|
|
244
244
|
|
|
245
|
-
running = this.#stateView
|
|
246
|
-
? Atomics.add(this.#stateView, RUNNING_INDEX, 1) + 1
|
|
247
|
-
: this.#running + 1
|
|
248
|
-
|
|
249
|
-
this.#counter += 1
|
|
250
245
|
this.#pending -= 1
|
|
251
246
|
this.#running += 1
|
|
247
|
+
|
|
248
|
+
if (this.#stateView) {
|
|
249
|
+
Atomics.add(this.#stateView, RUNNING_INDEX, 1)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
252
|
fn(opaque)
|
|
253
|
+
|
|
254
|
+
// Re-read running after fn() in case it synchronously called release()
|
|
255
|
+
running = this.#stateView ? Atomics.load(this.#stateView, RUNNING_INDEX) : this.#running
|
|
253
256
|
}
|
|
254
|
-
this.#releasing = false
|
|
255
257
|
} catch (err) {
|
|
256
258
|
// Throwing here is undefined behavior...
|
|
257
259
|
queueMicrotask(() => {
|
|
258
260
|
throw new Error('Scheduler task error', { cause: err })
|
|
259
261
|
})
|
|
262
|
+
} finally {
|
|
263
|
+
this.#releasing = false
|
|
260
264
|
}
|
|
261
265
|
}
|
|
262
266
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nxtedition/scheduler",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.6",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"types": "lib/index.d.ts",
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
"prepublishOnly": "yarn build",
|
|
19
19
|
"typecheck": "tsc --noEmit",
|
|
20
20
|
"test": "yarn build && node --test",
|
|
21
|
-
"test:ci": "yarn build && node --test"
|
|
21
|
+
"test:ci": "yarn build && node --test",
|
|
22
|
+
"test:coverage": "npx tsx --test --experimental-test-coverage src/index.test.ts"
|
|
22
23
|
},
|
|
23
24
|
"devDependencies": {
|
|
24
25
|
"@types/node": "^25.2.3",
|
|
@@ -27,5 +28,5 @@
|
|
|
27
28
|
"rimraf": "^6.1.2",
|
|
28
29
|
"typescript": "^5.9.3"
|
|
29
30
|
},
|
|
30
|
-
"gitHead": "
|
|
31
|
+
"gitHead": "80ace3245dc851f38a288b0c3b167ca0024f0469"
|
|
31
32
|
}
|