@boringnode/queue 0.0.1-alpha.3 → 0.0.1-alpha.4
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 +145 -34
- package/build/chunk-5PDDRF5O.js +26 -0
- package/build/chunk-5PDDRF5O.js.map +1 -0
- package/build/chunk-CD45GT6E.js +357 -0
- package/build/chunk-CD45GT6E.js.map +1 -0
- package/build/chunk-HMGNQSSG.js +103 -0
- package/build/chunk-HMGNQSSG.js.map +1 -0
- package/build/index-C3_tlebh.d.ts +776 -0
- package/build/index.d.ts +352 -4
- package/build/index.js +386 -117
- package/build/index.js.map +1 -1
- package/build/src/contracts/adapter.d.ts +1 -1
- package/build/src/drivers/knex_adapter.d.ts +2 -1
- package/build/src/drivers/knex_adapter.js +76 -17
- package/build/src/drivers/knex_adapter.js.map +1 -1
- package/build/src/drivers/redis_adapter.d.ts +2 -2
- package/build/src/drivers/redis_adapter.js +74 -27
- package/build/src/drivers/redis_adapter.js.map +1 -1
- package/build/src/drivers/sync_adapter.d.ts +7 -3
- package/build/src/drivers/sync_adapter.js +25 -8
- package/build/src/drivers/sync_adapter.js.map +1 -1
- package/build/src/types/index.d.ts +1 -0
- package/build/src/types/index.js +1 -0
- package/build/src/types/index.js.map +1 -0
- package/build/src/types/main.d.ts +1 -1
- package/package.json +14 -2
- package/build/chunk-Y6KR3UIR.js +0 -99
- package/build/chunk-Y6KR3UIR.js.map +0 -1
- package/build/job-Bd_c2lFK.d.ts +0 -149
package/build/index.js
CHANGED
|
@@ -1,116 +1,27 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
2
|
+
parse
|
|
3
|
+
} from "./chunk-5PDDRF5O.js";
|
|
4
|
+
import {
|
|
5
|
+
Locator,
|
|
6
|
+
QueueManager,
|
|
7
|
+
debug_default
|
|
8
|
+
} from "./chunk-CD45GT6E.js";
|
|
9
|
+
import {
|
|
10
|
+
DEFAULT_ERROR_RETRY_DELAY,
|
|
11
|
+
DEFAULT_IDLE_DELAY,
|
|
12
|
+
DEFAULT_PRIORITY,
|
|
13
|
+
DEFAULT_STALLED_INTERVAL,
|
|
14
|
+
DEFAULT_STALLED_THRESHOLD,
|
|
3
15
|
E_INVALID_BASE_DELAY,
|
|
4
|
-
E_INVALID_DURATION_EXPRESSION,
|
|
5
16
|
E_INVALID_MAX_DELAY,
|
|
6
17
|
E_INVALID_MULTIPLIER,
|
|
7
18
|
E_JOB_MAX_ATTEMPTS_REACHED,
|
|
8
19
|
E_JOB_TIMEOUT,
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
} from "./chunk-Y6KR3UIR.js";
|
|
20
|
+
exceptions_exports
|
|
21
|
+
} from "./chunk-HMGNQSSG.js";
|
|
12
22
|
|
|
13
23
|
// src/job_dispatcher.ts
|
|
14
24
|
import { randomUUID } from "crypto";
|
|
15
|
-
|
|
16
|
-
// src/queue_manager.ts
|
|
17
|
-
var QueueManagerSingleton = class {
|
|
18
|
-
#defaultAdapter;
|
|
19
|
-
#adapters = {};
|
|
20
|
-
#adapterInstances = /* @__PURE__ */ new Map();
|
|
21
|
-
#globalRetryConfig;
|
|
22
|
-
#queueConfigs = /* @__PURE__ */ new Map();
|
|
23
|
-
async init(config) {
|
|
24
|
-
debug_default("initializing queue manager with config: %O", config);
|
|
25
|
-
this.#validateConfig(config);
|
|
26
|
-
this.#adapterInstances.clear();
|
|
27
|
-
this.#defaultAdapter = config.default;
|
|
28
|
-
this.#adapters = config.adapters;
|
|
29
|
-
this.#globalRetryConfig = config.retry;
|
|
30
|
-
if (config.queues) {
|
|
31
|
-
for (const [queue, queueConfig] of Object.entries(config.queues)) {
|
|
32
|
-
this.#queueConfigs.set(queue, queueConfig);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
await Locator.registerFromGlob(config.locations);
|
|
36
|
-
return this;
|
|
37
|
-
}
|
|
38
|
-
use(adapter) {
|
|
39
|
-
if (!adapter) {
|
|
40
|
-
adapter = this.#defaultAdapter;
|
|
41
|
-
}
|
|
42
|
-
const cached = this.#adapterInstances.get(adapter);
|
|
43
|
-
if (cached) {
|
|
44
|
-
return cached;
|
|
45
|
-
}
|
|
46
|
-
const adapterFactory = this.#adapters[adapter];
|
|
47
|
-
if (!adapterFactory) {
|
|
48
|
-
throw new E_CONFIGURATION_ERROR([`Adapter "${adapter}" is not registered`]);
|
|
49
|
-
}
|
|
50
|
-
debug_default('using adapter "%s"', adapter);
|
|
51
|
-
try {
|
|
52
|
-
const instance = adapterFactory();
|
|
53
|
-
this.#adapterInstances.set(adapter, instance);
|
|
54
|
-
return instance;
|
|
55
|
-
} catch (error) {
|
|
56
|
-
throw new Error();
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
/**
|
|
60
|
-
* Priority: job > queue > global
|
|
61
|
-
*/
|
|
62
|
-
getMergedRetryConfig(queue, jobRetryConfig) {
|
|
63
|
-
const queueConfig = this.#queueConfigs.get(queue);
|
|
64
|
-
const queueRetryConfig = queueConfig?.retry || {};
|
|
65
|
-
let maxRetries = jobRetryConfig?.maxRetries || queueRetryConfig.maxRetries || this.#globalRetryConfig?.maxRetries || 0;
|
|
66
|
-
let backoff = jobRetryConfig?.backoff || queueRetryConfig.backoff || this.#globalRetryConfig?.backoff;
|
|
67
|
-
return { maxRetries, backoff };
|
|
68
|
-
}
|
|
69
|
-
#validateConfig(config) {
|
|
70
|
-
if (!config.adapters || Object.keys(config.adapters).length === 0) {
|
|
71
|
-
throw new E_CONFIGURATION_ERROR(["At least one adapter must be configured"]);
|
|
72
|
-
}
|
|
73
|
-
if (!config.default) {
|
|
74
|
-
throw new E_CONFIGURATION_ERROR(["Default adapter must be specified"]);
|
|
75
|
-
}
|
|
76
|
-
if (!config.locations || config.locations.length === 0) {
|
|
77
|
-
throw new E_CONFIGURATION_ERROR(["Job locations must be specified"]);
|
|
78
|
-
}
|
|
79
|
-
if (!config.adapters[config.default]) {
|
|
80
|
-
throw new E_CONFIGURATION_ERROR([
|
|
81
|
-
`Default adapter "${config.default}" not found in adapters configuration`
|
|
82
|
-
]);
|
|
83
|
-
}
|
|
84
|
-
for (const [name, factory] of Object.entries(config.adapters)) {
|
|
85
|
-
if (typeof factory !== "function") {
|
|
86
|
-
throw new E_CONFIGURATION_ERROR([`Adapter "${name}" must be a factory function`]);
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
async destroy() {
|
|
91
|
-
for (const [name, adapter] of this.#adapterInstances) {
|
|
92
|
-
debug_default('destroying adapter "%s"', name);
|
|
93
|
-
await adapter.destroy();
|
|
94
|
-
}
|
|
95
|
-
this.#adapterInstances.clear();
|
|
96
|
-
}
|
|
97
|
-
};
|
|
98
|
-
var QueueManager = new QueueManagerSingleton();
|
|
99
|
-
|
|
100
|
-
// src/utils.ts
|
|
101
|
-
import { parse as parseDuration } from "@lukeed/ms";
|
|
102
|
-
function parse(duration) {
|
|
103
|
-
if (typeof duration === "number") {
|
|
104
|
-
return duration;
|
|
105
|
-
}
|
|
106
|
-
const milliseconds = parseDuration(duration);
|
|
107
|
-
if (typeof milliseconds === "undefined") {
|
|
108
|
-
throw new E_INVALID_DURATION_EXPRESSION([duration]);
|
|
109
|
-
}
|
|
110
|
-
return milliseconds;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// src/job_dispatcher.ts
|
|
114
25
|
var JobDispatcher = class {
|
|
115
26
|
#name;
|
|
116
27
|
#payload;
|
|
@@ -118,26 +29,105 @@ var JobDispatcher = class {
|
|
|
118
29
|
#adapter;
|
|
119
30
|
#delay;
|
|
120
31
|
#priority;
|
|
32
|
+
/**
|
|
33
|
+
* Create a new job dispatcher.
|
|
34
|
+
*
|
|
35
|
+
* @param name - The job class name (used to locate the class at runtime)
|
|
36
|
+
* @param payload - The data to pass to the job
|
|
37
|
+
*/
|
|
121
38
|
constructor(name, payload) {
|
|
122
39
|
this.#name = name;
|
|
123
40
|
this.#payload = payload;
|
|
124
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* Set the target queue for this job.
|
|
44
|
+
*
|
|
45
|
+
* @param queue - Queue name (default: 'default')
|
|
46
|
+
* @returns This dispatcher for chaining
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* ```typescript
|
|
50
|
+
* await SendEmailJob.dispatch(payload).toQueue('emails')
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
125
53
|
toQueue(queue) {
|
|
126
54
|
this.#queue = queue;
|
|
127
55
|
return this;
|
|
128
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* Delay the job execution.
|
|
59
|
+
*
|
|
60
|
+
* The job will be stored in a delayed state and moved to pending
|
|
61
|
+
* after the delay expires.
|
|
62
|
+
*
|
|
63
|
+
* @param delay - Delay as milliseconds or duration string ('5s', '1h', '7d')
|
|
64
|
+
* @returns This dispatcher for chaining
|
|
65
|
+
*
|
|
66
|
+
* @example
|
|
67
|
+
* ```typescript
|
|
68
|
+
* // Send reminder in 24 hours
|
|
69
|
+
* await ReminderJob.dispatch(payload).in('24h')
|
|
70
|
+
*
|
|
71
|
+
* // Process in 5 minutes
|
|
72
|
+
* await CleanupJob.dispatch(payload).in('5m')
|
|
73
|
+
* ```
|
|
74
|
+
*/
|
|
129
75
|
in(delay) {
|
|
130
76
|
this.#delay = delay;
|
|
131
77
|
return this;
|
|
132
78
|
}
|
|
79
|
+
/**
|
|
80
|
+
* Set the job priority.
|
|
81
|
+
*
|
|
82
|
+
* Lower numbers = higher priority. Jobs with lower priority values
|
|
83
|
+
* are processed before jobs with higher values.
|
|
84
|
+
*
|
|
85
|
+
* @param priority - Priority level (1-10, default: 5)
|
|
86
|
+
* @returns This dispatcher for chaining
|
|
87
|
+
*
|
|
88
|
+
* @example
|
|
89
|
+
* ```typescript
|
|
90
|
+
* // High priority job
|
|
91
|
+
* await UrgentJob.dispatch(payload).priority(1)
|
|
92
|
+
*
|
|
93
|
+
* // Low priority job
|
|
94
|
+
* await BackgroundJob.dispatch(payload).priority(10)
|
|
95
|
+
* ```
|
|
96
|
+
*/
|
|
133
97
|
priority(priority) {
|
|
134
98
|
this.#priority = priority;
|
|
135
99
|
return this;
|
|
136
100
|
}
|
|
101
|
+
/**
|
|
102
|
+
* Use a specific adapter for this job.
|
|
103
|
+
*
|
|
104
|
+
* @param adapter - Adapter name or factory function
|
|
105
|
+
* @returns This dispatcher for chaining
|
|
106
|
+
*
|
|
107
|
+
* @example
|
|
108
|
+
* ```typescript
|
|
109
|
+
* // Use named adapter
|
|
110
|
+
* await Job.dispatch(payload).with('redis')
|
|
111
|
+
*
|
|
112
|
+
* // Use custom adapter instance
|
|
113
|
+
* await Job.dispatch(payload).with(() => new CustomAdapter())
|
|
114
|
+
* ```
|
|
115
|
+
*/
|
|
137
116
|
with(adapter) {
|
|
138
117
|
this.#adapter = adapter;
|
|
139
118
|
return this;
|
|
140
119
|
}
|
|
120
|
+
/**
|
|
121
|
+
* Dispatch the job to the queue.
|
|
122
|
+
*
|
|
123
|
+
* @returns The unique job ID
|
|
124
|
+
*
|
|
125
|
+
* @example
|
|
126
|
+
* ```typescript
|
|
127
|
+
* const jobId = await SendEmailJob.dispatch(payload).run()
|
|
128
|
+
* console.log(`Dispatched job: ${jobId}`)
|
|
129
|
+
* ```
|
|
130
|
+
*/
|
|
141
131
|
async run() {
|
|
142
132
|
const id = randomUUID();
|
|
143
133
|
debug_default("dispatching job %s with id %s using payload %s", this.#name, id, this.#payload);
|
|
@@ -157,6 +147,15 @@ var JobDispatcher = class {
|
|
|
157
147
|
}
|
|
158
148
|
return id;
|
|
159
149
|
}
|
|
150
|
+
/**
|
|
151
|
+
* Thenable implementation for auto-dispatch when awaited.
|
|
152
|
+
*
|
|
153
|
+
* Allows `await Job.dispatch(payload)` without explicit `.run()`.
|
|
154
|
+
*
|
|
155
|
+
* @param onFulfilled - Success callback
|
|
156
|
+
* @param onRejected - Error callback
|
|
157
|
+
* @returns Promise resolving to the job ID
|
|
158
|
+
*/
|
|
160
159
|
then(onFulfilled, onRejected) {
|
|
161
160
|
return this.run().then(onFulfilled, onRejected);
|
|
162
161
|
}
|
|
@@ -174,13 +173,65 @@ var JobDispatcher = class {
|
|
|
174
173
|
// src/job.ts
|
|
175
174
|
var Job = class {
|
|
176
175
|
#payload;
|
|
176
|
+
#context;
|
|
177
|
+
/** Static options for this job class (queue, retries, timeout, etc.) */
|
|
177
178
|
static options = {};
|
|
179
|
+
/** The payload data passed to this job instance */
|
|
178
180
|
get payload() {
|
|
179
181
|
return this.#payload;
|
|
180
182
|
}
|
|
181
|
-
|
|
183
|
+
/**
|
|
184
|
+
* Context information for the current job execution.
|
|
185
|
+
*
|
|
186
|
+
* Provides metadata such as job ID, current attempt number,
|
|
187
|
+
* queue name, priority, and timing information.
|
|
188
|
+
*
|
|
189
|
+
* @example
|
|
190
|
+
* ```typescript
|
|
191
|
+
* async execute() {
|
|
192
|
+
* if (this.context.attempt > 1) {
|
|
193
|
+
* console.log(`Retry attempt ${this.context.attempt}`)
|
|
194
|
+
* }
|
|
195
|
+
* console.log(`Processing job ${this.context.jobId} on queue ${this.context.queue}`)
|
|
196
|
+
* }
|
|
197
|
+
* ```
|
|
198
|
+
*/
|
|
199
|
+
get context() {
|
|
200
|
+
return this.#context;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Create a new job instance.
|
|
204
|
+
*
|
|
205
|
+
* @param payload - The data to be processed by this job
|
|
206
|
+
* @param context - The job execution context (provided by the worker)
|
|
207
|
+
*/
|
|
208
|
+
constructor(payload, context) {
|
|
182
209
|
this.#payload = payload;
|
|
210
|
+
this.#context = Object.freeze(context);
|
|
183
211
|
}
|
|
212
|
+
/**
|
|
213
|
+
* Dispatch this job to the queue.
|
|
214
|
+
*
|
|
215
|
+
* Returns a JobDispatcher for fluent configuration before dispatching.
|
|
216
|
+
* The job is not actually dispatched until `.run()` is called or the
|
|
217
|
+
* dispatcher is awaited.
|
|
218
|
+
*
|
|
219
|
+
* @param payload - The data to pass to the job
|
|
220
|
+
* @returns A JobDispatcher for fluent configuration
|
|
221
|
+
*
|
|
222
|
+
* @example
|
|
223
|
+
* ```typescript
|
|
224
|
+
* // Simple dispatch
|
|
225
|
+
* await SendEmailJob.dispatch({ to: 'user@example.com', subject: 'Hello' })
|
|
226
|
+
*
|
|
227
|
+
* // With options
|
|
228
|
+
* await SendEmailJob.dispatch({ to: 'user@example.com' })
|
|
229
|
+
* .toQueue('high-priority')
|
|
230
|
+
* .priority(1)
|
|
231
|
+
* .in('5m')
|
|
232
|
+
* .run()
|
|
233
|
+
* ```
|
|
234
|
+
*/
|
|
184
235
|
static dispatch(payload) {
|
|
185
236
|
const dispatcher = new JobDispatcher(
|
|
186
237
|
this.jobName,
|
|
@@ -206,18 +257,45 @@ import { setTimeout } from "timers/promises";
|
|
|
206
257
|
// src/job_pool.ts
|
|
207
258
|
var JobPool = class {
|
|
208
259
|
#activeJobs = /* @__PURE__ */ new Map();
|
|
260
|
+
/** Number of currently running jobs */
|
|
209
261
|
get size() {
|
|
210
262
|
return this.#activeJobs.size;
|
|
211
263
|
}
|
|
264
|
+
/**
|
|
265
|
+
* Check if the pool has no running jobs.
|
|
266
|
+
*
|
|
267
|
+
* @returns True if no jobs are running
|
|
268
|
+
*/
|
|
212
269
|
isEmpty() {
|
|
213
270
|
return this.#activeJobs.size === 0;
|
|
214
271
|
}
|
|
272
|
+
/**
|
|
273
|
+
* Check if the pool can accept more jobs.
|
|
274
|
+
*
|
|
275
|
+
* @param concurrency - Maximum number of concurrent jobs
|
|
276
|
+
* @returns True if there's room for more jobs
|
|
277
|
+
*/
|
|
215
278
|
hasCapacity(concurrency) {
|
|
216
279
|
return this.#activeJobs.size < concurrency;
|
|
217
280
|
}
|
|
281
|
+
/**
|
|
282
|
+
* Add a job to the pool.
|
|
283
|
+
*
|
|
284
|
+
* @param job - The acquired job data
|
|
285
|
+
* @param queue - The queue the job came from
|
|
286
|
+
* @param promise - Promise that resolves when the job completes
|
|
287
|
+
*/
|
|
218
288
|
add(job, queue, promise) {
|
|
219
289
|
this.#activeJobs.set(job.id, { promise, job, queue });
|
|
220
290
|
}
|
|
291
|
+
/**
|
|
292
|
+
* Wait for the next job to complete and return it.
|
|
293
|
+
*
|
|
294
|
+
* Uses `Promise.race()` internally, so the fastest job wins.
|
|
295
|
+
* The completed job is removed from the pool.
|
|
296
|
+
*
|
|
297
|
+
* @returns The first job to complete (success or failure)
|
|
298
|
+
*/
|
|
221
299
|
async waitForNextCompletion() {
|
|
222
300
|
const completedJobId = await Promise.race(
|
|
223
301
|
[...this.#activeJobs.entries()].map(async ([id, { promise }]) => {
|
|
@@ -232,6 +310,12 @@ var JobPool = class {
|
|
|
232
310
|
this.#activeJobs.delete(completedJobId);
|
|
233
311
|
return completed;
|
|
234
312
|
}
|
|
313
|
+
/**
|
|
314
|
+
* Wait for all running jobs to complete.
|
|
315
|
+
*
|
|
316
|
+
* Used during graceful shutdown to ensure no jobs are abandoned.
|
|
317
|
+
* Clears the pool after all jobs finish.
|
|
318
|
+
*/
|
|
235
319
|
async drain() {
|
|
236
320
|
const promises = [...this.#activeJobs.values()].map(async ({ promise }) => {
|
|
237
321
|
try {
|
|
@@ -248,19 +332,46 @@ var JobPool = class {
|
|
|
248
332
|
var Worker = class {
|
|
249
333
|
#id;
|
|
250
334
|
#config;
|
|
335
|
+
#idleDelay;
|
|
336
|
+
#stalledInterval;
|
|
337
|
+
#stalledThreshold;
|
|
338
|
+
#maxStalledCount;
|
|
339
|
+
#concurrency;
|
|
340
|
+
#gracefulShutdown;
|
|
341
|
+
#onShutdownSignal;
|
|
251
342
|
#adapter;
|
|
252
343
|
#running = false;
|
|
253
344
|
#initialized = false;
|
|
254
345
|
#generator;
|
|
255
346
|
#pool;
|
|
347
|
+
#lastStalledCheck = 0;
|
|
348
|
+
#shutdownHandler;
|
|
349
|
+
/** Unique identifier for this worker instance */
|
|
256
350
|
get id() {
|
|
257
351
|
return this.#id;
|
|
258
352
|
}
|
|
353
|
+
/**
|
|
354
|
+
* Create a new worker instance.
|
|
355
|
+
*
|
|
356
|
+
* @param config - Queue configuration including adapter and worker settings
|
|
357
|
+
*/
|
|
259
358
|
constructor(config) {
|
|
260
359
|
this.#config = config;
|
|
261
360
|
this.#id = randomUUID2();
|
|
361
|
+
this.#idleDelay = parse(config.worker?.idleDelay ?? DEFAULT_IDLE_DELAY);
|
|
362
|
+
this.#stalledInterval = parse(config.worker?.stalledInterval ?? DEFAULT_STALLED_INTERVAL);
|
|
363
|
+
this.#stalledThreshold = parse(config.worker?.stalledThreshold ?? DEFAULT_STALLED_THRESHOLD);
|
|
364
|
+
this.#maxStalledCount = config.worker?.maxStalledCount ?? 1;
|
|
365
|
+
this.#concurrency = config.worker?.concurrency ?? 1;
|
|
366
|
+
this.#gracefulShutdown = config.worker?.gracefulShutdown ?? true;
|
|
367
|
+
this.#onShutdownSignal = config.worker?.onShutdownSignal;
|
|
262
368
|
debug_default("created worker with id %s and config %O", this.#id, config);
|
|
263
369
|
}
|
|
370
|
+
/**
|
|
371
|
+
* Initialize the worker (called automatically by `start()`).
|
|
372
|
+
*
|
|
373
|
+
* Sets up the QueueManager and adapter connection.
|
|
374
|
+
*/
|
|
264
375
|
async init() {
|
|
265
376
|
if (this.#initialized) {
|
|
266
377
|
return;
|
|
@@ -272,6 +383,23 @@ var Worker = class {
|
|
|
272
383
|
this.#initialized = true;
|
|
273
384
|
debug_default("worker %s initialized", this.#id);
|
|
274
385
|
}
|
|
386
|
+
/**
|
|
387
|
+
* Start processing jobs from the specified queues.
|
|
388
|
+
*
|
|
389
|
+
* This method blocks until the worker is stopped (via `stop()` or signal).
|
|
390
|
+
* Jobs are processed concurrently up to the configured concurrency limit.
|
|
391
|
+
*
|
|
392
|
+
* @param queues - Queue names to process (default: ['default'])
|
|
393
|
+
*
|
|
394
|
+
* @example
|
|
395
|
+
* ```typescript
|
|
396
|
+
* // Process single queue
|
|
397
|
+
* await worker.start()
|
|
398
|
+
*
|
|
399
|
+
* // Process multiple queues (priority order)
|
|
400
|
+
* await worker.start(['high-priority', 'default', 'low-priority'])
|
|
401
|
+
* ```
|
|
402
|
+
*/
|
|
275
403
|
async start(queues = ["default"]) {
|
|
276
404
|
await this.init();
|
|
277
405
|
if (this.#running) {
|
|
@@ -280,7 +408,7 @@ var Worker = class {
|
|
|
280
408
|
}
|
|
281
409
|
this.#running = true;
|
|
282
410
|
debug_default("starting worker %s on queues: %O", this.#id, queues);
|
|
283
|
-
|
|
411
|
+
this.#setupGracefulShutdown();
|
|
284
412
|
for await (const cycle of this.process(queues)) {
|
|
285
413
|
if (["started", "completed"].includes(cycle.type)) {
|
|
286
414
|
continue;
|
|
@@ -296,6 +424,12 @@ var Worker = class {
|
|
|
296
424
|
}
|
|
297
425
|
}
|
|
298
426
|
}
|
|
427
|
+
/**
|
|
428
|
+
* Stop the worker gracefully.
|
|
429
|
+
*
|
|
430
|
+
* Waits for all running jobs to complete before shutting down.
|
|
431
|
+
* Called automatically on SIGINT/SIGTERM if gracefulShutdown is enabled.
|
|
432
|
+
*/
|
|
299
433
|
async stop() {
|
|
300
434
|
debug_default("stopping worker %s", this.#id);
|
|
301
435
|
this.#running = false;
|
|
@@ -306,7 +440,29 @@ var Worker = class {
|
|
|
306
440
|
if (this.#adapter) {
|
|
307
441
|
await this.#adapter.destroy();
|
|
308
442
|
}
|
|
443
|
+
this.#removeShutdownHandlers();
|
|
309
444
|
}
|
|
445
|
+
/**
|
|
446
|
+
* Process a single cycle and return the result.
|
|
447
|
+
*
|
|
448
|
+
* Useful for testing or when you need fine-grained control.
|
|
449
|
+
* Each cycle may start new jobs, complete a job, or return idle.
|
|
450
|
+
*
|
|
451
|
+
* @param queues - Queue names to process
|
|
452
|
+
* @returns The cycle result, or null if the worker was stopped
|
|
453
|
+
*
|
|
454
|
+
* @example
|
|
455
|
+
* ```typescript
|
|
456
|
+
* const worker = new Worker(config)
|
|
457
|
+
*
|
|
458
|
+
* // Process cycles manually
|
|
459
|
+
* let cycle = await worker.processCycle(['default'])
|
|
460
|
+
* while (cycle) {
|
|
461
|
+
* console.log('Cycle:', cycle.type)
|
|
462
|
+
* cycle = await worker.processCycle(['default'])
|
|
463
|
+
* }
|
|
464
|
+
* ```
|
|
465
|
+
*/
|
|
310
466
|
async processCycle(queues) {
|
|
311
467
|
await this.init();
|
|
312
468
|
this.#running = true;
|
|
@@ -320,26 +476,58 @@ var Worker = class {
|
|
|
320
476
|
}
|
|
321
477
|
return result.value;
|
|
322
478
|
}
|
|
479
|
+
/**
|
|
480
|
+
* Generator that yields worker cycle events.
|
|
481
|
+
*
|
|
482
|
+
* Low-level API for processing jobs. Yields events for:
|
|
483
|
+
* - `started`: A new job began execution
|
|
484
|
+
* - `completed`: A job finished (success or failure)
|
|
485
|
+
* - `idle`: No jobs available, suggest waiting
|
|
486
|
+
* - `error`: An error occurred during processing
|
|
487
|
+
*
|
|
488
|
+
* @param queues - Queue names to process
|
|
489
|
+
* @yields WorkerCycle events
|
|
490
|
+
*
|
|
491
|
+
* @example
|
|
492
|
+
* ```typescript
|
|
493
|
+
* for await (const cycle of worker.process(['default'])) {
|
|
494
|
+
* switch (cycle.type) {
|
|
495
|
+
* case 'started':
|
|
496
|
+
* console.log(`Started job ${cycle.job.id}`)
|
|
497
|
+
* break
|
|
498
|
+
* case 'completed':
|
|
499
|
+
* console.log(`Completed job ${cycle.job.id}`)
|
|
500
|
+
* break
|
|
501
|
+
* case 'idle':
|
|
502
|
+
* await sleep(cycle.suggestedDelay)
|
|
503
|
+
* break
|
|
504
|
+
* }
|
|
505
|
+
* }
|
|
506
|
+
* ```
|
|
507
|
+
*/
|
|
323
508
|
async *process(queues) {
|
|
324
|
-
const pollingInterval = parse(this.#config.worker?.pollingInterval || "2s");
|
|
325
509
|
this.#pool = new JobPool();
|
|
326
510
|
while (this.#running) {
|
|
327
511
|
try {
|
|
512
|
+
await this.#checkStalledJobs(queues);
|
|
328
513
|
yield* this.#fillPool(queues);
|
|
329
514
|
if (this.#pool.isEmpty()) {
|
|
330
|
-
yield { type: "idle", suggestedDelay:
|
|
515
|
+
yield { type: "idle", suggestedDelay: this.#idleDelay };
|
|
331
516
|
continue;
|
|
332
517
|
}
|
|
333
518
|
const completed = await this.#pool.waitForNextCompletion();
|
|
334
519
|
yield { type: "completed", queue: completed.queue, job: completed.job };
|
|
335
520
|
} catch (error) {
|
|
336
|
-
yield {
|
|
521
|
+
yield {
|
|
522
|
+
type: "error",
|
|
523
|
+
error,
|
|
524
|
+
suggestedDelay: parse(DEFAULT_ERROR_RETRY_DELAY)
|
|
525
|
+
};
|
|
337
526
|
}
|
|
338
527
|
}
|
|
339
528
|
}
|
|
340
529
|
async *#fillPool(queues) {
|
|
341
|
-
const
|
|
342
|
-
const slotsAvailable = concurrency - this.#pool.size;
|
|
530
|
+
const slotsAvailable = this.#concurrency - this.#pool.size;
|
|
343
531
|
if (slotsAvailable <= 0) return;
|
|
344
532
|
const popPromises = Array.from({ length: slotsAvailable }, () => this.#acquireNextJob(queues));
|
|
345
533
|
const results = await Promise.all(popPromises);
|
|
@@ -400,7 +588,17 @@ var Worker = class {
|
|
|
400
588
|
async #initJob(job, queue) {
|
|
401
589
|
try {
|
|
402
590
|
const JobClass = Locator.getOrThrow(job.name);
|
|
403
|
-
const
|
|
591
|
+
const context = Object.freeze({
|
|
592
|
+
jobId: job.id,
|
|
593
|
+
name: job.name,
|
|
594
|
+
attempt: job.attempts + 1,
|
|
595
|
+
queue,
|
|
596
|
+
priority: job.priority ?? DEFAULT_PRIORITY,
|
|
597
|
+
acquiredAt: new Date(job.acquiredAt),
|
|
598
|
+
stalledCount: job.stalledCount ?? 0
|
|
599
|
+
});
|
|
600
|
+
const jobFactory = QueueManager.getJobFactory();
|
|
601
|
+
const instance = jobFactory ? await jobFactory(JobClass, job.payload, context) : new JobClass(job.payload, context);
|
|
404
602
|
const options = JobClass.options || {};
|
|
405
603
|
const timeout = this.#getJobTimeout(options);
|
|
406
604
|
return { instance, options, timeout };
|
|
@@ -442,14 +640,43 @@ var Worker = class {
|
|
|
442
640
|
}
|
|
443
641
|
return null;
|
|
444
642
|
}
|
|
445
|
-
async #
|
|
446
|
-
const
|
|
643
|
+
async #checkStalledJobs(queues) {
|
|
644
|
+
const now = Date.now();
|
|
645
|
+
if (now - this.#lastStalledCheck < this.#stalledInterval) {
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
this.#lastStalledCheck = now;
|
|
649
|
+
for (const queue of queues) {
|
|
650
|
+
const recovered = await this.#adapter.recoverStalledJobs(
|
|
651
|
+
queue,
|
|
652
|
+
this.#stalledThreshold,
|
|
653
|
+
this.#maxStalledCount
|
|
654
|
+
);
|
|
655
|
+
if (recovered > 0) {
|
|
656
|
+
debug_default("worker %s: recovered %d stalled jobs from queue %s", this.#id, recovered, queue);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
#setupGracefulShutdown() {
|
|
661
|
+
if (!this.#gracefulShutdown) {
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
this.#shutdownHandler = async () => {
|
|
447
665
|
debug_default("received shutdown signal, stopping worker...");
|
|
666
|
+
if (this.#onShutdownSignal) {
|
|
667
|
+
await this.#onShutdownSignal();
|
|
668
|
+
}
|
|
448
669
|
await this.stop();
|
|
449
|
-
process.exit(0);
|
|
450
670
|
};
|
|
451
|
-
process.on("SIGINT",
|
|
452
|
-
process.on("SIGTERM",
|
|
671
|
+
process.on("SIGINT", this.#shutdownHandler);
|
|
672
|
+
process.on("SIGTERM", this.#shutdownHandler);
|
|
673
|
+
}
|
|
674
|
+
#removeShutdownHandlers() {
|
|
675
|
+
if (this.#shutdownHandler) {
|
|
676
|
+
process.off("SIGINT", this.#shutdownHandler);
|
|
677
|
+
process.off("SIGTERM", this.#shutdownHandler);
|
|
678
|
+
this.#shutdownHandler = void 0;
|
|
679
|
+
}
|
|
453
680
|
}
|
|
454
681
|
};
|
|
455
682
|
|
|
@@ -458,10 +685,33 @@ import { RuntimeException } from "@poppinss/utils";
|
|
|
458
685
|
import { assertUnreachable } from "@poppinss/utils/assert";
|
|
459
686
|
var BackoffStrategy = class {
|
|
460
687
|
#config;
|
|
688
|
+
/**
|
|
689
|
+
* Create a new backoff strategy.
|
|
690
|
+
*
|
|
691
|
+
* @param config - Backoff configuration
|
|
692
|
+
* @throws {E_INVALID_BASE_DELAY} If baseDelay is not positive
|
|
693
|
+
* @throws {E_INVALID_MAX_DELAY} If maxDelay is invalid
|
|
694
|
+
* @throws {E_INVALID_MULTIPLIER} If multiplier is invalid
|
|
695
|
+
*/
|
|
461
696
|
constructor(config) {
|
|
462
697
|
this.#config = config;
|
|
463
698
|
this.#validateConfig();
|
|
464
699
|
}
|
|
700
|
+
/**
|
|
701
|
+
* Calculate the delay for a given attempt number.
|
|
702
|
+
*
|
|
703
|
+
* @param attempt - The attempt number (1-based)
|
|
704
|
+
* @returns Delay in milliseconds
|
|
705
|
+
* @throws {RuntimeException} If attempt is less than 1
|
|
706
|
+
*
|
|
707
|
+
* @example
|
|
708
|
+
* ```typescript
|
|
709
|
+
* // Exponential: 1s, 2s, 4s, 8s, 16s, ...
|
|
710
|
+
* strategy.calculateDelay(1) // 1000
|
|
711
|
+
* strategy.calculateDelay(2) // 2000
|
|
712
|
+
* strategy.calculateDelay(3) // 4000
|
|
713
|
+
* ```
|
|
714
|
+
*/
|
|
465
715
|
calculateDelay(attempt) {
|
|
466
716
|
if (attempt < 1) {
|
|
467
717
|
throw new RuntimeException("Attempt number must be >= 1");
|
|
@@ -489,10 +739,27 @@ var BackoffStrategy = class {
|
|
|
489
739
|
}
|
|
490
740
|
return Math.floor(delay);
|
|
491
741
|
}
|
|
742
|
+
/**
|
|
743
|
+
* Get the Date when the next retry should occur.
|
|
744
|
+
*
|
|
745
|
+
* @param attempt - The attempt number (1-based)
|
|
746
|
+
* @returns Date for the next retry
|
|
747
|
+
*
|
|
748
|
+
* @example
|
|
749
|
+
* ```typescript
|
|
750
|
+
* const nextRetry = strategy.getNextRetryAt(3)
|
|
751
|
+
* console.log(`Retry at: ${nextRetry.toISOString()}`)
|
|
752
|
+
* ```
|
|
753
|
+
*/
|
|
492
754
|
getNextRetryAt(attempt) {
|
|
493
755
|
const delay = this.calculateDelay(attempt);
|
|
494
756
|
return new Date(Date.now() + delay);
|
|
495
757
|
}
|
|
758
|
+
/**
|
|
759
|
+
* Get a frozen copy of the configuration.
|
|
760
|
+
*
|
|
761
|
+
* @returns Readonly configuration object
|
|
762
|
+
*/
|
|
496
763
|
getConfig() {
|
|
497
764
|
return Object.freeze({ ...this.#config });
|
|
498
765
|
}
|
|
@@ -560,9 +827,11 @@ function customBackoff(config) {
|
|
|
560
827
|
}
|
|
561
828
|
export {
|
|
562
829
|
Job,
|
|
830
|
+
Locator,
|
|
563
831
|
QueueManager,
|
|
564
832
|
Worker,
|
|
565
833
|
customBackoff,
|
|
834
|
+
exceptions_exports as errors,
|
|
566
835
|
exponentialBackoff,
|
|
567
836
|
fixedBackoff,
|
|
568
837
|
linearBackoff
|