@backgroundjs/core
Version:
An extendible background job queue for js/ts applications
226 lines • 7.97 kB
JavaScript
/**
* MongoDB storage adapter for JobQueue
*
* This storage adapter uses MongoDB to store jobs, making it suitable
* for distributed environments with multiple instances/processes.
*
* Note: You must install the 'mongodb' package to use this adapter:
* npm install mongodb
*/
export class MongoDBJobStorage {
collection;
mongoClient;
logging = false;
staleJobTimeout = 1000 * 60 * 60 * 24; // 24 hours
constructor(mongoClient, options = {}) {
this.mongoClient = mongoClient;
this.collection = this.mongoClient
.db()
.collection(options.collectionName || "jobs");
this.logging = options.logging || false;
this.staleJobTimeout = options.staleJobTimeout || 1000 * 60 * 60 * 24; // 24 hours
}
/**
* Save a job
* @param job - The job to save
*/
async saveJob(job) {
try {
await this.collection.insertOne(job);
}
catch (error) {
if (this.logging) {
console.error("Error saving job", error);
}
}
}
/**
* Get a job by ID
* @param id - The ID of the job to get
* @returns The job with the given ID, or null if the job does not exist
*/
async getJob(id) {
try {
return await this.collection.findOne({ id });
}
catch (error) {
if (this.logging) {
console.error("Error getting job", error);
}
return null;
}
}
/**
* Get all jobs by status
* @param status - The status of the jobs to get
* @returns An array of jobs with the given status
*/
async getJobsByStatus(status) {
try {
return await this.collection.find({ status }).toArray();
}
catch (error) {
if (this.logging) {
console.error("Error getting jobs by status", error);
}
return [];
}
}
/**
* Update a job
* @param job - The job to update
* @throws Error if the job is not found
*/
async updateJob(job) {
try {
await this.collection.findOneAndUpdate({ id: job.id }, { $set: job });
}
catch (error) {
if (this.logging) {
console.error("Error updating job", error);
}
throw error;
}
}
/**
* Acquire the next pending job
* @returns The next pending job, or null if no pending jobs are available
*/
async acquireNextJob(handlerNames) {
try {
const staleThreshold = new Date(Date.now() - this.staleJobTimeout);
// First try to find pending jobs
let job = await this.collection.findOneAndUpdate({
...(handlerNames && { name: { $in: handlerNames } }),
status: "pending",
$or: [
{ scheduledAt: { $exists: false } },
{ scheduledAt: { $lte: new Date() } },
],
}, { $set: { status: "processing", startedAt: new Date() } }, { sort: { priority: 1, createdAt: 1 }, returnDocument: "after" });
if (!job) {
job = await this.collection.findOneAndUpdate({
...(handlerNames && { name: { $in: handlerNames } }),
status: "processing",
startedAt: { $lte: staleThreshold },
$or: [
{ completedAt: { $exists: false } },
{ completedAt: null }
]
}, { $set: { startedAt: new Date() } }, { sort: { priority: 1, createdAt: 1 }, returnDocument: "after" });
}
return job;
}
catch (error) {
if (this.logging) {
console.error(`[MongoDBJobStorage] Error acquiring next job:`, error);
}
return null;
}
}
/**
* Acquire multiple pending jobs for prefetching
* @param batchSize - Number of jobs to prefetch
* @returns Array of acquired jobs
*/
async acquireNextJobs(batchSize, handlerNames) {
const session = this.mongoClient.startSession();
try {
let jobs = [];
await session.withTransaction(async () => {
const now = new Date();
const staleThreshold = new Date(Date.now() - this.staleJobTimeout);
const availableJobs = await this.collection
.find({
...(handlerNames && { name: { $in: handlerNames } }),
$or: [
{
status: 'pending',
$or: [
{ scheduledAt: { $exists: false } },
{ scheduledAt: { $lte: now } }
]
},
{
status: 'processing',
startedAt: { $exists: true, $ne: undefined, $lt: staleThreshold },
completedAt: { $exists: false },
}
]
}, { session })
.sort({ priority: 1, createdAt: 1 })
.limit(batchSize)
.toArray();
if (availableJobs.length === 0) {
return;
}
const bulkOps = availableJobs.map(job => ({
updateOne: {
filter: { _id: job._id },
update: {
$set: {
status: 'processing',
startedAt: now,
}
}
}
}));
const bulkResult = await this.collection.bulkWrite(bulkOps, { session });
if (bulkResult.modifiedCount > 0) {
const updatedJobs = await this.collection
.find({
_id: { $in: availableJobs.map(job => job._id) },
status: 'processing',
startedAt: now
}, { session })
.toArray();
jobs = updatedJobs;
}
});
if (this.logging && jobs.length > 0) {
console.log(`[MongoDBJobStorage] Acquired ${jobs.length} jobs in batch`);
}
return jobs;
}
catch (error) {
if (this.logging) {
console.error(`[MongoDBJobStorage] Error acquiring batch jobs:`, error);
}
return [];
}
finally {
await session.endSession();
}
}
/**
* Complete a job
* @param jobId - The ID of the job to complete
* @param result - The result of the job
*/
async completeJob(jobId, result) {
try {
await this.collection.findOneAndUpdate({ id: jobId, status: "processing" }, { $set: { status: "completed", result, completedAt: new Date() } });
}
catch (error) {
if (this.logging) {
console.error(`[MongoDBJobStorage] Error completing job:`, error);
}
}
}
/**
* Fail a job
* @param jobId - The ID of the job to fail
* @param error - The error message
*/
async failJob(jobId, error) {
try {
await this.collection.findOneAndUpdate({ id: jobId, status: "processing" }, { $set: { status: "failed", error, completedAt: new Date() } });
}
catch (error) {
if (this.logging) {
console.error(`[MongoDBJobStorage] Error failing job:`, error);
}
}
}
}
//# sourceMappingURL=mongodb-storage.js.map