UNPKG

lynkr

Version:

Self-hosted LLM gateway and tier-routing proxy for Claude Code, Cursor, and Codex. Routes across Ollama, AWS Bedrock, OpenRouter, Databricks, Azure OpenAI, llama.cpp, and LM Studio with prompt caching, MCP tools, and 60-80% cost savings.

361 lines (307 loc) 9.41 kB
/** * Worker Thread Pool * * Offloads CPU-intensive operations (JSON parsing, cloning, compression) * from the main event loop to worker threads. * * @module workers/pool */ const { Worker } = require('worker_threads'); const os = require('os'); const path = require('path'); const logger = require('../logger'); class WorkerPool { constructor(options = {}) { this.size = options.size || Math.max(2, os.cpus().length - 1); this.workers = []; this.queue = []; this.taskId = 0; this.pendingTasks = new Map(); // taskId -> { resolve, reject, timeout } this.workerScript = path.join(__dirname, 'worker.js'); this.taskTimeout = options.taskTimeout || 5000; this.offloadThreshold = options.offloadThreshold || 10000; // 10KB min to offload this.initialized = false; this.shuttingDown = false; // Stats this.stats = { tasksProcessed: 0, tasksQueued: 0, tasksFailed: 0, tasksTimedOut: 0, workersRestarted: 0, avgProcessingTime: 0, }; } async initialize() { if (this.initialized) return; logger.info({ poolSize: this.size }, '[WorkerPool] Initializing worker pool'); const workerPromises = []; for (let i = 0; i < this.size; i++) { workerPromises.push(this._createWorker(i)); } await Promise.all(workerPromises); this.initialized = true; logger.info({ poolSize: this.size, offloadThreshold: this.offloadThreshold, taskTimeout: this.taskTimeout }, '[WorkerPool] Worker pool initialized'); } async _createWorker(id) { return new Promise((resolve, reject) => { const worker = new Worker(this.workerScript); worker.busy = false; worker.id = id; worker.taskCount = 0; const readyHandler = (msg) => { if (msg.type === 'ready') { worker.off('message', readyHandler); worker.on('message', (msg) => this._handleMessage(worker, msg)); resolve(worker); } }; worker.on('message', readyHandler); worker.on('error', (err) => this._handleError(worker, err)); worker.on('exit', (code) => this._handleExit(worker, code)); this.workers[id] = worker; // Timeout for worker initialization setTimeout(() => { worker.off('message', readyHandler); reject(new Error(`Worker ${id} failed to initialize within timeout`)); }, 5000); }); } _handleMessage(worker, msg) { const { taskId, result, error, processingTime } = msg; const pending = this.pendingTasks.get(taskId); if (pending) { clearTimeout(pending.timeout); this.pendingTasks.delete(taskId); // Update stats this.stats.tasksProcessed++; if (processingTime) { this.stats.avgProcessingTime = (this.stats.avgProcessingTime * (this.stats.tasksProcessed - 1) + processingTime) / this.stats.tasksProcessed; } if (error) { this.stats.tasksFailed++; pending.reject(new Error(error)); } else { pending.resolve(result); } } worker.busy = false; worker.taskCount++; this._processQueue(); } _handleError(worker, err) { logger.error({ workerId: worker.id, error: err.message }, '[WorkerPool] Worker error'); // Reject all pending tasks for this worker for (const [taskId, pending] of this.pendingTasks) { if (pending.workerId === worker.id) { clearTimeout(pending.timeout); this.pendingTasks.delete(taskId); this.stats.tasksFailed++; pending.reject(err); } } } _handleExit(worker, code) { if (this.shuttingDown) return; logger.warn({ workerId: worker.id, exitCode: code }, '[WorkerPool] Worker exited, replacing'); this.stats.workersRestarted++; // Replace dead worker const index = this.workers.indexOf(worker); if (index !== -1) { this._createWorker(index).catch(err => { logger.error({ workerId: index, error: err.message }, '[WorkerPool] Failed to replace worker'); }); } } _getAvailableWorker() { // Find least busy worker let bestWorker = null; let minTasks = Infinity; for (const worker of this.workers) { if (!worker.busy && worker.taskCount < minTasks) { bestWorker = worker; minTasks = worker.taskCount; } } return bestWorker; } _processQueue() { if (this.queue.length === 0) return; const worker = this._getAvailableWorker(); if (!worker) return; const task = this.queue.shift(); this.stats.tasksQueued = this.queue.length; this._executeTask(worker, task); } _executeTask(worker, task) { worker.busy = true; const timeout = setTimeout(() => { const pending = this.pendingTasks.get(task.taskId); if (pending) { this.pendingTasks.delete(task.taskId); this.stats.tasksTimedOut++; pending.reject(new Error(`Task ${task.type} timed out after ${this.taskTimeout}ms`)); worker.busy = false; this._processQueue(); } }, this.taskTimeout); this.pendingTasks.set(task.taskId, { resolve: task.resolve, reject: task.reject, timeout, workerId: worker.id, }); worker.postMessage({ taskId: task.taskId, type: task.type, payload: task.payload, }); } /** * Execute a task on a worker thread * @param {string} type - Task type: 'parse', 'stringify', 'clone', 'compress', 'transform' * @param {*} payload - Data to process * @returns {Promise<*>} - Processed result */ async exec(type, payload) { if (!this.initialized) { await this.initialize(); } if (this.shuttingDown) { throw new Error('Worker pool is shutting down'); } return new Promise((resolve, reject) => { const task = { taskId: ++this.taskId, type, payload, resolve, reject, }; const worker = this._getAvailableWorker(); if (worker) { this._executeTask(worker, task); } else { this.queue.push(task); this.stats.tasksQueued = this.queue.length; } }); } // ============== Convenience Methods ============== /** * Parse JSON string (offloads large payloads only) * @param {string} jsonString - JSON string to parse * @returns {Promise<*>} - Parsed object */ async parse(jsonString) { // Only offload large payloads if (typeof jsonString !== 'string' || jsonString.length < this.offloadThreshold) { return JSON.parse(jsonString); } return this.exec('parse', jsonString); } /** * Stringify object to JSON (offloads large objects only) * @param {*} obj - Object to stringify * @returns {Promise<string>} - JSON string */ async stringify(obj) { // Quick check for size - if small, do inline const quick = JSON.stringify(obj); if (quick.length < this.offloadThreshold) { return quick; } return this.exec('stringify', obj); } /** * Deep clone an object * @param {*} obj - Object to clone * @returns {Promise<*>} - Cloned object */ async clone(obj) { // For small objects, use inline structuredClone const size = JSON.stringify(obj).length; if (size < this.offloadThreshold) { return typeof structuredClone === 'function' ? structuredClone(obj) : JSON.parse(JSON.stringify(obj)); } return this.exec('clone', obj); } /** * Transform messages (compression, truncation, etc.) * @param {Array} messages - Messages to transform * @param {Object} options - Transform options * @returns {Promise<Object>} - Transformed messages with stats */ async transform(messages, options = {}) { return this.exec('transform', { messages, options }); } /** * Get pool statistics * @returns {Object} - Pool stats */ getStats() { return { ...this.stats, poolSize: this.size, activeWorkers: this.workers.filter(w => w?.busy).length, queueLength: this.queue.length, pendingTasks: this.pendingTasks.size, }; } /** * Graceful shutdown */ async shutdown() { if (this.shuttingDown) return; this.shuttingDown = true; logger.info('[WorkerPool] Shutting down...'); // Reject all pending tasks for (const [taskId, pending] of this.pendingTasks) { clearTimeout(pending.timeout); pending.reject(new Error('Worker pool shutting down')); } this.pendingTasks.clear(); this.queue = []; // Terminate all workers const terminatePromises = this.workers .filter(w => w) .map(w => w.terminate()); await Promise.all(terminatePromises); this.workers = []; this.initialized = false; logger.info({ stats: this.stats }, '[WorkerPool] Shutdown complete'); } } // Singleton instance let pool = null; /** * Get the singleton worker pool instance * @param {Object} options - Pool options (only used on first call) * @returns {WorkerPool} - Worker pool instance */ function getWorkerPool(options) { if (!pool) { pool = new WorkerPool(options); } return pool; } /** * Check if worker pool is available and initialized * @returns {boolean} */ function isWorkerPoolReady() { return pool?.initialized === true; } module.exports = { WorkerPool, getWorkerPool, isWorkerPoolReady, };