@cavai/adonis-queue
Version:
Basic AdonisJS queue provider
126 lines (125 loc) • 4.02 kB
JavaScript
import { join } from 'node:path';
import SuperJSON from 'superjson';
/**
* Config for manager looks like this
*
* {
* default: 'somename',
* queues: {
* somename: () => new DatabaseDrive({
* table_name: 'sjdasjk',
* })
* }
* }
*/
export class QueueManager {
config;
logger;
jobsRoot;
#cachedDrivers = {};
constructor(config, logger, jobsRoot) {
this.config = config;
this.logger = logger;
this.jobsRoot = jobsRoot;
}
use(queue) {
const queueToUse = queue ?? this.config.default;
if (this.#cachedDrivers[queueToUse]) {
return this.#cachedDrivers[queueToUse];
}
if (!this.config.queues[queueToUse]) {
throw Error(`Queue not defined: "${String(queueToUse)}"`);
}
this.#cachedDrivers[queueToUse] = this.config.queues[queueToUse]();
return this.#cachedDrivers[queueToUse];
}
/**
* Starts up given queue jobs execution
*
* @param queue Queue name to start
*/
async start(queue) {
/**
* Just log errors, but don't stop at any
* In case of error, will keep queue process alive
* Trying to execute next job in-line even after failure
*/
try {
if (this.use(queue).pollingDelay) {
/**
* Will keep queue running and checking for jobs infinitely
*/
while (true) {
await this.execute();
// Wait before next execution loop
await new Promise((res) => setTimeout(() => res(true), this.use(queue).pollingDelay));
}
}
else {
await this.execute();
}
}
catch (error) {
// Check if it's needed in first place
this.logger.error(error);
}
}
/**
* Executes next job in queue
*/
async execute() {
let job = await this.use().getNext();
// No job queued, continue with life
if (!job) {
this.logger.debug('No jobs in queue');
return;
}
this.logger.debug({ job }, 'Execution started');
/**
* Dynamically import job class on the fly
* Node stores all imported classes in-memory
* so queue has to be restarted if there has been
* any change to already imported job class
*/
const payload = SuperJSON.parse(job.payload);
const jobPath = join(this.jobsRoot, job.class_path);
const jobExports = await import(jobPath);
const JobClass = jobExports.default;
const jobClassInstance = new JobClass(...payload.data);
/**
* Wrap handler to try-catch
* it's just to deal with execution failures
* Other cases are handled in parent Ace command
*/
try {
job.attempts++;
await jobClassInstance.handle();
// After execution remove job from queue
await this.use().remove(job.id);
}
catch (error) {
this.logger.error(error, 'Job execution failed');
/**
* Check if job has depleted retries
* if so, then mark it as failed
*/
if (job.attempts >= JobClass.retries) {
this.logger.error(`Job ${job.id} failed for last time after ${JobClass.retries} retries`);
await this.use().markFailed(job);
return;
}
await this.use().reSchedule(job, JobClass.retryAfter);
}
this.logger.debug({ job }, 'Executed successfully');
}
/**
* Stores job to queue for future execution
*
* @param path Path to class file
* @param payload Job payload
* @param options Store options
*/
async store(path, payload, options) {
return this.use().store(path, payload, options);
}
}