PRIX / lib /src /services /job-queue.js
Rachit-Tw's picture
Upload 169 files
9284ad7 verified
Raw
History Blame Contribute Delete
5.71 kB
"use strict";
/**
* Asynchronous Job Queue for Webhook Processing
*
* GitHub webhooks timeout after 10 seconds. Long-running PR reviews
* can exceed this. This queue acknowledges webhooks immediately and
* processes them asynchronously.
*
* TODO: For production with horizontal scaling, replace with Redis-backed queue (Bull/BullMQ)
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.jobQueue = void 0;
const pino_1 = __importDefault(require("pino"));
const logger = (0, pino_1.default)({ level: process.env.LOG_LEVEL || 'info' });
const defaultConfig = {
maxConcurrent: 3,
pollIntervalMs: 1000,
maxAttempts: 3
};
class JobQueue {
jobs = new Map();
handlers = new Map();
processing = new Set();
config;
isRunning = false;
intervalId;
constructor(config = {}) {
this.config = { ...defaultConfig, ...config };
}
/**
* Register a job handler for a specific job type
*/
registerHandler(type, handler) {
this.handlers.set(type, handler);
logger.info({ type }, 'Registered job handler');
}
/**
* Add a job to the queue
*/
async add(type, data, maxAttempts) {
const id = `${type}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
const job = {
id,
type,
data,
attempts: 0,
maxAttempts: maxAttempts || this.config.maxAttempts,
createdAt: Date.now(),
status: 'pending'
};
this.jobs.set(id, job);
logger.info({ jobId: id, type }, 'Job added to queue');
// Start processing if not already running
if (!this.isRunning) {
this.start();
}
return id;
}
/**
* Get job status
*/
getStatus(jobId) {
return this.jobs.get(jobId);
}
/**
* Get queue metrics
*/
getMetrics() {
const jobs = Array.from(this.jobs.values());
return {
total: jobs.length,
pending: jobs.filter(j => j.status === 'pending').length,
processing: jobs.filter(j => j.status === 'processing').length,
completed: jobs.filter(j => j.status === 'completed').length,
failed: jobs.filter(j => j.status === 'failed').length
};
}
/**
* Start the job processor
*/
start() {
if (this.isRunning)
return;
this.isRunning = true;
logger.info('Job queue started');
this.intervalId = setInterval(() => {
this.processJobs();
}, this.config.pollIntervalMs);
}
/**
* Stop the job processor
*/
stop() {
this.isRunning = false;
if (this.intervalId) {
clearInterval(this.intervalId);
this.intervalId = undefined;
}
logger.info('Job queue stopped');
}
/**
* Process pending jobs
*/
async processJobs() {
if (this.processing.size >= this.config.maxConcurrent) {
return; // At capacity
}
const pendingJobs = Array.from(this.jobs.values())
.filter(j => j.status === 'pending')
.sort((a, b) => a.createdAt - b.createdAt); // FIFO
for (const job of pendingJobs) {
if (this.processing.size >= this.config.maxConcurrent) {
break;
}
this.processJob(job);
}
}
/**
* Process a single job
*/
async processJob(job) {
const handler = this.handlers.get(job.type);
if (!handler) {
logger.error({ jobId: job.id, type: job.type }, 'No handler registered for job type');
job.status = 'failed';
job.error = 'No handler registered';
return;
}
this.processing.add(job.id);
job.status = 'processing';
job.attempts++;
logger.info({ jobId: job.id, type: job.type, attempt: job.attempts }, 'Processing job');
try {
await handler(job);
job.status = 'completed';
logger.info({ jobId: job.id }, 'Job completed successfully');
}
catch (error) {
logger.error({ jobId: job.id, error: error.message }, 'Job failed');
if (job.attempts < job.maxAttempts) {
job.status = 'pending'; // Retry
job.error = error.message;
logger.info({ jobId: job.id, attemptsLeft: job.maxAttempts - job.attempts }, 'Job scheduled for retry');
}
else {
job.status = 'failed';
job.error = error.message;
logger.error({ jobId: job.id }, 'Job failed permanently after max attempts');
}
}
finally {
this.processing.delete(job.id);
}
}
/**
* Cleanup old completed jobs
*/
cleanup(maxAgeMs = 24 * 60 * 60 * 1000) {
const now = Date.now();
let cleaned = 0;
for (const [id, job] of this.jobs.entries()) {
if ((job.status === 'completed' || job.status === 'failed') &&
now - job.createdAt > maxAgeMs) {
this.jobs.delete(id);
cleaned++;
}
}
if (cleaned > 0) {
logger.info({ cleaned }, 'Cleaned up old jobs');
}
}
}
// Singleton instance
exports.jobQueue = new JobQueue();
// Auto-cleanup every hour
setInterval(() => {
exports.jobQueue.cleanup();
}, 60 * 60 * 1000);
//# sourceMappingURL=job-queue.js.map