UNPKG

@cloud-copilot/job

Version:

Async job runners with defined worker pools

165 lines 5.59 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ConcurrentWorkerPool = void 0; /** * A worker pool that will run jobs concurrently using promises up to a specified limit. * If no work is being done, it will wait for jobs to be added and run them up to the * maximum concurrency. * * This is designed to create a central queue for jobs of many types to be processed * in a streaming fashion, allowing for efficient resource usage and managed network requests. * */ class ConcurrentWorkerPool { concurrency; logger; jobCounter = 0; resolveMap = {}; queue = []; activeJobs = 0; waitingResolvers = []; workers = []; isAcceptingWork = true; workAvailablePromise = null; resolveWorkAvailable = null; /** * Create a new runner with the specified concurrency. * * @param concurrency - The maximum number of jobs to run concurrently. * @param logger - Logger instance for logging long-running jobs. */ constructor(concurrency, logger) { this.concurrency = concurrency; this.logger = logger; } async worker(workerId) { while (this.isAcceptingWork || this.queue.length > 0) { const job = this.queue.shift(); if (!job) { if (!this.isAcceptingWork) { // No longer accepting work and no jobs left, exit immediately return; } // No work available, wait for new work to be added await this.waitForWorkAvailable(); continue; } this.activeJobs++; const context = { workerId }; const startTime = Date.now(); const interval = setInterval(() => { this.logger.warn(`Long-running job detected.`, { minutes: Math.floor((Date.now() - startTime) / 60000) }, { ...context, ...job.properties }); }, 60_000); const resolve = this.resolveMap[job.concurrentJobId]; try { const value = await job.execute({ ...context, properties: job.properties }); resolve({ status: 'fulfilled', value, properties: job.properties }); } catch (reason) { resolve({ status: 'rejected', reason, properties: job.properties }); } finally { clearInterval(interval); this.activeJobs--; this.checkIfIdle(); delete this.resolveMap[job.concurrentJobId]; } } } waitForWorkAvailable() { if (!this.workAvailablePromise) { this.workAvailablePromise = new Promise((resolve) => { this.resolveWorkAvailable = resolve; }); } return this.workAvailablePromise; } ensureWorkers() { if (this.workers.length === 0 && this.isAcceptingWork) { for (let i = 0; i < this.concurrency; i++) { this.workers.push(this.worker(i + 1)); } } } notifyWorkersOfNewWork() { // Wake up waiting workers if (this.resolveWorkAvailable) { this.resolveWorkAvailable(); this.workAvailablePromise = null; this.resolveWorkAvailable = null; } } checkIfIdle() { if (this.activeJobs === 0 && this.queue.length === 0) { // Notify all waiting resolvers this.waitingResolvers.forEach((resolve) => resolve()); this.waitingResolvers = []; } } /** * Add a job to the queue */ enqueue(job) { if (!this.isAcceptingWork) { throw new Error('Cannot enqueue jobs after shutdown'); } return new Promise((resolve) => { const jobId = this.jobCounter++; const jobWithId = { ...job, concurrentJobId: jobId }; this.resolveMap[jobId] = resolve; this.queue.push(jobWithId); this.ensureWorkers(); this.notifyWorkersOfNewWork(); }); } /** * Add multiple jobs to the queue */ enqueueAll(jobs) { return jobs.map((job) => this.enqueue(job)); } /** * Returns a promise that resolves when all queued work is complete */ waitForIdle() { return new Promise((resolve) => { if (this.activeJobs === 0 && this.queue.length === 0) { resolve(); } else { this.waitingResolvers.push(resolve); } }); } /** * Shutdown the queue - no new jobs will be accepted, but existing jobs will complete. * * Returns when a promise that resolves when all jobs have been processed and * the onComplete callback has been called for each job. */ async finishAllWork() { this.isAcceptingWork = false; // Wake up any sleeping workers so they can process remaining jobs or exit this.notifyWorkersOfNewWork(); // Check if we're already idle and notify any waiting resolvers await Promise.all(this.workers); this.workers = []; } /** * Get the current queue length */ get queueLength() { return this.queue.length; } /** * Get the number of currently active jobs */ get activeJobCount() { return this.activeJobs; } } exports.ConcurrentWorkerPool = ConcurrentWorkerPool; //# sourceMappingURL=ConcurrentWorkerPool.js.map