@boringnode/queue 0.0.1-alpha → 0.0.1-alpha.1
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 +334 -0
- package/build/index.d.ts +2 -3
- package/build/index.js +98 -100
- package/build/index.js.map +1 -1
- package/build/{job-CcAUWe8j.d.ts → job-Bd_c2lFK.d.ts} +45 -21
- package/build/src/contracts/adapter.d.ts +1 -2
- package/build/src/drivers/knex_adapter.d.ts +43 -0
- package/build/src/drivers/knex_adapter.js +176 -0
- package/build/src/drivers/knex_adapter.js.map +1 -0
- package/build/src/drivers/redis_adapter.d.ts +19 -6
- package/build/src/drivers/redis_adapter.js +155 -35
- package/build/src/drivers/redis_adapter.js.map +1 -1
- package/build/src/drivers/sync_adapter.d.ts +11 -5
- package/build/src/drivers/sync_adapter.js +11 -3
- package/build/src/drivers/sync_adapter.js.map +1 -1
- package/build/src/types/main.d.ts +1 -2
- package/package.json +11 -3
- package/build/src/contracts/lease_manager.d.ts +0 -8
- package/build/src/contracts/lease_manager.js +0 -1
- package/build/src/contracts/lease_manager.js.map +0 -1
package/README.md
CHANGED
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
# @boringnode/queue
|
|
2
|
+
|
|
3
|
+
A simple and efficient queue system for Node.js applications. Built for simplicity and ease of use, `@boringnode/queue` allows you to dispatch background jobs and process them asynchronously with support for multiple queue adapters.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @boringnode/queue
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
- **Multiple Queue Adapters**: Support for Redis (production) and Sync (testing/development)
|
|
14
|
+
- **Type-Safe Jobs**: Define jobs as TypeScript classes with typed payloads
|
|
15
|
+
- **Delayed Jobs**: Schedule jobs to run after a specific delay
|
|
16
|
+
- **Multiple Queues**: Organize jobs into different queues for better organization
|
|
17
|
+
- **Worker Management**: Process jobs with configurable concurrency
|
|
18
|
+
- **Auto-Discovery**: Automatically discover and register jobs from specified locations
|
|
19
|
+
- **Priority Queues**: Process high-priority jobs first
|
|
20
|
+
- **Retry with Backoff**: Automatic retries with exponential, linear, or fixed backoff strategies
|
|
21
|
+
- **Job Timeout**: Automatically fail or retry jobs that exceed a time limit
|
|
22
|
+
|
|
23
|
+
## Quick Start
|
|
24
|
+
|
|
25
|
+
### 1. Define a Job
|
|
26
|
+
|
|
27
|
+
Create a job by extending the `Job` class:
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
import { Job } from '@boringnode/queue'
|
|
31
|
+
import type { JobOptions } from '@boringnode/queue/types/main'
|
|
32
|
+
|
|
33
|
+
interface SendEmailPayload {
|
|
34
|
+
to: string
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export default class SendEmailJob extends Job<SendEmailPayload> {
|
|
38
|
+
static readonly jobName = 'SendEmailJob'
|
|
39
|
+
|
|
40
|
+
static options: JobOptions = {
|
|
41
|
+
queue: 'email',
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async execute(): Promise<void> {
|
|
45
|
+
console.log(`Sending email to: ${this.payload.to}`)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### 2. Configure the Queue Manager
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
import { QueueManager } from '@boringnode/queue'
|
|
54
|
+
import { redis } from '@boringnode/queue/drivers/redis_adapter'
|
|
55
|
+
import { sync } from '@boringnode/queue/drivers/sync_adapter'
|
|
56
|
+
import { Redis } from 'ioredis'
|
|
57
|
+
|
|
58
|
+
const redisConnection = new Redis({
|
|
59
|
+
host: 'localhost',
|
|
60
|
+
port: 6379,
|
|
61
|
+
keyPrefix: 'boringnode::queue::',
|
|
62
|
+
db: 0,
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
const config = {
|
|
66
|
+
default: 'redis',
|
|
67
|
+
|
|
68
|
+
adapters: {
|
|
69
|
+
sync: sync(),
|
|
70
|
+
redis: redis(redisConnection),
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
worker: {
|
|
74
|
+
concurrency: 5,
|
|
75
|
+
pollingInterval: '10ms',
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
locations: ['./app/jobs/**/*.ts'],
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
await QueueManager.init(config)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### 3. Dispatch Jobs
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
import SendEmailJob from './jobs/send_email_job.ts'
|
|
88
|
+
|
|
89
|
+
// Dispatch immediately
|
|
90
|
+
await SendEmailJob.dispatch({ to: 'user@example.com' })
|
|
91
|
+
|
|
92
|
+
// Dispatch with delay
|
|
93
|
+
await SendEmailJob.dispatch({ to: 'user@example.com' }).in('5m')
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### 4. Start a Worker
|
|
97
|
+
|
|
98
|
+
Create a worker to process jobs:
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
import { Worker } from '@boringnode/queue'
|
|
102
|
+
|
|
103
|
+
const worker = new Worker(config)
|
|
104
|
+
await worker.start(['default', 'email', 'reports'])
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Configuration
|
|
108
|
+
|
|
109
|
+
### Queue Manager Options
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
interface QueueManagerConfig {
|
|
113
|
+
// Default adapter to use
|
|
114
|
+
default: string
|
|
115
|
+
|
|
116
|
+
// Available queue adapters
|
|
117
|
+
adapters: {
|
|
118
|
+
[key: string]: QueueAdapter
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Worker configuration
|
|
122
|
+
worker: {
|
|
123
|
+
concurrency: number
|
|
124
|
+
pollingInterval: string
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Job discovery locations
|
|
128
|
+
locations: string[]
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Job Options
|
|
133
|
+
|
|
134
|
+
Configure individual jobs with the `options` property:
|
|
135
|
+
|
|
136
|
+
```typescript
|
|
137
|
+
static options: JobOptions = {
|
|
138
|
+
queue: 'email', // Queue name (default: 'default')
|
|
139
|
+
adapter: 'redis', // Override default adapter
|
|
140
|
+
priority: 1, // Lower number = higher priority (default: 5)
|
|
141
|
+
maxRetries: 3, // Maximum retry attempts
|
|
142
|
+
timeout: '30s', // Job timeout duration
|
|
143
|
+
failOnTimeout: true, // Fail permanently on timeout (default: false, will retry)
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Adapters
|
|
148
|
+
|
|
149
|
+
### Redis Adapter
|
|
150
|
+
|
|
151
|
+
For production use with distributed systems:
|
|
152
|
+
|
|
153
|
+
```typescript
|
|
154
|
+
import { redis } from '@boringnode/queue/drivers/redis_adapter'
|
|
155
|
+
import { Redis } from 'ioredis'
|
|
156
|
+
|
|
157
|
+
const redisConnection = new Redis({
|
|
158
|
+
host: 'localhost',
|
|
159
|
+
port: 6379,
|
|
160
|
+
keyPrefix: 'boringnode::queue::',
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
const adapter = redis(redisConnection)
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Sync Adapter
|
|
167
|
+
|
|
168
|
+
For testing and development:
|
|
169
|
+
|
|
170
|
+
```typescript
|
|
171
|
+
import { sync } from '@boringnode/queue/drivers/sync_adapter'
|
|
172
|
+
|
|
173
|
+
const adapter = sync()
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## Worker Configuration
|
|
177
|
+
|
|
178
|
+
Workers process jobs from one or more queues:
|
|
179
|
+
|
|
180
|
+
```typescript
|
|
181
|
+
const worker = new Worker(config)
|
|
182
|
+
|
|
183
|
+
// Process specific queues
|
|
184
|
+
await worker.start(['default', 'email', 'reports'])
|
|
185
|
+
|
|
186
|
+
// Worker will:
|
|
187
|
+
// - Process jobs with configured concurrency
|
|
188
|
+
// - Poll queues at the configured interval
|
|
189
|
+
// - Execute jobs in the order they were queued
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## Delayed Jobs
|
|
193
|
+
|
|
194
|
+
Schedule jobs to run in the future:
|
|
195
|
+
|
|
196
|
+
```typescript
|
|
197
|
+
// Various time formats
|
|
198
|
+
await SendEmailJob.dispatch(payload).in('30s') // 30 seconds
|
|
199
|
+
await SendEmailJob.dispatch(payload).in('5m') // 5 minutes
|
|
200
|
+
await SendEmailJob.dispatch(payload).in('2h') // 2 hours
|
|
201
|
+
await SendEmailJob.dispatch(payload).in('1d') // 1 day
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
## Priority
|
|
205
|
+
|
|
206
|
+
Jobs with lower priority numbers are processed first:
|
|
207
|
+
|
|
208
|
+
```typescript
|
|
209
|
+
export default class UrgentJob extends Job<Payload> {
|
|
210
|
+
static readonly jobName = 'UrgentJob'
|
|
211
|
+
|
|
212
|
+
static options: JobOptions = {
|
|
213
|
+
priority: 1, // Processed before default priority (5)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async execute(): Promise<void> {
|
|
217
|
+
// ...
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
## Retry and Backoff
|
|
223
|
+
|
|
224
|
+
Configure automatic retries with backoff strategies:
|
|
225
|
+
|
|
226
|
+
```typescript
|
|
227
|
+
import { exponentialBackoff, linearBackoff, fixedBackoff } from '@boringnode/queue'
|
|
228
|
+
|
|
229
|
+
export default class ReliableJob extends Job<Payload> {
|
|
230
|
+
static readonly jobName = 'ReliableJob'
|
|
231
|
+
|
|
232
|
+
static options: JobOptions = {
|
|
233
|
+
maxRetries: 5,
|
|
234
|
+
retry: {
|
|
235
|
+
backoff: () => exponentialBackoff({
|
|
236
|
+
baseDelay: '1s',
|
|
237
|
+
maxDelay: '1m',
|
|
238
|
+
multiplier: 2,
|
|
239
|
+
jitter: true,
|
|
240
|
+
}),
|
|
241
|
+
},
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async execute(): Promise<void> {
|
|
245
|
+
// ...
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
Available backoff strategies:
|
|
251
|
+
|
|
252
|
+
- `exponentialBackoff({ baseDelay, maxDelay, multiplier, jitter })` - Exponential increase
|
|
253
|
+
- `linearBackoff({ baseDelay, maxDelay, multiplier })` - Linear increase
|
|
254
|
+
- `fixedBackoff({ baseDelay, jitter })` - Fixed delay between retries
|
|
255
|
+
|
|
256
|
+
## Job Timeout
|
|
257
|
+
|
|
258
|
+
Set a maximum execution time for jobs:
|
|
259
|
+
|
|
260
|
+
```typescript
|
|
261
|
+
export default class LimitedJob extends Job<Payload> {
|
|
262
|
+
static readonly jobName = 'LimitedJob'
|
|
263
|
+
|
|
264
|
+
static options: JobOptions = {
|
|
265
|
+
timeout: '30s', // Maximum execution time
|
|
266
|
+
failOnTimeout: false, // Retry on timeout (default)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
async execute(): Promise<void> {
|
|
270
|
+
// Long running operation...
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
You can also set a global timeout in the worker configuration:
|
|
276
|
+
|
|
277
|
+
```typescript
|
|
278
|
+
const config = {
|
|
279
|
+
worker: {
|
|
280
|
+
timeout: '1m', // Default timeout for all jobs
|
|
281
|
+
},
|
|
282
|
+
}
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
## Job Discovery
|
|
286
|
+
|
|
287
|
+
The queue manager automatically discovers and registers jobs from the specified locations:
|
|
288
|
+
|
|
289
|
+
```typescript
|
|
290
|
+
const config = {
|
|
291
|
+
locations: [
|
|
292
|
+
'./app/jobs/**/*.ts',
|
|
293
|
+
'./modules/**/jobs/**/*.ts',
|
|
294
|
+
],
|
|
295
|
+
}
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
Jobs must:
|
|
299
|
+
- Extend the `Job` class
|
|
300
|
+
- Have a static `jobName` property
|
|
301
|
+
- Implement the `execute` method
|
|
302
|
+
- Be exported as default
|
|
303
|
+
|
|
304
|
+
## Benchmarks
|
|
305
|
+
|
|
306
|
+
Performance comparison with BullMQ using realistic jobs (5ms simulated work per job):
|
|
307
|
+
|
|
308
|
+
| Jobs | Concurrency | @boringnode/queue | BullMQ | Diff |
|
|
309
|
+
|------|-------------|-------------------|--------|--------------|
|
|
310
|
+
| 100 | 1 | 562ms | 596ms | 5.7% faster |
|
|
311
|
+
| 100 | 5 | 116ms | 117ms | ~same |
|
|
312
|
+
| 100 | 10 | 62ms | 62ms | ~same |
|
|
313
|
+
| 500 | 1 | 2728ms | 2798ms | 2.5% faster |
|
|
314
|
+
| 500 | 5 | 565ms | 565ms | ~same |
|
|
315
|
+
| 500 | 10 | 287ms | 288ms | ~same |
|
|
316
|
+
| 1000 | 1 | 5450ms | 5547ms | 1.7% faster |
|
|
317
|
+
| 1000 | 5 | 1096ms | 1116ms | 1.8% faster |
|
|
318
|
+
| 1000 | 10 | 565ms | 579ms | 2.4% faster |
|
|
319
|
+
| 100K | 5 | 110.5s | 112.3s | 1.5% faster |
|
|
320
|
+
| 100K | 10 | 56.2s | 57.5s | 2.1% faster |
|
|
321
|
+
| 100K | 20 | 29.1s | 29.6s | 1.7% faster |
|
|
322
|
+
|
|
323
|
+
Run benchmarks yourself:
|
|
324
|
+
|
|
325
|
+
```bash
|
|
326
|
+
# Realistic benchmark (5ms job duration)
|
|
327
|
+
npm run benchmark -- --realistic
|
|
328
|
+
|
|
329
|
+
# Pure dequeue overhead (no-op jobs)
|
|
330
|
+
npm run benchmark
|
|
331
|
+
|
|
332
|
+
# Custom job duration
|
|
333
|
+
npm run benchmark -- --duration=10
|
|
334
|
+
```
|
package/build/index.d.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { Q as QueueManagerConfig, W as WorkerCycle, A as Adapter, R as RetryConfig } from './job-
|
|
2
|
-
export { J as Job, c as customBackoff, e as exponentialBackoff, f as fixedBackoff, l as linearBackoff } from './job-
|
|
3
|
-
import './src/contracts/lease_manager.js';
|
|
1
|
+
import { Q as QueueManagerConfig, W as WorkerCycle, A as Adapter, R as RetryConfig } from './job-Bd_c2lFK.js';
|
|
2
|
+
export { J as Job, c as customBackoff, e as exponentialBackoff, f as fixedBackoff, l as linearBackoff } from './job-Bd_c2lFK.js';
|
|
4
3
|
|
|
5
4
|
declare class Worker {
|
|
6
5
|
#private;
|
package/build/index.js
CHANGED
|
@@ -34,15 +34,16 @@ import debug from "#src/debug";
|
|
|
34
34
|
import { parse } from "#src/utils";
|
|
35
35
|
import * as errors from "#src/exceptions";
|
|
36
36
|
import { QueueManager } from "#src/queue_manager";
|
|
37
|
+
import { JobPool } from "#src/job_pool";
|
|
37
38
|
import { Locator } from "#src/locator";
|
|
38
39
|
var Worker = class {
|
|
39
40
|
#id;
|
|
40
41
|
#config;
|
|
41
42
|
#adapter;
|
|
42
|
-
#leaseManager;
|
|
43
43
|
#running = false;
|
|
44
44
|
#initialized = false;
|
|
45
45
|
#generator;
|
|
46
|
+
#pool;
|
|
46
47
|
get id() {
|
|
47
48
|
return this.#id;
|
|
48
49
|
}
|
|
@@ -58,11 +59,7 @@ var Worker = class {
|
|
|
58
59
|
debug("initializing worker %s", this.#id);
|
|
59
60
|
await QueueManager.init(this.#config);
|
|
60
61
|
this.#adapter = QueueManager.use();
|
|
61
|
-
this.#
|
|
62
|
-
workerId: this.#id,
|
|
63
|
-
leaseTimeout: parse(this.#config.worker?.leaseTimeout || "5m"),
|
|
64
|
-
renewalInterval: parse(this.#config.worker?.renewalInterval || "5m")
|
|
65
|
-
});
|
|
62
|
+
this.#adapter.setWorkerId(this.#id);
|
|
66
63
|
this.#initialized = true;
|
|
67
64
|
debug("worker %s initialized", this.#id);
|
|
68
65
|
}
|
|
@@ -93,8 +90,9 @@ var Worker = class {
|
|
|
93
90
|
async stop() {
|
|
94
91
|
debug("stopping worker %s", this.#id);
|
|
95
92
|
this.#running = false;
|
|
96
|
-
if (this.#
|
|
97
|
-
|
|
93
|
+
if (this.#pool) {
|
|
94
|
+
debug("worker %s: waiting for %d running jobs to complete", this.#id, this.#pool.size);
|
|
95
|
+
await this.#pool.drain();
|
|
98
96
|
}
|
|
99
97
|
if (this.#adapter) {
|
|
100
98
|
await this.#adapter.destroy();
|
|
@@ -114,56 +112,58 @@ var Worker = class {
|
|
|
114
112
|
return result.value;
|
|
115
113
|
}
|
|
116
114
|
async *process(queues) {
|
|
117
|
-
const
|
|
118
|
-
|
|
115
|
+
const pollingInterval = parse(this.#config.worker?.pollingInterval || "2s");
|
|
116
|
+
this.#pool = new JobPool();
|
|
117
|
+
while (this.#running) {
|
|
119
118
|
try {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
yield { type: "started", queue, job };
|
|
125
|
-
}
|
|
126
|
-
const results = await Promise.allSettled(jobs.map((job) => this.#execute(job, queue)));
|
|
127
|
-
for (const job of jobs) {
|
|
128
|
-
yield { type: "completed", queue, job };
|
|
129
|
-
}
|
|
130
|
-
const hasError = results.some((r) => r.status === "rejected");
|
|
131
|
-
if (hasError) {
|
|
132
|
-
const error = results.find((r) => r.status === "rejected");
|
|
133
|
-
yield { type: "error", error: error.reason, suggestedDelay: parse("5s") };
|
|
134
|
-
}
|
|
135
|
-
continue runningLoop;
|
|
136
|
-
}
|
|
119
|
+
yield* this.#fillPool(queues);
|
|
120
|
+
if (this.#pool.isEmpty()) {
|
|
121
|
+
yield { type: "idle", suggestedDelay: pollingInterval };
|
|
122
|
+
continue;
|
|
137
123
|
}
|
|
138
|
-
const
|
|
139
|
-
yield { type: "
|
|
124
|
+
const completed = await this.#pool.waitForNextCompletion();
|
|
125
|
+
yield { type: "completed", queue: completed.queue, job: completed.job };
|
|
140
126
|
} catch (error) {
|
|
141
127
|
yield { type: "error", error, suggestedDelay: parse("5s") };
|
|
142
128
|
}
|
|
143
129
|
}
|
|
144
130
|
}
|
|
131
|
+
async *#fillPool(queues) {
|
|
132
|
+
const concurrency = this.#config.worker?.concurrency || 1;
|
|
133
|
+
const slotsAvailable = concurrency - this.#pool.size;
|
|
134
|
+
if (slotsAvailable <= 0) return;
|
|
135
|
+
const popPromises = Array.from({ length: slotsAvailable }, () => this.#acquireNextJob(queues));
|
|
136
|
+
const results = await Promise.all(popPromises);
|
|
137
|
+
for (const result of results) {
|
|
138
|
+
if (!result) continue;
|
|
139
|
+
const { job, queue } = result;
|
|
140
|
+
const promise = this.#execute(job, queue);
|
|
141
|
+
this.#pool.add(job, queue, promise);
|
|
142
|
+
yield { type: "started", queue, job };
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
145
|
async #execute(job, queue) {
|
|
146
146
|
const startTime = performance.now();
|
|
147
147
|
debug("worker %s: executing job %s (%s)", this.#id, job.id, job.name);
|
|
148
|
-
|
|
149
|
-
try {
|
|
150
|
-
JobClass = Locator.getOrThrow(job.name);
|
|
151
|
-
} catch (error) {
|
|
152
|
-
debug("worker %s: job class %s not found for job %s", this.#id, job.name, job.id);
|
|
153
|
-
throw error;
|
|
154
|
-
}
|
|
155
|
-
const instance = new JobClass(job.payload);
|
|
156
|
-
const options = JobClass.options || {};
|
|
148
|
+
const { instance, options, timeout } = await this.#initJob(job, queue);
|
|
157
149
|
try {
|
|
158
|
-
await instance
|
|
159
|
-
await job.
|
|
150
|
+
await this.#executeWithTimeout(instance, timeout);
|
|
151
|
+
await this.#adapter.completeJob(job.id, queue);
|
|
160
152
|
const duration = (performance.now() - startTime).toFixed(2);
|
|
161
153
|
debug("worker %s: successfully executed job %s in %dms", this.#id, job.id, duration);
|
|
162
154
|
} catch (e) {
|
|
155
|
+
const isTimeout = e instanceof errors.E_JOB_TIMEOUT;
|
|
156
|
+
if (isTimeout && options.failOnTimeout) {
|
|
157
|
+
debug("worker %s: job %s timed out and failOnTimeout is set", this.#id, job.id);
|
|
158
|
+
await this.#adapter.failJob(job.id, queue, e);
|
|
159
|
+
await instance.failed?.(e);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
163
162
|
const mergedConfig = QueueManager.getMergedRetryConfig(queue, options.retry);
|
|
164
163
|
if (typeof mergedConfig.maxRetries === "undefined" || mergedConfig.maxRetries <= 0) {
|
|
165
164
|
debug("worker %s: job %s has no retries configured, marking as failed", this.#id, job.id);
|
|
166
|
-
await
|
|
165
|
+
await this.#adapter.failJob(job.id, queue, e);
|
|
166
|
+
await instance.failed?.(e);
|
|
167
167
|
return;
|
|
168
168
|
}
|
|
169
169
|
if (job.attempts >= mergedConfig.maxRetries) {
|
|
@@ -173,76 +173,65 @@ var Worker = class {
|
|
|
173
173
|
job.id,
|
|
174
174
|
mergedConfig.maxRetries
|
|
175
175
|
);
|
|
176
|
+
await this.#adapter.failJob(job.id, queue, e);
|
|
176
177
|
const exception = new errors.E_JOB_MAX_ATTEMPTS_REACHED([job.name]);
|
|
177
|
-
await instance.failed(exception);
|
|
178
|
+
await instance.failed?.(exception);
|
|
178
179
|
return;
|
|
179
180
|
}
|
|
180
181
|
if (mergedConfig.backoff) {
|
|
181
182
|
const strategy = mergedConfig.backoff();
|
|
182
183
|
const nextRetryAt = strategy.getNextRetryAt(job.attempts + 1);
|
|
183
184
|
debug("worker %s: job %s will retry at %s", this.#id, job.id, nextRetryAt.toISOString());
|
|
184
|
-
await this.#
|
|
185
|
+
await this.#adapter.retryJob(job.id, queue, nextRetryAt);
|
|
185
186
|
return;
|
|
186
187
|
}
|
|
187
|
-
await this.#
|
|
188
|
+
await this.#adapter.retryJob(job.id, queue);
|
|
188
189
|
}
|
|
189
190
|
}
|
|
190
|
-
async #
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
debug("worker %s: failed to acquire lease for job %s", this.#id, job.id);
|
|
202
|
-
await this.#adapter.pushOn(queue, job);
|
|
203
|
-
continue;
|
|
204
|
-
}
|
|
205
|
-
debug("worker %s: acquired lease for job %s", this.#id, job.id);
|
|
206
|
-
jobs.push({
|
|
207
|
-
...job,
|
|
208
|
-
_lease: {
|
|
209
|
-
commit: () => this.#commitJob(job.id),
|
|
210
|
-
rollback: () => this.#rollbackJob(job, queue)
|
|
211
|
-
}
|
|
212
|
-
});
|
|
213
|
-
} catch (error) {
|
|
214
|
-
console.log(error);
|
|
215
|
-
throw error;
|
|
216
|
-
}
|
|
191
|
+
async #initJob(job, queue) {
|
|
192
|
+
try {
|
|
193
|
+
const JobClass = Locator.getOrThrow(job.name);
|
|
194
|
+
const instance = new JobClass(job.payload);
|
|
195
|
+
const options = JobClass.options || {};
|
|
196
|
+
const timeout = this.#getJobTimeout(options);
|
|
197
|
+
return { instance, options, timeout };
|
|
198
|
+
} catch (error) {
|
|
199
|
+
debug("worker %s: failed to initialize job %s (%s)", this.#id, job.id, job.name);
|
|
200
|
+
await this.#adapter.failJob(job.id, queue, error);
|
|
201
|
+
throw error;
|
|
217
202
|
}
|
|
218
|
-
return jobs;
|
|
219
203
|
}
|
|
220
|
-
#
|
|
221
|
-
|
|
222
|
-
|
|
204
|
+
#getJobTimeout(options) {
|
|
205
|
+
if (options.timeout !== void 0) {
|
|
206
|
+
return parse(options.timeout);
|
|
207
|
+
}
|
|
208
|
+
if (this.#config.worker?.timeout !== void 0) {
|
|
209
|
+
return parse(this.#config.worker.timeout);
|
|
210
|
+
}
|
|
211
|
+
return void 0;
|
|
223
212
|
}
|
|
224
|
-
async #
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
213
|
+
async #executeWithTimeout(instance, timeout) {
|
|
214
|
+
if (!timeout) {
|
|
215
|
+
return instance.execute();
|
|
216
|
+
}
|
|
217
|
+
const signal = AbortSignal.timeout(timeout);
|
|
218
|
+
const abortPromise = new Promise((_, reject) => {
|
|
219
|
+
signal.addEventListener("abort", () => {
|
|
220
|
+
reject(new errors.E_JOB_TIMEOUT([instance.constructor.name, timeout]));
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
await Promise.race([instance.execute(signal), abortPromise]);
|
|
231
224
|
}
|
|
232
|
-
async #
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
const delay = nextRetryAt.getTime() - Date.now();
|
|
241
|
-
if (delay > 0) {
|
|
242
|
-
await this.#adapter.pushLaterOn(queue, updatedJob, delay);
|
|
243
|
-
} else {
|
|
244
|
-
await this.#adapter.pushOn(queue, updatedJob);
|
|
225
|
+
async #acquireNextJob(queues) {
|
|
226
|
+
for (const queue of queues) {
|
|
227
|
+
const job = await this.#adapter.popFrom(queue);
|
|
228
|
+
if (!job) {
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
debug("worker %s: acquired job %s", this.#id, job.id);
|
|
232
|
+
return { job, queue };
|
|
245
233
|
}
|
|
234
|
+
return null;
|
|
246
235
|
}
|
|
247
236
|
async #setupGracefulShutdown() {
|
|
248
237
|
const shutdown = async () => {
|
|
@@ -262,11 +251,13 @@ import { Locator as Locator2 } from "#src/locator";
|
|
|
262
251
|
var QueueManagerSingleton = class {
|
|
263
252
|
#defaultAdapter;
|
|
264
253
|
#adapters = {};
|
|
254
|
+
#adapterInstances = /* @__PURE__ */ new Map();
|
|
265
255
|
#globalRetryConfig;
|
|
266
256
|
#queueConfigs = /* @__PURE__ */ new Map();
|
|
267
257
|
async init(config) {
|
|
268
258
|
debug2("initializing queue manager with config: %O", config);
|
|
269
259
|
this.#validateConfig(config);
|
|
260
|
+
this.#adapterInstances.clear();
|
|
270
261
|
this.#defaultAdapter = config.default;
|
|
271
262
|
this.#adapters = config.adapters;
|
|
272
263
|
this.#globalRetryConfig = config.retry;
|
|
@@ -282,13 +273,19 @@ var QueueManagerSingleton = class {
|
|
|
282
273
|
if (!adapter) {
|
|
283
274
|
adapter = this.#defaultAdapter;
|
|
284
275
|
}
|
|
285
|
-
const
|
|
286
|
-
if (
|
|
276
|
+
const cached = this.#adapterInstances.get(adapter);
|
|
277
|
+
if (cached) {
|
|
278
|
+
return cached;
|
|
279
|
+
}
|
|
280
|
+
const adapterFactory = this.#adapters[adapter];
|
|
281
|
+
if (!adapterFactory) {
|
|
287
282
|
throw new errors2.E_CONFIGURATION_ERROR([`Adapter "${adapter}" is not registered`]);
|
|
288
283
|
}
|
|
289
284
|
debug2('using adapter "%s"', adapter);
|
|
290
285
|
try {
|
|
291
|
-
|
|
286
|
+
const instance = adapterFactory();
|
|
287
|
+
this.#adapterInstances.set(adapter, instance);
|
|
288
|
+
return instance;
|
|
292
289
|
} catch (error) {
|
|
293
290
|
throw new Error();
|
|
294
291
|
}
|
|
@@ -325,10 +322,11 @@ var QueueManagerSingleton = class {
|
|
|
325
322
|
}
|
|
326
323
|
}
|
|
327
324
|
async destroy() {
|
|
328
|
-
for (const
|
|
329
|
-
|
|
325
|
+
for (const [name, adapter] of this.#adapterInstances) {
|
|
326
|
+
debug2('destroying adapter "%s"', name);
|
|
330
327
|
await adapter.destroy();
|
|
331
328
|
}
|
|
329
|
+
this.#adapterInstances.clear();
|
|
332
330
|
}
|
|
333
331
|
};
|
|
334
332
|
var QueueManager2 = new QueueManagerSingleton();
|