crawlforge-mcp-server 3.0.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/CLAUDE.md +315 -0
- package/LICENSE +21 -0
- package/README.md +181 -0
- package/package.json +115 -0
- package/server.js +1963 -0
- package/setup.js +112 -0
- package/src/constants/config.js +615 -0
- package/src/core/ActionExecutor.js +1104 -0
- package/src/core/AlertNotificationSystem.js +601 -0
- package/src/core/AuthManager.js +315 -0
- package/src/core/ChangeTracker.js +2306 -0
- package/src/core/JobManager.js +687 -0
- package/src/core/LLMsTxtAnalyzer.js +753 -0
- package/src/core/LocalizationManager.js +1615 -0
- package/src/core/PerformanceManager.js +828 -0
- package/src/core/ResearchOrchestrator.js +1327 -0
- package/src/core/SnapshotManager.js +1037 -0
- package/src/core/StealthBrowserManager.js +1795 -0
- package/src/core/WebhookDispatcher.js +745 -0
- package/src/core/analysis/ContentAnalyzer.js +749 -0
- package/src/core/analysis/LinkAnalyzer.js +972 -0
- package/src/core/cache/CacheManager.js +821 -0
- package/src/core/connections/ConnectionPool.js +553 -0
- package/src/core/crawlers/BFSCrawler.js +845 -0
- package/src/core/integrations/PerformanceIntegration.js +377 -0
- package/src/core/llm/AnthropicProvider.js +135 -0
- package/src/core/llm/LLMManager.js +415 -0
- package/src/core/llm/LLMProvider.js +97 -0
- package/src/core/llm/OpenAIProvider.js +127 -0
- package/src/core/processing/BrowserProcessor.js +986 -0
- package/src/core/processing/ContentProcessor.js +505 -0
- package/src/core/processing/PDFProcessor.js +448 -0
- package/src/core/processing/StreamProcessor.js +673 -0
- package/src/core/queue/QueueManager.js +98 -0
- package/src/core/workers/WorkerPool.js +585 -0
- package/src/core/workers/worker.js +743 -0
- package/src/monitoring/healthCheck.js +600 -0
- package/src/monitoring/metrics.js +761 -0
- package/src/optimization/wave3-optimizations.js +932 -0
- package/src/security/security-patches.js +120 -0
- package/src/security/security-tests.js +355 -0
- package/src/security/wave3-security.js +652 -0
- package/src/tools/advanced/BatchScrapeTool.js +1089 -0
- package/src/tools/advanced/ScrapeWithActionsTool.js +669 -0
- package/src/tools/crawl/crawlDeep.js +449 -0
- package/src/tools/crawl/mapSite.js +400 -0
- package/src/tools/extract/analyzeContent.js +624 -0
- package/src/tools/extract/extractContent.js +329 -0
- package/src/tools/extract/processDocument.js +503 -0
- package/src/tools/extract/summarizeContent.js +376 -0
- package/src/tools/llmstxt/generateLLMsTxt.js +570 -0
- package/src/tools/research/deepResearch.js +706 -0
- package/src/tools/search/adapters/duckduckgoSearch.js +398 -0
- package/src/tools/search/adapters/googleSearch.js +236 -0
- package/src/tools/search/adapters/searchProviderFactory.js +96 -0
- package/src/tools/search/queryExpander.js +543 -0
- package/src/tools/search/ranking/ResultDeduplicator.js +676 -0
- package/src/tools/search/ranking/ResultRanker.js +497 -0
- package/src/tools/search/searchWeb.js +482 -0
- package/src/tools/tracking/trackChanges.js +1355 -0
- package/src/utils/CircuitBreaker.js +515 -0
- package/src/utils/ErrorHandlingConfig.js +342 -0
- package/src/utils/HumanBehaviorSimulator.js +569 -0
- package/src/utils/Logger.js +568 -0
- package/src/utils/MemoryMonitor.js +173 -0
- package/src/utils/RetryManager.js +386 -0
- package/src/utils/contentUtils.js +588 -0
- package/src/utils/domainFilter.js +612 -0
- package/src/utils/inputValidation.js +766 -0
- package/src/utils/rateLimiter.js +196 -0
- package/src/utils/robotsChecker.js +91 -0
- package/src/utils/securityMiddleware.js +416 -0
- package/src/utils/sitemapParser.js +678 -0
- package/src/utils/ssrfProtection.js +640 -0
- package/src/utils/urlNormalizer.js +168 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import PQueue from 'p-queue';
|
|
2
|
+
|
|
3
|
+
export class QueueManager {
|
|
4
|
+
constructor(options = {}) {
|
|
5
|
+
const {
|
|
6
|
+
concurrency = 10,
|
|
7
|
+
interval = 1000,
|
|
8
|
+
intervalCap = 10,
|
|
9
|
+
timeout = 30000
|
|
10
|
+
} = options;
|
|
11
|
+
|
|
12
|
+
this.queue = new PQueue({
|
|
13
|
+
concurrency,
|
|
14
|
+
interval,
|
|
15
|
+
intervalCap,
|
|
16
|
+
timeout,
|
|
17
|
+
throwOnTimeout: true
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
this.stats = {
|
|
21
|
+
processed: 0,
|
|
22
|
+
failed: 0,
|
|
23
|
+
pending: 0,
|
|
24
|
+
active: 0
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
this.setupEventHandlers();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
setupEventHandlers() {
|
|
31
|
+
this.queue.on('active', () => {
|
|
32
|
+
this.stats.active = this.queue.pending;
|
|
33
|
+
this.stats.pending = this.queue.size;
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
this.queue.on('completed', () => {
|
|
37
|
+
this.stats.processed++;
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
this.queue.on('error', (error) => {
|
|
41
|
+
this.stats.failed++;
|
|
42
|
+
console.error('Queue error:', error);
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async add(fn, options = {}) {
|
|
47
|
+
const { priority = 0 } = options;
|
|
48
|
+
return this.queue.add(fn, { priority });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async addAll(tasks, options = {}) {
|
|
52
|
+
const promises = tasks.map(task => this.add(task, options));
|
|
53
|
+
return Promise.all(promises);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
pause() {
|
|
57
|
+
this.queue.pause();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
start() {
|
|
61
|
+
this.queue.start();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
clear() {
|
|
65
|
+
this.queue.clear();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async onEmpty() {
|
|
69
|
+
return this.queue.onEmpty();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async onIdle() {
|
|
73
|
+
return this.queue.onIdle();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
getStats() {
|
|
77
|
+
return {
|
|
78
|
+
...this.stats,
|
|
79
|
+
size: this.queue.size,
|
|
80
|
+
pending: this.queue.pending,
|
|
81
|
+
isPaused: this.queue.isPaused
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
get size() {
|
|
86
|
+
return this.queue.size;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
get pending() {
|
|
90
|
+
return this.queue.pending;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
get isPaused() {
|
|
94
|
+
return this.queue.isPaused;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export default QueueManager;
|
|
@@ -0,0 +1,585 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WorkerPool - Manages a pool of worker threads for CPU-intensive tasks
|
|
3
|
+
* Integrates with QueueManager for efficient task distribution
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Worker } from 'worker_threads';
|
|
7
|
+
import { fileURLToPath } from 'url';
|
|
8
|
+
import { dirname, join } from 'path';
|
|
9
|
+
import { EventEmitter } from 'events';
|
|
10
|
+
import { config } from '../../constants/config.js';
|
|
11
|
+
|
|
12
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
13
|
+
const __dirname = dirname(__filename);
|
|
14
|
+
|
|
15
|
+
export class WorkerPool extends EventEmitter {
|
|
16
|
+
constructor(options = {}) {
|
|
17
|
+
super();
|
|
18
|
+
|
|
19
|
+
const {
|
|
20
|
+
maxWorkers = config.performance.maxWorkers,
|
|
21
|
+
workerScript = join(__dirname, 'worker.js'),
|
|
22
|
+
taskTimeout = 30000,
|
|
23
|
+
idleTimeout = 60000,
|
|
24
|
+
retryAttempts = 3
|
|
25
|
+
} = options;
|
|
26
|
+
|
|
27
|
+
this.maxWorkers = maxWorkers;
|
|
28
|
+
this.workerScript = workerScript;
|
|
29
|
+
this.taskTimeout = taskTimeout;
|
|
30
|
+
this.idleTimeout = idleTimeout;
|
|
31
|
+
this.retryAttempts = retryAttempts;
|
|
32
|
+
|
|
33
|
+
// Worker management
|
|
34
|
+
this.workers = new Set();
|
|
35
|
+
this.availableWorkers = [];
|
|
36
|
+
this.busyWorkers = new Map();
|
|
37
|
+
this.taskQueue = [];
|
|
38
|
+
|
|
39
|
+
// Statistics
|
|
40
|
+
this.stats = {
|
|
41
|
+
tasksCompleted: 0,
|
|
42
|
+
tasksFailed: 0,
|
|
43
|
+
tasksQueued: 0,
|
|
44
|
+
workersCreated: 0,
|
|
45
|
+
workersDestroyed: 0,
|
|
46
|
+
avgTaskDuration: 0,
|
|
47
|
+
peakWorkerCount: 0
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// Task tracking
|
|
51
|
+
this.activeTasks = new Map();
|
|
52
|
+
this.taskIdCounter = 0;
|
|
53
|
+
|
|
54
|
+
// Cleanup interval
|
|
55
|
+
this.cleanupInterval = setInterval(() => {
|
|
56
|
+
this.cleanupIdleWorkers();
|
|
57
|
+
}, this.idleTimeout / 2);
|
|
58
|
+
|
|
59
|
+
this.setupGracefulShutdown();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Execute a task using the worker pool
|
|
64
|
+
* @param {string|Object} taskType - Type of task to execute, or task object with {type, data, options}
|
|
65
|
+
* @param {any} data - Task data
|
|
66
|
+
* @param {Object} options - Task options
|
|
67
|
+
* @returns {Promise<any>} - Task result
|
|
68
|
+
*/
|
|
69
|
+
async execute(taskType, data, options = {}) {
|
|
70
|
+
// Handle object-style task input from PerformanceManager
|
|
71
|
+
if (typeof taskType === 'object' && taskType.type) {
|
|
72
|
+
const taskObj = taskType;
|
|
73
|
+
taskType = taskObj.type;
|
|
74
|
+
data = taskObj.data;
|
|
75
|
+
options = taskObj.options || {};
|
|
76
|
+
}
|
|
77
|
+
const taskId = this.generateTaskId();
|
|
78
|
+
const { timeout = this.taskTimeout, priority = 0, retries = this.retryAttempts } = options;
|
|
79
|
+
|
|
80
|
+
const task = {
|
|
81
|
+
id: taskId,
|
|
82
|
+
type: taskType,
|
|
83
|
+
data,
|
|
84
|
+
timeout,
|
|
85
|
+
priority,
|
|
86
|
+
retries,
|
|
87
|
+
startTime: Date.now(),
|
|
88
|
+
attempts: 0
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
this.stats.tasksQueued++;
|
|
92
|
+
this.emit('taskQueued', { taskId, taskType });
|
|
93
|
+
|
|
94
|
+
return new Promise((resolve, reject) => {
|
|
95
|
+
task.resolve = resolve;
|
|
96
|
+
task.reject = reject;
|
|
97
|
+
|
|
98
|
+
// Add to queue or execute immediately if worker available
|
|
99
|
+
if (this.availableWorkers.length > 0) {
|
|
100
|
+
this.executeTask(task);
|
|
101
|
+
} else {
|
|
102
|
+
this.addToQueue(task);
|
|
103
|
+
this.maybeCreateWorker();
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Execute multiple tasks in parallel with optional batching
|
|
110
|
+
* @param {Array} tasks - Array of {taskType, data, options} objects
|
|
111
|
+
* @param {Object} batchOptions - Batching options
|
|
112
|
+
* @returns {Promise<Array>} - Array of results
|
|
113
|
+
*/
|
|
114
|
+
async executeBatch(tasks, batchOptions = {}) {
|
|
115
|
+
const { maxConcurrent = this.maxWorkers, failFast = false } = batchOptions;
|
|
116
|
+
|
|
117
|
+
const chunks = this.chunkArray(tasks, maxConcurrent);
|
|
118
|
+
const results = [];
|
|
119
|
+
|
|
120
|
+
for (const chunk of chunks) {
|
|
121
|
+
const chunkPromises = chunk.map(({ taskType, data, options }) =>
|
|
122
|
+
this.execute(taskType, data, options).catch(error => {
|
|
123
|
+
if (failFast) throw error;
|
|
124
|
+
return { error: error.message };
|
|
125
|
+
})
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
const chunkResults = await Promise.all(chunkPromises);
|
|
129
|
+
results.push(...chunkResults);
|
|
130
|
+
|
|
131
|
+
if (failFast && chunkResults.some(result => result && result.error)) {
|
|
132
|
+
throw new Error('Batch execution failed fast');
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return results;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Add task to priority queue
|
|
141
|
+
* @param {Object} task - Task object
|
|
142
|
+
*/
|
|
143
|
+
addToQueue(task) {
|
|
144
|
+
// Insert task in priority order (higher priority first)
|
|
145
|
+
let insertIndex = this.taskQueue.length;
|
|
146
|
+
for (let i = 0; i < this.taskQueue.length; i++) {
|
|
147
|
+
if (this.taskQueue[i].priority < task.priority) {
|
|
148
|
+
insertIndex = i;
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
this.taskQueue.splice(insertIndex, 0, task);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Execute a task using an available worker
|
|
157
|
+
* @param {Object} task - Task object
|
|
158
|
+
*/
|
|
159
|
+
async executeTask(task) {
|
|
160
|
+
let worker = this.getAvailableWorker();
|
|
161
|
+
|
|
162
|
+
if (!worker) {
|
|
163
|
+
// Create new worker if under limit
|
|
164
|
+
if (this.workers.size < this.maxWorkers) {
|
|
165
|
+
worker = await this.createWorker();
|
|
166
|
+
} else {
|
|
167
|
+
// Queue the task
|
|
168
|
+
this.addToQueue(task);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
this.assignTaskToWorker(task, worker);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Assign a task to a specific worker
|
|
178
|
+
* @param {Object} task - Task object
|
|
179
|
+
* @param {Worker} worker - Worker instance
|
|
180
|
+
*/
|
|
181
|
+
assignTaskToWorker(task, worker) {
|
|
182
|
+
task.attempts++;
|
|
183
|
+
task.worker = worker;
|
|
184
|
+
|
|
185
|
+
// Remove worker from available pool
|
|
186
|
+
const workerIndex = this.availableWorkers.indexOf(worker);
|
|
187
|
+
if (workerIndex !== -1) {
|
|
188
|
+
this.availableWorkers.splice(workerIndex, 1);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Add to busy workers
|
|
192
|
+
this.busyWorkers.set(worker, task);
|
|
193
|
+
this.activeTasks.set(task.id, task);
|
|
194
|
+
|
|
195
|
+
// Set up timeout
|
|
196
|
+
const timeoutId = setTimeout(() => {
|
|
197
|
+
this.handleTaskTimeout(task);
|
|
198
|
+
}, task.timeout);
|
|
199
|
+
|
|
200
|
+
task.timeoutId = timeoutId;
|
|
201
|
+
|
|
202
|
+
// Send task to worker
|
|
203
|
+
worker.postMessage({
|
|
204
|
+
taskId: task.id,
|
|
205
|
+
type: task.type,
|
|
206
|
+
data: task.data
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
this.emit('taskStarted', {
|
|
210
|
+
taskId: task.id,
|
|
211
|
+
taskType: task.type,
|
|
212
|
+
workerId: worker.threadId
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Handle task completion
|
|
218
|
+
* @param {Object} task - Task object
|
|
219
|
+
* @param {any} result - Task result
|
|
220
|
+
* @param {Error} error - Task error (if any)
|
|
221
|
+
*/
|
|
222
|
+
handleTaskCompletion(task, result, error) {
|
|
223
|
+
const duration = Date.now() - task.startTime;
|
|
224
|
+
|
|
225
|
+
// Update statistics
|
|
226
|
+
this.updateTaskStats(duration, !error);
|
|
227
|
+
|
|
228
|
+
// Clear timeout
|
|
229
|
+
if (task.timeoutId) {
|
|
230
|
+
clearTimeout(task.timeoutId);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Remove from active tasks
|
|
234
|
+
this.activeTasks.delete(task.id);
|
|
235
|
+
|
|
236
|
+
// Return worker to available pool
|
|
237
|
+
if (task.worker) {
|
|
238
|
+
this.busyWorkers.delete(task.worker);
|
|
239
|
+
this.availableWorkers.push(task.worker);
|
|
240
|
+
task.worker.lastUsed = Date.now();
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Resolve or reject the task promise
|
|
244
|
+
if (error && task.attempts < task.retries) {
|
|
245
|
+
// Retry the task
|
|
246
|
+
setTimeout(() => {
|
|
247
|
+
this.executeTask(task);
|
|
248
|
+
}, 1000 * task.attempts); // Exponential backoff
|
|
249
|
+
} else if (error) {
|
|
250
|
+
task.reject(error);
|
|
251
|
+
this.emit('taskFailed', {
|
|
252
|
+
taskId: task.id,
|
|
253
|
+
taskType: task.type,
|
|
254
|
+
error: error.message,
|
|
255
|
+
attempts: task.attempts
|
|
256
|
+
});
|
|
257
|
+
} else {
|
|
258
|
+
task.resolve(result);
|
|
259
|
+
this.emit('taskCompleted', {
|
|
260
|
+
taskId: task.id,
|
|
261
|
+
taskType: task.type,
|
|
262
|
+
duration
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Process next task in queue
|
|
267
|
+
this.processNextTask();
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Handle task timeout
|
|
272
|
+
* @param {Object} task - Task object
|
|
273
|
+
*/
|
|
274
|
+
handleTaskTimeout(task) {
|
|
275
|
+
const error = new Error(`Task ${task.id} timed out after ${task.timeout}ms`);
|
|
276
|
+
|
|
277
|
+
// Terminate the worker if it's unresponsive
|
|
278
|
+
if (task.worker) {
|
|
279
|
+
this.terminateWorker(task.worker);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
this.handleTaskCompletion(task, null, error);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Process the next task in the queue
|
|
287
|
+
*/
|
|
288
|
+
processNextTask() {
|
|
289
|
+
if (this.taskQueue.length > 0 && this.availableWorkers.length > 0) {
|
|
290
|
+
const nextTask = this.taskQueue.shift();
|
|
291
|
+
this.executeTask(nextTask);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Get an available worker from the pool
|
|
297
|
+
* @returns {Worker|null} - Available worker or null
|
|
298
|
+
*/
|
|
299
|
+
getAvailableWorker() {
|
|
300
|
+
return this.availableWorkers.length > 0 ? this.availableWorkers[0] : null;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Create a new worker
|
|
305
|
+
* @returns {Promise<Worker>} - Worker instance
|
|
306
|
+
*/
|
|
307
|
+
async createWorker() {
|
|
308
|
+
return new Promise((resolve, reject) => {
|
|
309
|
+
const worker = new Worker(this.workerScript);
|
|
310
|
+
|
|
311
|
+
worker.on('message', (message) => {
|
|
312
|
+
this.handleWorkerMessage(worker, message);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
worker.on('error', (error) => {
|
|
316
|
+
this.handleWorkerError(worker, error);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
worker.on('exit', (code) => {
|
|
320
|
+
this.handleWorkerExit(worker, code);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
worker.on('online', () => {
|
|
324
|
+
this.workers.add(worker);
|
|
325
|
+
this.availableWorkers.push(worker);
|
|
326
|
+
worker.createdAt = Date.now();
|
|
327
|
+
worker.lastUsed = Date.now();
|
|
328
|
+
|
|
329
|
+
this.stats.workersCreated++;
|
|
330
|
+
this.stats.peakWorkerCount = Math.max(this.stats.peakWorkerCount, this.workers.size);
|
|
331
|
+
|
|
332
|
+
this.emit('workerCreated', { workerId: worker.threadId });
|
|
333
|
+
resolve(worker);
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
// Set creation timeout
|
|
337
|
+
const timeout = setTimeout(() => {
|
|
338
|
+
reject(new Error('Worker creation timeout'));
|
|
339
|
+
}, 10000);
|
|
340
|
+
|
|
341
|
+
worker.once('online', () => {
|
|
342
|
+
clearTimeout(timeout);
|
|
343
|
+
});
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Handle worker message
|
|
349
|
+
* @param {Worker} worker - Worker instance
|
|
350
|
+
* @param {Object} message - Message from worker
|
|
351
|
+
*/
|
|
352
|
+
handleWorkerMessage(worker, message) {
|
|
353
|
+
// Handle worker ready signal
|
|
354
|
+
if (message && message.type === 'ready') {
|
|
355
|
+
return; // Ignore ready signals
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const { taskId, result, error } = message;
|
|
359
|
+
const task = this.activeTasks.get(taskId);
|
|
360
|
+
|
|
361
|
+
if (!task) {
|
|
362
|
+
console.warn(`Received message for unknown task: ${taskId}`);
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const taskError = error ? new Error(error) : null;
|
|
367
|
+
this.handleTaskCompletion(task, result, taskError);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Handle worker error
|
|
372
|
+
* @param {Worker} worker - Worker instance
|
|
373
|
+
* @param {Error} error - Worker error
|
|
374
|
+
*/
|
|
375
|
+
handleWorkerError(worker, error) {
|
|
376
|
+
console.error(`Worker ${worker.threadId} error:`, error);
|
|
377
|
+
|
|
378
|
+
const task = this.busyWorkers.get(worker);
|
|
379
|
+
if (task) {
|
|
380
|
+
this.handleTaskCompletion(task, null, error);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
this.terminateWorker(worker);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Handle worker exit
|
|
388
|
+
* @param {Worker} worker - Worker instance
|
|
389
|
+
* @param {number} code - Exit code
|
|
390
|
+
*/
|
|
391
|
+
handleWorkerExit(worker, code) {
|
|
392
|
+
this.workers.delete(worker);
|
|
393
|
+
this.stats.workersDestroyed++;
|
|
394
|
+
|
|
395
|
+
// Remove from available workers
|
|
396
|
+
const availableIndex = this.availableWorkers.indexOf(worker);
|
|
397
|
+
if (availableIndex !== -1) {
|
|
398
|
+
this.availableWorkers.splice(availableIndex, 1);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Handle any active task
|
|
402
|
+
const task = this.busyWorkers.get(worker);
|
|
403
|
+
if (task) {
|
|
404
|
+
const error = new Error(`Worker exited with code ${code}`);
|
|
405
|
+
this.handleTaskCompletion(task, null, error);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
this.emit('workerExited', { workerId: worker.threadId, code });
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Terminate a worker
|
|
413
|
+
* @param {Worker} worker - Worker instance
|
|
414
|
+
*/
|
|
415
|
+
async terminateWorker(worker) {
|
|
416
|
+
try {
|
|
417
|
+
await worker.terminate();
|
|
418
|
+
} catch (error) {
|
|
419
|
+
console.error(`Error terminating worker ${worker.threadId}:`, error);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Create a new worker if conditions are met
|
|
425
|
+
*/
|
|
426
|
+
maybeCreateWorker() {
|
|
427
|
+
if (this.workers.size < this.maxWorkers &&
|
|
428
|
+
this.taskQueue.length > 0 &&
|
|
429
|
+
this.availableWorkers.length === 0) {
|
|
430
|
+
this.createWorker().catch(error => {
|
|
431
|
+
console.error('Failed to create worker:', error);
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Clean up idle workers
|
|
438
|
+
*/
|
|
439
|
+
cleanupIdleWorkers() {
|
|
440
|
+
const now = Date.now();
|
|
441
|
+
const workersToTerminate = [];
|
|
442
|
+
|
|
443
|
+
for (const worker of this.availableWorkers) {
|
|
444
|
+
if (now - worker.lastUsed > this.idleTimeout) {
|
|
445
|
+
workersToTerminate.push(worker);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Keep at least one worker alive
|
|
450
|
+
if (this.availableWorkers.length - workersToTerminate.length < 1) {
|
|
451
|
+
workersToTerminate.pop();
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
for (const worker of workersToTerminate) {
|
|
455
|
+
this.terminateWorker(worker);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Update task statistics
|
|
461
|
+
* @param {number} duration - Task duration
|
|
462
|
+
* @param {boolean} success - Whether task succeeded
|
|
463
|
+
*/
|
|
464
|
+
updateTaskStats(duration, success) {
|
|
465
|
+
if (success) {
|
|
466
|
+
this.stats.tasksCompleted++;
|
|
467
|
+
} else {
|
|
468
|
+
this.stats.tasksFailed++;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Update average duration
|
|
472
|
+
const totalTasks = this.stats.tasksCompleted + this.stats.tasksFailed;
|
|
473
|
+
this.stats.avgTaskDuration = (
|
|
474
|
+
(this.stats.avgTaskDuration * (totalTasks - 1) + duration) / totalTasks
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Generate unique task ID
|
|
480
|
+
* @returns {string} - Task ID
|
|
481
|
+
*/
|
|
482
|
+
generateTaskId() {
|
|
483
|
+
return `task_${++this.taskIdCounter}_${Date.now()}`;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Split array into chunks
|
|
488
|
+
* @param {Array} array - Array to chunk
|
|
489
|
+
* @param {number} chunkSize - Size of each chunk
|
|
490
|
+
* @returns {Array} - Array of chunks
|
|
491
|
+
*/
|
|
492
|
+
chunkArray(array, chunkSize) {
|
|
493
|
+
const chunks = [];
|
|
494
|
+
for (let i = 0; i < array.length; i += chunkSize) {
|
|
495
|
+
chunks.push(array.slice(i, i + chunkSize));
|
|
496
|
+
}
|
|
497
|
+
return chunks;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* Get worker pool statistics
|
|
502
|
+
* @returns {Object} - Statistics object
|
|
503
|
+
*/
|
|
504
|
+
getStats() {
|
|
505
|
+
return {
|
|
506
|
+
...this.stats,
|
|
507
|
+
activeWorkers: this.workers.size,
|
|
508
|
+
availableWorkers: this.availableWorkers.length,
|
|
509
|
+
busyWorkers: this.busyWorkers.size,
|
|
510
|
+
queuedTasks: this.taskQueue.length,
|
|
511
|
+
activeTasks: this.activeTasks.size
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* Pause the worker pool
|
|
517
|
+
*/
|
|
518
|
+
pause() {
|
|
519
|
+
this.paused = true;
|
|
520
|
+
this.emit('paused');
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Resume the worker pool
|
|
525
|
+
*/
|
|
526
|
+
resume() {
|
|
527
|
+
this.paused = false;
|
|
528
|
+
this.emit('resumed');
|
|
529
|
+
|
|
530
|
+
// Process any queued tasks
|
|
531
|
+
while (this.taskQueue.length > 0 && this.availableWorkers.length > 0) {
|
|
532
|
+
this.processNextTask();
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* Gracefully shutdown the worker pool
|
|
538
|
+
* @returns {Promise<void>}
|
|
539
|
+
*/
|
|
540
|
+
async shutdown() {
|
|
541
|
+
this.emit('shutdown');
|
|
542
|
+
|
|
543
|
+
// Clear cleanup interval
|
|
544
|
+
if (this.cleanupInterval) {
|
|
545
|
+
clearInterval(this.cleanupInterval);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// Wait for active tasks to complete or timeout
|
|
549
|
+
const shutdownTimeout = 30000; // 30 seconds
|
|
550
|
+
const startTime = Date.now();
|
|
551
|
+
|
|
552
|
+
while (this.activeTasks.size > 0 && (Date.now() - startTime) < shutdownTimeout) {
|
|
553
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// Terminate all workers
|
|
557
|
+
const terminationPromises = Array.from(this.workers).map(worker =>
|
|
558
|
+
this.terminateWorker(worker)
|
|
559
|
+
);
|
|
560
|
+
|
|
561
|
+
await Promise.all(terminationPromises);
|
|
562
|
+
|
|
563
|
+
this.workers.clear();
|
|
564
|
+
this.availableWorkers.length = 0;
|
|
565
|
+
this.busyWorkers.clear();
|
|
566
|
+
this.activeTasks.clear();
|
|
567
|
+
this.taskQueue.length = 0;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* Setup graceful shutdown handlers
|
|
572
|
+
*/
|
|
573
|
+
setupGracefulShutdown() {
|
|
574
|
+
const shutdown = async () => {
|
|
575
|
+
console.log('WorkerPool: Graceful shutdown initiated');
|
|
576
|
+
await this.shutdown();
|
|
577
|
+
process.exit(0);
|
|
578
|
+
};
|
|
579
|
+
|
|
580
|
+
process.on('SIGTERM', shutdown);
|
|
581
|
+
process.on('SIGINT', shutdown);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
export default WorkerPool;
|