@zudojs/queue 1.0.0 → 1.1.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/worker/worker.core.js +64 -26
- package/package.json +8 -4
|
@@ -17,6 +17,7 @@ export function createWorker(id, queue, options) {
|
|
|
17
17
|
const drainTimeout = options?.drainTimeout ?? DEFAULT_DRAIN_TIMEOUT_MS;
|
|
18
18
|
let pollTimer = null;
|
|
19
19
|
let activeJobs = 0;
|
|
20
|
+
let polling = false;
|
|
20
21
|
let abortController = null;
|
|
21
22
|
const onError = options?.onError ??
|
|
22
23
|
((error) => {
|
|
@@ -24,10 +25,20 @@ export function createWorker(id, queue, options) {
|
|
|
24
25
|
console.error(`[@zudojs/queue] Worker "${id}" poll failed.`, error);
|
|
25
26
|
});
|
|
26
27
|
});
|
|
28
|
+
/**
|
|
29
|
+
* Arms the next poll. At most one timer is ever armed: a delayed poll
|
|
30
|
+
* already pending is left alone, while an immediate poll (capacity just
|
|
31
|
+
* freed up, or a job was just dispatched) supersedes it.
|
|
32
|
+
*/
|
|
27
33
|
const scheduleNextPoll = (delay) => {
|
|
28
34
|
if (state !== WorkerState.RUNNING) {
|
|
29
35
|
return;
|
|
30
36
|
}
|
|
37
|
+
if (pollTimer !== null) {
|
|
38
|
+
if (delay > 0)
|
|
39
|
+
return;
|
|
40
|
+
clearTimeout(pollTimer);
|
|
41
|
+
}
|
|
31
42
|
pollTimer = setTimeout(runPoll, delay);
|
|
32
43
|
pollTimer.unref?.();
|
|
33
44
|
};
|
|
@@ -37,34 +48,20 @@ export function createWorker(id, queue, options) {
|
|
|
37
48
|
* fatal, would take down the whole application.
|
|
38
49
|
*/
|
|
39
50
|
const runPoll = () => {
|
|
51
|
+
pollTimer = null;
|
|
40
52
|
void poll().catch((error) => {
|
|
41
53
|
onError(error);
|
|
42
54
|
scheduleNextPoll(pollInterval);
|
|
43
55
|
});
|
|
44
56
|
};
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const job = await queue.claimNextJob();
|
|
54
|
-
if (!job) {
|
|
55
|
-
scheduleNextPoll(pollInterval);
|
|
56
|
-
return;
|
|
57
|
-
}
|
|
58
|
-
const proc = queue.getProcessor(job.name);
|
|
59
|
-
if (!proc) {
|
|
60
|
-
// Claimed but unrunnable: release it rather than stranding it in
|
|
61
|
-
// `active` where nothing would ever pick it up again.
|
|
62
|
-
await queue.releaseJob(job.id);
|
|
63
|
-
scheduleNextPoll(pollInterval);
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
activeJobs++;
|
|
67
|
-
stats.processed++;
|
|
57
|
+
/**
|
|
58
|
+
* Runs one claimed job to completion and frees its concurrency slot.
|
|
59
|
+
*
|
|
60
|
+
* Deliberately not awaited by `poll`: awaiting it there serialised the
|
|
61
|
+
* worker, so `concurrency` was reported by `getStats()` and honoured by
|
|
62
|
+
* nothing.
|
|
63
|
+
*/
|
|
64
|
+
const runClaimedJob = async (job) => {
|
|
68
65
|
try {
|
|
69
66
|
// Dispatch through the queue rather than invoking the processor
|
|
70
67
|
// directly. The queue owns job state, retry, dead-lettering and
|
|
@@ -91,11 +88,47 @@ export function createWorker(id, queue, options) {
|
|
|
91
88
|
}
|
|
92
89
|
finally {
|
|
93
90
|
activeJobs--;
|
|
94
|
-
//
|
|
95
|
-
//
|
|
91
|
+
// A slot just opened: poll again immediately, yielding to the event
|
|
92
|
+
// loop first so a saturated queue cannot starve timers.
|
|
96
93
|
scheduleNextPoll(0);
|
|
97
94
|
}
|
|
98
95
|
};
|
|
96
|
+
const poll = async () => {
|
|
97
|
+
if (polling ||
|
|
98
|
+
state !== WorkerState.RUNNING ||
|
|
99
|
+
abortController?.signal.aborted) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
polling = true;
|
|
103
|
+
try {
|
|
104
|
+
if (activeJobs >= concurrency) {
|
|
105
|
+
scheduleNextPoll(pollInterval);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const job = await queue.claimNextJob();
|
|
109
|
+
if (!job) {
|
|
110
|
+
scheduleNextPoll(pollInterval);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
const proc = queue.getProcessor(job.name);
|
|
114
|
+
if (!proc) {
|
|
115
|
+
// Claimed but unrunnable: release it rather than stranding it in
|
|
116
|
+
// `active` where nothing would ever pick it up again.
|
|
117
|
+
await queue.releaseJob(job.id);
|
|
118
|
+
scheduleNextPoll(pollInterval);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
activeJobs++;
|
|
122
|
+
stats.processed++;
|
|
123
|
+
void runClaimedJob(job);
|
|
124
|
+
// Capacity may remain: look for more work now, not after this job
|
|
125
|
+
// settles.
|
|
126
|
+
scheduleNextPoll(0);
|
|
127
|
+
}
|
|
128
|
+
finally {
|
|
129
|
+
polling = false;
|
|
130
|
+
}
|
|
131
|
+
};
|
|
99
132
|
const clearPollTimer = () => {
|
|
100
133
|
if (pollTimer) {
|
|
101
134
|
clearTimeout(pollTimer);
|
|
@@ -138,13 +171,18 @@ export function createWorker(id, queue, options) {
|
|
|
138
171
|
}
|
|
139
172
|
state = WorkerState.DRAINING;
|
|
140
173
|
clearPollTimer();
|
|
141
|
-
|
|
174
|
+
// Graceful means graceful: in-flight jobs get `drainTimeout` to
|
|
175
|
+
// finish on their own. Aborting them up front — as this once did —
|
|
176
|
+
// made `stop()` indistinguishable from `forceStop()` for any
|
|
177
|
+
// processor that honours its signal, and turned every routine
|
|
178
|
+
// shutdown into a batch of failed jobs.
|
|
142
179
|
const deadline = Date.now() + Math.max(0, drainTimeout);
|
|
143
180
|
while (activeJobs > 0 && Date.now() < deadline) {
|
|
144
181
|
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
145
182
|
}
|
|
146
183
|
if (activeJobs > 0) {
|
|
147
184
|
onError(new WorkerLifecycleError(`Worker "${id}" still had ${activeJobs} job(s) in flight after ${drainTimeout}ms; forcing stop.`, { workerId: id }));
|
|
185
|
+
abortController?.abort();
|
|
148
186
|
}
|
|
149
187
|
clearPollTimer();
|
|
150
188
|
state = WorkerState.STOPPED;
|
package/package.json
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zudojs/queue",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Background job and asynchronous task infrastructure with in-memory and adapter-based queue implementations.",
|
|
5
5
|
"license": "MIT",
|
|
6
|
+
"author": {
|
|
7
|
+
"name": "Oluwayemi Oyinlola",
|
|
8
|
+
"url": "https://github.com/oyinlola-tech"
|
|
9
|
+
},
|
|
6
10
|
"type": "module",
|
|
7
11
|
"main": "./dist/index.js",
|
|
8
12
|
"module": "./dist/index.js",
|
|
@@ -21,9 +25,9 @@
|
|
|
21
25
|
"!dist/.tsbuildinfo"
|
|
22
26
|
],
|
|
23
27
|
"dependencies": {
|
|
24
|
-
"@zudojs/errors": "1.0.
|
|
25
|
-
"@zudojs/constants": "1.0.
|
|
26
|
-
"@zudojs/serialization": "1.0.
|
|
28
|
+
"@zudojs/errors": "1.0.1",
|
|
29
|
+
"@zudojs/constants": "1.0.1",
|
|
30
|
+
"@zudojs/serialization": "1.0.1"
|
|
27
31
|
},
|
|
28
32
|
"devDependencies": {
|
|
29
33
|
"typescript": "7.0.2",
|