@elysiumjs/heracles 0.4.3 → 0.5.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/README.md +83 -75
- package/dist/heracles.d.ts +13 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +2 -0
- package/dist/job.d.ts +249 -0
- package/dist/queue.d.ts +247 -0
- package/dist/transport.d.ts +427 -0
- package/dist/transports/redis.transport.d.ts +251 -0
- package/dist/utils.d.ts +34 -0
- package/dist/worker.d.ts +250 -0
- package/dist/workers/base.worker.d.ts +216 -0
- package/dist/workers/redis.worker.d.ts +57 -0
- package/package.json +13 -11
- package/tsconfig.json +34 -33
- package/USAGE.md +0 -527
- package/index.ts +0 -16
- package/src/heracles.ts +0 -68
- package/src/index.ts +0 -28
- package/src/job.ts +0 -413
- package/src/queue.ts +0 -567
- package/src/transport.ts +0 -518
- package/src/transports/redis.transport.ts +0 -1558
- package/src/transports/thread.transport.ts +0 -289
- package/src/utils.ts +0 -61
- package/src/worker.ts +0 -312
- package/src/worker_pool.ts +0 -393
- package/src/workers/base.worker.ts +0 -804
- package/src/workers/redis.worker.ts +0 -523
- package/src/workers/thread.worker.entry.ts +0 -171
- package/src/workers/thread.worker.ts +0 -525
- package/types.d.ts +0 -21
|
@@ -1,523 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2025-present Workbud Technologies Inc. All rights reserved.
|
|
2
|
-
//
|
|
3
|
-
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
-
// you may not use this file except in compliance with the License.
|
|
5
|
-
// You may obtain a copy of the License at
|
|
6
|
-
//
|
|
7
|
-
// http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
-
//
|
|
9
|
-
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
-
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
-
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
-
// See the License for the specific language governing permissions and
|
|
13
|
-
// limitations under the License.
|
|
14
|
-
|
|
15
|
-
import type { JobClass } from '../job';
|
|
16
|
-
import type { JobDispatchOptions, QueueOptions } from '../queue';
|
|
17
|
-
import type { TransportEvent } from '../transport';
|
|
18
|
-
|
|
19
|
-
import { Service } from '@elysiumjs/core';
|
|
20
|
-
|
|
21
|
-
import { Job, JobOverlapBehavior, JobStatus, WithId } from '../job';
|
|
22
|
-
import { TransportMode } from '../transport';
|
|
23
|
-
import { RedisTransport } from '../transports/redis.transport';
|
|
24
|
-
import { getJobMetadata } from '../utils';
|
|
25
|
-
import { BaseWorker } from './base.worker';
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Worker implementation that uses Redis for communication.
|
|
29
|
-
* This worker can run in a separate process from the dispatcher.
|
|
30
|
-
* @author Axel Nana <axel.nana@workbud.com>
|
|
31
|
-
*/
|
|
32
|
-
export class RedisWorker extends BaseWorker {
|
|
33
|
-
/**
|
|
34
|
-
* Transport instance used for Redis communication
|
|
35
|
-
*/
|
|
36
|
-
private transport: RedisTransport;
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Whether the worker is currently running
|
|
40
|
-
*/
|
|
41
|
-
private isRunning: boolean = false;
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Queues registered with this worker
|
|
45
|
-
*/
|
|
46
|
-
private registeredQueues: Set<string> = new Set();
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Creates a new RedisWorker instance
|
|
50
|
-
* @param connectionName Name of the Redis connection to use
|
|
51
|
-
* @param options Additional options for the Redis transport
|
|
52
|
-
*/
|
|
53
|
-
constructor(connectionName: string = 'default', options: Record<string, any> = {}) {
|
|
54
|
-
super(options.id);
|
|
55
|
-
|
|
56
|
-
// Create transport in consumer mode
|
|
57
|
-
this.transport = new RedisTransport(TransportMode.CONSUMER, {
|
|
58
|
-
connection: connectionName,
|
|
59
|
-
consumerName: `worker-${this.id}`,
|
|
60
|
-
...options
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
// Set up message handling
|
|
64
|
-
this.transport.onMessage(this.handleTransportMessage.bind(this));
|
|
65
|
-
|
|
66
|
-
this.debug(`RedisWorker ${this.id} created with connection: ${connectionName}`);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Start the worker and begin processing jobs
|
|
71
|
-
*/
|
|
72
|
-
public async start(): Promise<void> {
|
|
73
|
-
// Call parent implementation first
|
|
74
|
-
await super.start();
|
|
75
|
-
|
|
76
|
-
if (this.isRunning) {
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
this.isRunning = true;
|
|
81
|
-
|
|
82
|
-
// Store registered queues
|
|
83
|
-
for (const queueName of this.queues.keys()) {
|
|
84
|
-
this.registeredQueues.add(queueName);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// Start the transport
|
|
88
|
-
await this.transport.start();
|
|
89
|
-
|
|
90
|
-
// Register worker with queues
|
|
91
|
-
await this.transport.registerWorker(this.id, Array.from(this.registeredQueues));
|
|
92
|
-
|
|
93
|
-
this.info(
|
|
94
|
-
`Redis worker ${this.id} started for queues: ${Array.from(this.registeredQueues).join(', ')}`
|
|
95
|
-
);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Stop the worker
|
|
100
|
-
* @param force If true, cancel all jobs in progress
|
|
101
|
-
*/
|
|
102
|
-
public async stop(force: boolean = false): Promise<void> {
|
|
103
|
-
this.isRunning = false;
|
|
104
|
-
|
|
105
|
-
// Call parent implementation
|
|
106
|
-
await super.stop(force);
|
|
107
|
-
|
|
108
|
-
// Unregister worker
|
|
109
|
-
await this.transport.unregisterWorker(this.id);
|
|
110
|
-
|
|
111
|
-
// Stop the transport
|
|
112
|
-
await this.transport.stop();
|
|
113
|
-
|
|
114
|
-
this.info(`Redis worker ${this.id} stopped`);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Process the next job in a queue, respecting overlap settings
|
|
119
|
-
* @param queueName Name of the queue
|
|
120
|
-
*/
|
|
121
|
-
protected async processNextJob(queueName: string): Promise<void> {
|
|
122
|
-
const queue = this.queues.get(queueName);
|
|
123
|
-
if (!queue || queue.paused || queue.processing >= queue.options.concurrency!) {
|
|
124
|
-
return;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// Find the next eligible job based on overlap behavior
|
|
128
|
-
let eligibleJobIndex = -1;
|
|
129
|
-
|
|
130
|
-
const now = new Date();
|
|
131
|
-
|
|
132
|
-
for (let i = 0; i < queue.jobs.length; i++) {
|
|
133
|
-
const queuedJob = queue.jobs[i];
|
|
134
|
-
|
|
135
|
-
// Skip jobs scheduled for the future
|
|
136
|
-
if (queuedJob.scheduledFor && queuedJob.scheduledFor > now) {
|
|
137
|
-
continue;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// Remove cancelled jobs
|
|
141
|
-
if (queuedJob.job.status === JobStatus.CANCELLED) {
|
|
142
|
-
eligibleJobIndex = i;
|
|
143
|
-
break;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// Check job overlap constraints
|
|
147
|
-
const jobOptions = queuedJob.job.constructor as JobClass<Job>;
|
|
148
|
-
const metadata = getJobMetadata(jobOptions);
|
|
149
|
-
|
|
150
|
-
// If this job has NO_OVERLAP behavior, check if another job with the same ID is running
|
|
151
|
-
if (metadata?.overlapBehavior === JobOverlapBehavior.NO_OVERLAP) {
|
|
152
|
-
const isLocked = await this.transport.isJobLocked(queuedJob.job.id, queueName);
|
|
153
|
-
|
|
154
|
-
if (isLocked) {
|
|
155
|
-
// Skip this job for now, as another job with the same ID is running
|
|
156
|
-
continue;
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// This job is eligible for processing
|
|
161
|
-
eligibleJobIndex = i;
|
|
162
|
-
break;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// If no eligible job was found, return
|
|
166
|
-
if (eligibleJobIndex === -1) {
|
|
167
|
-
return;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// Get the eligible job and remove it from the queue
|
|
171
|
-
const queuedJob = queue.jobs.splice(eligibleJobIndex, 1)[0];
|
|
172
|
-
|
|
173
|
-
if (!queuedJob) {
|
|
174
|
-
return;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
// Skip cancelled jobs
|
|
178
|
-
if (queuedJob.job.status === JobStatus.CANCELLED) {
|
|
179
|
-
this.info(`Skipping cancelled job ${queuedJob.job.id}`);
|
|
180
|
-
|
|
181
|
-
await this.sendJobStatusUpdate(
|
|
182
|
-
queuedJob.job.id,
|
|
183
|
-
queuedJob.job.dispatchId,
|
|
184
|
-
queueName,
|
|
185
|
-
JobStatus.CANCELLED
|
|
186
|
-
);
|
|
187
|
-
|
|
188
|
-
return this.processQueueJobs(queueName);
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
// If job has NO_OVERLAP behavior, acquire a lock
|
|
192
|
-
const jobOptions = queuedJob.job.constructor as JobClass<Job>;
|
|
193
|
-
const metadata = getJobMetadata(jobOptions);
|
|
194
|
-
|
|
195
|
-
if (metadata?.overlapBehavior === JobOverlapBehavior.NO_OVERLAP) {
|
|
196
|
-
const transport = this.transport as RedisTransport;
|
|
197
|
-
const lockAcquired = await transport.acquireJobLock(queuedJob.job.id, queueName);
|
|
198
|
-
|
|
199
|
-
if (!lockAcquired) {
|
|
200
|
-
// Another worker acquired the lock first, put the job back in the queue
|
|
201
|
-
queue.jobs.push(queuedJob);
|
|
202
|
-
return;
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
// Track job as active
|
|
207
|
-
queue.activeJobs.set(queuedJob.job.id, queuedJob);
|
|
208
|
-
queue.processing++;
|
|
209
|
-
|
|
210
|
-
// Update job status to running
|
|
211
|
-
await this.sendJobStatusUpdate(
|
|
212
|
-
queuedJob.job.id,
|
|
213
|
-
queuedJob.job.dispatchId,
|
|
214
|
-
queueName,
|
|
215
|
-
JobStatus.RUNNING,
|
|
216
|
-
undefined
|
|
217
|
-
);
|
|
218
|
-
|
|
219
|
-
try {
|
|
220
|
-
this.debug(
|
|
221
|
-
`Processing job ${queuedJob.job.id} (dispatch: ${queuedJob.job.dispatchId}) from queue '${queueName}'`
|
|
222
|
-
);
|
|
223
|
-
|
|
224
|
-
await queuedJob.job.run();
|
|
225
|
-
|
|
226
|
-
// Handle job completion based on status
|
|
227
|
-
if (queuedJob.job.status === JobStatus.FAILED) {
|
|
228
|
-
// Get max retries
|
|
229
|
-
const maxRetries =
|
|
230
|
-
queuedJob.maxRetries !== undefined ? queuedJob.maxRetries : queue.options.maxRetries!;
|
|
231
|
-
|
|
232
|
-
if (queuedJob.retries < maxRetries) {
|
|
233
|
-
// Increment retry count
|
|
234
|
-
queuedJob.retries++;
|
|
235
|
-
queuedJob.job.incrementRetries();
|
|
236
|
-
this.warning(
|
|
237
|
-
`Job ${queuedJob.job.id} failed, retrying (${queuedJob.retries}/${maxRetries})`
|
|
238
|
-
);
|
|
239
|
-
|
|
240
|
-
// Get retry delay
|
|
241
|
-
const retryDelay =
|
|
242
|
-
queuedJob.retryDelay !== undefined ? queuedJob.retryDelay : queue.options.retryDelay!;
|
|
243
|
-
|
|
244
|
-
// Update job status to scheduled for retry
|
|
245
|
-
await this.sendJobStatusUpdate(
|
|
246
|
-
queuedJob.job.id,
|
|
247
|
-
queuedJob.job.dispatchId,
|
|
248
|
-
queueName,
|
|
249
|
-
JobStatus.SCHEDULED_FOR_RETRY,
|
|
250
|
-
queuedJob.job.lastError?.message
|
|
251
|
-
);
|
|
252
|
-
|
|
253
|
-
// Schedule retry
|
|
254
|
-
setTimeout(() => {
|
|
255
|
-
if (queuedJob.job.status !== JobStatus.CANCELLED) {
|
|
256
|
-
queue.jobs.push(queuedJob);
|
|
257
|
-
this.processQueueJobs(queueName);
|
|
258
|
-
} else {
|
|
259
|
-
this.info(`Skipping cancelled job ${queuedJob.job.id} during retry delay`);
|
|
260
|
-
this.sendJobStatusUpdate(
|
|
261
|
-
queuedJob.job.id,
|
|
262
|
-
queuedJob.job.dispatchId,
|
|
263
|
-
queueName,
|
|
264
|
-
JobStatus.CANCELLED
|
|
265
|
-
);
|
|
266
|
-
}
|
|
267
|
-
}, retryDelay);
|
|
268
|
-
} else {
|
|
269
|
-
// Job failed after all retries
|
|
270
|
-
this.error(
|
|
271
|
-
`Job ${queuedJob.job.id} failed after ${queuedJob.retries} retries (max: ${maxRetries}): ${queuedJob.job.lastError?.message}`
|
|
272
|
-
);
|
|
273
|
-
|
|
274
|
-
// Update job status to failed
|
|
275
|
-
await this.sendJobStatusUpdate(
|
|
276
|
-
queuedJob.job.id,
|
|
277
|
-
queuedJob.job.dispatchId,
|
|
278
|
-
queueName,
|
|
279
|
-
JobStatus.FAILED,
|
|
280
|
-
queuedJob.job.lastError?.message
|
|
281
|
-
);
|
|
282
|
-
|
|
283
|
-
// Pause queue if configured to do so
|
|
284
|
-
if (queue.options.pauseOnError) {
|
|
285
|
-
this.pause(queueName);
|
|
286
|
-
this.warning(`Queue '${queueName}' paused due to job failure`);
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
} else if (queuedJob.job.status === JobStatus.COMPLETED) {
|
|
290
|
-
// Job completed successfully
|
|
291
|
-
this.success(`Job ${queuedJob.job.id} completed successfully`);
|
|
292
|
-
|
|
293
|
-
// Update job status to completed
|
|
294
|
-
await this.sendJobStatusUpdate(
|
|
295
|
-
queuedJob.job.id,
|
|
296
|
-
queuedJob.job.dispatchId,
|
|
297
|
-
queueName,
|
|
298
|
-
JobStatus.COMPLETED
|
|
299
|
-
);
|
|
300
|
-
} else if ((queuedJob.job.status as JobStatus) === JobStatus.CANCELLED) {
|
|
301
|
-
// Job was cancelled while running
|
|
302
|
-
this.info(`Job ${queuedJob.job.id} cancelled while running`);
|
|
303
|
-
|
|
304
|
-
// Update job status to cancelled
|
|
305
|
-
await this.sendJobStatusUpdate(
|
|
306
|
-
queuedJob.job.id,
|
|
307
|
-
queuedJob.job.dispatchId,
|
|
308
|
-
queueName,
|
|
309
|
-
JobStatus.CANCELLED
|
|
310
|
-
);
|
|
311
|
-
}
|
|
312
|
-
} catch (error) {
|
|
313
|
-
// Handle unexpected errors during job execution
|
|
314
|
-
this.error(`Unexpected error processing job ${queuedJob.job.id}: ${error}`);
|
|
315
|
-
|
|
316
|
-
// Update job status to failed
|
|
317
|
-
await this.sendJobStatusUpdate(
|
|
318
|
-
queuedJob.job.id,
|
|
319
|
-
queuedJob.job.dispatchId,
|
|
320
|
-
queueName,
|
|
321
|
-
JobStatus.FAILED,
|
|
322
|
-
error instanceof Error ? error.message : String(error)
|
|
323
|
-
);
|
|
324
|
-
|
|
325
|
-
// Pause queue if configured to do so
|
|
326
|
-
if (queue.options.pauseOnError) {
|
|
327
|
-
this.pause(queueName);
|
|
328
|
-
this.warning(`Queue '${queueName}' paused due to unexpected error`);
|
|
329
|
-
}
|
|
330
|
-
} finally {
|
|
331
|
-
// Release lock if job has NO_OVERLAP behavior
|
|
332
|
-
if (metadata?.overlapBehavior === JobOverlapBehavior.NO_OVERLAP) {
|
|
333
|
-
// If there's an overlap delay, keep the lock for that duration
|
|
334
|
-
if (metadata.overlapDelay && metadata.overlapDelay > 0) {
|
|
335
|
-
setTimeout(() => {
|
|
336
|
-
this.transport.releaseJobLock(queuedJob.job.id, queueName);
|
|
337
|
-
}, metadata.overlapDelay);
|
|
338
|
-
} else {
|
|
339
|
-
// Release lock immediately
|
|
340
|
-
await this.transport.releaseJobLock(queuedJob.job.id, queueName);
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
// Remove job from active jobs
|
|
345
|
-
queue.activeJobs.delete(queuedJob.job.id);
|
|
346
|
-
queue.processing--;
|
|
347
|
-
|
|
348
|
-
// Process next job in queue
|
|
349
|
-
this.processQueueJobs(queueName);
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
/**
|
|
354
|
-
* Create a new queue with the specified options
|
|
355
|
-
* @param options Queue options
|
|
356
|
-
*/
|
|
357
|
-
public async createQueue(options: QueueOptions): Promise<BaseWorker> {
|
|
358
|
-
const result = await super.createQueue(options);
|
|
359
|
-
|
|
360
|
-
// Register the new queue with the transport if worker is running
|
|
361
|
-
if (this.isRunning && !this.registeredQueues.has(options.name)) {
|
|
362
|
-
this.registeredQueues.add(options.name);
|
|
363
|
-
await this.transport.registerWorker(this.id, Array.from(this.registeredQueues));
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
return result as BaseWorker;
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
/**
|
|
370
|
-
* Handle a message from the transport
|
|
371
|
-
* @param message The message to handle
|
|
372
|
-
*/
|
|
373
|
-
private async handleTransportMessage(message: TransportEvent): Promise<void> {
|
|
374
|
-
try {
|
|
375
|
-
switch (message.type) {
|
|
376
|
-
case 'job:process': {
|
|
377
|
-
// Add job to queue
|
|
378
|
-
const {
|
|
379
|
-
job: jobName,
|
|
380
|
-
args = [],
|
|
381
|
-
queue = 'default',
|
|
382
|
-
options = {},
|
|
383
|
-
jobId,
|
|
384
|
-
dispatchId
|
|
385
|
-
} = message;
|
|
386
|
-
await this.handleProcessJob(jobId, dispatchId, jobName, args, queue, options);
|
|
387
|
-
break;
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
case 'job:cancel': {
|
|
391
|
-
// Cancel job
|
|
392
|
-
const { jobId, dispatchId, queue = 'default' } = message;
|
|
393
|
-
const cancelled = await this.cancelJob(jobId, queue);
|
|
394
|
-
|
|
395
|
-
// Send response
|
|
396
|
-
if (cancelled) {
|
|
397
|
-
await this.sendJobStatusUpdate(jobId, dispatchId, queue, JobStatus.CANCELLED);
|
|
398
|
-
}
|
|
399
|
-
break;
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
case 'job:cancelAll': {
|
|
403
|
-
// Cancel all jobs in queue
|
|
404
|
-
const { queue = 'default' } = message;
|
|
405
|
-
await this.cancelAllJobs(queue);
|
|
406
|
-
break;
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
} catch (error) {
|
|
410
|
-
this.error(`Error handling transport message: ${error}`);
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
/**
|
|
415
|
-
* Handle a job:process message
|
|
416
|
-
* @param jobName Job class name
|
|
417
|
-
* @param args Job constructor arguments
|
|
418
|
-
* @param queueName Queue name
|
|
419
|
-
* @param options Job options
|
|
420
|
-
*/
|
|
421
|
-
private async handleProcessJob(
|
|
422
|
-
jobId: string,
|
|
423
|
-
dispatchId: string,
|
|
424
|
-
jobName: string,
|
|
425
|
-
args: any[],
|
|
426
|
-
queueName: string,
|
|
427
|
-
messageOptions: any
|
|
428
|
-
): Promise<void> {
|
|
429
|
-
try {
|
|
430
|
-
// Get job class from Service container
|
|
431
|
-
const JobClass = Service.get(jobName) as JobClass<Job>;
|
|
432
|
-
|
|
433
|
-
if (!JobClass) {
|
|
434
|
-
throw new Error(`Job class not found: ${jobName}`);
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
// Create job instance
|
|
438
|
-
const job = new (WithId(JobClass, jobId, dispatchId))(...(args || []));
|
|
439
|
-
job.queueName = queueName;
|
|
440
|
-
|
|
441
|
-
// Parse job options
|
|
442
|
-
const jobOptions: JobDispatchOptions = {};
|
|
443
|
-
|
|
444
|
-
if (messageOptions.scheduledFor) {
|
|
445
|
-
jobOptions.scheduledFor =
|
|
446
|
-
typeof messageOptions.scheduledFor === 'string'
|
|
447
|
-
? new Date(messageOptions.scheduledFor)
|
|
448
|
-
: messageOptions.scheduledFor;
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
if (messageOptions.priority !== undefined) {
|
|
452
|
-
jobOptions.priority = Number(messageOptions.priority);
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
if (messageOptions.maxRetries !== undefined) {
|
|
456
|
-
jobOptions.maxRetries = Number(messageOptions.maxRetries);
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
if (messageOptions.retryDelay !== undefined) {
|
|
460
|
-
jobOptions.retryDelay = Number(messageOptions.retryDelay);
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
// Check if we should schedule the job for later
|
|
464
|
-
if (jobOptions.scheduledFor && jobOptions.scheduledFor > new Date()) {
|
|
465
|
-
// For scheduled jobs, set status to SCHEDULED_FOR_RETRY
|
|
466
|
-
await this.sendJobStatusUpdate(
|
|
467
|
-
job.id,
|
|
468
|
-
job.dispatchId,
|
|
469
|
-
queueName,
|
|
470
|
-
JobStatus.SCHEDULED_FOR_RETRY
|
|
471
|
-
);
|
|
472
|
-
this.debug(
|
|
473
|
-
`Scheduled job ${job.id} for future execution at ${jobOptions.scheduledFor.toISOString()}`
|
|
474
|
-
);
|
|
475
|
-
} else {
|
|
476
|
-
// For immediate jobs, set status to PENDING
|
|
477
|
-
await this.sendJobStatusUpdate(job.id, job.dispatchId, queueName, JobStatus.PENDING);
|
|
478
|
-
this.debug(`Added job ${job.id} to queue ${queueName} with initial status: pending`);
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
// Add job to queue for processing
|
|
482
|
-
await this.addJob(job, queueName, jobOptions);
|
|
483
|
-
} catch (error) {
|
|
484
|
-
this.error(`Error processing job ${jobName}: ${error}`);
|
|
485
|
-
throw error;
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
/**
|
|
490
|
-
* Send job status update
|
|
491
|
-
* @param jobId Job ID
|
|
492
|
-
* @param queue Queue name
|
|
493
|
-
* @param status Job status
|
|
494
|
-
* @param error Optional error message
|
|
495
|
-
*/
|
|
496
|
-
private async sendJobStatusUpdate(
|
|
497
|
-
jobId: string,
|
|
498
|
-
dispatchId: string,
|
|
499
|
-
queue: string,
|
|
500
|
-
status: JobStatus,
|
|
501
|
-
error?: string
|
|
502
|
-
): Promise<void> {
|
|
503
|
-
try {
|
|
504
|
-
// Direct call to updateJobStatus (required by Transport interface)
|
|
505
|
-
await this.transport.updateJobStatus(jobId, dispatchId, queue, {
|
|
506
|
-
dispatchId,
|
|
507
|
-
status,
|
|
508
|
-
error,
|
|
509
|
-
retries: 0,
|
|
510
|
-
startedAt: status === JobStatus.RUNNING ? new Date().toISOString() : undefined,
|
|
511
|
-
completedAt:
|
|
512
|
-
status === JobStatus.COMPLETED ||
|
|
513
|
-
status === JobStatus.FAILED ||
|
|
514
|
-
status === JobStatus.CANCELLED
|
|
515
|
-
? new Date().toISOString()
|
|
516
|
-
: undefined,
|
|
517
|
-
updatedAt: Date.now().toString()
|
|
518
|
-
});
|
|
519
|
-
} catch (error) {
|
|
520
|
-
this.error(`Error sending job status update: ${error}`);
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
}
|
|
@@ -1,171 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2025-present Workbud Technologies Inc. All rights reserved.
|
|
2
|
-
//
|
|
3
|
-
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
-
// you may not use this file except in compliance with the License.
|
|
5
|
-
// You may obtain a copy of the License at
|
|
6
|
-
//
|
|
7
|
-
// http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
-
//
|
|
9
|
-
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
-
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
-
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
-
// See the License for the specific language governing permissions and
|
|
13
|
-
// limitations under the License.
|
|
14
|
-
|
|
15
|
-
import 'reflect-metadata';
|
|
16
|
-
|
|
17
|
-
import type { Class } from 'type-fest';
|
|
18
|
-
import type { Job } from '../job';
|
|
19
|
-
import type { TransportEvent } from '../transport';
|
|
20
|
-
|
|
21
|
-
import { Service } from '@elysiumjs/core';
|
|
22
|
-
|
|
23
|
-
import { WorkerStatus } from '../worker';
|
|
24
|
-
import { ThreadWorker } from './thread.worker';
|
|
25
|
-
|
|
26
|
-
// Define self for TypeScript
|
|
27
|
-
declare const self: Worker;
|
|
28
|
-
|
|
29
|
-
// Ensure we're in a worker context
|
|
30
|
-
if (typeof self === 'undefined') {
|
|
31
|
-
throw new Error('This script must be run in a worker context');
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// Log worker startup
|
|
35
|
-
console.log(`Heracles thread worker starting...`);
|
|
36
|
-
|
|
37
|
-
// Set a global worker reference
|
|
38
|
-
let worker: ThreadWorker | null = null;
|
|
39
|
-
|
|
40
|
-
// For storing pre-imported job classes
|
|
41
|
-
const jobClasses = new Map<string, Class<Job>>();
|
|
42
|
-
|
|
43
|
-
// Handle all messages from the main thread
|
|
44
|
-
self.addEventListener('message', async (event) => {
|
|
45
|
-
try {
|
|
46
|
-
// Process the message based on its type
|
|
47
|
-
if (event.data && typeof event.data === 'object') {
|
|
48
|
-
const message = event.data as TransportEvent;
|
|
49
|
-
|
|
50
|
-
switch (message.type) {
|
|
51
|
-
case 'worker:init': {
|
|
52
|
-
// Initialize worker if not already initialized
|
|
53
|
-
if (!worker) {
|
|
54
|
-
const { queues = ['default'], options = {} } = message as any;
|
|
55
|
-
|
|
56
|
-
console.log(`Initializing worker with queues: ${queues.join(', ')}`);
|
|
57
|
-
|
|
58
|
-
// Initialize worker with provided queues
|
|
59
|
-
worker = new ThreadWorker(queues);
|
|
60
|
-
|
|
61
|
-
// Configure worker with options
|
|
62
|
-
if (options.concurrency) {
|
|
63
|
-
for (const queue of queues) {
|
|
64
|
-
await worker.setConcurrency(queue, options.concurrency);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// Store worker in global scope for debug purposes
|
|
69
|
-
(self as any).__worker = worker;
|
|
70
|
-
|
|
71
|
-
// Start the worker
|
|
72
|
-
await worker.start();
|
|
73
|
-
|
|
74
|
-
// Send ready message back to main thread
|
|
75
|
-
self.postMessage({
|
|
76
|
-
type: 'worker:ready',
|
|
77
|
-
id: worker.id,
|
|
78
|
-
queues: Array.from(worker.getInfo().queues),
|
|
79
|
-
status: worker.status,
|
|
80
|
-
timestamp: Date.now()
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
break;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
case 'job:process': {
|
|
87
|
-
// Process a job
|
|
88
|
-
if (!worker) {
|
|
89
|
-
throw new Error('Worker not initialized');
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// Handle the job processing
|
|
93
|
-
const { job: jobName, args = [], queue = 'default', options } = message;
|
|
94
|
-
await handleMessage(message);
|
|
95
|
-
break;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
default:
|
|
99
|
-
// Other messages will be handled by the ThreadWorker itself
|
|
100
|
-
if (worker) {
|
|
101
|
-
await handleMessage(message);
|
|
102
|
-
} else {
|
|
103
|
-
console.warn(`Received message before worker initialization: ${message.type}`);
|
|
104
|
-
|
|
105
|
-
// Try to handle initialization on the fly
|
|
106
|
-
worker = new ThreadWorker(['default']);
|
|
107
|
-
await worker.start();
|
|
108
|
-
await handleMessage(message);
|
|
109
|
-
}
|
|
110
|
-
break;
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
} catch (error) {
|
|
114
|
-
console.error('Error handling message in worker thread:', error);
|
|
115
|
-
|
|
116
|
-
// Report error to main thread
|
|
117
|
-
self.postMessage({
|
|
118
|
-
type: 'worker:error',
|
|
119
|
-
error: error instanceof Error ? error.message : String(error),
|
|
120
|
-
timestamp: Date.now()
|
|
121
|
-
});
|
|
122
|
-
}
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* Handle a transport message
|
|
127
|
-
*/
|
|
128
|
-
async function handleMessage(message: TransportEvent): Promise<void> {
|
|
129
|
-
if (!worker) {
|
|
130
|
-
throw new Error('Worker not initialized');
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// Forward the message to the worker instance for processing
|
|
134
|
-
await worker.handleMessage(message);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// Send open message to main thread to indicate worker is ready for initialization
|
|
138
|
-
self.postMessage({ type: 'worker:open' });
|
|
139
|
-
|
|
140
|
-
// Handle uncaught errors
|
|
141
|
-
self.addEventListener('error', (event) => {
|
|
142
|
-
console.error('Uncaught error in worker thread:', event.error || event.message);
|
|
143
|
-
|
|
144
|
-
// Report error to main thread
|
|
145
|
-
self.postMessage({
|
|
146
|
-
type: 'worker:error',
|
|
147
|
-
error: event.error?.message || event.message || 'Unknown error',
|
|
148
|
-
timestamp: Date.now()
|
|
149
|
-
});
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
// Handle unhandled promise rejections
|
|
153
|
-
self.addEventListener('unhandledrejection', (event) => {
|
|
154
|
-
console.error('Unhandled rejection in worker thread:', event.reason);
|
|
155
|
-
|
|
156
|
-
// Report error to main thread
|
|
157
|
-
self.postMessage({
|
|
158
|
-
type: 'worker:error',
|
|
159
|
-
error: event.reason?.message || String(event.reason) || 'Unhandled promise rejection',
|
|
160
|
-
timestamp: Date.now()
|
|
161
|
-
});
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
// Initialize custom module loading if needed
|
|
165
|
-
if (typeof Bun !== 'undefined') {
|
|
166
|
-
// Bun-specific initialization
|
|
167
|
-
console.log('Running in Bun environment');
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// Export ThreadWorker and other components for testing/debugging
|
|
171
|
-
export { ThreadWorker, WorkerStatus };
|