@claude-flow/cli 3.0.0-alpha.29 → 3.0.0-alpha.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/commands/daemon.d.ts.map +1 -1
- package/dist/src/commands/daemon.js +33 -9
- package/dist/src/commands/daemon.js.map +1 -1
- package/dist/src/init/settings-generator.d.ts.map +1 -1
- package/dist/src/init/settings-generator.js +9 -6
- package/dist/src/init/settings-generator.js.map +1 -1
- package/dist/src/services/container-worker-pool.d.ts +197 -0
- package/dist/src/services/container-worker-pool.d.ts.map +1 -0
- package/dist/src/services/container-worker-pool.js +581 -0
- package/dist/src/services/container-worker-pool.js.map +1 -0
- package/dist/src/services/headless-worker-executor.d.ts +304 -0
- package/dist/src/services/headless-worker-executor.d.ts.map +1 -0
- package/dist/src/services/headless-worker-executor.js +997 -0
- package/dist/src/services/headless-worker-executor.js.map +1 -0
- package/dist/src/services/index.d.ts +6 -0
- package/dist/src/services/index.d.ts.map +1 -1
- package/dist/src/services/index.js +5 -0
- package/dist/src/services/index.js.map +1 -1
- package/dist/src/services/worker-daemon.d.ts +55 -5
- package/dist/src/services/worker-daemon.d.ts.map +1 -1
- package/dist/src/services/worker-daemon.js +191 -13
- package/dist/src/services/worker-daemon.js.map +1 -1
- package/dist/src/services/worker-queue.d.ts +194 -0
- package/dist/src/services/worker-queue.d.ts.map +1 -0
- package/dist/src/services/worker-queue.js +511 -0
- package/dist/src/services/worker-queue.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Worker Queue Service
|
|
3
|
+
* Redis-based task queue for distributed headless worker execution.
|
|
4
|
+
*
|
|
5
|
+
* ADR-020: Headless Worker Integration Architecture - Phase 4
|
|
6
|
+
* - Priority-based task scheduling
|
|
7
|
+
* - Result persistence and retrieval
|
|
8
|
+
* - Distributed locking for task assignment
|
|
9
|
+
* - Dead letter queue for failed tasks
|
|
10
|
+
* - Metrics and monitoring
|
|
11
|
+
*
|
|
12
|
+
* Key Features:
|
|
13
|
+
* - FIFO with priority levels (critical, high, normal, low)
|
|
14
|
+
* - Automatic retry with exponential backoff
|
|
15
|
+
* - Task timeout detection
|
|
16
|
+
* - Result caching with TTL
|
|
17
|
+
* - Worker heartbeat monitoring
|
|
18
|
+
*/
|
|
19
|
+
import { EventEmitter } from 'events';
|
|
20
|
+
import type { HeadlessWorkerType, HeadlessExecutionResult, WorkerPriority } from './headless-worker-executor.js';
|
|
21
|
+
/**
|
|
22
|
+
* Task status
|
|
23
|
+
*/
|
|
24
|
+
export type TaskStatus = 'pending' | 'processing' | 'completed' | 'failed' | 'timeout' | 'cancelled';
|
|
25
|
+
/**
|
|
26
|
+
* Queue task
|
|
27
|
+
*/
|
|
28
|
+
export interface QueueTask {
|
|
29
|
+
id: string;
|
|
30
|
+
workerType: HeadlessWorkerType;
|
|
31
|
+
priority: WorkerPriority;
|
|
32
|
+
payload: {
|
|
33
|
+
prompt?: string;
|
|
34
|
+
contextPatterns?: string[];
|
|
35
|
+
sandbox?: string;
|
|
36
|
+
model?: string;
|
|
37
|
+
timeoutMs?: number;
|
|
38
|
+
};
|
|
39
|
+
status: TaskStatus;
|
|
40
|
+
createdAt: Date;
|
|
41
|
+
startedAt?: Date;
|
|
42
|
+
completedAt?: Date;
|
|
43
|
+
workerId?: string;
|
|
44
|
+
retryCount: number;
|
|
45
|
+
maxRetries: number;
|
|
46
|
+
result?: HeadlessExecutionResult;
|
|
47
|
+
error?: string;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Queue configuration
|
|
51
|
+
*/
|
|
52
|
+
export interface WorkerQueueConfig {
|
|
53
|
+
/** Redis connection URL */
|
|
54
|
+
redisUrl: string;
|
|
55
|
+
/** Queue name prefix */
|
|
56
|
+
queuePrefix: string;
|
|
57
|
+
/** Default task timeout in ms */
|
|
58
|
+
defaultTimeoutMs: number;
|
|
59
|
+
/** Maximum retries for failed tasks */
|
|
60
|
+
maxRetries: number;
|
|
61
|
+
/** Task result TTL in seconds */
|
|
62
|
+
resultTtlSeconds: number;
|
|
63
|
+
/** Worker heartbeat interval in ms */
|
|
64
|
+
heartbeatIntervalMs: number;
|
|
65
|
+
/** Dead letter queue enabled */
|
|
66
|
+
deadLetterEnabled: boolean;
|
|
67
|
+
/** Visibility timeout in ms (task processing lock) */
|
|
68
|
+
visibilityTimeoutMs: number;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Queue statistics
|
|
72
|
+
*/
|
|
73
|
+
export interface QueueStats {
|
|
74
|
+
pending: number;
|
|
75
|
+
processing: number;
|
|
76
|
+
completed: number;
|
|
77
|
+
failed: number;
|
|
78
|
+
deadLetter: number;
|
|
79
|
+
byPriority: Record<WorkerPriority, number>;
|
|
80
|
+
byWorkerType: Partial<Record<HeadlessWorkerType, number>>;
|
|
81
|
+
averageWaitTimeMs: number;
|
|
82
|
+
averageProcessingTimeMs: number;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Worker registration info
|
|
86
|
+
*/
|
|
87
|
+
export interface WorkerRegistration {
|
|
88
|
+
workerId: string;
|
|
89
|
+
workerTypes: HeadlessWorkerType[];
|
|
90
|
+
maxConcurrent: number;
|
|
91
|
+
currentTasks: number;
|
|
92
|
+
lastHeartbeat: Date;
|
|
93
|
+
registeredAt: Date;
|
|
94
|
+
hostname?: string;
|
|
95
|
+
containerId?: string;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* WorkerQueue - Redis-based task queue for distributed worker execution
|
|
99
|
+
*/
|
|
100
|
+
export declare class WorkerQueue extends EventEmitter {
|
|
101
|
+
private config;
|
|
102
|
+
private store;
|
|
103
|
+
private workerId;
|
|
104
|
+
private heartbeatTimer?;
|
|
105
|
+
private processingTasks;
|
|
106
|
+
private isShuttingDown;
|
|
107
|
+
private maxConcurrent;
|
|
108
|
+
private initialized;
|
|
109
|
+
constructor(config?: Partial<WorkerQueueConfig>);
|
|
110
|
+
/**
|
|
111
|
+
* Initialize the queue (starts cleanup timers)
|
|
112
|
+
*/
|
|
113
|
+
initialize(): Promise<void>;
|
|
114
|
+
/**
|
|
115
|
+
* Enqueue a new task
|
|
116
|
+
*/
|
|
117
|
+
enqueue(workerType: HeadlessWorkerType, payload?: QueueTask['payload'], options?: {
|
|
118
|
+
priority?: WorkerPriority;
|
|
119
|
+
maxRetries?: number;
|
|
120
|
+
timeoutMs?: number;
|
|
121
|
+
}): Promise<string>;
|
|
122
|
+
/**
|
|
123
|
+
* Dequeue a task for processing
|
|
124
|
+
*/
|
|
125
|
+
dequeue(workerTypes: HeadlessWorkerType[]): Promise<QueueTask | null>;
|
|
126
|
+
/**
|
|
127
|
+
* Complete a task with result
|
|
128
|
+
*/
|
|
129
|
+
complete(taskId: string, result: HeadlessExecutionResult): Promise<void>;
|
|
130
|
+
/**
|
|
131
|
+
* Fail a task with error
|
|
132
|
+
*/
|
|
133
|
+
fail(taskId: string, error: string, retryable?: boolean): Promise<void>;
|
|
134
|
+
/**
|
|
135
|
+
* Get task status
|
|
136
|
+
*/
|
|
137
|
+
getTask(taskId: string): Promise<QueueTask | null>;
|
|
138
|
+
/**
|
|
139
|
+
* Get task result
|
|
140
|
+
*/
|
|
141
|
+
getResult(taskId: string): Promise<HeadlessExecutionResult | null>;
|
|
142
|
+
/**
|
|
143
|
+
* Cancel a pending task
|
|
144
|
+
*/
|
|
145
|
+
cancel(taskId: string): Promise<boolean>;
|
|
146
|
+
/**
|
|
147
|
+
* Register this instance as a worker
|
|
148
|
+
*/
|
|
149
|
+
registerWorker(workerTypes: HeadlessWorkerType[], options?: {
|
|
150
|
+
maxConcurrent?: number;
|
|
151
|
+
hostname?: string;
|
|
152
|
+
containerId?: string;
|
|
153
|
+
}): Promise<string>;
|
|
154
|
+
/**
|
|
155
|
+
* Unregister this worker
|
|
156
|
+
*/
|
|
157
|
+
unregisterWorker(): Promise<void>;
|
|
158
|
+
/**
|
|
159
|
+
* Get all registered workers
|
|
160
|
+
*/
|
|
161
|
+
getWorkers(): Promise<WorkerRegistration[]>;
|
|
162
|
+
/**
|
|
163
|
+
* Get queue statistics
|
|
164
|
+
*/
|
|
165
|
+
getStats(): Promise<QueueStats>;
|
|
166
|
+
/**
|
|
167
|
+
* Start processing tasks
|
|
168
|
+
*/
|
|
169
|
+
start(workerTypes: HeadlessWorkerType[], handler: (task: QueueTask) => Promise<HeadlessExecutionResult>, options?: {
|
|
170
|
+
maxConcurrent?: number;
|
|
171
|
+
}): Promise<void>;
|
|
172
|
+
/**
|
|
173
|
+
* Process a single task
|
|
174
|
+
*/
|
|
175
|
+
private processTask;
|
|
176
|
+
/**
|
|
177
|
+
* Shutdown the queue gracefully
|
|
178
|
+
*/
|
|
179
|
+
shutdown(): Promise<void>;
|
|
180
|
+
/**
|
|
181
|
+
* Get queue name for worker type
|
|
182
|
+
*/
|
|
183
|
+
private getQueueName;
|
|
184
|
+
/**
|
|
185
|
+
* Start heartbeat timer
|
|
186
|
+
*/
|
|
187
|
+
private startHeartbeat;
|
|
188
|
+
/**
|
|
189
|
+
* Stop heartbeat timer
|
|
190
|
+
*/
|
|
191
|
+
private stopHeartbeat;
|
|
192
|
+
}
|
|
193
|
+
export default WorkerQueue;
|
|
194
|
+
//# sourceMappingURL=worker-queue.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"worker-queue.d.ts","sourceRoot":"","sources":["../../../src/services/worker-queue.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,511 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Worker Queue Service
|
|
3
|
+
* Redis-based task queue for distributed headless worker execution.
|
|
4
|
+
*
|
|
5
|
+
* ADR-020: Headless Worker Integration Architecture - Phase 4
|
|
6
|
+
* - Priority-based task scheduling
|
|
7
|
+
* - Result persistence and retrieval
|
|
8
|
+
* - Distributed locking for task assignment
|
|
9
|
+
* - Dead letter queue for failed tasks
|
|
10
|
+
* - Metrics and monitoring
|
|
11
|
+
*
|
|
12
|
+
* Key Features:
|
|
13
|
+
* - FIFO with priority levels (critical, high, normal, low)
|
|
14
|
+
* - Automatic retry with exponential backoff
|
|
15
|
+
* - Task timeout detection
|
|
16
|
+
* - Result caching with TTL
|
|
17
|
+
* - Worker heartbeat monitoring
|
|
18
|
+
*/
|
|
19
|
+
import { EventEmitter } from 'events';
|
|
20
|
+
import { randomUUID } from 'crypto';
|
|
21
|
+
// ============================================
|
|
22
|
+
// Constants
|
|
23
|
+
// ============================================
|
|
24
|
+
const DEFAULT_CONFIG = {
|
|
25
|
+
redisUrl: 'redis://localhost:6379',
|
|
26
|
+
queuePrefix: 'claude-flow:queue',
|
|
27
|
+
defaultTimeoutMs: 300000, // 5 minutes
|
|
28
|
+
maxRetries: 3,
|
|
29
|
+
resultTtlSeconds: 86400, // 24 hours
|
|
30
|
+
heartbeatIntervalMs: 30000, // 30 seconds
|
|
31
|
+
deadLetterEnabled: true,
|
|
32
|
+
visibilityTimeoutMs: 60000, // 1 minute
|
|
33
|
+
};
|
|
34
|
+
const PRIORITY_SCORES = {
|
|
35
|
+
critical: 4,
|
|
36
|
+
high: 3,
|
|
37
|
+
normal: 2,
|
|
38
|
+
low: 1,
|
|
39
|
+
};
|
|
40
|
+
// ============================================
|
|
41
|
+
// In-Memory Redis Simulation (for non-Redis environments)
|
|
42
|
+
// ============================================
|
|
43
|
+
/**
|
|
44
|
+
* Simple in-memory queue implementation for environments without Redis
|
|
45
|
+
* Production should use actual Redis connection
|
|
46
|
+
*/
|
|
47
|
+
class InMemoryStore {
|
|
48
|
+
tasks = new Map();
|
|
49
|
+
queues = new Map();
|
|
50
|
+
workers = new Map();
|
|
51
|
+
results = new Map();
|
|
52
|
+
cleanupTimer;
|
|
53
|
+
/**
|
|
54
|
+
* Start cleanup timer (called after initialization)
|
|
55
|
+
*/
|
|
56
|
+
startCleanup() {
|
|
57
|
+
if (this.cleanupTimer)
|
|
58
|
+
return;
|
|
59
|
+
this.cleanupTimer = setInterval(() => this.cleanupExpired(), 60000);
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Stop cleanup timer
|
|
63
|
+
*/
|
|
64
|
+
stopCleanup() {
|
|
65
|
+
if (this.cleanupTimer) {
|
|
66
|
+
clearInterval(this.cleanupTimer);
|
|
67
|
+
this.cleanupTimer = undefined;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// Task operations
|
|
71
|
+
setTask(id, task) {
|
|
72
|
+
this.tasks.set(id, task);
|
|
73
|
+
}
|
|
74
|
+
getTask(id) {
|
|
75
|
+
return this.tasks.get(id);
|
|
76
|
+
}
|
|
77
|
+
deleteTask(id) {
|
|
78
|
+
this.tasks.delete(id);
|
|
79
|
+
}
|
|
80
|
+
// Queue operations
|
|
81
|
+
pushToQueue(queue, taskId, priority) {
|
|
82
|
+
const queueTasks = this.queues.get(queue) || [];
|
|
83
|
+
// Insert based on priority (higher priority = earlier in queue)
|
|
84
|
+
let insertIndex = queueTasks.length;
|
|
85
|
+
for (let i = 0; i < queueTasks.length; i++) {
|
|
86
|
+
const task = this.tasks.get(queueTasks[i]);
|
|
87
|
+
if (task && PRIORITY_SCORES[task.priority] < priority) {
|
|
88
|
+
insertIndex = i;
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
queueTasks.splice(insertIndex, 0, taskId);
|
|
93
|
+
this.queues.set(queue, queueTasks);
|
|
94
|
+
}
|
|
95
|
+
popFromQueue(queue) {
|
|
96
|
+
const queueTasks = this.queues.get(queue) || [];
|
|
97
|
+
if (queueTasks.length === 0)
|
|
98
|
+
return null;
|
|
99
|
+
return queueTasks.shift() || null;
|
|
100
|
+
}
|
|
101
|
+
getQueueLength(queue) {
|
|
102
|
+
return (this.queues.get(queue) || []).length;
|
|
103
|
+
}
|
|
104
|
+
// Worker operations
|
|
105
|
+
setWorker(workerId, registration) {
|
|
106
|
+
this.workers.set(workerId, registration);
|
|
107
|
+
}
|
|
108
|
+
getWorker(workerId) {
|
|
109
|
+
return this.workers.get(workerId);
|
|
110
|
+
}
|
|
111
|
+
deleteWorker(workerId) {
|
|
112
|
+
this.workers.delete(workerId);
|
|
113
|
+
}
|
|
114
|
+
getAllWorkers() {
|
|
115
|
+
return Array.from(this.workers.values());
|
|
116
|
+
}
|
|
117
|
+
// Result operations
|
|
118
|
+
setResult(taskId, result, ttlSeconds) {
|
|
119
|
+
this.results.set(taskId, {
|
|
120
|
+
result,
|
|
121
|
+
expiresAt: Date.now() + ttlSeconds * 1000,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
getResult(taskId) {
|
|
125
|
+
const entry = this.results.get(taskId);
|
|
126
|
+
if (!entry)
|
|
127
|
+
return undefined;
|
|
128
|
+
if (Date.now() > entry.expiresAt) {
|
|
129
|
+
this.results.delete(taskId);
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
132
|
+
return entry.result;
|
|
133
|
+
}
|
|
134
|
+
// Stats
|
|
135
|
+
getStats() {
|
|
136
|
+
return {
|
|
137
|
+
tasks: this.tasks.size,
|
|
138
|
+
workers: this.workers.size,
|
|
139
|
+
results: this.results.size,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
// Cleanup
|
|
143
|
+
cleanupExpired() {
|
|
144
|
+
const now = Date.now();
|
|
145
|
+
for (const [id, entry] of this.results) {
|
|
146
|
+
if (now > entry.expiresAt) {
|
|
147
|
+
this.results.delete(id);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
// ============================================
|
|
153
|
+
// WorkerQueue Class
|
|
154
|
+
// ============================================
|
|
155
|
+
/**
|
|
156
|
+
* WorkerQueue - Redis-based task queue for distributed worker execution
|
|
157
|
+
*/
|
|
158
|
+
export class WorkerQueue extends EventEmitter {
|
|
159
|
+
config;
|
|
160
|
+
store;
|
|
161
|
+
workerId;
|
|
162
|
+
heartbeatTimer;
|
|
163
|
+
processingTasks = new Set();
|
|
164
|
+
isShuttingDown = false;
|
|
165
|
+
maxConcurrent = 1;
|
|
166
|
+
initialized = false;
|
|
167
|
+
constructor(config) {
|
|
168
|
+
super();
|
|
169
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
170
|
+
this.store = new InMemoryStore();
|
|
171
|
+
this.workerId = `worker-${randomUUID().slice(0, 8)}`;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Initialize the queue (starts cleanup timers)
|
|
175
|
+
*/
|
|
176
|
+
async initialize() {
|
|
177
|
+
if (this.initialized)
|
|
178
|
+
return;
|
|
179
|
+
this.store.startCleanup();
|
|
180
|
+
this.initialized = true;
|
|
181
|
+
this.emit('initialized', { workerId: this.workerId });
|
|
182
|
+
}
|
|
183
|
+
// ============================================
|
|
184
|
+
// Public API - Task Management
|
|
185
|
+
// ============================================
|
|
186
|
+
/**
|
|
187
|
+
* Enqueue a new task
|
|
188
|
+
*/
|
|
189
|
+
async enqueue(workerType, payload = {}, options) {
|
|
190
|
+
// Initialize if needed
|
|
191
|
+
if (!this.initialized) {
|
|
192
|
+
await this.initialize();
|
|
193
|
+
}
|
|
194
|
+
// Validate worker type
|
|
195
|
+
if (!workerType || typeof workerType !== 'string') {
|
|
196
|
+
throw new Error('Invalid worker type');
|
|
197
|
+
}
|
|
198
|
+
// Validate priority
|
|
199
|
+
const priority = options?.priority || 'normal';
|
|
200
|
+
if (!['critical', 'high', 'normal', 'low'].includes(priority)) {
|
|
201
|
+
throw new Error(`Invalid priority: ${priority}`);
|
|
202
|
+
}
|
|
203
|
+
const taskId = `task-${Date.now()}-${randomUUID().slice(0, 8)}`;
|
|
204
|
+
const task = {
|
|
205
|
+
id: taskId,
|
|
206
|
+
workerType,
|
|
207
|
+
priority,
|
|
208
|
+
payload: {
|
|
209
|
+
...payload,
|
|
210
|
+
timeoutMs: options?.timeoutMs || this.config.defaultTimeoutMs,
|
|
211
|
+
},
|
|
212
|
+
status: 'pending',
|
|
213
|
+
createdAt: new Date(),
|
|
214
|
+
retryCount: 0,
|
|
215
|
+
maxRetries: options?.maxRetries ?? this.config.maxRetries,
|
|
216
|
+
};
|
|
217
|
+
// Store task
|
|
218
|
+
this.store.setTask(taskId, task);
|
|
219
|
+
// Add to priority queue
|
|
220
|
+
const queueName = this.getQueueName(workerType);
|
|
221
|
+
this.store.pushToQueue(queueName, taskId, PRIORITY_SCORES[priority]);
|
|
222
|
+
this.emit('taskEnqueued', { taskId, workerType, priority });
|
|
223
|
+
return taskId;
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Dequeue a task for processing
|
|
227
|
+
*/
|
|
228
|
+
async dequeue(workerTypes) {
|
|
229
|
+
if (this.isShuttingDown)
|
|
230
|
+
return null;
|
|
231
|
+
// Check queues in priority order
|
|
232
|
+
for (const workerType of workerTypes) {
|
|
233
|
+
const queueName = this.getQueueName(workerType);
|
|
234
|
+
const taskId = this.store.popFromQueue(queueName);
|
|
235
|
+
if (taskId) {
|
|
236
|
+
const task = this.store.getTask(taskId);
|
|
237
|
+
if (task && task.status === 'pending') {
|
|
238
|
+
task.status = 'processing';
|
|
239
|
+
task.startedAt = new Date();
|
|
240
|
+
task.workerId = this.workerId;
|
|
241
|
+
this.store.setTask(taskId, task);
|
|
242
|
+
this.processingTasks.add(taskId);
|
|
243
|
+
this.emit('taskDequeued', { taskId, workerType });
|
|
244
|
+
return task;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Complete a task with result
|
|
252
|
+
*/
|
|
253
|
+
async complete(taskId, result) {
|
|
254
|
+
const task = this.store.getTask(taskId);
|
|
255
|
+
if (!task) {
|
|
256
|
+
this.emit('warning', { message: `Task ${taskId} not found for completion` });
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
task.status = 'completed';
|
|
260
|
+
task.completedAt = new Date();
|
|
261
|
+
task.result = result;
|
|
262
|
+
this.store.setTask(taskId, task);
|
|
263
|
+
// Store result with TTL
|
|
264
|
+
this.store.setResult(taskId, result, this.config.resultTtlSeconds);
|
|
265
|
+
this.processingTasks.delete(taskId);
|
|
266
|
+
this.emit('taskCompleted', { taskId, result, duration: task.completedAt.getTime() - (task.startedAt?.getTime() || 0) });
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Fail a task with error
|
|
270
|
+
*/
|
|
271
|
+
async fail(taskId, error, retryable = true) {
|
|
272
|
+
const task = this.store.getTask(taskId);
|
|
273
|
+
if (!task) {
|
|
274
|
+
this.emit('warning', { message: `Task ${taskId} not found for failure` });
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
this.processingTasks.delete(taskId);
|
|
278
|
+
// Check if we should retry
|
|
279
|
+
if (retryable && task.retryCount < task.maxRetries) {
|
|
280
|
+
task.retryCount++;
|
|
281
|
+
task.status = 'pending';
|
|
282
|
+
task.startedAt = undefined;
|
|
283
|
+
task.workerId = undefined;
|
|
284
|
+
task.error = error;
|
|
285
|
+
this.store.setTask(taskId, task);
|
|
286
|
+
// Re-queue with delay (exponential backoff)
|
|
287
|
+
const delay = Math.min(30000, 1000 * Math.pow(2, task.retryCount));
|
|
288
|
+
setTimeout(() => {
|
|
289
|
+
const queueName = this.getQueueName(task.workerType);
|
|
290
|
+
this.store.pushToQueue(queueName, taskId, PRIORITY_SCORES[task.priority]);
|
|
291
|
+
}, delay);
|
|
292
|
+
this.emit('taskRetrying', { taskId, retryCount: task.retryCount, delay });
|
|
293
|
+
}
|
|
294
|
+
else {
|
|
295
|
+
// Move to failed/dead letter
|
|
296
|
+
task.status = 'failed';
|
|
297
|
+
task.completedAt = new Date();
|
|
298
|
+
task.error = error;
|
|
299
|
+
this.store.setTask(taskId, task);
|
|
300
|
+
if (this.config.deadLetterEnabled) {
|
|
301
|
+
const dlqName = `${this.config.queuePrefix}:dlq`;
|
|
302
|
+
this.store.pushToQueue(dlqName, taskId, 0);
|
|
303
|
+
}
|
|
304
|
+
this.emit('taskFailed', { taskId, error, retryCount: task.retryCount });
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Get task status
|
|
309
|
+
*/
|
|
310
|
+
async getTask(taskId) {
|
|
311
|
+
return this.store.getTask(taskId) || null;
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Get task result
|
|
315
|
+
*/
|
|
316
|
+
async getResult(taskId) {
|
|
317
|
+
return this.store.getResult(taskId) || null;
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Cancel a pending task
|
|
321
|
+
*/
|
|
322
|
+
async cancel(taskId) {
|
|
323
|
+
const task = this.store.getTask(taskId);
|
|
324
|
+
if (!task || task.status !== 'pending') {
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
327
|
+
task.status = 'cancelled';
|
|
328
|
+
task.completedAt = new Date();
|
|
329
|
+
this.store.setTask(taskId, task);
|
|
330
|
+
this.emit('taskCancelled', { taskId });
|
|
331
|
+
return true;
|
|
332
|
+
}
|
|
333
|
+
// ============================================
|
|
334
|
+
// Public API - Worker Management
|
|
335
|
+
// ============================================
|
|
336
|
+
/**
|
|
337
|
+
* Register this instance as a worker
|
|
338
|
+
*/
|
|
339
|
+
async registerWorker(workerTypes, options) {
|
|
340
|
+
// Initialize if needed
|
|
341
|
+
if (!this.initialized) {
|
|
342
|
+
await this.initialize();
|
|
343
|
+
}
|
|
344
|
+
// Validate worker types
|
|
345
|
+
if (!Array.isArray(workerTypes) || workerTypes.length === 0) {
|
|
346
|
+
throw new Error('Worker types must be a non-empty array');
|
|
347
|
+
}
|
|
348
|
+
this.maxConcurrent = options?.maxConcurrent || 1;
|
|
349
|
+
const registration = {
|
|
350
|
+
workerId: this.workerId,
|
|
351
|
+
workerTypes,
|
|
352
|
+
maxConcurrent: this.maxConcurrent,
|
|
353
|
+
currentTasks: 0,
|
|
354
|
+
lastHeartbeat: new Date(),
|
|
355
|
+
registeredAt: new Date(),
|
|
356
|
+
hostname: options?.hostname,
|
|
357
|
+
containerId: options?.containerId,
|
|
358
|
+
};
|
|
359
|
+
this.store.setWorker(this.workerId, registration);
|
|
360
|
+
// Start heartbeat
|
|
361
|
+
this.startHeartbeat();
|
|
362
|
+
this.emit('workerRegistered', { workerId: this.workerId, workerTypes });
|
|
363
|
+
return this.workerId;
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* Unregister this worker
|
|
367
|
+
*/
|
|
368
|
+
async unregisterWorker() {
|
|
369
|
+
this.stopHeartbeat();
|
|
370
|
+
this.store.deleteWorker(this.workerId);
|
|
371
|
+
this.emit('workerUnregistered', { workerId: this.workerId });
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Get all registered workers
|
|
375
|
+
*/
|
|
376
|
+
async getWorkers() {
|
|
377
|
+
return this.store.getAllWorkers();
|
|
378
|
+
}
|
|
379
|
+
// ============================================
|
|
380
|
+
// Public API - Statistics
|
|
381
|
+
// ============================================
|
|
382
|
+
/**
|
|
383
|
+
* Get queue statistics
|
|
384
|
+
*/
|
|
385
|
+
async getStats() {
|
|
386
|
+
const storeStats = this.store.getStats();
|
|
387
|
+
// This is a simplified implementation
|
|
388
|
+
// Full implementation would aggregate across all queues
|
|
389
|
+
const stats = {
|
|
390
|
+
pending: 0,
|
|
391
|
+
processing: this.processingTasks.size,
|
|
392
|
+
completed: 0,
|
|
393
|
+
failed: 0,
|
|
394
|
+
deadLetter: 0,
|
|
395
|
+
byPriority: { critical: 0, high: 0, normal: 0, low: 0 },
|
|
396
|
+
byWorkerType: {},
|
|
397
|
+
averageWaitTimeMs: 0,
|
|
398
|
+
averageProcessingTimeMs: 0,
|
|
399
|
+
};
|
|
400
|
+
return stats;
|
|
401
|
+
}
|
|
402
|
+
// ============================================
|
|
403
|
+
// Public API - Lifecycle
|
|
404
|
+
// ============================================
|
|
405
|
+
/**
|
|
406
|
+
* Start processing tasks
|
|
407
|
+
*/
|
|
408
|
+
async start(workerTypes, handler, options) {
|
|
409
|
+
await this.registerWorker(workerTypes, { maxConcurrent: options?.maxConcurrent });
|
|
410
|
+
const processLoop = async () => {
|
|
411
|
+
while (!this.isShuttingDown) {
|
|
412
|
+
try {
|
|
413
|
+
// Respect concurrency limit
|
|
414
|
+
if (this.processingTasks.size >= this.maxConcurrent) {
|
|
415
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
const task = await this.dequeue(workerTypes);
|
|
419
|
+
if (task) {
|
|
420
|
+
// Process task without blocking the loop (allows concurrency)
|
|
421
|
+
this.processTask(task, handler).catch(error => {
|
|
422
|
+
this.emit('error', { taskId: task.id, error: String(error) });
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
else {
|
|
426
|
+
// No task available, wait before polling again
|
|
427
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
catch (error) {
|
|
431
|
+
this.emit('error', { error: error instanceof Error ? error.message : String(error) });
|
|
432
|
+
// Wait before retrying to avoid tight error loop
|
|
433
|
+
await new Promise(resolve => setTimeout(resolve, 5000));
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
};
|
|
437
|
+
// Start processing
|
|
438
|
+
processLoop().catch(error => {
|
|
439
|
+
this.emit('error', { error: error instanceof Error ? error.message : String(error) });
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
/**
|
|
443
|
+
* Process a single task
|
|
444
|
+
*/
|
|
445
|
+
async processTask(task, handler) {
|
|
446
|
+
try {
|
|
447
|
+
const result = await handler(task);
|
|
448
|
+
await this.complete(task.id, result);
|
|
449
|
+
}
|
|
450
|
+
catch (error) {
|
|
451
|
+
await this.fail(task.id, error instanceof Error ? error.message : String(error));
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* Shutdown the queue gracefully
|
|
456
|
+
*/
|
|
457
|
+
async shutdown() {
|
|
458
|
+
if (this.isShuttingDown)
|
|
459
|
+
return;
|
|
460
|
+
this.isShuttingDown = true;
|
|
461
|
+
// Wait for processing tasks to complete (with timeout)
|
|
462
|
+
const timeout = 30000;
|
|
463
|
+
const start = Date.now();
|
|
464
|
+
while (this.processingTasks.size > 0 && Date.now() - start < timeout) {
|
|
465
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
466
|
+
}
|
|
467
|
+
// Force fail remaining tasks
|
|
468
|
+
for (const taskId of this.processingTasks) {
|
|
469
|
+
await this.fail(taskId, 'Worker shutdown', false);
|
|
470
|
+
}
|
|
471
|
+
// Stop store cleanup
|
|
472
|
+
this.store.stopCleanup();
|
|
473
|
+
await this.unregisterWorker();
|
|
474
|
+
this.initialized = false;
|
|
475
|
+
this.emit('shutdown', {});
|
|
476
|
+
}
|
|
477
|
+
// ============================================
|
|
478
|
+
// Private Methods
|
|
479
|
+
// ============================================
|
|
480
|
+
/**
|
|
481
|
+
* Get queue name for worker type
|
|
482
|
+
*/
|
|
483
|
+
getQueueName(workerType) {
|
|
484
|
+
return `${this.config.queuePrefix}:${workerType}`;
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* Start heartbeat timer
|
|
488
|
+
*/
|
|
489
|
+
startHeartbeat() {
|
|
490
|
+
this.heartbeatTimer = setInterval(() => {
|
|
491
|
+
const registration = this.store.getWorker(this.workerId);
|
|
492
|
+
if (registration) {
|
|
493
|
+
registration.lastHeartbeat = new Date();
|
|
494
|
+
registration.currentTasks = this.processingTasks.size;
|
|
495
|
+
this.store.setWorker(this.workerId, registration);
|
|
496
|
+
}
|
|
497
|
+
}, this.config.heartbeatIntervalMs);
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* Stop heartbeat timer
|
|
501
|
+
*/
|
|
502
|
+
stopHeartbeat() {
|
|
503
|
+
if (this.heartbeatTimer) {
|
|
504
|
+
clearInterval(this.heartbeatTimer);
|
|
505
|
+
this.heartbeatTimer = undefined;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
// Export default
|
|
510
|
+
export default WorkerQueue;
|
|
511
|
+
//# sourceMappingURL=worker-queue.js.map
|