@elysiumjs/heracles 0.4.4 → 0.5.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.
@@ -1,393 +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 { Job, JobDispatchOptions } from './job';
16
- import type { WorkerInfo } from './worker';
17
-
18
- import { InteractsWithConsole } from '@elysiumjs/core';
19
-
20
- /**
21
- * Interface for tracking worker selection state per queue.
22
- */
23
- interface QueueWorkerState {
24
- /**
25
- * All workers assigned to this queue
26
- */
27
- workers: WorkerInfo[];
28
-
29
- /**
30
- * Index of the last selected worker for round-robin
31
- */
32
- lastWorkerIndex: number;
33
- }
34
-
35
- /**
36
- * WorkerPool manages a pool of workers and distributes jobs among them
37
- * using a round-robin algorithm.
38
- */
39
- export class WorkerPool extends InteractsWithConsole {
40
- /**
41
- * Singleton instance of the worker pool
42
- */
43
- private static _instance: WorkerPool;
44
-
45
- /**
46
- * All workers in the pool, indexed by worker ID
47
- */
48
- private workers: Map<string, WorkerInfo> = new Map();
49
-
50
- /**
51
- * Queue-to-workers mapping for tracking which workers handle each queue
52
- */
53
- private queueWorkers: Map<string, QueueWorkerState> = new Map();
54
-
55
- /**
56
- * Last used worker per queue for true round-robin
57
- */
58
- private lastUsedWorker: Map<string, string> = new Map();
59
-
60
- /**
61
- * Get the singleton instance of the worker pool
62
- */
63
- public static get instance(): WorkerPool {
64
- if (!WorkerPool._instance) {
65
- WorkerPool._instance = new WorkerPool();
66
- }
67
- return WorkerPool._instance;
68
- }
69
-
70
- /**
71
- * Private constructor to enforce singleton pattern
72
- */
73
- private constructor() {
74
- super();
75
- }
76
-
77
- /**
78
- * Initialize the worker pool
79
- */
80
- public async init(): Promise<void> {
81
- this.info('WorkerPool initialized');
82
- }
83
-
84
- /**
85
- * Add a worker to the pool and associate it with queues
86
- *
87
- * @param worker The worker information to add
88
- * @param queues The queues this worker will handle
89
- */
90
- public async addWorker(worker: WorkerInfo, queues: string[] = []): Promise<void> {
91
- if (this.workers.has(worker.id)) {
92
- this.warning(`Worker ${worker.id} is already registered in the pool`);
93
- return;
94
- }
95
-
96
- // Store the worker
97
- this.workers.set(worker.id, worker);
98
-
99
- // Associate worker with each queue
100
- for (const queueName of queues) {
101
- // Get or create queue state
102
- const queueState = this.getOrCreateQueueState(queueName);
103
-
104
- // Add worker to queue if not already present
105
- if (!queueState.workers.some((w) => w.id === worker.id)) {
106
- queueState.workers.push(worker);
107
- this.debug(`Worker ${worker.id} assigned to queue '${queueName}'`);
108
- }
109
- }
110
-
111
- this.info(`Worker ${worker.id} added to pool with ${queues.length} queues`);
112
- }
113
-
114
- /**
115
- * Remove a worker from the pool
116
- *
117
- * @param workerId The ID of the worker to remove
118
- */
119
- public async removeWorker(workerId: string): Promise<void> {
120
- const worker = this.workers.get(workerId);
121
- if (!worker) {
122
- this.warning(`Attempted to remove non-existent worker ${workerId}`);
123
- return;
124
- }
125
-
126
- // Remove worker from all queues
127
- for (const [queueName, queueState] of this.queueWorkers.entries()) {
128
- const workerIndex = queueState.workers.findIndex((w) => w.id === workerId);
129
- if (workerIndex >= 0) {
130
- queueState.workers.splice(workerIndex, 1);
131
-
132
- // Adjust lastWorkerIndex if needed
133
- if (queueState.lastWorkerIndex >= queueState.workers.length) {
134
- queueState.lastWorkerIndex = queueState.workers.length > 0 ? 0 : -1;
135
- }
136
-
137
- // Also adjust lastUsedWorker if it was this worker
138
- if (this.lastUsedWorker.get(queueName) === workerId) {
139
- this.lastUsedWorker.delete(queueName);
140
- }
141
-
142
- this.debug(`Worker ${workerId} removed from queue '${queueName}'`);
143
- }
144
- }
145
-
146
- // Remove worker from the pool
147
- this.workers.delete(workerId);
148
- this.info(`Worker ${workerId} removed from pool`);
149
- }
150
-
151
- /**
152
- * Terminate all workers in the pool
153
- */
154
- public async terminate(): Promise<void> {
155
- for (const workerId of this.workers.keys()) {
156
- await this.removeWorker(workerId);
157
- }
158
-
159
- this.info('WorkerPool terminated');
160
- }
161
-
162
- /**
163
- * Get the next available worker for a queue using round-robin
164
- *
165
- * @param queueName The name of the queue to get a worker for
166
- * @returns The selected worker info or null if none available
167
- */
168
- public getNextWorker(queueName: string): WorkerInfo | null {
169
- const queueState = this.queueWorkers.get(queueName);
170
- if (!queueState || queueState.workers.length === 0) {
171
- this.warning(`No workers available for queue '${queueName}'`);
172
- return null;
173
- }
174
-
175
- // True round-robin selection that considers worker load
176
- // First, sort workers by number of active jobs (lightest load first)
177
- const sortedWorkers = [...queueState.workers].sort((a, b) => a.activeJobs - b.activeJobs);
178
-
179
- // If there's a big difference in load (e.g., more than 5 jobs), prefer the least loaded worker
180
- const leastLoaded = sortedWorkers[0];
181
- const mostLoaded = sortedWorkers[sortedWorkers.length - 1];
182
-
183
- if (mostLoaded.activeJobs - leastLoaded.activeJobs > 5) {
184
- this.debug(
185
- `Selected least loaded worker ${leastLoaded.id} for queue '${queueName}' (load: ${leastLoaded.activeJobs}/${mostLoaded.activeJobs})`
186
- );
187
- return leastLoaded;
188
- }
189
-
190
- // Otherwise, use round-robin to ensure fair distribution
191
- // Find the index of the last used worker, or -1 if none was used yet
192
- const lastUsedWorkerId = this.lastUsedWorker.get(queueName);
193
- let lastIndex = -1;
194
-
195
- if (lastUsedWorkerId) {
196
- lastIndex = queueState.workers.findIndex((w) => w.id === lastUsedWorkerId);
197
- }
198
-
199
- // Move to the next worker
200
- const nextIndex = (lastIndex + 1) % queueState.workers.length;
201
- const selectedWorker = queueState.workers[nextIndex];
202
-
203
- // Update the last used worker
204
- this.lastUsedWorker.set(queueName, selectedWorker.id);
205
- queueState.lastWorkerIndex = nextIndex;
206
-
207
- this.debug(
208
- `Selected worker ${selectedWorker.id} for queue '${queueName}' (worker ${nextIndex + 1}/${queueState.workers.length}, active jobs: ${selectedWorker.activeJobs})`
209
- );
210
- return selectedWorker;
211
- }
212
-
213
- /**
214
- * Execute a job using a worker from the pool
215
- *
216
- * @param job The job to execute
217
- * @param queueName The queue the job belongs to
218
- * @param options Optional job dispatch options
219
- * @returns A promise that resolves when the job is dispatched to a worker
220
- */
221
- public async runJob(
222
- job: Job,
223
- queueName: string,
224
- options: JobDispatchOptions = {}
225
- ): Promise<void> {
226
- const worker = this.getNextWorker(queueName);
227
- if (!worker) {
228
- throw new Error(`No workers available for queue '${queueName}'`);
229
- }
230
-
231
- // Assign queue name to job
232
- job.queueName = queueName;
233
-
234
- // Apply job options
235
- const jobOptions = {
236
- scheduledFor: options.scheduledFor,
237
- priority: options.priority || 10,
238
- maxRetries: options.maxRetries,
239
- retryDelay: options.retryDelay
240
- };
241
-
242
- // Dispatch job to worker
243
- try {
244
- this.debug(`Dispatching job ${job.id} to worker ${worker.id} in queue '${queueName}'`);
245
- // Pass job options to worker
246
- if (typeof worker.addJob === 'function') {
247
- const result = await worker.addJob(job, queueName);
248
- if (!result) {
249
- throw new Error(`Worker ${worker.id} failed to accept job ${job.id}`);
250
- }
251
- } else {
252
- throw new Error(`Worker ${worker.id} does not implement addJob method`);
253
- }
254
- } catch (error) {
255
- // If this worker fails, remove it and try another worker
256
- this.error(`Failed to dispatch job ${job.id} to worker ${worker.id}: ${error}`);
257
- await this.removeWorker(worker.id);
258
-
259
- // Retry with another worker
260
- await this.runJob(job, queueName, options);
261
- }
262
- }
263
-
264
- /**
265
- * Cancel a job across all workers
266
- *
267
- * @param jobId The ID of the job to cancel
268
- * @param queueName Optional queue name to limit search
269
- * @returns True if the job was canceled, false otherwise
270
- */
271
- public async cancelJob(jobId: string, queueName?: string): Promise<boolean> {
272
- let canceled = false;
273
-
274
- if (queueName) {
275
- // Try to cancel in specific queue
276
- const queueState = this.queueWorkers.get(queueName);
277
- if (queueState) {
278
- // Try each worker in the queue
279
- for (const worker of queueState.workers) {
280
- try {
281
- if (await worker.cancelJob(jobId, queueName)) {
282
- canceled = true;
283
- this.debug(`Job ${jobId} cancelled on worker ${worker.id}`);
284
- break;
285
- }
286
- } catch (error) {
287
- this.warning(`Error cancelling job ${jobId} on worker ${worker.id}: ${error}`);
288
- }
289
- }
290
- }
291
- } else {
292
- // Try all workers
293
- for (const worker of this.workers.values()) {
294
- try {
295
- if (await worker.cancelJob(jobId)) {
296
- canceled = true;
297
- this.debug(`Job ${jobId} cancelled on worker ${worker.id}`);
298
- break;
299
- }
300
- } catch (error) {
301
- this.warning(`Error cancelling job ${jobId} on worker ${worker.id}: ${error}`);
302
- }
303
- }
304
- }
305
-
306
- return canceled;
307
- }
308
-
309
- /**
310
- * Cancel all jobs across all workers
311
- *
312
- * @param queueName Optional queue name to limit cancellation
313
- * @returns The number of jobs canceled
314
- */
315
- public async cancelAllJobs(queueName?: string): Promise<number> {
316
- let total = 0;
317
-
318
- if (queueName) {
319
- // Cancel in specific queue
320
- const queueState = this.queueWorkers.get(queueName);
321
- if (queueState) {
322
- for (const worker of queueState.workers) {
323
- try {
324
- const count = await worker.cancelAllJobs(queueName);
325
- total += count;
326
- this.debug(`Cancelled ${count} jobs on worker ${worker.id} in queue ${queueName}`);
327
- } catch (error) {
328
- this.warning(`Error cancelling jobs on worker ${worker.id}: ${error}`);
329
- }
330
- }
331
- }
332
- } else {
333
- // Cancel in all workers
334
- for (const worker of this.workers.values()) {
335
- try {
336
- const count = await worker.cancelAllJobs();
337
- total += count;
338
- this.debug(`Cancelled ${count} jobs on worker ${worker.id}`);
339
- } catch (error) {
340
- this.warning(`Error cancelling jobs on worker ${worker.id}: ${error}`);
341
- }
342
- }
343
- }
344
-
345
- return total;
346
- }
347
-
348
- /**
349
- * Get all queues being handled by this pool
350
- *
351
- * @returns An array of queue names
352
- */
353
- public getQueues(): string[] {
354
- return Array.from(this.queueWorkers.keys());
355
- }
356
-
357
- /**
358
- * Get all workers in the pool
359
- *
360
- * @returns An array of worker infos
361
- */
362
- public getWorkers(): WorkerInfo[] {
363
- return Array.from(this.workers.values());
364
- }
365
-
366
- /**
367
- * Get workers for a specific queue
368
- *
369
- * @param queueName The name of the queue
370
- * @returns An array of worker infos for the queue
371
- */
372
- public getWorkersForQueue(queueName: string): WorkerInfo[] {
373
- const queueState = this.queueWorkers.get(queueName);
374
- return queueState ? [...queueState.workers] : [];
375
- }
376
-
377
- /**
378
- * Get or create queue state for tracking workers
379
- *
380
- * @param queueName The name of the queue
381
- * @returns The queue worker state
382
- */
383
- private getOrCreateQueueState(queueName: string): QueueWorkerState {
384
- if (!this.queueWorkers.has(queueName)) {
385
- this.queueWorkers.set(queueName, {
386
- workers: [],
387
- lastWorkerIndex: -1
388
- });
389
- }
390
-
391
- return this.queueWorkers.get(queueName)!;
392
- }
393
- }